Files
gridpilot.gg/apps/website/ui/StatCard.tsx
2026-01-15 19:55:46 +01:00

116 lines
3.1 KiB
TypeScript

import { motion, useReducedMotion } from 'framer-motion';
import { ArrowDownRight, ArrowUpRight, LucideIcon } from 'lucide-react';
import { Box } from './Box';
import { Card } from './Card';
import { Icon } from './Icon';
import { Stack } from './Stack';
import { Text } from './Text';
interface StatCardProps {
label: string;
value: string | number;
subValue?: string;
icon?: LucideIcon;
variant?: 'blue' | 'purple' | 'green' | 'orange';
className?: string;
trend?: {
value: number;
isPositive: boolean;
};
prefix?: string;
suffix?: string;
delay?: number;
}
export function StatCard({
label,
value,
subValue,
icon,
variant = 'blue',
className = '',
trend,
prefix = '',
suffix = '',
delay = 0,
}: StatCardProps) {
const shouldReduceMotion = useReducedMotion();
const variantClasses = {
blue: 'bg-gradient-to-br from-blue-900/20 to-blue-700/10 border-blue-500/30',
purple: 'bg-gradient-to-br from-purple-900/20 to-purple-700/10 border-purple-500/30',
green: 'bg-gradient-to-br from-green-900/20 to-green-700/10 border-green-500/30',
orange: 'bg-gradient-to-br from-orange-900/20 to-orange-700/10 border-orange-500/30'
};
const iconColorClasses = {
blue: 'text-primary-blue',
purple: 'text-purple-400',
green: 'text-performance-green',
orange: 'text-warning-amber'
};
const cardContent = (
<Card className={`${variantClasses[variant]} ${className} h-full`} p={5}>
<Stack gap={3}>
<Stack direction="row" align="start" justify="between">
{icon && (
<Box
width="11"
height="11"
rounded="xl"
display="flex"
center
bg="bg-iron-gray/50"
border={true}
borderColor="border-charcoal-outline"
>
<Icon icon={icon} size={5} className={iconColorClasses[variant]} />
</Box>
)}
{trend && (
<Stack
direction="row"
align="center"
gap={1}
color={trend.isPositive ? 'text-performance-green' : 'text-error-red'}
>
<Icon icon={trend.isPositive ? ArrowUpRight : ArrowDownRight} size={4} />
<Text size="sm" weight="medium">{Math.abs(trend.value)}%</Text>
</Stack>
)}
</Stack>
<Box>
<Text size="2xl" weight="bold" color="text-white" block mb={1}>
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
</Text>
<Text size="sm" color="text-gray-400" block>{label}</Text>
{subValue && (
<Text size="xs" color="text-gray-500" block mt={1}>
{subValue}
</Text>
)}
</Box>
</Stack>
</Card>
);
if (shouldReduceMotion) {
return <Box fullHeight>{cardContent}</Box>;
}
return (
<Box
as={motion.div}
fullHeight
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
>
{cardContent}
</Box>
);
}