'use client'; import { useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; import { User, Globe, Flag, Car, Heart, Clock, Check, ChevronRight, ChevronLeft, Gamepad2, Target, Zap, Trophy, Users, MapPin, Mail, Calendar, AlertCircle, } from 'lucide-react'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import Heading from '@/components/ui/Heading'; import { Driver } from '@gridpilot/racing'; import { getDriverRepository } from '@/lib/di-container'; // ============================================================================ // TYPES // ============================================================================ type OnboardingStep = 1 | 2 | 3 | 4; interface PersonalInfo { firstName: string; lastName: string; displayName: string; email: string; country: string; timezone: string; } interface RacingInfo { iracingId: string; experienceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro'; preferredDiscipline: string; yearsRacing: string; } interface PreferencesInfo { favoriteTrack: string; favoriteCar: string; racingStyle: string; availability: string; lookingForTeam: boolean; openToRequests: boolean; } interface BioInfo { bio: string; goals: string; } interface FormErrors { firstName?: string; lastName?: string; displayName?: string; email?: string; country?: string; iracingId?: string; submit?: string; } // ============================================================================ // CONSTANTS // ============================================================================ const COUNTRIES = [ { code: 'US', name: 'United States' }, { code: 'GB', name: 'United Kingdom' }, { code: 'DE', name: 'Germany' }, { code: 'NL', name: 'Netherlands' }, { code: 'FR', name: 'France' }, { code: 'IT', name: 'Italy' }, { code: 'ES', name: 'Spain' }, { code: 'AU', name: 'Australia' }, { code: 'CA', name: 'Canada' }, { code: 'BR', name: 'Brazil' }, { code: 'JP', name: 'Japan' }, { code: 'BE', name: 'Belgium' }, { code: 'AT', name: 'Austria' }, { code: 'CH', name: 'Switzerland' }, { code: 'SE', name: 'Sweden' }, { code: 'NO', name: 'Norway' }, { code: 'DK', name: 'Denmark' }, { code: 'FI', name: 'Finland' }, { code: 'PL', name: 'Poland' }, { code: 'PT', name: 'Portugal' }, ]; const TIMEZONES = [ { value: 'America/New_York', label: 'Eastern Time (ET)' }, { value: 'America/Chicago', label: 'Central Time (CT)' }, { value: 'America/Denver', label: 'Mountain Time (MT)' }, { value: 'America/Los_Angeles', label: 'Pacific Time (PT)' }, { value: 'Europe/London', label: 'Greenwich Mean Time (GMT)' }, { value: 'Europe/Berlin', label: 'Central European Time (CET)' }, { value: 'Europe/Paris', label: 'Central European Time (CET)' }, { value: 'Australia/Sydney', label: 'Australian Eastern Time (AET)' }, { value: 'Asia/Tokyo', label: 'Japan Standard Time (JST)' }, { value: 'America/Sao_Paulo', label: 'BrasΓ­lia Time (BRT)' }, ]; const EXPERIENCE_LEVELS = [ { value: 'beginner', label: 'Beginner', description: 'Just getting started with sim racing' }, { value: 'intermediate', label: 'Intermediate', description: '1-2 years of experience' }, { value: 'advanced', label: 'Advanced', description: '3+ years, competitive racing' }, { value: 'pro', label: 'Pro/Semi-Pro', description: 'Professional level competition' }, ]; const DISCIPLINES = [ { value: 'road', label: 'Road Racing', icon: '🏎️' }, { value: 'oval', label: 'Oval Racing', icon: '🏁' }, { value: 'dirt-road', label: 'Dirt Road', icon: 'πŸš—' }, { value: 'dirt-oval', label: 'Dirt Oval', icon: '🏎️' }, { value: 'multi', label: 'Multi-discipline', icon: '🎯' }, ]; const RACING_STYLES = [ { value: 'aggressive', label: 'Aggressive Overtaker', icon: '⚑' }, { value: 'consistent', label: 'Consistent Pacer', icon: 'πŸ“ˆ' }, { value: 'strategic', label: 'Strategic Calculator', icon: '🧠' }, { value: 'late-braker', label: 'Late Braker', icon: '🎯' }, { value: 'smooth', label: 'Smooth Operator', icon: '✨' }, ]; const AVAILABILITY = [ { value: 'weekday-evenings', label: 'Weekday Evenings (18:00-23:00)' }, { value: 'weekends', label: 'Weekends Only' }, { value: 'late-nights', label: 'Late Nights (22:00-02:00)' }, { value: 'flexible', label: 'Flexible Schedule' }, { value: 'mornings', label: 'Mornings (06:00-12:00)' }, ]; // ============================================================================ // HELPER COMPONENTS // ============================================================================ function StepIndicator({ currentStep, totalSteps }: { currentStep: number; totalSteps: number }) { const steps = [ { id: 1, label: 'Personal', icon: User }, { id: 2, label: 'Racing', icon: Gamepad2 }, { id: 3, label: 'Preferences', icon: Heart }, { id: 4, label: 'Bio & Goals', icon: Target }, ]; return (
{steps.map((step, index) => { const Icon = step.icon; const isCompleted = step.id < currentStep; const isCurrent = step.id === currentStep; return (
{isCompleted ? ( ) : ( )}
{step.label}
{index < steps.length - 1 && (
)}
); })}
); } function SelectableCard({ selected, onClick, icon, label, description, className = '', }: { selected: boolean; onClick: () => void; icon?: string | React.ReactNode; label: string; description?: string; className?: string; }) { return ( ); } // ============================================================================ // MAIN COMPONENT // ============================================================================ export default function OnboardingWizard() { const router = useRouter(); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState({}); // Form state const [personalInfo, setPersonalInfo] = useState({ firstName: '', lastName: '', displayName: '', email: '', country: '', timezone: '', }); const [racingInfo, setRacingInfo] = useState({ iracingId: '', experienceLevel: 'intermediate', preferredDiscipline: 'road', yearsRacing: '', }); const [preferencesInfo, setPreferencesInfo] = useState({ favoriteTrack: '', favoriteCar: '', racingStyle: 'consistent', availability: 'weekday-evenings', lookingForTeam: false, openToRequests: true, }); const [bioInfo, setBioInfo] = useState({ bio: '', goals: '', }); // Validation const validateStep = async (currentStep: OnboardingStep): Promise => { const newErrors: FormErrors = {}; if (currentStep === 1) { if (!personalInfo.firstName.trim()) { newErrors.firstName = 'First name is required'; } if (!personalInfo.lastName.trim()) { newErrors.lastName = 'Last name is required'; } if (!personalInfo.displayName.trim()) { newErrors.displayName = 'Display name is required'; } else if (personalInfo.displayName.length < 3) { newErrors.displayName = 'Display name must be at least 3 characters'; } if (!personalInfo.country) { newErrors.country = 'Please select your country'; } } if (currentStep === 2) { if (!racingInfo.iracingId.trim()) { newErrors.iracingId = 'iRacing ID is required'; } else { // Check if iRacing ID already exists const driverRepo = getDriverRepository(); const exists = await driverRepo.existsByIRacingId(racingInfo.iracingId); if (exists) { newErrors.iracingId = 'This iRacing ID is already registered'; } } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleNext = async () => { const isValid = await validateStep(step); if (isValid && step < 4) { setStep((step + 1) as OnboardingStep); } }; const handleBack = () => { if (step > 1) { setStep((step - 1) as OnboardingStep); } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (loading) return; // Validate all steps for (let s = 1; s <= 4; s++) { const isValid = await validateStep(s as OnboardingStep); if (!isValid) { setStep(s as OnboardingStep); return; } } setLoading(true); setErrors({}); try { const driverRepo = getDriverRepository(); // Build bio with all the additional info const fullBio = [ bioInfo.bio, bioInfo.goals ? `Goals: ${bioInfo.goals}` : '', `Experience: ${EXPERIENCE_LEVELS.find(e => e.value === racingInfo.experienceLevel)?.label}`, racingInfo.yearsRacing ? `Years Racing: ${racingInfo.yearsRacing}` : '', `Discipline: ${DISCIPLINES.find(d => d.value === racingInfo.preferredDiscipline)?.label}`, `Style: ${RACING_STYLES.find(s => s.value === preferencesInfo.racingStyle)?.label}`, preferencesInfo.favoriteTrack ? `Favorite Track: ${preferencesInfo.favoriteTrack}` : '', preferencesInfo.favoriteCar ? `Favorite Car: ${preferencesInfo.favoriteCar}` : '', `Available: ${AVAILABILITY.find(a => a.value === preferencesInfo.availability)?.label}`, preferencesInfo.lookingForTeam ? 'πŸ” Looking for team' : '', preferencesInfo.openToRequests ? 'πŸ‘‹ Open to friend requests' : '', ].filter(Boolean).join('\n'); const driver = Driver.create({ id: crypto.randomUUID(), iracingId: racingInfo.iracingId.trim(), name: personalInfo.displayName.trim(), country: personalInfo.country, bio: fullBio || undefined, }); await driverRepo.create(driver); router.push('/dashboard'); router.refresh(); } catch (error) { setErrors({ submit: error instanceof Error ? error.message : 'Failed to create profile', }); setLoading(false); } }; const getCountryFlag = (countryCode: string): string => { const code = countryCode.toUpperCase(); if (code.length === 2) { const codePoints = [...code].map(char => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); } return '🏁'; }; return (
{/* Header */}
Welcome to GridPilot

