import { useState } from 'react'; import { useTrends, type TimeRange, type GroupBy } from '../../hooks/useTrends'; import { DateRangeSelector } from './DateRangeSelector'; import { TrendChart } from './TrendChart'; import { StackedTrendChart, type PerAnimeDataPoint } from './StackedTrendChart'; import { buildTrendDashboard, type ChartPoint } from '../../lib/dashboard-data'; import { localDayFromMs } from '../../lib/formatters'; import type { SessionSummary } from '../../types/stats'; const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function buildWatchTimeByDayOfWeek(sessions: SessionSummary[]): ChartPoint[] { const totals = new Array(7).fill(0); for (const s of sessions) { const dow = new Date(s.startedAtMs).getDay(); totals[dow] += s.activeWatchedMs; } return DAY_NAMES.map((name, i) => ({ label: name, value: Math.round(totals[i] / 60_000) })); } function buildWatchTimeByHour(sessions: SessionSummary[]): ChartPoint[] { const totals = new Array(24).fill(0); for (const s of sessions) { const hour = new Date(s.startedAtMs).getHours(); totals[hour] += s.activeWatchedMs; } return totals.map((ms, i) => ({ label: `${String(i).padStart(2, '0')}:00`, value: Math.round(ms / 60_000), })); } function buildCumulativePerAnime(points: PerAnimeDataPoint[]): PerAnimeDataPoint[] { const byAnime = new Map>(); const allDays = new Set(); for (const p of points) { const dayMap = byAnime.get(p.animeTitle) ?? new Map(); dayMap.set(p.epochDay, (dayMap.get(p.epochDay) ?? 0) + p.value); byAnime.set(p.animeTitle, dayMap); allDays.add(p.epochDay); } const sortedDays = [...allDays].sort((a, b) => a - b); if (sortedDays.length < 2) return points; const minDay = sortedDays[0]!; const maxDay = sortedDays[sortedDays.length - 1]!; const everyDay: number[] = []; for (let d = minDay; d <= maxDay; d++) { everyDay.push(d); } const result: PerAnimeDataPoint[] = []; for (const [animeTitle, dayMap] of byAnime) { let cumulative = 0; const firstDay = Math.min(...dayMap.keys()); for (const day of everyDay) { if (day < firstDay) continue; cumulative += dayMap.get(day) ?? 0; result.push({ epochDay: day, animeTitle, value: cumulative }); } } return result; } function buildPerAnimeFromSessions( sessions: SessionSummary[], getValue: (s: SessionSummary) => number, ): PerAnimeDataPoint[] { const map = new Map>(); for (const s of sessions) { const title = s.animeTitle ?? s.canonicalTitle ?? 'Unknown'; const day = localDayFromMs(s.startedAtMs); const animeMap = map.get(title) ?? new Map(); animeMap.set(day, (animeMap.get(day) ?? 0) + getValue(s)); map.set(title, animeMap); } const points: PerAnimeDataPoint[] = []; for (const [animeTitle, dayMap] of map) { for (const [epochDay, value] of dayMap) { points.push({ epochDay, animeTitle, value }); } } return points; } function buildEpisodesPerAnimeFromSessions(sessions: SessionSummary[]): PerAnimeDataPoint[] { // Group by anime+day, counting distinct videoIds const map = new Map>>(); for (const s of sessions) { const title = s.animeTitle ?? s.canonicalTitle ?? 'Unknown'; const day = localDayFromMs(s.startedAtMs); const animeMap = map.get(title) ?? new Map(); const videoSet = animeMap.get(day) ?? new Set(); videoSet.add(s.videoId); animeMap.set(day, videoSet); map.set(title, animeMap); } const points: PerAnimeDataPoint[] = []; for (const [animeTitle, dayMap] of map) { for (const [epochDay, videoSet] of dayMap) { points.push({ epochDay, animeTitle, value: videoSet.size }); } } return points; } function SectionHeader({ children }: { children: React.ReactNode }) { return (

{children}

); } export function TrendsTab() { const [range, setRange] = useState('30d'); const [groupBy, setGroupBy] = useState('day'); const { data, loading, error } = useTrends(range, groupBy); if (loading) return
Loading...
; if (error) return
Error: {error}
; const dashboard = buildTrendDashboard(data.rollups); const watchByDow = buildWatchTimeByDayOfWeek(data.sessions); const watchByHour = buildWatchTimeByHour(data.sessions); const watchTimePerAnime = data.watchTimePerAnime.map((e) => ({ epochDay: e.epochDay, animeTitle: e.animeTitle, value: e.totalActiveMin, })); const episodesPerAnime = buildEpisodesPerAnimeFromSessions(data.sessions); const cardsPerAnime = buildPerAnimeFromSessions(data.sessions, (s) => s.cardsMined); const wordsPerAnime = buildPerAnimeFromSessions(data.sessions, (s) => s.wordsSeen); const animeProgress = buildCumulativePerAnime(episodesPerAnime); const cardsProgress = buildCumulativePerAnime(cardsPerAnime); const wordsProgress = buildCumulativePerAnime(wordsPerAnime); return (
Activity Anime — Per Day Anime — Cumulative Patterns
); }