feat: quiz confetti
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user