"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")}
);
}