import { createFileRoute } from "@tanstack/react-router" import type { ReactNode } from "react" import { Trans, useTranslation } from "react-i18next" import { AlertTriangle, CalendarDays, Clapperboard, Clock, Lightbulb, Sparkles, TrendingDown, TrendingUp, } from "lucide-react" import { BackButton } from "@/components/back-button" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { EmptyState } from "@/components/empty-state" import { Skeleton } from "@/components/ui/skeleton" import { WATCH_MEDIUMS } from "@/lib/watch-mediums" import { useInsights, type Insight } from "@/features/insights" import { useDocumentTitle } from "@/hooks/use-document-title" export const Route = createFileRoute("/_app/insights")({ component: InsightsPage, }) const MONTH_BASE = new Date(2024, 0, 1) // any year; only the month name matters const MONDAY_BASE = new Date(2024, 2, 4) // 2024-03-04 is a Monday const hl = function isKind(kind: K) { return (insight: Insight): insight is Extract => insight.kind === kind } function monthName(month: number): string { return new Intl.DateTimeFormat(undefined, { month: "long" }).format( new Date(MONTH_BASE.getFullYear(), month - 1, 1), ) } function dayNames(days: number[]): string[] { const fmt = new Intl.DateTimeFormat(undefined, { weekday: "short" }) return days.map((d) => fmt.format(new Date(MONDAY_BASE.getFullYear(), MONDAY_BASE.getMonth(), MONDAY_BASE.getDate() + d - 1))) } function mediumLabel(medium: string, t: (key: string) => string): string { const def = WATCH_MEDIUMS.find((m) => m.value === medium) return def ? t(def.labelKey) : medium } function hourLabel(hour: number): string { return `${String(hour).padStart(2, "0")}:00` } function InsightsPage() { const { t } = useTranslation() const { data, isPending, isError, refetch } = useInsights() useDocumentTitle(t("insights.title")) if (isPending) return const insights = data?.insights ?? [] return (

{t("insights.title")}

{t("insights.subtitle")}

{isError ? (
) : insights.length === 0 ? ( ) : ( <> } /> } /> } /> } /> )}
) } function InsightGroup({ title, items, render, }: { title: string items: T[] render: (insight: T) => ReactNode }) { if (items.length === 0) return null return (

{title}

{items.map((insight, i) => (
{render(insight)}
))}
) } type RatingLiftCardProps = { insight: Extract } type SeasonalityCardProps = { insight: Extract } type MediumTimingCardProps = { insight: Extract } type RhythmCardProps = { insight: Extract } function RatingLiftCard({ insight }: RatingLiftCardProps) { const { t } = useTranslation() const up = insight.delta > 0 const Trend = up ? TrendingUp : TrendingDown return (
{insight.genre_avg.toFixed(1)}★ · {insight.genre} {t("insights.yourAverage")}: {insight.user_avg.toFixed(1)}★
) } function SeasonalityCard({ insight }: SeasonalityCardProps) { return (
{insight.multiplier.toFixed(1)}× · {monthName(insight.month)} {insight.genre}
) } function MediumTimingCard({ insight }: MediumTimingCardProps) { const { t } = useTranslation() return (
{Math.round(insight.weekend_share * 100)}% · {t("insights.weekends")} {t("insights.yourBaseline")}: {Math.round(insight.baseline_share * 100)}%
) } function RhythmCard({ insight }: RhythmCardProps) { return (
{hourLabel(insight.peak_hour_start)}–{hourLabel(insight.peak_hour_end)} {dayNames(insight.top_days).join(", ")}
) } // Rating lift counts distinct films (first watches only); every other // category counts diary entries, rewatches included. function SampleBadge({ unit, count }: { unit: "films" | "watches"; count: number }) { const { t } = useTranslation() const key = unit === "films" ? "insights.filmsSample" : "insights.watchesSample" return {t(key, { count })} } function InsightsSkeleton() { return (
{[0, 1, 2].map((i) => ( ))}
) }