import { useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { motion } from 'framer-motion' import { Clock, Flame, Film, Tv, Award, Activity, CalendarStar } from '../lib/icons' import { useLibraryItems } from '../hooks/use-jellyfin' import { useDiary } from '../stores/diary-store' import { genreBreakdown, totalHoursWatched, longestBinge, watchStreak } from '../lib/watch-stats' import { hoursPerGenre, hoursPerStudio, completion, topByPersonRole, totalTimeSavedSeconds, } from '../lib/stats' import { formatTimeSaved } from '../lib/time-saved' const CURRENT_YEAR = new Date().getFullYear() /** * Detailed stats / year-in-watching. Profile page surfaces the at-a-glance * summary; this one digs deeper into hours-weighted breakdowns, top * directors / actors, completion ratios, and the time-saved-by-skipping * total across every series. */ export default function StatsPage() { const navigate = useNavigate() const { data, isLoading } = useLibraryItems(undefined, { includeItemTypes: ['Movie', 'Series', 'Episode'], sortBy: ['DatePlayed'], sortOrder: ['Descending'], filters: ['IsPlayed'], limit: 2000, includePeople: true, }) const items = useMemo(() => data?.Items || [], [data?.Items]) const yearItems = useMemo( () => items.filter(it => { const raw = it.UserData?.LastPlayedDate if (!raw) return false return new Date(raw).getFullYear() === CURRENT_YEAR }), [items], ) const totalHoursAll = useMemo(() => totalHoursWatched(items), [items]) const totalHoursYear = useMemo(() => totalHoursWatched(yearItems), [yearItems]) const streak = useMemo(() => watchStreak(items), [items]) const binge = useMemo(() => longestBinge(items), [items]) const genreShare = useMemo(() => genreBreakdown(items).slice(0, 10), [items]) const hoursGenre = useMemo(() => hoursPerGenre(items).slice(0, 10), [items]) const hoursStudio = useMemo(() => hoursPerStudio(items).slice(0, 8), [items]) const comp = useMemo(() => completion(items), [items]) const topDirectors = useMemo( () => topByPersonRole(items, (_role, type) => type === 'Director', 8), [items], ) const topActors = useMemo( () => topByPersonRole(items, (_role, type) => type === 'Actor', 10), [items], ) const timeSaved = useMemo(() => totalTimeSavedSeconds(), []) const diary = useDiary(s => s.entries) const diaryYearCount = useMemo( () => diary.filter(d => { const t = Date.parse(d.watchedAt) return Number.isFinite(t) && new Date(t).getFullYear() === CURRENT_YEAR }).length, [diary], ) const moviesCount = useMemo(() => items.filter(i => i.Type === 'Movie').length, [items]) const episodesCount = useMemo(() => items.filter(i => i.Type === 'Episode').length, [items]) return (

Stats

Your year in watching

A deeper look at everything you've played - {items.length.toLocaleString()} {items.length === 1 ? 'item' : 'items'} tracked.

{isLoading && items.length === 0 && (

Crunching the numbers...

)} {/* Headline tiles */}
} hint={`${totalHoursAll.toFixed(0)} all-time`} accent /> } /> } /> } hint={timeSaved.series > 0 ? `across ${timeSaved.series} ${timeSaved.series === 1 ? 'show' : 'shows'}` : 'no skips yet'} /> } hint={streak.longest > streak.current ? `Best: ${streak.longest}` : 'Keep going'} /> } />
{/* Two-column: hours per genre + hours per studio */}
{hoursGenre.map((g, i) => ( ))}
{hoursStudio.map((s, i) => ( ))}
{/* Completion ratios */}

{(comp.completionRate * 100).toFixed(1)}% of tracked items reached the finish line.

{/* People */}
navigate(`/search?q=${encodeURIComponent(n)}`)} />
navigate(`/search?q=${encodeURIComponent(n)}`)} />
{/* Side note: longest binge + breakdown */}
{binge.count > 0 ? (

{binge.count} items in one day

On {binge.day ? new Date(binge.day + 'T00:00:00').toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) : 'unknown'}.

) : (

No watched items yet.

)}
{genreShare.map((g, i) => ( ))}
{/* Time saved breakdown */}
{timeSaved.total > 0 ? (
} />
) : (

No auto-skips recorded yet. Turn intro / credits skipping on in Playback settings.

)}
) } function StatTile({ label, value, unit, hint, icon, accent, }: { label: string value: string unit?: string hint?: string icon?: React.ReactNode accent?: boolean }) { return (
{icon} {label}
{value} {unit && {unit}}
{hint &&

{hint}

}
) } function Section({ title, children, empty, className, }: { title: string children: React.ReactNode empty?: string | null className?: string }) { return (

{title}

{empty ?

{empty}

: children}
) } function Bar({ label, share, index, suffix, }: { label: string share: number index: number suffix?: string }) { const pct = Math.max(2, Math.round(share * 100)) return ( {label}
{suffix ?? `${pct}%`}
) } function CompletionTile({ label, value, total, accent, }: { label: string value: number total: number accent?: boolean }) { const pct = total > 0 ? Math.round((value / total) * 100) : 0 return (

{label}

{value.toLocaleString()}

{pct}%

) } function PersonList({ rows, onClick, }: { rows: { name: string; count: number }[] onClick: (name: string) => void }) { return (
    {rows.map((p, i) => (
  1. ))}
) } function BadgeTile({ label, value, icon, }: { label: string value: string icon?: React.ReactNode }) { return (

{icon} {label}

{value}

) }