301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||
import { useEffect, useState } from 'react';
|
||
|
||
export default function DriverProfileMockup() {
|
||
const shouldReduceMotion = useReducedMotion();
|
||
const [isMobile, setIsMobile] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setIsMobile(window.innerWidth < 768);
|
||
}, []);
|
||
|
||
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 (
|
||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-3">
|
||
<div className="relative h-12 w-12 rounded-full border-2 border-primary-blue/50 overflow-hidden bg-charcoal-outline">
|
||
<div className="absolute inset-0 flex items-center justify-center text-2xl">🏎️</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-base font-bold text-white">Driver Profile</div>
|
||
<div className="text-xs text-white/50">Cross-league</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-2xl font-bold text-charcoal-outline">#33</div>
|
||
</div>
|
||
|
||
<div className="relative h-2 bg-charcoal-outline rounded-full overflow-hidden mb-1">
|
||
<div
|
||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full"
|
||
style={{ width: '86%' }}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-end">
|
||
<span className="text-xs text-gray-400">2150 GP Rating</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div className="text-sm font-semibold text-white mb-2">Career Stats</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{stats.slice(0, 3).map((stat) => (
|
||
<div
|
||
key={stat.label}
|
||
className="bg-iron-gray/50 border border-charcoal-outline rounded-lg p-2 text-center"
|
||
>
|
||
<div className="text-base font-bold text-white font-mono">
|
||
{stat.value}{stat.suffix}
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-0.5">{stat.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div className="text-sm font-semibold text-white mb-2">Recent Form</div>
|
||
<div className="h-16 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-2 flex items-end gap-1">
|
||
{formData.slice(-6).map((value, i) => (
|
||
<div
|
||
key={i}
|
||
className="flex-1 bg-gradient-to-t from-performance-green to-primary-blue rounded-sm"
|
||
style={{ height: `${value}%` }}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||
>
|
||
<div className="flex items-center justify-between mb-1.5 sm:mb-2 md:mb-3 lg:mb-4">
|
||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4">
|
||
<div className="relative h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12 lg:h-16 lg:w-16 rounded-full border-2 border-primary-blue/50 overflow-hidden bg-charcoal-outline">
|
||
<div className="absolute inset-0 flex items-center justify-center text-base sm:text-xl md:text-2xl lg:text-3xl">🏎️</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm sm:text-base md:text-lg lg:text-xl font-bold text-white mb-1 sm:mb-1.5 md:mb-2">Driver Profile</div>
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50">Cross-league racing identity</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold text-charcoal-outline">#33</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-1 sm:gap-2 md:gap-3 lg:gap-4 mb-1 sm:mb-1.5 md:mb-2">
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400">GridPilot Rating:</div>
|
||
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={2150} />
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400">iRating:</div>
|
||
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={3200} />
|
||
</div>
|
||
|
||
<div className="relative h-1.5 sm:h-2 md:h-2.5 lg: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-[8px] sm:text-[10px] md:text-xs text-gray-400">86%</span>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
variants={containerVariants}
|
||
initial="hidden"
|
||
animate="visible"
|
||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||
>
|
||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Career Statistics</div>
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Aggregated across all leagues</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-5 gap-1.5 sm:gap-2 md: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-1.5 sm:p-2 md:p-3 text-center"
|
||
>
|
||
<AnimatedCounter
|
||
value={stat.value}
|
||
shouldReduceMotion={shouldReduceMotion ?? false}
|
||
delay={index * 0.1}
|
||
suffix={stat.suffix ?? ''}
|
||
/>
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400 mt-0.5">{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-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||
>
|
||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Recent Form</div>
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Performance trend over last 10 races</div>
|
||
|
||
<div className="h-12 sm:h-16 md:h-20 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 flex items-end gap-0.5">
|
||
{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-0.5 text-[8px] sm:text-[10px] md: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="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Teams</div>
|
||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Current and past team memberships</div>
|
||
|
||
<div className="space-y-1 sm:space-y-1.5 md: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-1 sm:p-1.5 md:p-2 text-[10px] sm:text-xs md:text-sm"
|
||
>
|
||
<div className="flex items-center gap-2 sm:gap-3">
|
||
<div className="h-5 w-5 sm:h-6 sm:w-6 md:h-8 md:w-8 rounded border border-primary-blue/30 bg-charcoal-outline flex items-center justify-center text-sm sm:text-base md:text-lg">
|
||
🏁
|
||
</div>
|
||
<div className="h-1.5 sm:h-2 md:h-3 w-16 sm:w-20 md:w-32 bg-white/10 rounded"></div>
|
||
</div>
|
||
<span className={`text-[8px] sm:text-[10px] md:text-xs px-1 sm:px-1.5 md: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-2 sm:mt-3 md:mt-4 text-center text-[8px] sm:text-[10px] md: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-sm sm:text-base md: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-sm sm:text-base md:text-lg lg:text-xl font-bold text-white font-mono">
|
||
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
|
||
</div>
|
||
);
|
||
} |