feat(stats): build anime-centric stats dashboard frontend

5-tab React dashboard with Catppuccin Mocha theme:
- Overview: hero stats, streak calendar, watch time chart, recent sessions
- Anime: grid with cover art, episode list with completion %, detail view
- Trends: 15 charts across Activity, Efficiency, Anime, and Patterns
- Vocabulary: POS-filtered word/kanji lists with detail panels
- Sessions: expandable session history with event timeline

Features:
- Cross-tab navigation (anime <-> vocabulary)
- Global word detail panel overlay
- Expandable episode detail with Anki card links (Expression field)
- Per-anime multi-line trend charts
- Watch time by day-of-week and hour-of-day
- Collapsible sections with accessibility (aria-expanded)
- Card size selector for anime grid
- Cover art caching via AniList
- HTTP API client with file:// protocol fallback for Electron overlay
This commit is contained in:
2026-03-14 22:15:02 -07:00
parent 950263bd66
commit 0f44107beb
68 changed files with 5372 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import { todayLocalDay } from '../../lib/formatters';
import type { DailyRollup } from '../../types/stats';
interface QuickStatsProps {
rollups: DailyRollup[];
}
export function QuickStats({ rollups }: QuickStatsProps) {
const daysWithActivity = new Set(
rollups.filter((r) => r.totalActiveMin > 0).map((r) => r.rollupDayOrMonth),
);
const today = todayLocalDay();
const streakStart = daysWithActivity.has(today) ? today : today - 1;
let streak = 0;
for (let d = streakStart; daysWithActivity.has(d); d--) {
streak++;
}
const weekStart = today - 6;
const weekRollups = rollups.filter((r) => r.rollupDayOrMonth >= weekStart);
const weekMinutes = weekRollups.reduce((sum, r) => sum + r.totalActiveMin, 0);
const weekCards = weekRollups.reduce((sum, r) => sum + r.totalCards, 0);
const avgMinPerDay = Math.round(weekMinutes / 7);
return (
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
<h3 className="text-sm font-semibold text-ctp-text mb-3">Quick Stats</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-ctp-subtext0">Streak</span>
<span className="text-ctp-peach font-medium">
{streak} day{streak !== 1 ? 's' : ''}
</span>
</div>
<div className="flex justify-between">
<span className="text-ctp-subtext0">Avg/day this week</span>
<span className="text-ctp-text">{avgMinPerDay}m</span>
</div>
<div className="flex justify-between">
<span className="text-ctp-subtext0">Cards this week</span>
<span className="text-ctp-green font-medium">{weekCards}</span>
</div>
</div>
</div>
);
}