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