"use client"; import { ArrowDownRightIcon, ArrowUpRightIcon, BackpackIcon, ChartColumnIcon, ChartLineIcon, CoffeeIcon, HeadphonesIcon, type LucideIcon, ShirtIcon, WatchIcon, } from "lucide-react"; import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react"; import { useEffect, useId, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type SalesMetric = "revenue" | "orders" | "aov" | "customers"; export interface SalesMonth { month: string; revenue: number; orders: number; customers: number; } export interface SalesProduct { name: string; category: string; icon: LucideIcon; revenue: number; units: number; } export interface SalesChannel { name: string; revenueShare: number; orderShare: number; } export interface SalesYear { label: string; months: SalesMonth[]; products: SalesProduct[]; channels: SalesChannel[]; } export interface SalesOverviewProps { years?: SalesYear[]; title?: string; currency?: string; className?: string; } const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; function sampleYear(label: string, scale: number, seed: number): SalesYear { let s = seed; const rand = () => { s = (s * 16807) % 2147483647; return s / 2147483647; }; const months = monthNames.map((month, i) => { const season = 1 + 0.18 * Math.sin((i - 2) / 1.9) + (i >= 10 ? 0.35 : 0); const orders = Math.round(1180 * scale * season * (0.88 + rand() * 0.24)); return { month, orders, revenue: Math.round(orders * (52 + rand() * 9)), customers: Math.round(orders * (0.42 + rand() * 0.08)), }; }); return { label, months, products: [ { name: "Trail Shell Jacket", category: "Outerwear", icon: ShirtIcon, revenue: Math.round(184_000 * scale), units: Math.round(1310 * scale) }, { name: "Daypack 22L", category: "Bags", icon: BackpackIcon, revenue: Math.round(126_500 * scale), units: Math.round(1890 * scale) }, { name: "Field Watch", category: "Accessories", icon: WatchIcon, revenue: Math.round(142_300 * scale), units: Math.round(620 * scale) }, { name: "Studio Headphones", category: "Audio", icon: HeadphonesIcon, revenue: Math.round(98_400 * scale), units: Math.round(740 * scale) }, { name: "Camp Mug Set", category: "Kitchen", icon: CoffeeIcon, revenue: Math.round(41_200 * scale), units: Math.round(2210 * scale) }, ], channels: [ { name: "Direct", revenueShare: 0.34, orderShare: 0.29 }, { name: "Organic search", revenueShare: 0.27, orderShare: 0.31 }, { name: "Paid social", revenueShare: 0.18, orderShare: 0.2 }, { name: "Email", revenueShare: 0.13, orderShare: 0.12 }, { name: "Referral", revenueShare: 0.08, orderShare: 0.08 }, ], }; } const fallbackYears = [sampleYear("2026", 1.18, 4242), sampleYear("2025", 1, 1717)]; const metricInfo: Record = { revenue: { label: "Revenue", hint: "Gross sales" }, orders: { label: "Orders", hint: "Completed checkouts" }, aov: { label: "Avg. order", hint: "Revenue per order" }, customers: { label: "Customers", hint: "First-time buyers" }, }; const channelTones = [ "bg-blue-500 dark:bg-blue-400", "bg-emerald-500 dark:bg-emerald-400", "bg-amber-500 dark:bg-amber-400", "bg-violet-500 dark:bg-violet-400", "bg-rose-500 dark:bg-rose-400", ]; const accent = "text-blue-600 dark:text-blue-400"; const settle = { type: "spring", stiffness: 300, damping: 34 } as const; function Roll({ text, className }: { text: string; className?: string }) { const reduce = useReducedMotion(); const chars = text.split(""); return ( {text} {chars.map((char, i) => { const key = chars.length - i; if (!/\d/.test(char)) { return ( {char} ); } return ( {Array.from({ length: 10 }, (_, n) => ( {n} ))} ); })} ); } function niceStep(value: number) { const exp = 10 ** Math.floor(Math.log10(Math.max(1, value))); const f = value / exp; const nice = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10; return nice * exp; } function compact(n: number) { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`; if (n >= 1000) return `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1).replace(/\.0$/, "")}k`; return `${Math.round(n)}`; } export function SalesOverview({ years = fallbackYears, title = "Sales overview", currency = "$", className, }: SalesOverviewProps) { const uid = useId().replace(/:/g, ""); const reduce = useReducedMotion(); const [yearIndex, setYearIndex] = useState(0); const [metric, setMetric] = useState("revenue"); const [mode, setMode] = useState<"line" | "bars">("line"); const [hover, setHover] = useState(null); const chartRef = useRef(null); const [size, setSize] = useState({ width: 640, height: 260 }); const width = size.width; const lastSize = useRef(size); const resized = lastSize.current !== size; useEffect(() => { lastSize.current = size; }); useEffect(() => { const el = chartRef.current; if (!el) return; const observer = new ResizeObserver(([entry]) => setSize({ width: entry.contentRect.width, height: Math.max(220, entry.contentRect.height) }), ); observer.observe(el); return () => observer.disconnect(); }, []); const year = years[yearIndex]; const prior = years[yearIndex + 1]; const monthValue = (m: SalesMonth, key: SalesMetric) => key === "aov" ? m.revenue / Math.max(1, m.orders) : m[key]; const totalOf = (y: SalesYear | undefined, key: SalesMetric) => { if (!y) return 0; const rev = y.months.reduce((a, m) => a + m.revenue, 0); const ord = y.months.reduce((a, m) => a + m.orders, 0); if (key === "aov") return rev / Math.max(1, ord); if (key === "revenue") return rev; if (key === "orders") return ord; return y.months.reduce((a, m) => a + m.customers, 0); }; const format = (key: SalesMetric, n: number) => { if (key === "revenue") return `${currency}${Math.round(n).toLocaleString("en-US")}`; if (key === "aov") return `${currency}${n.toFixed(2)}`; return Math.round(n).toLocaleString("en-US"); }; const H = size.height; const padL = 40; const padB = 26; const padT = 12; const plotW = Math.max(120, width - padL); const plotH = H - padB - padT; const values = year.months.map((m) => monthValue(m, metric)); const tickStep = niceStep((Math.max(...values) * 1.08) / 5); const top = Math.ceil((Math.max(...values) * 1.08) / tickStep) * tickStep; const slot = plotW / values.length; const xAt = (i: number) => padL + slot * i + slot / 2; const yAt = (v: number) => padT + plotH - (v / top) * plotH; const linePath = values.map((v, i) => `${i ? "L" : "M"}${xAt(i).toFixed(1)},${yAt(v).toFixed(1)}`).join(" "); const ticks = Array.from({ length: Math.round(top / tickStep) + 1 }, (_, i) => i * tickStep); const areaPath = `${linePath} L${xAt(values.length - 1).toFixed(1)},${yAt(0)} L${xAt(0).toFixed(1)},${yAt(0)} Z`; const barW = Math.min(36, slot * 0.56); const productValue = (p: SalesProduct) => (metric === "orders" || metric === "customers" ? p.units : p.revenue); const products = [...year.products].sort((a, b) => productValue(b) - productValue(a)); const topProduct = products[0] ? productValue(products[0]) : 1; const channelMetricTotal = totalOf(year, metric === "aov" ? "revenue" : metric); const shareOf = (c: SalesChannel) => (metric === "orders" || metric === "customers" ? c.orderShare : c.revenueShare); const spring = reduce ? { duration: 0 } : ({ type: "spring", stiffness: 380, damping: 38 } as const); const morph = resized ? { duration: 0 } : spring; return (

