- );
-}
diff --git a/components/quiz/quiz-browser.tsx b/components/quiz/quiz-browser.tsx
new file mode 100644
index 00000000..298d90db
--- /dev/null
+++ b/components/quiz/quiz-browser.tsx
@@ -0,0 +1,213 @@
+"use client";
+
+/**
+ * The "All" tab of /quizzes, as a drill-down:
+ *
+ * All → Books → a book → its quizzes
+ * → Lectures & Conversations → a year → its quizzes
+ * → Other quizzes (stand-alone)
+ *
+ * Where you are lives in the URL (?in=books&book=bg, ?in=lectures&year=1972)
+ * so a refresh, Back, or a shared link lands in the same place. Every level
+ * only offers categories that actually hold quizzes; a stale or hand-typed
+ * URL pointing at an empty one falls back to its parent.
+ */
+
+import Link from "next/link";
+import {
+ BookOpen,
+ CalendarDays,
+ ChevronRight,
+ ClipboardCheck,
+ Mic,
+} from "lucide-react";
+import type { QuizCard } from "@/lib/quiz";
+import { groupQuizzes, type QuizGroup } from "@/lib/quiz-groups";
+import { QuizCategoryCard } from "./quiz-category-card";
+import { QuizGrid } from "./quiz-grid";
+
+export type QuizBrowsePath = {
+ in?: string | null;
+ book?: string | null;
+ year?: string | null;
+};
+
+const BOOKS = "books";
+const LECTURES = "lectures";
+const OTHER = "other";
+
+/** /quizzes URL for a browse position (the All tab is the default). */
+export function quizzesHref(p: QuizBrowsePath): string {
+ const qs = new URLSearchParams();
+ if (p.in) qs.set("in", p.in);
+ if (p.book) qs.set("book", p.book);
+ if (p.year) qs.set("year", p.year);
+ const s = qs.toString();
+ return s ? `/quizzes?${s}` : "/quizzes";
+}
+
+export function QuizBrowser({
+ quizzes,
+ path,
+}: {
+ quizzes: QuizCard[];
+ path: QuizBrowsePath;
+}) {
+ const { books, lectures, other } = groupQuizzes(quizzes);
+ const all = (gs: QuizGroup[]) => gs.flatMap((g) => g.quizzes);
+
+ const book =
+ path.in === BOOKS ? books.find((g) => g.key === path.book) : undefined;
+ const year =
+ path.in === LECTURES
+ ? lectures.find((g) => g.key === path.year)
+ : undefined;
+
+ // Books → a book: its quizzes.
+ if (book) {
+ return (
+
+
+
+ );
+ }
+ // Lectures → a year: its quizzes.
+ if (year) {
+ return (
+
+
+
+ );
+ }
+ // Books: a card per book.
+ if (path.in === BOOKS && books.length > 0) {
+ return (
+
+
+ {books.map((g) => (
+ }
+ />
+ ))}
+
+
+ );
+ }
+ // Lectures: a card per year.
+ if (path.in === LECTURES && lectures.length > 0) {
+ return (
+
+
+ {lectures.map((g) => (
+ }
+ />
+ ))}
+
+
+ );
+ }
+ // Stand-alone quizzes.
+ if (path.in === OTHER && other.length > 0) {
+ return (
+
+
+
+ );
+ }
+
+ // Top level: one card per category that has quizzes.
+ return (
+
+ {books.length > 0 && (
+ }
+ />
+ )}
+ {lectures.length > 0 && (
+ }
+ />
+ )}
+ {other.length > 0 && (
+ }
+ />
+ )}
+
+ );
+}
+
+function CardGrid({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+/** A level below the top: a breadcrumb back up, then its content. */
+function Level({
+ crumbs,
+ children,
+}: {
+ /** [label, href?] — the last (current) one has no link. */
+ crumbs: [string, string?][];
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/components/quiz/quiz-category-card.tsx b/components/quiz/quiz-category-card.tsx
new file mode 100644
index 00000000..8f0af7f6
--- /dev/null
+++ b/components/quiz/quiz-category-card.tsx
@@ -0,0 +1,47 @@
+/**
+ * A clickable category in the quiz browser — Books, a book, Lectures, a
+ * year. Shows how many quizzes it holds and how many are still to take.
+ */
+
+import Link from "next/link";
+import { ChevronRight } from "lucide-react";
+import type { QuizCard } from "@/lib/quiz";
+
+export function QuizCategoryCard({
+ href,
+ label,
+ quizzes,
+ icon,
+}: {
+ href: string;
+ label: string;
+ quizzes: QuizCard[];
+ icon: React.ReactNode;
+}) {
+ const todo = quizzes.filter((q) => q.my.state !== "completed").length;
+ const n = quizzes.length;
+ return (
+
+
+ {icon}
+
+
+
+ {label}
+
+
+ {n} {n === 1 ? "quiz" : "quizzes"}
+ {todo > 0 && todo < n ? ` · ${todo} to do` : ""}
+ {todo === 0 ? " · all finished" : ""}
+
+
+
+
+ );
+}
diff --git a/lib/quiz-groups.ts b/lib/quiz-groups.ts
index 596bdbf5..c801471f 100644
--- a/lib/quiz-groups.ts
+++ b/lib/quiz-groups.ts
@@ -72,7 +72,12 @@ export function groupQuizzes(cards: QuizCard[]): QuizSections {
.map(([code, quizzes]) => ({
key: code,
label: BOOK_NAMES[code] ?? code.toUpperCase(),
- quizzes,
+ // Chapter order (/books/bg/2 before /books/bg/10), not newest first.
+ quizzes: [...quizzes].sort((a, b) =>
+ (a.target_path ?? "").localeCompare(b.target_path ?? "", undefined, {
+ numeric: true,
+ }),
+ ),
})),
lectures: [...years.entries()]
// Oldest first; undated (null) last.