116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
|
|
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import { ArrowDownRight, ArrowUpRight, LucideIcon } from 'lucide-react';
|
|
import { Box } from './primitives/Box';
|
|
import { Card } from './Card';
|
|
import { Icon } from './Icon';
|
|
import { Stack } from './primitives/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>
|
|
);
|
|
}
|