"use client"; import { useEffect, useId, useState, type CSSProperties } from "react"; import "./lesson-summary.css"; export type LessonSummaryProps = { xp: number; /** 0–100 */ accuracy: number; seconds: number; /** Seven days ending today: true for days the goal was met. */ week: boolean[]; /** Labels for the seven days ending today; defaults to narrow weekday names in the viewer's locale. */ dayLabels?: string[]; locale?: string; streak: number; onContinue?: () => void; onReview?: () => void; theme?: "light" | "dark"; motion?: "auto" | "reduced"; className?: string; }; const clock = (s: number) => `${Math.floor(s / 60)}:${String(Math.round(s % 60)).padStart(2, "0")}`; const grade = (a: number) => (a >= 100 ? "Flawless" : a >= 90 ? "Amazing" : a >= 75 ? "Good" : "Accuracy"); /** Counts from 0 to `to` once it is visible, easing out; jumps straight there under reduced motion. */ function useCount(to: number, delay: number, still: boolean) { const [value, setValue] = useState(still ? to : 0); useEffect(() => { if (still || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setValue(to); return; } let frame = 0; const start = performance.now() + delay; const tick = (now: number) => { const t = Math.min(1, Math.max(0, (now - start) / 900)); setValue(Math.round(to * (1 - Math.pow(1 - t, 3)))); if (t < 1) frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }, [to, delay, still]); return value; } /** * Lesson Summary * The results screen at the end of a lesson: three chunky tiles count up the * XP earned, the accuracy and the time, then the week's streak fills in today. */ export function LessonSummary({ xp, accuracy, seconds, week, dayLabels, locale, streak, onContinue, onReview, theme = "light", motion = "auto", className = "" }: LessonSummaryProps) { const still = motion === "reduced"; const xpNow = useCount(xp, 200, still); const accNow = useCount(Math.round(accuracy), 350, still); const secNow = useCount(seconds, 500, still); const titleId = useId(); const days = week.slice(-7); // Weekday names depend on the viewer's clock, so they are filled in after mount. const [labels, setLabels] = useState(dayLabels ?? []); useEffect(() => { if (dayLabels) { setLabels(dayLabels); return; } const fmt = new Intl.DateTimeFormat(locale, { weekday: "narrow" }); const today = new Date(); setLabels(Array.from({ length: 7 }, (_, i) => fmt.format(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6 + i)))); }, [dayLabels, locale]); return (

Lesson complete!

{`${xp} XP earned, ${Math.round(accuracy)}% accuracy, ${clock(seconds)} taken. ${streak} day streak.`}

{streak} day streak

    {days.map((met, i) => (
  1. {met ? "goal met" : "missed"}{i === days.length - 1 ? ", today" : ""}
  2. ))}
{onReview && }
); }