Joey Eamigh 224a9046fc
fix: add "use cache" directives, fix server-client icon serialization
- Add Next.js 16 "use cache" with cacheLife profiles: seedData (1h),
  prices (5min), demand (5min), commodities (30min), ticker (1min),
  alerts (2min)
- Add cacheTag for parameterized server actions
- Fix MetricCard icon prop: pass rendered JSX instead of component
  references across the server-client boundary
2026-02-11 13:29:47 -05:00

78 lines
2.4 KiB
TypeScript

'use client';
import { Sparkline } from '@/components/charts/sparkline.js';
import { AnimatedNumber } from '@/components/dashboard/animated-number.js';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
import { cn } from '@/lib/utils.js';
import type { ReactNode } from 'react';
import { useCallback } from 'react';
type AnimatedFormat = 'dollar' | 'compact' | 'integer';
const FORMAT_FNS: Record<AnimatedFormat, (n: number) => string> = {
dollar: (n: number) => `$${n.toFixed(2)}`,
compact: (n: number) => {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toFixed(1);
},
integer: (n: number) => Math.round(n).toLocaleString(),
};
interface MetricCardProps {
title: string;
value: string;
/** When provided with animatedFormat, the value animates via spring physics on change. */
numericValue?: number;
/** Named format preset for the animated value. */
animatedFormat?: AnimatedFormat;
unit?: string;
icon: ReactNode;
className?: string;
sparklineData?: { value: number }[];
sparklineColor?: string;
}
export function MetricCard({
title,
value,
numericValue,
animatedFormat,
unit,
icon,
className,
sparklineData,
sparklineColor,
}: MetricCardProps) {
const formatFn = useCallback(
(n: number) => (animatedFormat ? FORMAT_FNS[animatedFormat](n) : n.toFixed(2)),
[animatedFormat],
);
return (
<Card className={cn('gap-0 py-4', className)}>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
{icon}
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-1.5">
{numericValue !== undefined && animatedFormat ? (
<AnimatedNumber value={numericValue} format={formatFn} className="text-3xl font-bold tracking-tight" />
) : (
<span className="text-3xl font-bold tracking-tight">{value}</span>
)}
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
</div>
{sparklineData && sparklineData.length >= 2 && (
<div className="mt-2">
<Sparkline data={sparklineData} color={sparklineColor} height={28} />
</div>
)}
</CardContent>
</Card>
);
}