'use client'; import { useId } from 'react'; import { ArrowDownRight, ArrowUpRight, type LucideIcon } from 'lucide-react'; export type StatsSparklineTone = 'green' | 'violet' | 'orange' | 'blue'; const TONES: Record< StatsSparklineTone, { stroke: string; iconBox: string; value: string } > = { green: { stroke: 'var(--success)', iconBox: 'bg-success/10 border-success/25 text-success', value: 'text-success', }, violet: { stroke: 'var(--info)', iconBox: 'bg-info/10 border-info/25 text-info', value: 'text-info', }, orange: { stroke: 'var(--warning)', iconBox: 'bg-warning/10 border-warning/30 text-warning', value: 'text-warning', }, blue: { stroke: '#3b82f6', iconBox: 'bg-[#3b82f6]/10 border-[#3b82f6]/25 text-[#3b82f6]', value: 'text-[#3b82f6]', }, }; export function datesToSparkline( dates: Array, days = 7 ): number[] { const buckets = Array.from({ length: days }, () => 0); const today = new Date(); today.setHours(0, 0, 0, 0); for (const raw of dates) { if (!raw) continue; const d = new Date(raw); if (Number.isNaN(d.getTime())) continue; d.setHours(0, 0, 0, 0); const diff = Math.round((today.getTime() - d.getTime()) / 86400000); if (diff >= 0 && diff < days) buckets[days - 1 - diff] += 1; } return buckets; } export function weekOverWeekChange(dates: Array): number { const now = Date.now(); const week = 7 * 86400000; let thisWeek = 0; let lastWeek = 0; for (const raw of dates) { if (!raw) continue; const t = new Date(raw).getTime(); if (Number.isNaN(t)) continue; const age = now - t; if (age >= 0 && age < week) thisWeek += 1; else if (age >= week && age < week * 2) lastWeek += 1; } if (lastWeek === 0) return thisWeek === 0 ? 0 : 100; return ((thisWeek - lastWeek) / lastWeek) * 100; } function MiniSparkline({ data, color }: { data: number[]; color: string }) { const gradientId = useId(); const width = 118; const height = 52; const padX = 2; const padY = 6; const values = data.length > 1 ? data : [0, 0]; const max = Math.max(...values, 1); const points = values.map((value, index) => { const x = padX + (index / Math.max(values.length - 1, 1)) * (width - padX * 2); const y = height - padY - (value / max) * (height - padY * 2); return { x, y }; }); const line = points.map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x},${point.y}`).join(' '); const area = `${line} L${points[points.length - 1].x},${height - 2} L${points[0].x},${height - 2} Z`; return ( ); } interface StatsSparklineCardProps { title: string; value: string | number; icon: LucideIcon; tone: StatsSparklineTone; series: number[]; deltaPercent?: number; deltaLabel?: string; active?: boolean; onClick?: () => void; } export function StatsSparklineCard({ title, value, icon: Icon, tone, series, deltaPercent = 0, deltaLabel = 'vs Last Week', active = false, onClick, }: StatsSparklineCardProps) { const theme = TONES[tone]; const positive = deltaPercent >= 0; const TrendIcon = positive ? ArrowUpRight : ArrowDownRight; const deltaText = `${positive ? '+' : ''}${deltaPercent.toFixed(1)}%`; return ( ); }