import { AvatarInfo, AvatarStep } from '@/components/onboarding/AvatarStep'; import { OnboardingError } from '@/components/onboarding/OnboardingError'; import { OnboardingHelpPanel } from '@/components/onboarding/OnboardingHelpPanel'; import { OnboardingPrimaryActions } from '@/components/onboarding/OnboardingPrimaryActions'; import { OnboardingShell } from '@/components/onboarding/OnboardingShell'; import { OnboardingStepPanel } from '@/components/onboarding/OnboardingStepPanel'; import { OnboardingStepper } from '@/components/onboarding/OnboardingStepper'; import { PersonalInfo, PersonalInfoStep } from '@/components/onboarding/PersonalInfoStep'; import { ViewData } from '@/lib/contracts/view-data/ViewData'; import { Box } from '@/ui/Box'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { FormEvent } from 'react'; type OnboardingStep = 1 | 2; interface FormErrors { [key: string]: string | undefined; firstName?: string; lastName?: string; displayName?: string; country?: string; facePhoto?: string; avatar?: string; submit?: string; } export interface OnboardingViewData extends ViewData { onCompleted: () => void; onCompleteOnboarding: (data: { firstName: string; lastName: string; displayName: string; country: string; timezone?: string; }) => Promise<{ success: boolean; error?: string }>; onGenerateAvatars: (params: { facePhotoData: string; suitColor: string; }) => Promise<{ success: boolean; data?: { success: boolean; avatarUrls?: string[]; errorMessage?: string }; error?: string }>; isProcessing: boolean; step: OnboardingStep; setStep: (step: OnboardingStep) => void; errors: FormErrors; setErrors: (errors: FormErrors) => void; personalInfo: PersonalInfo; setPersonalInfo: (info: PersonalInfo) => void; avatarInfo: AvatarInfo; setAvatarInfo: (info: AvatarInfo) => void; } interface OnboardingTemplateProps { viewData: OnboardingViewData; } export function OnboardingTemplate({ viewData }: OnboardingTemplateProps) { const { onCompleted, onCompleteOnboarding, onGenerateAvatars, isProcessing, step, setStep, errors, setErrors, personalInfo, setPersonalInfo, avatarInfo, setAvatarInfo } = viewData; const steps = ['Personal Info', 'Racing Avatar']; // Validation const validateStep = (currentStep: OnboardingStep): boolean => { 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 (!avatarInfo.facePhoto) { newErrors.facePhoto = 'Please upload a photo of your face'; } if (avatarInfo.generatedAvatars.length > 0 && avatarInfo.selectedAvatarIndex === null) { newErrors.avatar = 'Please select one of the generated avatars'; } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleNext = () => { const isValid = validateStep(step); if (isValid && step < 2) { setStep((step + 1) as OnboardingStep); } }; const handleBack = () => { if (step > 1) { setStep((step - 1) as OnboardingStep); } }; const generateAvatars = async () => { if (!avatarInfo.facePhoto) { setErrors({ ...errors, facePhoto: 'Please upload a photo first' }); return; } setAvatarInfo({ ...avatarInfo, isGenerating: true, generatedAvatars: [], selectedAvatarIndex: null }); const newErrors = { ...errors }; delete newErrors.avatar; setErrors(newErrors); try { const result = await onGenerateAvatars({ facePhotoData: avatarInfo.facePhoto, suitColor: avatarInfo.suitColor, }); if (result.success && result.data?.success && result.data.avatarUrls) { setAvatarInfo({ ...avatarInfo, generatedAvatars: result.data.avatarUrls, isGenerating: false, }); } else { setErrors({ ...errors, avatar: result.data?.errorMessage || result.error || 'Failed to generate avatars' }); setAvatarInfo({ ...avatarInfo, isGenerating: false }); } } catch (error) { setErrors({ ...errors, avatar: 'Failed to generate avatars. Please try again.' }); setAvatarInfo({ ...avatarInfo, isGenerating: false }); } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!validateStep(2)) { return; } if (avatarInfo.selectedAvatarIndex === null) { setErrors({ ...errors, avatar: 'Please select an avatar' }); return; } setErrors({}); try { const result = await onCompleteOnboarding({ firstName: personalInfo.firstName.trim(), lastName: personalInfo.lastName.trim(), displayName: personalInfo.displayName.trim(), country: personalInfo.country, timezone: personalInfo.timezone || undefined, }); if (result.success) { onCompleted(); } else { setErrors({ submit: result.error || 'Failed to create profile' }); } } catch (error) { setErrors({ submit: 'Failed to create profile' }); } }; const header = ( GridPilot Onboarding System Initialization ); const sidebar = ( Welcome to GridPilot. We're setting up your racing identity. This process ensures you're ready for the track with a complete profile and a unique AI-generated avatar. {step === 2 && ( Our AI uses your photo to create a professional racing avatar. For best results, use a clear, front-facing photo with good lighting. )} ); return ( {step === 1 && ( )} {step === 2 && ( )} {errors.submit && ( )} 1 ? handleBack : undefined} onNext={step < 2 ? handleNext : undefined} isLastStep={step === 2} canNext={step === 1 ? true : avatarInfo.selectedAvatarIndex !== null} isLoading={isProcessing} type={step === 2 ? 'submit' : 'button'} /> ); }