feat: quiz confetti

This commit is contained in:
gosangam
2026-10-16 15:27:46 +05:30
parent 8dd72de359
commit 0968b6046d
+30 -2
View File
@@ -15,6 +15,9 @@
*
* Signed in only: visitors get the sign-in modal over the page, and home if
* they dismiss it. Nothing about the quiz loads until they're signed in.
*
* Finishing with every answer right sets off confetti — once, on finishing,
* not on revisiting the results later.
*/
import { useCallback, useEffect, useState } from "react";
@@ -40,8 +43,13 @@ import {
} from "@/lib/quiz";
import { QuizResultView } from "@/components/quiz/quiz-result-view";
import { CopyPageLinkButton } from "@/components/layout/copy-page-link-button";
import { Confetti } from "@/components/sadhna/confetti";
import { cn } from "@/lib/utils";
// The confetti's longest piece falls for 4.2s after a 0.5s delay; the layer
// is removed just after, so nothing lingers over the page once it's done.
const CONFETTI_MS = 5000;
type Stage =
| { kind: "loading" }
| { kind: "intro"; card: QuizCard }
@@ -56,6 +64,13 @@ export function QuizPlayer({ id }: { id: string }) {
const [stage, setStage] = useState<Stage>({ kind: "loading" });
const [selected, setSelected] = useState<string[]>([]);
const [submitting, setSubmitting] = useState(false);
const [celebrate, setCelebrate] = useState(false);
useEffect(() => {
if (!celebrate) return;
const t = setTimeout(() => setCelebrate(false), CONFETTI_MS);
return () => clearTimeout(t);
}, [celebrate]);
// Every new question starts with nothing selected.
const goTo = (card: QuizCard, play: QuizPlay) => {
@@ -78,6 +93,7 @@ export function QuizPlayer({ id }: { id: string }) {
async (card: QuizCard) => {
const result = await quizApi.result(id);
setStage({ kind: "result", card, result });
return result;
},
[id],
);
@@ -117,8 +133,12 @@ export function QuizPlayer({ id }: { id: string }) {
setSubmitting(true);
try {
const next = await quizApi.answer(id, play.question.id, selected);
if (next.done) await showResult(card);
else goTo(card, next);
if (next.done) {
const result = await showResult(card);
if (result.total > 0 && result.score === result.total) {
setCelebrate(true);
}
} else goTo(card, next);
} catch (err) {
// Most likely the question was already answered in another tab: pick up
// wherever the attempt actually is rather than leaving this one stuck.
@@ -309,6 +329,14 @@ export function QuizPlayer({ id }: { id: string }) {
)}
{stage.kind === "result" && <QuizResultView result={stage.result} />}
{/* Full marks. Fixed over the viewport but pointer-events-none, so the
page stays usable while it falls, and unmounted once it's done. */}
{celebrate && (
<div className="pointer-events-none fixed inset-0 z-50" aria-hidden>
<Confetti />
</div>
)}
</main>
);
}