"use client"; import Link from "next/link"; import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { Button, ProgressBar } from "@/components/ui"; import { ensureLearningHydrated, learningStore } from "@/lib/history"; import { useI18n } from "@/lib/i18n"; import { computeStats, dayKey } from "@/lib/learning"; import { settingsStore } from "@/lib/settings"; import { cn } from "@/lib/utils"; /** * Progress: day streak, daily-goal progress, a five-week calendar, accuracy * per subject and per deck, and the weakest topics (at least three answers). */ export function StatsScreen() { const { t, plural, locale } = useI18n(); const answers = useSyncExternalStore( learningStore.subscribe, learningStore.getAnswersSnapshot, learningStore.getServerAnswers, ); const dailyGoal = useSyncExternalStore( settingsStore.subscribe, () => settingsStore.getSnapshot().dailyGoal, () => 20, ); const [ready, setReady] = useState(false); useEffect(() => { void ensureLearningHydrated().then(() => setReady(true)); }, []); const stats = useMemo(() => computeStats(answers), [answers]); if (!ready) { return (
); } if (stats.total === 0) { return (

📈

{t("stats.empty")}

{t("stats.emptyBody")}

); } // Calendar: five weeks, columns aligned to weekdays (Monday first). const today = new Date(); const weekdayIndex = (today.getDay() + 6) % 7; const start = new Date(today); start.setDate(today.getDate() - weekdayIndex - 28); const byDay = new Map(stats.days.map((entry) => [entry.day, entry])); const cells = Array.from({ length: 35 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); const key = dayKey(date.getTime()); return { key, date, future: date.getTime() > today.getTime() + 86_400_000, entry: byDay.get(key) }; }); const weekdayLabels = Array.from({ length: 7 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return new Intl.DateTimeFormat(locale, { weekday: "narrow" }).format(date); }); const intensity = (count: number) => count === 0 ? "bg-[var(--surface-sunken)]" : count <= 2 ? "bg-sky-200/80 dark:bg-sky-400/20" : count <= 5 ? "bg-sky-400/70 dark:bg-sky-400/40" : "grad-ice"; return (

{t("stats.title")}

{t("stats.subtitle")}

{t("stats.streak")}

{stats.streak}

{plural(stats.total, "stats.answers")}

{t("stats.today")}

{t("stats.todayOf", { n: stats.todayCount, goal: dailyGoal })}

{stats.todayCount >= dailyGoal ? (

{t("stats.goalDone")}

) : null}

{t("stats.accuracy")}

{Math.round(stats.accuracy * 100)}%

{t("stats.correctOf", { correct: stats.correct, total: stats.total })}

{t("stats.calendar")}

{t("stats.legendLess")} {t("stats.legendMore")}

{weekdayLabels.map((label, index) => ( {label} ))} {cells.map((cell) => ( ))}
{stats.subjects.length > 0 ? (

{t("stats.bySubject")}

{stats.subjects.map((entry) => (
{entry.label} {Math.round((entry.correct / Math.max(1, entry.answered)) * 100)}% ·{" "} {t("stats.correctOf", { correct: entry.correct, total: entry.answered })}
))}
) : null}

{t("stats.byDeck")}

{stats.decks.map((entry) => (
{entry.label} {Math.round((entry.correct / Math.max(1, entry.answered)) * 100)}% ·{" "} {t("stats.correctOf", { correct: entry.correct, total: entry.answered })}
))}
{stats.weakestTopics.length > 0 ? (

{t("stats.weakest")}

{stats.weakestTopics.map((entry) => (
{entry.label} {Math.round((entry.correct / Math.max(1, entry.answered)) * 100)}%
))}
) : null}
{t("review.title")} {t("exam.title")}
); }