220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
|
import { useEffect } from 'react';
|
|
|
|
export default function DriverProfileMockup() {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
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];
|
|
|
|
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 (
|
|
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-6 md:p-8 overflow-hidden">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="mb-6"
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<div className="h-6 w-48 bg-white/10 rounded mb-2"></div>
|
|
<div className="h-4 w-24 bg-white/5 rounded"></div>
|
|
</div>
|
|
<div className="text-4xl font-bold text-charcoal-outline">#33</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 mb-2">
|
|
<div className="text-xs text-gray-400">GridPilot Rating:</div>
|
|
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={2150} />
|
|
<div className="text-xs text-gray-400 ml-4">iRating:</div>
|
|
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={3200} />
|
|
</div>
|
|
|
|
<div className="relative h-3 bg-charcoal-outline rounded-full overflow-hidden">
|
|
<motion.div
|
|
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full"
|
|
initial={{ width: '0%' }}
|
|
animate={{ width: '86%' }}
|
|
transition={{ delay: shouldReduceMotion ? 0 : 0.4, duration: 0.8, ease: 'easeOut' }}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end mt-1">
|
|
<span className="text-xs text-gray-400">86%</span>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
className="mb-6"
|
|
>
|
|
<div className="h-4 w-32 bg-white/10 rounded mb-3"></div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
|
{stats.map((stat, index) => (
|
|
<motion.div
|
|
key={stat.label}
|
|
variants={itemVariants}
|
|
className="bg-iron-gray/50 border border-charcoal-outline rounded-lg p-3 text-center"
|
|
>
|
|
<AnimatedCounter
|
|
value={stat.value}
|
|
shouldReduceMotion={shouldReduceMotion ?? false}
|
|
delay={index * 0.1}
|
|
suffix={stat.suffix}
|
|
/>
|
|
<div className="text-xs text-gray-400 mt-1">{stat.label}</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: shouldReduceMotion ? 0 : 0.6 }}
|
|
className="mb-6"
|
|
>
|
|
<div className="h-4 w-28 bg-white/10 rounded mb-3"></div>
|
|
|
|
<div className="h-20 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-3 flex items-end gap-1">
|
|
{formData.map((value, i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="flex-1 bg-gradient-to-t from-performance-green to-primary-blue rounded-sm"
|
|
initial={{ height: 0 }}
|
|
animate={{ height: `${value}%` }}
|
|
transition={{
|
|
delay: shouldReduceMotion ? 0 : 0.8 + i * 0.05,
|
|
duration: 0.4,
|
|
ease: 'easeOut'
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-between mt-1 text-xs text-gray-500">
|
|
<span>Last 10 races</span>
|
|
<span>Recent</span>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: shouldReduceMotion ? 0 : 0.8 }}
|
|
>
|
|
<div className="h-4 w-20 bg-white/10 rounded mb-3"></div>
|
|
|
|
<div className="space-y-2">
|
|
{[
|
|
{ team: 'Red Bull Racing', status: 'Current', color: 'primary-blue' },
|
|
{ team: 'Mercedes AMG', status: '2023', color: 'charcoal-outline' }
|
|
].map((team, i) => (
|
|
<motion.div
|
|
key={team.team}
|
|
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: shouldReduceMotion ? 0 : 0.9 + i * 0.1 }}
|
|
className="flex items-center justify-between bg-iron-gray/30 border border-charcoal-outline rounded-lg p-2 text-sm"
|
|
>
|
|
<div className="h-3 w-32 bg-white/10 rounded"></div>
|
|
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
team.status === 'Current'
|
|
? 'bg-primary-blue/20 text-primary-blue'
|
|
: 'bg-charcoal-outline text-gray-400'
|
|
}`}>
|
|
{team.status}
|
|
</span>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: shouldReduceMotion ? 0 : 1.2 }}
|
|
className="mt-4 text-center text-xs text-gray-400"
|
|
>
|
|
Active in 3 leagues
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) {
|
|
const count = useMotionValue(0);
|
|
const rounded = useTransform(count, (v) => Math.round(v));
|
|
const spring = useSpring(count, { stiffness: 50, damping: 25 });
|
|
|
|
useEffect(() => {
|
|
if (shouldReduceMotion) {
|
|
count.set(value);
|
|
} else {
|
|
const timeout = setTimeout(() => count.set(value), 200);
|
|
return () => clearTimeout(timeout);
|
|
}
|
|
}, [shouldReduceMotion, count, value]);
|
|
|
|
return (
|
|
<motion.span className="text-lg font-bold text-primary-blue font-mono">
|
|
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
|
|
</motion.span>
|
|
);
|
|
}
|
|
|
|
function AnimatedCounter({
|
|
value,
|
|
shouldReduceMotion,
|
|
delay,
|
|
suffix = ''
|
|
}: {
|
|
value: number;
|
|
shouldReduceMotion: boolean;
|
|
delay: number;
|
|
suffix?: string;
|
|
}) {
|
|
const count = useMotionValue(0);
|
|
const rounded = useTransform(count, (v) => 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 (
|
|
<div className="text-xl font-bold text-white font-mono">
|
|
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
|
|
</div>
|
|
);
|
|
} |