{title}

January – December {year.label} {prior ? ` · compared with ${prior.label}` : ""}

{years.length > 1 && (
{years.map((y, i) => ( ))}
)}
{(Object.keys(metricInfo) as SalesMetric[]).map((key) => { const value = totalOf(year, key); const before = totalOf(prior, key); const change = before ? (value - before) / before : 0; const up = change >= 0; const active = metric === key; return ( ); })}

{metricInfo[metric].label} by month

{hover !== null ? `${year.months[hover].month} ${year.label}: ${format(metric, values[hover])}` : `Peak in ${year.months[values.indexOf(Math.max(...values))].month}`}

{( [ { value: "line", label: "Line", icon: ChartLineIcon }, { value: "bars", label: "Bars", icon: ChartColumnIcon }, ] as const ).map((opt) => ( ))}
{ticks.map((t) => ( {metric === "revenue" || metric === "aov" ? currency : ""} {compact(t)} ))} {year.months.map((m, i) => ( {slot < 34 ? m.month[0] : m.month} ))} {hover !== null && ( )} {mode === "bars" ? ( {values.map((v, i) => ( ))} ) : ( {values.map((v, i) => ( ))} )}
{year.months.map((m, i) => (
{hover !== null && ( 6 ? "-translate-x-[calc(100%+14px)]" : "translate-x-3.5", )} exit={{ opacity: 0 }} initial={{ opacity: 0 }} style={{ left: xAt(hover), top: Math.max(0, Math.min(yAt(values[hover]) - 30, H - 110)) }} transition={{ duration: 0.12, ease: "easeOut" }} >

{year.months[hover].month} {year.label}

{format(metric, values[hover])}

{prior && (

{prior.label}: {format(metric, monthValue(prior.months[hover], metric))}

)}
)}

Top products

by {metric === "orders" || metric === "customers" ? "units" : "revenue"}
    {products.map((p, i) => { const value = productValue(p); return ( {p.name} {metric === "orders" || metric === "customers" ? ( ) : ( )} ); })}

Channels

share of {metricInfo[metric === "aov" ? "revenue" : metric].label.toLowerCase()}
{year.channels.map((c, i) => ( ))}
    {year.channels.map((c, i) => (
  • {c.name} {metric === "revenue" || metric === "aov" ? currency : ""} {compact(shareOf(c) * channelMetricTotal)}
  • ))}
); }