Untitled
Anonymous
tsx
10/17/2026 8:52 PM
41.8 KB
5
Indexable
import { useState, useMemo, useEffect, useCallback, lazy, Suspense } from 'react';
import { Link } from 'react-router-dom';
import {
Settings as SettingsIcon,
Search,
SlidersHorizontal,
Star,
Gauge,
Zap,
CircleDot,
ArrowRight,
ChevronDown,
X,
Heart,
Trophy,
CarFront,
RotateCcw,
} from 'lucide-react';
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
import { ALL_CARS } from '@/data/cars';
import CarSilhouette from '@/components/CarSilhouette';
import TuneModal from '@/components/TuneModal';
const DrivingGame = lazy(() => import('@/components/DrivingGame'));
const CLASS_FILTERS = ['All', 'Hyper', 'Super', 'Sports', 'Muscle', 'GT', 'Sedan', 'Hatch', 'SUV', 'Truck', 'Classic'];
const TUNES_KEY = 'vd-tunes';
const FAVORITES_KEY = 'vd-favorites';
const screenMotion = {
initial: { opacity: 0, y: 12 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -8 },
transition: { duration: 0.2, ease: 'easeOut' },
};
const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number(value) || 0));
function readStorage(key, fallback) {
if (typeof window === 'undefined') return fallback;
try {
const value = JSON.parse(window.localStorage.getItem(key) || 'null');
return value ?? fallback;
} catch {
return fallback;
}
}
function normalizeTunes(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value).filter(([, tune]) => tune && typeof tune === 'object' && !Array.isArray(tune)),
);
}
function DriveLoading() {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-[#05070c] text-white">
<div className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-orange-400/30 bg-orange-400/10">
<CarFront className="h-7 w-7 text-orange-300" />
<span className="absolute -inset-1 animate-ping rounded-2xl border border-orange-400/20" />
</div>
<div className="text-center">
<p className="text-xs font-black uppercase tracking-[0.28em] text-orange-300">Starting engine</p>
<p className="mt-1 text-xs text-white/40">Loading the driving session…</p>
</div>
</div>
);
}
function StatBar({ label, value, color, icon: Icon }) {
const safeValue = clamp(value);
return (
<div className="grid grid-cols-[82px_minmax(0,1fr)_30px] items-center gap-2.5">
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-[0.15em] text-white/45">
{Icon ? <Icon className="h-3.5 w-3.5" aria-hidden="true" /> : null}
{label}
</span>
<div className="h-1.5 overflow-hidden rounded-full bg-white/[0.08]">
<motion.div
className={`h-full rounded-full ${color}`}
initial={false}
animate={{ width: `${safeValue}%` }}
transition={{ type: 'spring', stiffness: 120, damping: 22 }}
/>
</div>
<span className="text-right font-mono text-[10px] tabular-nums text-white/60">{Math.round(safeValue)}</span>
</div>
);
}
function GarageEmpty({ onReset }) {
return (
<div className="col-span-full flex min-h-56 flex-col items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/[0.02] px-5 text-center">
<Search className="h-8 w-8 text-white/25" aria-hidden="true" />
<h3 className="mt-3 text-sm font-bold text-white/80">No cars match those filters</h3>
<p className="mt-1 max-w-xs text-xs leading-5 text-white/40">Try another model name, choose a different class, or reset the filters.</p>
<button onClick={onReset} className="mt-4 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-semibold text-white/70 transition hover:bg-white/10 hover:text-white">
<RotateCcw className="h-3.5 w-3.5" /> Reset garage filters
</button>
</div>
);
}
function ModeCard({ icon: Icon, title, eyebrow, description, accent, onClick }) {
return (
<button
type="button"
onClick={onClick}
className="group relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.035] p-5 text-left transition duration-200 hover:-translate-y-0.5 hover:border-orange-400/40 hover:bg-orange-400/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-400"
>
<div className={`absolute -right-5 -top-5 h-28 w-28 rounded-full blur-3xl transition group-hover:opacity-80 ${accent}`} />
<div className="relative">
<div className="mb-7 flex items-start justify-between">
<span className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-black/20">
<Icon className="h-6 w-6 text-orange-200" />
</span>
<span className="rounded-full border border-white/10 px-2.5 py-1 text-[9px] font-black uppercase tracking-[0.18em] text-white/45">{eyebrow}</span>
</div>
<h3 className="text-xl font-black tracking-tight text-white">{title}</h3>
<p className="mt-2 max-w-sm text-xs leading-5 text-white/50">{description}</p>
<div className="mt-5 flex items-center gap-2 text-xs font-bold text-orange-300">
Enter event <ArrowRight className="h-3.5 w-3.5 transition group-hover:translate-x-1" />
</div>
</div>
</button>
);
}
export default function Game() {
const reduceMotion = useReducedMotion();
const firstCarId = ALL_CARS?.[0]?.id ?? null;
const [filter, setFilter] = useState('All');
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('featured');
const [favoritesOnly, setFavoritesOnly] = useState(false);
const [selectedId, setSelectedId] = useState(firstCarId);
const [screen, setScreen] = useState('garage'); // 'garage' | 'mode' | 'drive'
const [mode, setMode] = useState('open');
const [tuneOpen, setTuneOpen] = useState(false);
const [tunes, setTunes] = useState(() => normalizeTunes(readStorage(TUNES_KEY, {})));
const [favorites, setFavorites] = useState(() => {
const stored = readStorage(FAVORITES_KEY, []);
return Array.isArray(stored) ? stored.filter((id) => typeof id === 'string' || typeof id === 'number') : [];
});
const [modeDialogOpen, setModeDialogOpen] = useState(false);
const selected = useMemo(
() => ALL_CARS.find((car) => car.id === selectedId) || ALL_CARS[0] || null,
[selectedId],
);
// Persist separately from state updates: React updater functions remain pure.
useEffect(() => {
try { window.localStorage.setItem(TUNES_KEY, JSON.stringify(tunes)); } catch { /* storage can be blocked or full */ }
}, [tunes]);
useEffect(() => {
try { window.localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites)); } catch { /* storage is optional */ }
}, [favorites]);
// The base car data remains unchanged; tuning is an overlay for the driving component.
const tuned = useMemo(() => {
if (!selected) return null;
const tune = tunes[selected.id] || {};
const speedDelta = Number.isFinite(Number(tune.engine)) ? Number(tune.engine) : 0;
const accelDelta = Number.isFinite(Number(tune.turbo)) ? Number(tune.turbo) : 0;
const gripDelta = Number.isFinite(Number(tune.tires)) ? Number(tune.tires) : 0;
const nosValue = Number(tune.nos);
return {
...selected,
topSpeed: clamp(Number(selected.topSpeed) + speedDelta, 30, 100),
accel: clamp(Number(selected.accel) + accelDelta, 30, 100),
grip: clamp(Number(selected.grip) + gripDelta, 30, 100),
nitroMax: Number.isFinite(nosValue) ? clamp(nosValue, 0, 200) : 100,
};
}, [selected, tunes]);
const list = useMemo(() => {
const normalizedSearch = search.trim().toLocaleLowerCase();
const filtered = ALL_CARS.filter((car) => {
const classMatches = filter === 'All' || car.class === filter;
const favoriteMatches = !favoritesOnly || favorites.includes(car.id);
const searchMatches = !normalizedSearch || [car.name, car.brand, car.class, car.id]
.some((value) => String(value ?? '').toLocaleLowerCase().includes(normalizedSearch));
return classMatches && favoriteMatches && searchMatches;
});
if (sortBy === 'speed') return [...filtered].sort((a, b) => (Number(b.topSpeed) || 0) - (Number(a.topSpeed) || 0));
if (sortBy === 'accel') return [...filtered].sort((a, b) => (Number(b.accel) || 0) - (Number(a.accel) || 0));
if (sortBy === 'name') return [...filtered].sort((a, b) => String(a.name).localeCompare(String(b.name)));
if (sortBy === 'brand') return [...filtered].sort((a, b) => String(a.brand).localeCompare(String(b.brand)) || String(a.name).localeCompare(String(b.name)));
return filtered;
}, [filter, search, sortBy, favorites, favoritesOnly]);
const setTune = useCallback((id, nextTune) => {
if (id == null || !nextTune || typeof nextTune !== 'object') return;
setTunes((previous) => ({ ...previous, [id]: { ...(previous[id] || {}), ...nextTune } }));
}, []);
const resetFilters = useCallback(() => {
setFilter('All');
setSearch('');
setSortBy('featured');
setFavoritesOnly(false);
}, []);
const toggleFavorite = useCallback((id) => {
setFavorites((previous) => previous.includes(id) ? previous.filter((item) => item !== id) : [...previous, id]);
}, []);
const chooseCar = useCallback((id) => {
setSelectedId(id);
setTuneOpen(false);
}, []);
const closeModeDialog = useCallback(() => {
setModeDialogOpen(false);
setScreen('garage');
}, []);
const enterMode = useCallback((nextMode) => {
setMode(nextMode);
setModeDialogOpen(false);
setTuneOpen(false);
setScreen('drive');
}, []);
if (!selected || !tuned) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#070910] px-6 text-center text-white">
<div className="max-w-md rounded-3xl border border-white/10 bg-white/[0.03] p-8">
<CarFront className="mx-auto h-9 w-9 text-orange-300" />
<h1 className="mt-4 text-xl font-black">Garage unavailable</h1>
<p className="mt-2 text-sm leading-6 text-white/50">No vehicle data was found in <code>ALL_CARS</code>. Add at least one car to <code>@/data/cars</code> and reload the garage.</p>
</div>
</div>
);
}
return (
<AnimatePresence mode="wait" initial={false}>
{screen === 'drive' ? (
<motion.div key="drive" {...screenMotion} transition={reduceMotion ? { duration: 0 } : screenMotion.transition} className="fixed inset-0 z-50 bg-[#05070c]">
<Suspense fallback={<DriveLoading />}>
<DrivingGame car={tuned} mode={mode} onExit={() => setScreen('garage')} />
</Suspense>
</motion.div>
) : (
<motion.div key="garage" {...screenMotion} transition={reduceMotion ? { duration: 0 } : screenMotion.transition} className="min-h-screen overflow-hidden bg-[#070910] text-white selection:bg-orange-400/30">
<div className="pointer-events-none fixed inset-0 opacity-40" aria-hidden="true" style={{ background: 'radial-gradient(ellipse at 12% -5%, rgba(249,115,22,.15), transparent 32%), radial-gradient(ellipse at 90% 35%, rgba(14,165,233,.08), transparent 36%)' }} />
<header className="sticky top-0 z-30 border-b border-white/[0.07] bg-[#070910]/85 backdrop-blur-2xl">
<div className="mx-auto flex max-w-[1500px] items-center justify-between gap-4 px-4 py-3.5 sm:px-6 lg:px-8">
<Link to="/game" className="group flex min-w-0 items-center gap-3" aria-label="Velocity Drift home">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-orange-400/25 bg-orange-400/10 text-orange-300 shadow-lg shadow-orange-950/20">
<Gauge className="h-5 w-5" />
</span>
<span className="min-w-0">
<span className="block truncate text-lg font-black tracking-tight sm:text-xl">VELOCITY<span className="text-orange-400">DRIFT</span></span>
<span className="block truncate text-[9px] font-bold uppercase tracking-[0.22em] text-white/35">Street racing · tuned by you</span>
</span>
</Link>
<div className="hidden items-center gap-5 md:flex">
<div className="text-right">
<p className="text-[10px] font-bold uppercase tracking-[0.19em] text-white/35">Garage status</p>
<p className="mt-0.5 text-xs font-semibold text-white/75">{ALL_CARS.length} machines · {favorites.length} saved</p>
</div>
<div className="h-8 w-px bg-white/10" />
</div>
<Link to="/settings" aria-label="Settings" title="Settings" className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-white/[0.04] text-white/55 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-400">
<SettingsIcon className="h-4 w-4" />
</Link>
</div>
</header>
<main className="relative mx-auto max-w-[1500px] px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
<div className="mb-6 flex flex-wrap items-end justify-between gap-3">
<div>
<p className="mb-2 flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.25em] text-orange-300"><span className="h-px w-6 bg-orange-400" /> Driver profile / 001</p>
<h1 className="text-3xl font-black tracking-[-0.045em] sm:text-4xl">Your next <span className="text-white/40">machine.</span></h1>
<p className="mt-2 max-w-xl text-xs leading-5 text-white/40 sm:text-sm">Inspect your build, fine-tune its setup, and choose how you want to drive.</p>
</div>
<div className="flex items-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.03] px-3 py-2.5">
<span className="relative flex h-2 w-2"><span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400/60" /><span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" /></span>
<span className="text-[10px] font-bold uppercase tracking-[0.15em] text-white/55">Systems ready</span>
</div>
</div>
<section className="mb-9 grid gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(330px,.75fr)]">
<div className="group relative isolate min-h-[330px] overflow-hidden rounded-[28px] border border-white/10 bg-[#0d111a] p-5 sm:min-h-[390px] sm:p-8">
<div className="absolute inset-0 -z-20 bg-gradient-to-br from-[#141b27] via-[#0c111a] to-[#080b11]" />
<div className="absolute -right-24 -top-28 -z-10 h-80 w-80 rounded-full blur-3xl transition duration-700 group-hover:scale-110" style={{ background: `radial-gradient(circle, ${selected.color || '#f97316'}3b 0%, transparent 69%)` }} />
<div className="absolute inset-x-0 bottom-0 -z-10 h-32 bg-gradient-to-t from-black/55 to-transparent" />
<div className="absolute bottom-12 left-0 right-0 -z-10 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent" />
<div className="relative flex h-full min-h-[280px] flex-col">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-orange-300/25 bg-orange-300/10 px-2 py-1 text-[9px] font-black uppercase tracking-[0.18em] text-orange-200">{selected.class}</span>
<span className="text-[11px] font-semibold text-white/40">{selected.brand}</span>
<button onClick={() => toggleFavorite(selected.id)} className={`ml-auto inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[10px] font-bold transition ${favorites.includes(selected.id) ? 'border-rose-400/30 bg-rose-400/10 text-rose-200' : 'border-white/10 bg-black/10 text-white/45 hover:text-white'}`} aria-pressed={favorites.includes(selected.id)}>
<Heart className={`h-3.5 w-3.5 ${favorites.includes(selected.id) ? 'fill-current' : ''}`} />
{favorites.includes(selected.id) ? 'Saved' : 'Save car'}
</button>
</div>
<div className="mt-4">
<h2 className="max-w-3xl text-3xl font-black tracking-[-0.045em] sm:text-5xl">{selected.name}</h2>
<p className="mt-2 text-xs text-white/40 sm:text-sm">Selected vehicle <span className="mx-1.5 text-white/20">/</span> {String(selected.id)}</p>
</div>
<div className="relative mt-auto flex min-h-[150px] flex-1 items-center justify-center pt-5 sm:min-h-[190px]">
<div className="absolute bottom-3 h-10 w-[78%] rounded-[50%] bg-black/60 blur-2xl" />
<div className="absolute bottom-6 h-px w-[86%] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
<CarSilhouette car={selected} className="relative z-10 h-40 w-full max-w-[610px] drop-shadow-[0_25px_25px_rgba(0,0,0,0.65)] sm:h-52" />
</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-3 border-t border-white/[0.08] pt-4">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-[0.13em] text-white/40"><CircleDot className="h-3.5 w-3.5 text-orange-300" /> Build preview</div>
<div className="text-right text-[10px] text-white/30">Paint <span className="ml-1 inline-block h-2.5 w-2.5 rounded-full border border-white/30 align-middle" style={{ background: selected.color || '#888' }} /></div>
</div>
</div>
</div>
<div className="flex flex-col rounded-[28px] border border-white/10 bg-[#0c1018] p-5 sm:p-7">
<div className="flex items-start justify-between gap-3">
<div><p className="text-[9px] font-black uppercase tracking-[0.24em] text-orange-300">Vehicle telemetry</p><h3 className="mt-1 text-xl font-black tracking-tight">Performance profile</h3></div>
<span className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/[0.035] text-orange-200"><Gauge className="h-5 w-5" /></span>
</div>
<div className="mb-5 mt-7 space-y-4">
<StatBar label="Speed" value={tuned.topSpeed} color="bg-gradient-to-r from-orange-500 to-orange-300" icon={Gauge} />
<StatBar label="Accel" value={tuned.accel} color="bg-gradient-to-r from-amber-500 to-yellow-300" icon={Zap} />
<StatBar label="Grip" value={tuned.grip} color="bg-gradient-to-r from-cyan-500 to-sky-300" icon={CircleDot} />
</div>
<div className="grid grid-cols-3 gap-2">
{[{ label: 'Top speed', value: tuned.topSpeed, unit: '/100' }, { label: 'Launch', value: tuned.accel, unit: '/100' }, { label: 'Cornering', value: tuned.grip, unit: '/100' }].map((stat) => (
<div key={stat.label} className="rounded-2xl border border-white/[0.07] bg-white/[0.025] p-3">
<div className="font-mono text-xl font-black tabular-nums sm:text-2xl">{stat.value}<span className="ml-0.5 text-[9px] font-semibold text-white/25">{stat.unit}</span></div>
<div className="mt-1 text-[9px] font-bold uppercase tracking-[0.11em] text-white/35">{stat.label}</div>
</div>
))}
</div>
<div className="mt-auto space-y-2.5 pt-7">
<button onClick={() => { setModeDialogOpen(true); setScreen('mode'); }} className="group flex w-full items-center justify-between rounded-2xl bg-gradient-to-r from-orange-500 to-red-600 px-5 py-4 text-left font-black shadow-lg shadow-orange-950/30 transition hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-300">
<span><span className="block text-[9px] font-black uppercase tracking-[0.2em] text-white/70">Ready when you are</span><span className="mt-0.5 block text-sm">Start engine</span></span>
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-black/15 transition group-hover:translate-x-0.5"><ArrowRight className="h-4 w-4" /></span>
</button>
<button onClick={() => setTuneOpen(true)} className="flex w-full items-center justify-center gap-2 rounded-2xl border border-white/10 bg-white/[0.035] py-3 text-xs font-bold text-white/70 transition hover:border-white/20 hover:bg-white/[0.06] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-300">
<SlidersHorizontal className="h-4 w-4" /> Open tuning shop
</button>
</div>
</div>
</section>
<section aria-labelledby="garage-heading">
<div className="mb-4 flex flex-wrap items-end justify-between gap-3">
<div><p className="text-[9px] font-black uppercase tracking-[0.23em] text-orange-300">Vehicle collection</p><h2 id="garage-heading" className="mt-1 text-2xl font-black tracking-tight sm:text-3xl">Choose your ride<span className="text-white/25">.</span></h2><p className="mt-1.5 text-xs text-white/40">{list.length} of {ALL_CARS.length} vehicles shown</p></div>
<button onClick={() => setFavoritesOnly((value) => !value)} aria-pressed={favoritesOnly} className={`inline-flex items-center gap-2 rounded-xl border px-3 py-2.5 text-xs font-bold transition ${favoritesOnly ? 'border-rose-300/30 bg-rose-300/10 text-rose-200' : 'border-white/10 bg-white/[0.03] text-white/55 hover:text-white'}`}><Star className={`h-3.5 w-3.5 ${favoritesOnly ? 'fill-current' : ''}`} /> Favorites <span className="font-mono">{favorites.length}</span></button>
</div>
<div className="mb-4 grid gap-2 sm:grid-cols-[minmax(0,1fr)_170px]">
<label className="relative block">
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-white/30" />
<input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search make, model or class…" aria-label="Search cars" className="h-11 w-full rounded-xl border border-white/[0.09] bg-white/[0.025] pl-10 pr-4 text-xs text-white outline-none transition placeholder:text-white/25 focus:border-orange-400/50 focus:bg-white/[0.04] focus:ring-2 focus:ring-orange-400/10" />
</label>
<label className="relative flex items-center">
<span className="sr-only">Sort vehicles</span>
<select value={sortBy} onChange={(event) => setSortBy(event.target.value)} className="h-11 w-full appearance-none rounded-xl border border-white/[0.09] bg-[#0c1018] px-3.5 pr-9 text-xs text-white/70 outline-none focus:border-orange-400/50">
<option value="featured">Featured order</option><option value="speed">Highest speed</option><option value="accel">Best acceleration</option><option value="name">Name A–Z</option><option value="brand">Make A–Z</option>
</select><ChevronDown className="pointer-events-none absolute right-3 h-3.5 w-3.5 text-white/35" />
</label>
</div>
<div className="mb-5 flex flex-wrap gap-2">
{CLASS_FILTERS.map((className) => (
<button key={className} onClick={() => setFilter(className)} aria-pressed={filter === className} className={`rounded-full border px-3.5 py-2 text-[10px] font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-300 ${filter === className ? 'border-orange-400/40 bg-orange-400 text-[#180e08] shadow-md shadow-orange-950/20' : 'border-white/[0.07] bg-white/[0.025] text-white/50 hover:border-white/15 hover:text-white/80'}`}>
{className}
</button>
))}
{(search || filter !== 'All' || favoritesOnly || sortBy !== 'featured') && <button onClick={resetFilters} className="inline-flex items-center gap-1.5 px-2 text-[10px] font-bold text-orange-200/70 transition hover:text-orange-100"><X className="h-3 w-3" /> Reset</button>}
</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
{list.length === 0 ? <GarageEmpty onReset={resetFilters} /> : list.map((car, index) => {
const active = selectedId === car.id;
const favorite = favorites.includes(car.id);
const carTune = tunes[car.id] || {};
const displayedSpeed = clamp(Number(car.topSpeed) + (Number(carTune.engine) || 0), 30, 100);
return (
<motion.div
layout={!reduceMotion}
key={car.id}
className={`group relative overflow-hidden rounded-2xl border p-3 transition duration-200 ${active ? 'border-orange-400/55 bg-orange-400/[0.055] shadow-lg shadow-orange-950/10' : 'border-white/[0.07] bg-[#0c1018] hover:-translate-y-0.5 hover:border-white/20 hover:bg-[#101520]'}`}
>
<button type="button" onClick={() => chooseCar(car.id)} aria-pressed={active} aria-label={`Select ${car.brand} ${car.name}`} className="block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-orange-400">
<div className="relative mb-3 flex h-[94px] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-b from-white/[0.045] to-black/20 sm:h-[105px]">
<div className="absolute inset-x-3 bottom-4 h-4 rounded-[50%] bg-black/45 blur-xl" />
<div className="absolute inset-x-4 bottom-3 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent" />
<CarSilhouette car={car} className="relative h-[70px] w-full max-w-[230px] transition duration-300 group-hover:scale-[1.045] sm:h-[80px]" />
</div>
<div className="flex items-center gap-1.5 text-[9px] font-bold uppercase tracking-[0.14em] text-white/35"><span className="h-2 w-2 rounded-full border border-white/20" style={{ background: car.color || '#888' }} />{car.brand}<span className="ml-auto truncate text-white/25">{car.class}</span></div>
<div className="mt-1.5 truncate pr-1 text-sm font-bold tracking-tight text-white/90">{car.name}</div>
<div className="mt-3 flex items-center justify-between border-t border-white/[0.07] pt-2.5">
<span className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-[0.12em] text-white/35"><Gauge className="h-3 w-3" /> Speed</span>
<span className="font-mono text-[11px] font-bold tabular-nums text-white/75">{displayedSpeed}<span className="ml-0.5 text-[9px] text-white/30">/100</span></span>
</div>
<div className={`mt-2 h-0.5 overflow-hidden rounded-full bg-white/[0.07] ${active ? 'opacity-100' : 'opacity-0 group-hover:opacity-70'}`}><div className="h-full rounded-full bg-gradient-to-r from-orange-500 to-amber-300" style={{ width: `${displayedSpeed}%` }} /></div>
{active && <span className="absolute bottom-3 right-3 rounded-md bg-orange-400 px-1.5 py-1 text-[8px] font-black uppercase tracking-wider text-[#170b05]">Selected</span>}
</button>
<button type="button" onClick={() => toggleFavorite(car.id)} aria-label={favorite ? `Remove ${car.name} from favorites` : `Add ${car.name} to favorites`} aria-pressed={favorite} className={`absolute right-2 top-2 z-10 flex h-7 w-7 items-center justify-center rounded-lg border backdrop-blur-md transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-400 ${favorite ? 'border-rose-300/25 bg-rose-400/15 text-rose-200' : 'border-white/[0.07] bg-black/25 text-white/35 opacity-75 hover:text-white group-hover:opacity-100'}`}>
<Heart className={`h-3.5 w-3.5 ${favorite ? 'fill-current' : ''}`} />
</button>
</motion.div>
);
})}
</div>
</section>
</main>
<footer className="relative mt-8 border-t border-white/[0.07] bg-black/15 px-4 py-5 text-center sm:px-6">
<p className="text-[9px] font-bold uppercase tracking-[0.22em] text-white/25">VelocityDrift · Built for the late-night run</p>
</footer>
<AnimatePresence>
{tuneOpen && (
<motion.div key="tune-modal" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="relative z-40">
<TuneModal car={tuned} tune={tunes[selectedId]} onChange={(nextTune) => setTune(selectedId, nextTune)} onClose={() => setTuneOpen(false)} />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{screen === 'mode' && modeDialogOpen && (
<motion.div key="mode-dialog" role="presentation" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-40 flex items-center justify-center bg-black/75 p-4 backdrop-blur-md" onMouseDown={(event) => { if (event.target === event.currentTarget) closeModeDialog(); }}>
<motion.div role="dialog" aria-modal="true" aria-labelledby="mode-title" initial={{ opacity: 0, y: 12, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 8 }} className="w-full max-w-2xl rounded-[28px] border border-white/10 bg-[#0b1018] p-5 shadow-2xl shadow-black/60 sm:p-8">
<div className="flex items-start justify-between gap-4">
<div><p className="text-[9px] font-black uppercase tracking-[0.24em] text-orange-300">Event selection</p><h2 id="mode-title" className="mt-1 text-2xl font-black tracking-tight sm:text-3xl">Pick your race.</h2><p className="mt-2 text-xs text-white/40">Ready to drive: <span className="font-semibold text-white/75">{selected.brand} {selected.name}</span></p></div>
<button onClick={closeModeDialog} aria-label="Close event selection" className="flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-white/[0.03] text-white/50 transition hover:text-white"><X className="h-4 w-4" /></button>
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<ModeCard icon={CarFront} title="Open World" eyebrow="Free roam" accent="bg-cyan-500/15" description="Cruise a neon-lit city, weave through traffic, explore at your own pace, and test your tuned build." onClick={() => enterMode('open')} />
<ModeCard icon={Trophy} title="Drag Race" eyebrow="402 metres" accent="bg-orange-500/15" description="Launch hard over a true quarter-mile, work through your gears, and use nitro to chase a better time." onClick={() => enterMode('drag')} />
</div>
<button onClick={closeModeDialog} className="mt-4 w-full rounded-xl py-2.5 text-xs font-semibold text-white/40 transition hover:bg-white/[0.04] hover:text-white/70">Back to garage</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
);
}
Editor is loading...
Leave a Comment