"use client";
import { useEffect, useState } from "react";
import { KpiTile } from "@/components/ui/kpi-tile";
const settings = {
/** Days in each sparkline, 2 to 30. */
days: 14,
/** Tiles with no data yet. */
empty: false,
/** A new day every two seconds. */
live: true,
/** Skeletons, as while the first numbers load. */
loading: false,
};
const DAY_MS = 86_400_000;
/** The slow trend is a long wave, so a demo left running never drifts out of range. */
const TREND_DAYS = 60;
const FIRST_DAY = Date.UTC(2026, 9, 1);
const TICK_MS = 2000;
const DAY_NAME = new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "short",
timeZone: "UTC",
});
type Metric = {
label: string;
base: number;
/** Growth per day, and how far a day wobbles around it. */
growth: number;
swing: number;
phase: number;
format: (value: number) => string;
lowerIsBetter?: boolean;
};
const whole = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
const METRICS: Metric[] = [
{
base: 41_800,
format: (value) => `$${whole.format(value)}`,
growth: 0.011,
label: "Revenue",
phase: 0,
swing: 0.05,
},
{
base: 9640,
format: (value) => whole.format(value),
growth: 0.006,
label: "Active users",
phase: 1.6,
swing: 0.03,
},
{
base: 3.1,
format: (value) => `${value.toFixed(2)}%`,
growth: 0.003,
label: "Conversion",
phase: 3,
swing: 0.07,
},
{
base: 248,
format: (value) => `${Math.round(value)} ms`,
growth: -0.004,
label: "p95 latency",
lowerIsBetter: true,
phase: 1.6,
swing: 0.06,
},
];
/** The same story on every visit: a slow trend with a repeatable wobble of two sines. */
const valueOn = (metric: Metric, day: number) =>
metric.base *
(1 + metric.growth * TREND_DAYS * Math.sin(day / TREND_DAYS)) *
(1 +
metric.swing *
(Math.sin(day * 1.7 + metric.phase) * 0.6 +
Math.sin(day * 0.6 + metric.phase * 2) * 0.4));
/** Four live KPIs: three that should grow, and a latency where lower is better. */
export default function Demo(props: Partial<typeof settings>) {
const { days, empty, live, loading } = { ...settings, ...props };
const count = Number.isFinite(days)
? Math.min(Math.max(Math.round(days), 2), 30)
: 14;
const [today, setToday] = useState(30);
useEffect(() => {
if (!live) {
return;
}
const timer = window.setInterval(() => setToday((day) => day + 1), TICK_MS);
return () => window.clearInterval(timer);
}, [live]);
const inView = Array.from(
{ length: count },
(_, index) => today - count + 1 + index
);
const labels = inView.map((day) => DAY_NAME.format(FIRST_DAY + day * DAY_MS));
return (
<div className="flex min-h-[360px] w-full items-center justify-center bg-background p-4 sm:p-8">
<div className="grid w-full max-w-5xl gap-4 sm:grid-cols-2 lg:grid-cols-4">
{METRICS.map((metric) => {
const trend = inView.map((day) => valueOn(metric, day));
const now = trend.at(-1) ?? 0;
const before = trend.at(-2) ?? now;
return (
<KpiTile
delta={empty ? undefined : ((now - before) / before) * 100}
deltaLabel="vs yesterday"
format={metric.format}
key={metric.label}
label={metric.label}
live={live}
loading={loading}
lowerIsBetter={metric.lowerIsBetter}
trend={empty ? [] : trend}
trendLabels={labels}
value={empty ? undefined : now}
/>
);
})}
</div>
</div>
);
}