import { createMemo, For } from "solid-js"; import { getDb } from "../db/client"; import { reviewTimesSince, scheduledDueTimes, totalReviewCount } from "../db/reviews"; import { toIso, toLocalDay } from "../lib/time"; import { card } from "../lib/ui"; const HISTORY_DAYS = 30; const FORECAST_DAYS = 14; async function fetchStats() { const db = await getDb(); const since = Temporal.Now.instant().subtract({ hours: 24 * 366 }); const [reviews, total, dues] = [ await reviewTimesSince(db, toIso(since)), await totalReviewCount(db), await scheduledDueTimes(db), ]; return { reviews, total, dues }; } /** Consecutive days with at least one review, ending today (or yesterday). */ function currentStreak(reviewDays: Set): number { let day = Temporal.Now.plainDateISO(); if (!reviewDays.has(day.toString())) day = day.subtract({ days: 1 }); let streak = 0; while (reviewDays.has(day.toString())) { streak += 1; day = day.subtract({ days: 1 }); } return streak; } interface DayBucket { date: Temporal.PlainDate; count: number; } function BarChart(props: { days: DayBucket[]; class?: string }) { const max = () => Math.max(...props.days.map((d) => d.count), 1); return (
{(day) => (
)}
); } export default function StatsPage() { const stats = createMemo(() => fetchStats()); const reviewsByDay = createMemo(() => { const byDay = new Map(); for (const iso of stats().reviews) { const key = toLocalDay(iso).toString(); byDay.set(key, (byDay.get(key) ?? 0) + 1); } return byDay; }); const history = createMemo(() => { const today = Temporal.Now.plainDateISO(); return Array.from({ length: HISTORY_DAYS }, (_, offset) => { const date = today.subtract({ days: HISTORY_DAYS - 1 - offset }); return { date, count: reviewsByDay().get(date.toString()) ?? 0 }; }); }); const forecast = createMemo(() => { const today = Temporal.Now.plainDateISO(); const buckets = Array.from({ length: FORECAST_DAYS }, (_, offset) => ({ date: today.add({ days: offset }), count: 0, })); for (const iso of stats().dues) { let day = toLocalDay(iso); // Overdue cards count as due today. if (Temporal.PlainDate.compare(day, today) < 0) day = today; const offset = today.until(day).days; if (offset < FORECAST_DAYS) buckets[offset]!.count += 1; } return buckets; }); const reviewedToday = () => reviewsByDay().get(Temporal.Now.plainDateISO().toString()) ?? 0; return (

Reviews — last {HISTORY_DAYS} days

Due forecast — next {FORECAST_DAYS} days

); } function StatTile(props: { label: string; value: number }) { return (

{props.value}

{props.label}

); }