"use client"; import { Lightbulb, TrendingDown, TrendingUp } from "lucide-react"; import { motion, useReducedMotion, type Variants } from "motion/react"; import { type ReactNode, useId, useMemo, useState } from "react"; import type { DateRange } from "@/components/motion/calendar"; import { AreaChart } from "@/components/motion/chart/area-chart"; import { seriesColor } from "@/components/motion/chart/shared"; import { DateRangePicker, type DateRangePreset } from "@/components/motion/date-range-picker"; import { Skeleton, SkeletonSwap } from "@/components/motion/skeleton"; import { StatCard, type StatFormat } from "@/components/motion/stat-card"; import { Switch } from "@/components/motion/switch"; import { EASE_OUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface ReportKpi { id: string; label: string; value: number; /** Same metric for the comparison period; drives the delta badge. */ previous?: number; format?: StatFormat; /** Which way is good news. Default "up". */ goodDirection?: "up" | "down"; icon?: ReactNode; } export interface ReportTrendPoint { /** ISO date (YYYY-MM-DD), read as UTC. */ date: string; current: number; previous?: number; } export interface ReportBreakdownRow { id: string; label: string; value: number; previous?: number; } export interface PerformanceReportData { kpis: ReportKpi[]; trend: ReportTrendPoint[]; /** Name of the trended metric, e.g. "Sessions". */ trendLabel?: string; breakdown: ReportBreakdownRow[]; /** Name of the breakdown dimension, e.g. "Channel". */ breakdownLabel?: string; } export type InsightTone = "positive" | "negative" | "neutral"; export interface ReportInsight { id: string; tone: InsightTone; text: ReactNode; } export interface PerformanceReportProps { /** Report contents for the current range. `null` renders the loading state. */ data: PerformanceReportData | null; /** Show skeletons (e.g. while a new range loads). */ loading?: boolean; range?: DateRange | null; defaultRange?: DateRange | null; onRangeChange?: (range: { from: Date; to: Date }) => void; presets?: DateRangePreset[]; /** Controlled previous-period comparison. */ compare?: boolean; defaultCompare?: boolean; onCompareChange?: (compare: boolean) => void; /** Override the auto-generated highlights. */ insights?: ReportInsight[]; title?: ReactNode; /** Default "en-US". */ locale?: string; /** Trend chart plot height in px. Default 110. */ chartHeight?: number; className?: string; } const UTC_DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }); const formatDay = (iso: string) => { const ms = Date.parse(`${iso}T00:00:00Z`); return Number.isNaN(ms) ? iso : UTC_DAY.format(ms); }; const TONE_TEXT: Record = { positive: "text-[color-mix(in_oklab,var(--success)_60%,var(--foreground))]", negative: "text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]", neutral: "text-muted-foreground", }; const TONE_BG: Record = { positive: "bg-[color-mix(in_oklab,var(--success)_14%,transparent)]", negative: "bg-[color-mix(in_oklab,var(--destructive)_12%,transparent)]", neutral: "bg-foreground/5", }; const change = (value: number, previous?: number) => previous === undefined || previous === 0 ? undefined : (value - previous) / Math.abs(previous); /** * Plain-language highlights derived from the report: the biggest KPI move vs * the previous period, the top and fastest-growing breakdown rows, and the * peak day of the trend. Exported so it can feed other surfaces. */ export function generateInsights( data: PerformanceReportData, { compare = true, locale = "en-US", max = 4 }: { compare?: boolean; locale?: string; max?: number } = {}, ): ReportInsight[] { const pct = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }); const num = new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }); const out: ReportInsight[] = []; if (compare) { const moves = data.kpis .map((k) => ({ k, d: change(k.value, k.previous) })) .filter((m): m is { k: ReportKpi; d: number } => m.d !== undefined && m.d !== 0) .sort((a, b) => Math.abs(b.d) - Math.abs(a.d)); const best = moves.find((m) => (m.d > 0) === ((m.k.goodDirection ?? "up") === "up")); const worst = moves.find((m) => (m.d > 0) !== ((m.k.goodDirection ?? "up") === "up")); if (best) out.push({ id: `kpi-${best.k.id}`, tone: "positive", text: `${best.k.label} ${best.d > 0 ? "up" : "down"} ${pct.format(Math.abs(best.d))} vs the previous period.`, }); if (worst) out.push({ id: `kpi-${worst.k.id}`, tone: "negative", text: `${worst.k.label} ${worst.d > 0 ? "rose" : "slipped"} ${pct.format(Math.abs(worst.d))} — worth a look.`, }); } const total = data.breakdown.reduce((s, r) => s + r.value, 0); const top = [...data.breakdown].sort((a, b) => b.value - a.value)[0]; if (top && total > 0) out.push({ id: `top-${top.id}`, tone: "neutral", text: `${top.label} drove ${pct.format(top.value / total)} of ${(data.trendLabel ?? "volume").toLowerCase()}.`, }); if (compare) { const fastest = data.breakdown .map((r) => ({ r, d: change(r.value, r.previous) })) .filter((m): m is { r: ReportBreakdownRow; d: number } => m.d !== undefined) .sort((a, b) => b.d - a.d)[0]; if (fastest && fastest.d > 0 && fastest.r.id !== top?.id) out.push({ id: `fast-${fastest.r.id}`, tone: "positive", text: `${fastest.r.label} grew fastest, +${pct.format(fastest.d)}.`, }); } const peak = data.trend.reduce((p, c) => (!p || c.current > p.current ? c : p), null); if (peak) out.push({ id: `peak-${peak.date}`, tone: "neutral", text: `Peak day was ${formatDay(peak.date)} with ${num.format(peak.current)} ${(data.trendLabel ?? "").toLowerCase()}.`, }); return out.slice(0, max); } /** * Dashboard report: a range picker and previous-period toggle head a row of * rolling KPI cards, a trend chart (current vs previous as a second, muted * series), a breakdown table with inline bars, and auto-generated highlights * that stagger in. While a range loads every section holds its footprint as * a skeleton, then cross-fades back. */ export function PerformanceReport({ data, loading = false, range, defaultRange, onRangeChange, presets, compare: compareProp, defaultCompare = true, onCompareChange, insights: insightsProp, title = "Performance", locale = "en-US", chartHeight = 110, className, }: PerformanceReportProps) { const reduce = useReducedMotion() ?? false; const baseId = useId(); const [compareState, setCompareState] = useState(defaultCompare); const compare = compareProp ?? compareState; const setCompare = (next: boolean) => { if (compareProp === undefined) setCompareState(next); onCompareChange?.(next); }; const busy = loading || data === null; const compact = useMemo( () => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }), [locale], ); const full = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }), [locale]); const pct = useMemo( () => new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1, signDisplay: "exceptZero" }), [locale], ); const insights = useMemo( () => insightsProp ?? (data ? generateInsights(data, { compare, locale }) : []), [insightsProp, data, compare, locale], ); const trendLabel = data?.trendLabel ?? "Value"; const series = [ { key: "current", label: "This period" }, ...(compare ? [{ key: "previous", label: "Previous", color: "var(--muted-foreground)" }] : []), ]; const chartData = (data?.trend ?? []).map((p) => ({ date: p.date, current: p.current, previous: p.previous ?? 0, })); const maxRow = Math.max(1, ...(data?.breakdown ?? []).map((r) => r.value)); const dataKey = data ? `${data.trend[0]?.date ?? ""}-${data.trend.length}` : "none"; const listVariants: Variants = { hidden: {}, shown: { transition: { staggerChildren: reduce ? 0 : 0.07, delayChildren: 0.15 } }, }; const itemVariants: Variants = { hidden: { opacity: 0, y: reduce ? 0 : 6 }, shown: { opacity: 1, y: 0, transition: { duration: 0.28, ease: EASE_OUT } }, }; return (

{title}

{(data?.kpis ?? PLACEHOLDER_KPIS).map((k) => ( ))}

{trendLabel} over time

    {series.map((s, i) => (
  • {s.label}
  • ))}
} > full.format(v)} title={`${trendLabel} per day${compare ? ", with previous period" : ""}`} />
{[0, 1, 2, 3, 4].map((i) => ( ))}
} > {compare ? ( ) : null} {(data?.breakdown ?? []).map((r, i) => { const d = change(r.value, r.previous); return ( {compare ? ( ) : null} ); })}
{trendLabel} by {(data?.breakdownLabel ?? "segment").toLowerCase()}
{data?.breakdownLabel ?? "Segment"} {trendLabel} Share Δ
{r.label} {compact.format(r.value)} {/* Fill grows from the left edge: scaleX on an HTML element. */} = 0 ? "positive" : "negative"], )} > {d === undefined ? "—" : pct.format(d)}

Highlights

{[0, 1, 2].map((i) => (
))}
} > {insights.map((ins) => { const Icon = ins.tone === "negative" ? TrendingDown : ins.tone === "positive" ? TrendingUp : Lightbulb; return ( {ins.text} ); })}
); } // Footprint for the first load, before any data arrives. const PLACEHOLDER_KPIS: ReportKpi[] = [0, 1, 2, 3].map((i) => ({ id: `placeholder-${i}`, label: "Loading", value: 0, }));