The All tab is now a click-through of category cards — Books, Lectures & Conversations, Other quizzes — each opening its books or years, then the quizzes themselves. Only categories with quizzes get a card. The position and tab live in the URL (?in=books&book=bg, ?tab=todo), so refresh, Back and shared links keep your place. Book quizzes list in chapter order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* /quizzes — every published quiz, with where you are in each.
|
|
*
|
|
* Reached from the Quizzes card on the search page. Quizzes are deliberately
|
|
* not in search results: they're something to do, not something to look up.
|
|
* Signed in only: visitors get the sign-in modal, and home if they dismiss it.
|
|
*
|
|
* The tab (?tab=todo) and, under All, the category you've drilled into
|
|
* (?in=books&book=bg) both live in the URL, so a refresh keeps your place.
|
|
*/
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
import { useSignedInPage } from "@/components/auth/use-signed-in-page";
|
|
import { quiz, type QuizCard } from "@/lib/quiz";
|
|
import { QuizGrid } from "@/components/quiz/quiz-grid";
|
|
import { QuizBrowser } from "@/components/quiz/quiz-browser";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type Filter = "all" | "todo" | "done";
|
|
const FILTERS: readonly Filter[] = ["all", "todo", "done"];
|
|
|
|
export function QuizzesList() {
|
|
const { user } = useSignedInPage();
|
|
const userId = user?.id;
|
|
const [list, setList] = useState<QuizCard[] | null>(null);
|
|
const params = useSearchParams();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const tabParam = params.get("tab") as Filter | null;
|
|
const filter: Filter =
|
|
tabParam && FILTERS.includes(tabParam) ? tabParam : "all";
|
|
// Switching tab starts that tab at its top; All is the bare URL.
|
|
const setFilter = (f: Filter) =>
|
|
router.replace(f === "all" ? pathname : `${pathname}?tab=${f}`, {
|
|
scroll: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!userId) return;
|
|
let cancelled = false;
|
|
quiz
|
|
.list()
|
|
.then((l) => !cancelled && setList(l))
|
|
.catch(() => !cancelled && setList([]));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [userId]);
|
|
|
|
const shown = useMemo(
|
|
() =>
|
|
(list ?? []).filter((q) =>
|
|
filter === "all"
|
|
? true
|
|
: filter === "done"
|
|
? q.my.state === "completed"
|
|
: q.my.state !== "completed",
|
|
),
|
|
[list, filter],
|
|
);
|
|
|
|
return (
|
|
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-6 sm:px-6 sm:py-10">
|
|
<header className="mb-6">
|
|
<h1 className="font-[family-name:var(--font-title)] text-2xl font-semibold text-foreground sm:text-3xl">
|
|
Quizzes
|
|
</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Test what you've read and heard. Each quiz can be taken once, and
|
|
only you can see your results.
|
|
</p>
|
|
</header>
|
|
|
|
{user && (
|
|
// Phones: three equal, thumb-sized tabs across the width. From sm up
|
|
// they shrink back to compact pills.
|
|
<div className="mb-4 grid grid-cols-3 gap-1.5 sm:flex sm:gap-1">
|
|
{(
|
|
[
|
|
["all", "All"],
|
|
["todo", "To do"],
|
|
["done", "Finished"],
|
|
] as const
|
|
).map(([k, label]) => (
|
|
<button
|
|
key={k}
|
|
type="button"
|
|
onClick={() => setFilter(k)}
|
|
className={cn(
|
|
"min-h-10 rounded-full px-4 py-2 text-[15px] font-medium transition-colors sm:min-h-0 sm:px-3 sm:py-1 sm:text-sm sm:font-normal",
|
|
filter === k
|
|
? "bg-primary text-primary-foreground dark:text-white"
|
|
: "bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground sm:bg-transparent",
|
|
)}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!user ? null : list === null ? (
|
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
) : shown.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
{list.length === 0
|
|
? "No quizzes yet — check back soon."
|
|
: "Nothing here."}
|
|
</p>
|
|
) : filter !== "all" ? (
|
|
<QuizGrid quizzes={shown} />
|
|
) : (
|
|
<QuizBrowser
|
|
quizzes={shown}
|
|
path={{
|
|
in: params.get("in"),
|
|
book: params.get("book"),
|
|
year: params.get("year"),
|
|
}}
|
|
/>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|