Files
gridpilot.gg/apps/website/components/mockups/DriverProfileMockup.tsx
2026-01-19 12:35:16 +01:00

272 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from 'framer-motion';
import { useEffect, useState } from 'react';
export function DriverProfileMockup() {
const shouldReduceMotion = useReducedMotion();
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const stats = [
{ label: 'Wins', value: 24 },
{ label: 'Podiums', value: 48 },
{ label: 'Championships', value: 3 },
{ label: 'Races', value: 156 },
{ label: 'Finish Rate', value: 94, suffix: '%' }
];
const formData = [85, 72, 68, 91, 88, 95, 88, 79, 82, 91];
if (isMobile) {
return (
<Stack position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" p={3} overflow="hidden">
<Stack gap={4}>
<Stack>
<Stack display="flex" alignItems="center" justifyContent="between" mb={2}>
<Stack display="flex" alignItems="center" gap={3}>
<Stack position="relative" h="12" w="12" rounded="none" border borderWidth="1px" borderColor="primary-accent/50" overflow="hidden" bg="panel-gray" display="flex" alignItems="center" justifyContent="center">
<Text size="2xl">🏎</Text>
<Stack position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent" />
</Stack>
<Stack>
<Text weight="bold" color="text-white" block className="uppercase tracking-widest">Driver Profile</Text>
<Text size="xs" color="text-gray-500" block font="mono">CROSS-LEAGUE</Text>
</Stack>
</Stack>
<Text size="2xl" weight="bold" color="text-gray-800" font="mono">#33</Text>
</Stack>
<Stack position="relative" h="1" bg="white/5" rounded="none" overflow="hidden" mb={1}>
<Stack
position="absolute"
insetY="0"
left="0"
bg="primary-accent"
style={{ width: '86%' }}
/>
</Stack>
<Stack display="flex" justifyContent="end">
<Text size="xs" color="text-primary-accent" font="mono" weight="bold">2150 GP RATING</Text>
</Stack>
</Stack>
<Stack>
<Text size="xs" weight="bold" color="text-gray-500" mb={2} block className="uppercase tracking-widest">Career Stats</Text>
<Stack display="grid" gridCols={3} gap={2}>
{stats.slice(0, 3).map((stat) => (
<Stack
key={stat.label}
bg="panel-gray/40"
border
borderColor="border-gray/50"
rounded="none"
p={2}
textAlign="center"
>
<Text weight="bold" color="text-white" font="mono" block>
{stat.value}{stat.suffix}
</Text>
<Text size="xs" color="text-gray-500" mt={0.5} block className="uppercase tracking-tighter">{stat.label}</Text>
</Stack>
))}
</Stack>
</Stack>
</Stack>
</Stack>
);
}
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: shouldReduceMotion ? 0 : 0.1 }
}
};
const itemVariants = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 10 },
visible: {
opacity: 1,
y: 0,
transition: { type: 'spring' as const, stiffness: 200, damping: 20 }
}
};
return (
<Stack position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" p={{ base: 1.5, sm: 3, md: 5, lg: 8 }} overflow="hidden">
<Stack
as={motion.div}
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
animate={{ opacity: 1, y: 0 }}
mb={{ base: 1.5, sm: 3, md: 4, lg: 6 }}
>
<Stack display="flex" alignItems="center" justifyContent="between" mb={{ base: 1.5, sm: 2, md: 3, lg: 4 }}>
<Stack display="flex" alignItems="center" gap={{ base: 1.5, sm: 2, md: 3, lg: 4 }}>
<Stack position="relative" h={{ base: 8, sm: 10, md: 12, lg: 16 }} w={{ base: 8, sm: 10, md: 12, lg: 16 }} rounded="none" border borderWidth="1px" borderColor="primary-accent/50" overflow="hidden" bg="panel-gray" display="flex" alignItems="center" justifyContent="center">
<Text size={{ base: 'base', sm: 'xl', md: '2xl', lg: '3xl' }}>🏎</Text>
<Stack position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent" />
</Stack>
<Stack>
<Text size={{ base: 'sm', sm: 'base', md: 'lg', lg: 'xl' }} weight="bold" color="text-white" mb={{ base: 1, sm: 1.5, md: 2 }} block className="uppercase tracking-widest">Driver Profile</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
block
font="mono"
className="uppercase tracking-widest"
>
Cross-league racing identity
</Text>
</Stack>
</Stack>
<Text size={{ base: 'xl', sm: '2xl', md: '3xl', lg: '4xl' }} weight="bold" color="text-gray-800" font="mono">#33</Text>
</Stack>
<Stack display="flex" flexWrap="wrap" alignItems="center" gap={{ base: 1, sm: 2, md: 3, lg: 4 }} mb={{ base: 1, sm: 1.5, md: 2 }}>
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">GP RATING:</Text>
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={2150} />
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest ml-4">iRATING:</Text>
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={3200} />
</Stack>
<Stack position="relative" h="1" bg="white/5" rounded="none" overflow="hidden">
<Stack
as={motion.div}
position="absolute"
insetY="0"
left="0"
bg="primary-accent"
initial={{ width: '0%' }}
animate={{ width: '86%' }}
transition={{ delay: shouldReduceMotion ? 0 : 0.4, duration: 0.8, ease: 'easeOut' }}
/>
</Stack>
</Stack>
<Stack
as={motion.div}
variants={containerVariants}
initial="hidden"
animate="visible"
mb={{ base: 1.5, sm: 3, md: 4, lg: 6 }}
>
<Text size="xs" weight="bold" color="text-gray-500" mb={3} block className="uppercase tracking-[0.2em]">Career Statistics</Text>
<Stack display="grid" gridCols={{ base: 2, md: 5 }} gap={{ base: 1.5, sm: 2, md: 3 }}>
{stats.map((stat, index) => (
<Stack
key={stat.label}
as={motion.div}
variants={itemVariants}
bg="panel-gray/40"
border
borderColor="border-gray/50"
rounded="none"
p={{ base: 1.5, sm: 2, md: 3 }}
textAlign="center"
>
<AnimatedCounter
value={stat.value}
shouldReduceMotion={shouldReduceMotion ?? false}
delay={index * 0.1}
suffix={stat.suffix ?? ''}
/>
<Text size="xs" color="text-gray-500" mt={0.5} block className="uppercase tracking-tighter font-bold">{stat.label}</Text>
</Stack>
))}
</Stack>
</Stack>
<Stack
as={motion.div}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
>
<Text size="xs" weight="bold" color="text-gray-500" mb={3} block className="uppercase tracking-[0.2em]">Recent Form</Text>
<Stack h={{ base: 12, sm: 16, md: 20 }} bg="panel-gray/20" border borderColor="border-gray/50" rounded="none" p={{ base: 1.5, sm: 2, md: 3 }} display="flex" alignItems="end" gap={1}>
{formData.map((value, i) => (
<Stack
key={i}
as={motion.div}
flexGrow={1}
bg="primary-accent"
opacity={0.4 + (i * 0.06)}
rounded="none"
initial={{ height: 0 }}
animate={{ height: `${value}%` }}
transition={{
delay: shouldReduceMotion ? 0 : 0.8 + i * 0.05,
duration: 0.4,
ease: 'easeOut'
}}
/>
))}
</Stack>
</Stack>
</Stack>
);
}
function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) {
const count = useMotionValue(0);
const spring = useSpring(count, { stiffness: 50, damping: 25 });
const rounded = useTransform(spring, (v: number) => Math.round(v));
useEffect(() => {
if (shouldReduceMotion) {
count.set(value);
} else {
const timeout = setTimeout(() => count.set(value), 200);
return () => clearTimeout(timeout);
}
}, [shouldReduceMotion, count, value]);
return (
<Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}>
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
</Stack>
);
}
function AnimatedCounter({
value,
shouldReduceMotion,
delay,
suffix = ''
}: {
value: number;
shouldReduceMotion: boolean;
delay: number;
suffix?: string;
}) {
const count = useMotionValue(0);
const rounded = useTransform(count, (v: number) => Math.round(v));
useEffect(() => {
if (shouldReduceMotion) {
count.set(value);
} else {
const timeout = setTimeout(() => count.set(value), delay * 1000 + 400);
return () => clearTimeout(timeout);
}
}, [shouldReduceMotion, count, value, delay]);
return (
<Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}>
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
</Stack>
);
}