quiz seo image and authguard

This commit is contained in:
gosangam
2026-10-15 21:09:37 +05:30
parent 6a6143ceb2
commit 0e2e0d7300
8 changed files with 602 additions and 447 deletions
+9 -142
View File
@@ -1,147 +1,14 @@
"use client";
import type { Metadata } from "next";
import { ogForQuizzes } from "@/lib/og";
import { QuizzesList } from "./quizzes-list";
/**
* /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.
*/
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { ClipboardCheck } from "lucide-react";
import { useAuth } from "@/components/auth/auth-provider";
import { quiz, 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 const metadata: Metadata = {
title: "Quizzes",
description:
"Test your understanding of Śrīla Prabhupāda's books, lectures and conversations.",
...ogForQuizzes(),
};
type Filter = "all" | "todo" | "done";
export default function QuizzesPage() {
const { user } = useAuth();
const [list, setList] = useState<QuizCard[] | null>(null);
const [filter, setFilter] = useState<Filter>("all");
useEffect(() => {
let cancelled = false;
quiz
.list()
.then((l) => !cancelled && setList(l))
.catch(() => !cancelled && setList([]));
return () => {
cancelled = true;
};
}, [user?.id]);
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&apos;ve read and heard. Each quiz can be taken once, and
only you can see your results.
</p>
</header>
{user && (
<div className="mb-4 flex gap-1">
{(
[
["all", "All"],
["todo", "To do"],
["done", "Finished"],
] as const
).map(([k, label]) => (
<button
key={k}
type="button"
onClick={() => setFilter(k)}
className={cn(
"rounded-full px-3 py-1 text-sm transition-colors",
filter === k
? "bg-primary text-primary-foreground dark:text-white"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{label}
</button>
))}
</div>
)}
{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>
) : (
<ul className="grid gap-3 sm:grid-cols-2">
{shown.map((q) => {
const done = q.my.state === "completed";
return (
<li key={q.id}>
<Link
href={`/quizzes/${q.id}`}
className="flex h-full 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 font-semibold leading-snug text-foreground">
{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 items-center justify-between gap-2 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 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>
</li>
);
})}
</ul>
)}
</main>
);
return <QuizzesList />;
}