ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:07:20 +08:00
parent 56d984a26a
commit fbef7cb6e6
283 changed files with 25961 additions and 1072 deletions
@@ -0,0 +1,208 @@
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, ClipboardList, NotebookPen, CheckCircle2, Circle } from "lucide-react";
import { useCourses } from "@/contexts/AdminCoursesContext";
import { PageMeta } from "@/contexts/MetadataContext";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
// ─── Helpers ──────────────────────────────────────────────────────────────────
function InfoRow({ label, children }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground uppercase tracking-wide">{label}</span>
<span className="text-sm font-medium">{children ?? <span className="italic text-muted-foreground">—</span>}</span>
</div>
);
}
function SectionCard({ title, children }) {
return (
<div className="rounded-lg border bg-card p-5 space-y-4">
{title && (
<>
<h2 className="text-sm font-semibold">{title}</h2>
<Separator />
</>
)}
{children}
</div>
);
}
const TYPE_LABELS = {
multiple_choice: "Multiple Choice",
multi_select: "Multi Select",
true_false: "True / False",
};
function QuestionView({ question, index }) {
return (
<div className="rounded-lg border bg-card p-5 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2 min-w-0">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted text-xs font-semibold text-muted-foreground mt-0.5">
{index + 1}
</span>
<p className="text-sm font-medium leading-snug">
{question.question?.trim() || <span className="italic text-muted-foreground">Untitled</span>}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge variant="outline" className="text-xs">
{TYPE_LABELS[question.type] ?? question.type}
</Badge>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{question.points ?? 1} pt{(question.points ?? 1) !== 1 ? "s" : ""}
</span>
</div>
</div>
<ul className="space-y-1.5 pl-8">
{(question.options ?? []).map((opt, oi) => (
<li key={opt.option_id ?? oi} className="flex items-center gap-2 text-sm">
{opt.is_correct ? (
<CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
) : (
<Circle className="h-4 w-4 text-muted-foreground/40 shrink-0" />
)}
<span className={opt.is_correct ? "font-medium text-green-700 dark:text-green-400" : "text-muted-foreground"}>
{opt.text}
</span>
</li>
))}
</ul>
</div>
);
}
function LoadingSkeleton() {
return (
<div className="space-y-4">
<Skeleton className="h-32 w-full rounded-lg" />
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 w-full rounded-lg" />)}
</div>
);
}
// ─── Page ─────────────────────────────────────────────────────────────────────
export default function ViewAssessment() {
const navigate = useNavigate();
const { courseId } = useParams();
const { fetchAssessment, assessment, loading } = useCourses();
useEffect(() => {
fetchAssessment(courseId);
}, [courseId]);
const questions = assessment?.questions ?? [];
const totalPoints = questions.reduce((sum, q) => sum + (q.points ?? 1), 0);
return (
<div className="flex flex-col min-h-screen bg-muted/60">
<PageMeta title="View Assessment - STARR" />
{/* ── Header ── */}
<div
className="sticky z-20 shrink-0 border-b bg-white/70 dark:bg-zinc-900/70 backdrop-blur-md"
style={{ top: "var(--navbar-h)" }}
>
<div className="lg:container lg:mx-auto lg:px-6 px-4">
<div className="flex items-center gap-3 py-3">
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex-1 min-w-0">
<h1 className="text-xl font-semibold leading-tight flex items-center gap-2">
<ClipboardList className="h-5 w-5 text-muted-foreground" />
View Assessment
</h1>
{assessment && (
<p className="text-sm text-muted-foreground">
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/admin/courses/${courseId}/assessment`)}
>
<NotebookPen className="h-4 w-4 mr-2" />
Modify Assessment
</Button>
</div>
</div>
</div>
{/* ── Content ── */}
<div className="lg:container lg:mx-auto lg:px-6 px-4 py-6">
<div className="max-w-3xl mx-auto space-y-5 pb-16">
{loading && !assessment ? (
<LoadingSkeleton />
) : !assessment ? (
<div className="rounded-lg border border-dashed bg-card p-12 flex flex-col items-center gap-3 text-center">
<ClipboardList className="h-8 w-8 text-muted-foreground/40" />
<p className="text-sm text-muted-foreground">No assessment has been created for this course yet.</p>
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/admin/courses/${courseId}/assessment`)}
>
<NotebookPen className="h-4 w-4 mr-2" />
Create Assessment
</Button>
</div>
) : (
<>
{/* ── Settings ── */}
<SectionCard title="Settings">
<div className="grid grid-cols-2 gap-4">
<InfoRow label="Title">{assessment.title || "Course Assessment"}</InfoRow>
<InfoRow label="Required">
<Badge variant={assessment.is_required ? "default" : "secondary"} className="mt-0.5">
{assessment.is_required ? "Required" : "Optional"}
</Badge>
</InfoRow>
<InfoRow label="Passing Score">{assessment.passing_score ?? 70}%</InfoRow>
<InfoRow label="Time Limit">
{assessment.time_limit_minutes ? `${assessment.time_limit_minutes} mins` : "No limit"}
</InfoRow>
<InfoRow label="Questions in Pool">{questions.length}</InfoRow>
<InfoRow label="Max Shown per Attempt">
{assessment.max_questions
? `${assessment.max_questions} (random)`
: `All (${questions.length})`}
</InfoRow>
<InfoRow label="Total Points">{totalPoints}</InfoRow>
</div>
</SectionCard>
{/* ── Questions ── */}
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground px-1">
Questions
</p>
{questions.length === 0 ? (
<div className="rounded-lg border border-dashed bg-card p-10 flex flex-col items-center gap-2 text-center">
<p className="text-sm text-muted-foreground">No questions added yet.</p>
</div>
) : (
questions.map((q, i) => (
<QuestionView key={q.question_id ?? i} question={q} index={i} />
))
)}
</div>
</>
)}
</div>
</div>
</div>
);
}