From 0968b6046d86a8799325fe52ae74da3fe4e0ae76 Mon Sep 17 00:00:00 2001 From: gosangam Date: Fri, 16 Oct 2026 15:27:46 +0530 Subject: [PATCH] feat: quiz confetti --- app/(app)/quizzes/[id]/quiz-player.tsx | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/app/(app)/quizzes/[id]/quiz-player.tsx b/app/(app)/quizzes/[id]/quiz-player.tsx index 1f8b2983..2cd7d8c2 100644 --- a/app/(app)/quizzes/[id]/quiz-player.tsx +++ b/app/(app)/quizzes/[id]/quiz-player.tsx @@ -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({ kind: "loading" }); const [selected, setSelected] = useState([]); 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" && } + + {/* 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 && ( +
+ +
+ )} ); }