- 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
78 lines
2.4 KiB
TypeScript
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>
|
|
);
|
|
}
|