quiz grouping

This commit is contained in:
gosangam
2026-10-18 13:02:30 +05:30
parent fed12b0d46
commit 3af4e1bcd3
6 changed files with 239 additions and 55 deletions
+74
View File
@@ -0,0 +1,74 @@
import type { QuizSections } from "@/lib/quiz-groups";
import { QuizGrid } from "./quiz-grid";
/**
* The "All" tab: Books → book, Lectures & Conversations → year, then the
* stand-alone quizzes. groupQuizzes only returns non-empty groups, and each
* section here renders only when it has groups, so nothing empty shows.
*/
export function GroupedQuizzes({ sections }: { sections: QuizSections }) {
return (
<div className="space-y-10">
{sections.books.length > 0 && (
<Section title="Books">
{sections.books.map((g) => (
<Group key={g.key} label={g.label} count={g.quizzes.length}>
<QuizGrid quizzes={g.quizzes} />
</Group>
))}
</Section>
)}
{sections.lectures.length > 0 && (
<Section title="Lectures & Conversations">
{sections.lectures.map((g) => (
<Group key={g.key} label={g.label} count={g.quizzes.length}>
<QuizGrid quizzes={g.quizzes} />
</Group>
))}
</Section>
)}
{sections.other.length > 0 && (
<Section title="Other quizzes">
<QuizGrid quizzes={sections.other} />
</Section>
)}
</div>
);
}
function Section({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<section className="space-y-5">
<h2 className="font-[family-name:var(--font-title)] text-lg font-semibold text-foreground sm:text-xl">
{title}
</h2>
{children}
</section>
);
}
function Group({
label,
count,
children,
}: {
label: string;
count: number;
children: React.ReactNode;
}) {
return (
<div className="space-y-2.5">
<h3 className="flex min-w-0 items-baseline gap-2 text-sm font-medium text-muted-foreground">
<span className="min-w-0 truncate">{label}</span>
<span className="shrink-0 text-xs tabular-nums">{count}</span>
</h3>
{children}
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import type { QuizCard } from "@/lib/quiz";
import { QuizListCard } from "./quiz-list-card";
/**
* Quiz cards, two across from sm up. minmax(0,1fr) + min-w-0: grid items
* otherwise refuse to shrink below their content, so one long word pushes a
* card off-screen.
*/
export function QuizGrid({ quizzes }: { quizzes: QuizCard[] }) {
return (
<ul className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{quizzes.map((q) => (
<li key={q.id} className="min-w-0">
<QuizListCard q={q} />
</li>
))}
</ul>
);
}
+50
View File
@@ -0,0 +1,50 @@
"use client";
/** One quiz in the /quizzes list: what it's on, its title, and your progress. */
import Link from "next/link";
import { ClipboardCheck } from "lucide-react";
import { quizActionLabel, type QuizCard } from "@/lib/quiz";
import { cn } from "@/lib/utils";
const KIND: Record<QuizCard["target_kind"], string> = {
chapter: "Book chapter",
lecture: "Lecture",
conversation: "Conversation",
standalone: "Quiz",
};
export function QuizListCard({ q }: { q: QuizCard }) {
const done = q.my.state === "completed";
return (
<Link
href={`/quizzes/${q.id}`}
className="flex h-full min-w-0 flex-col rounded-2xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-muted/30"
>
<p className="text-xs font-medium text-primary">{KIND[q.target_kind]}</p>
<p className="mt-1 break-words font-semibold leading-snug text-foreground [overflow-wrap:anywhere]">
{q.title}
</p>
{q.target_title && (
<p className="mt-0.5 truncate text-sm text-muted-foreground">
{q.target_title}
</p>
)}
<div className="mt-auto flex flex-wrap items-center justify-between gap-x-3 gap-y-1 pt-4 text-xs">
<span className="text-muted-foreground">
{q.question_count} {q.question_count === 1 ? "question" : "questions"}
</span>
<span
className={cn(
"inline-flex items-center gap-1 whitespace-nowrap font-medium",
done ? "text-muted-foreground" : "text-primary",
)}
>
<ClipboardCheck className="size-3.5" />
{quizActionLabel(q.my.state)}
{done && q.my.score !== undefined && ` · ${q.my.score}/${q.my.total}`}
</span>
</div>
</Link>
);
}