Let's set up your racing profile in just a few steps

{/* Progress Indicator */} {/* Form Card */} {/* Background accent */}
{/* Step 1: Personal Information */} {step === 1 && (
Personal Information

Tell us a bit about yourself

setPersonalInfo({ ...personalInfo, firstName: e.target.value }) } error={!!errors.firstName} errorMessage={errors.firstName} placeholder="Max" disabled={loading} />
setPersonalInfo({ ...personalInfo, lastName: e.target.value }) } error={!!errors.lastName} errorMessage={errors.lastName} placeholder="Verstappen" disabled={loading} />
setPersonalInfo({ ...personalInfo, displayName: e.target.value }) } error={!!errors.displayName} errorMessage={errors.displayName} placeholder="SuperMax33" disabled={loading} />
setPersonalInfo({ ...personalInfo, email: e.target.value }) } placeholder="max@racing.com" disabled={loading} className="pl-10" />
{errors.country && (

{errors.country}

)}
)} {/* Step 2: Racing Information */} {step === 2 && (
Racing Background

Tell us about your racing experience

# setRacingInfo({ ...racingInfo, iracingId: e.target.value }) } error={!!errors.iracingId} errorMessage={errors.iracingId} placeholder="123456" disabled={loading} className="pl-8" />

Find this in your iRacing account settings

{EXPERIENCE_LEVELS.map((level) => ( setRacingInfo({ ...racingInfo, experienceLevel: level.value as RacingInfo['experienceLevel'], }) } label={level.label} description={level.description} /> ))}
{DISCIPLINES.map((discipline) => ( setRacingInfo({ ...racingInfo, preferredDiscipline: discipline.value, }) } icon={discipline.icon} label={discipline.label} /> ))}
setRacingInfo({ ...racingInfo, yearsRacing: e.target.value }) } placeholder="e.g., 3 years" disabled={loading} className="pl-10" />
)} {/* Step 3: Preferences */} {step === 3 && (
Racing Preferences

Customize your racing profile

setPreferencesInfo({ ...preferencesInfo, favoriteTrack: e.target.value }) } placeholder="e.g., Spa-Francorchamps" disabled={loading} className="pl-10" />
setPreferencesInfo({ ...preferencesInfo, favoriteCar: e.target.value }) } placeholder="e.g., Porsche 911 GT3 R" disabled={loading} className="pl-10" />
{RACING_STYLES.map((style) => ( setPreferencesInfo({ ...preferencesInfo, racingStyle: style.value, }) } icon={style.icon} label={style.label} /> ))}
{AVAILABILITY.map((avail) => ( setPreferencesInfo({ ...preferencesInfo, availability: avail.value, }) } icon={} label={avail.label} /> ))}
)} {/* Step 4: Bio & Goals */} {step === 4 && (
Bio & Goals

Tell the community about yourself and your racing aspirations