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,40 @@
import { formatDuration, formatRelativeDate, formatNumber } from '../../lib/formatters';
import type { SessionSummary } from '../../types/stats';
interface MediaSessionListProps {
sessions: SessionSummary[];
}
export function MediaSessionList({ sessions }: MediaSessionListProps) {
if (sessions.length === 0) {
return <div className="text-sm text-ctp-overlay2">No sessions recorded</div>;
}
return (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-ctp-text">Session History</h3>
{sessions.map((s) => (
<div
key={s.sessionId}
className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-3 flex items-center justify-between"
>
<div className="min-w-0">
<div className="text-sm text-ctp-text">
{formatRelativeDate(s.startedAtMs)} · {formatDuration(s.activeWatchedMs)} active
</div>
</div>
<div className="flex gap-4 text-xs text-center shrink-0">
<div>
<div className="text-ctp-green font-medium">{formatNumber(s.cardsMined)}</div>
<div className="text-ctp-overlay2">cards</div>
</div>
<div>
<div className="text-ctp-mauve font-medium">{formatNumber(s.wordsSeen)}</div>
<div className="text-ctp-overlay2">words</div>
</div>
</div>
</div>
))}
</div>
);
}