import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getStats, deleteTasteTag } from "../api"; import { Link } from "react-router-dom"; function fmt(seconds) { if (!seconds) return "0m"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h === 0) return `${m}m`; if (m === 0) return `${h}h`; return `${h}h ${m}m`; } function fmtBytes(bytes) { if (!bytes) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0, v = bytes; while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function StatCard({ label, value, sub }) { return (

{label}

{value}

{sub &&

{sub}

}
); } export default function Stats() { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["stats"], queryFn: () => getStats().then(r => r.data), staleTime: 5 * 60_000, }); const deleteTag = useMutation({ mutationFn: (tag) => deleteTasteTag(tag), onSuccess: () => qc.invalidateQueries({ queryKey: ["stats"] }), }); if (isLoading) { return (
); } if (!data) return null; const maxSeconds = Math.max(...(data.top_channels.map(c => c.watch_seconds || 0)), 1); const today = new Date(); const days = Array.from({ length: 30 }, (_, i) => { const d = new Date(today); d.setDate(today.getDate() - (29 - i)); return d.toISOString().slice(0, 10); }); const dailyMap = Object.fromEntries((data.daily || []).map(d => [d.date, d])); const maxDayCount = Math.max(...days.map(d => dailyMap[d]?.count || 0), 1); const maxCatCount = Math.max(...(data.top_categories || []).map(c => c.watch_count || 0), 1); const topTags = (data.taste_profile || []).slice(0, 12); const maxTagScore = Math.max(...topTags.map(t => t.score || 0), 1); return (

Stats

{/* Top numbers */}
{/* Engagement row */}
{/* Activity chart */}

Activity — last 30 days

{days.map(date => { const entry = dailyMap[date]; const count = entry?.count || 0; const pct = count / maxDayCount; return (
); })}
30 days agoToday
{/* Top channels */} {data.top_channels.length > 0 && (

Top channels by watch time

{data.top_channels.map(ch => { const pct = (ch.watch_seconds || 0) / maxSeconds; return (
{ch.name} {ch.watch_count} · {fmt(ch.watch_seconds)}
); })}
)} {/* Top categories */} {data.top_categories?.length > 0 && (

Top categories

{data.top_categories.map(cat => { const pct = cat.watch_count / maxCatCount; const comp = cat.avg_completion ? Math.round(cat.avg_completion) : null; return (
{cat.category} {cat.watch_count}{comp !== null ? ` · ${comp}% avg` : ""}
); })}
)}
{/* Disk usage */} {data.disk?.total_bytes && (

Disk usage

Downloads {fmtBytes(data.disk.download_bytes)}
{fmtBytes(data.disk.used_bytes)} used {fmtBytes(data.disk.free_bytes)} free · {fmtBytes(data.disk.total_bytes)} total
)} {/* Taste profile */} {topTags.length > 0 && (

Taste profile

built from your watches, likes and bookmarks

{topTags.map(t => { const intensity = t.score / maxTagScore; return ( {t.tag} ); })}
)}
); }