'use client' import { useState, useEffect } from 'react' import { useParams, useRouter } from 'next/navigation' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { motion, AnimatePresence } from 'motion/react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Waves, AlertCircle, Loader2, CheckCircle, ArrowLeft, ArrowRight, Clock, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { StepWelcome, StepContact, StepProject, StepTeam, StepAdditional, StepReview, } from '@/components/forms/apply-steps' import { CompetitionCategory, OceanIssue, TeamMemberRole } from '@prisma/client' import { cn } from '@/lib/utils' // Form validation schema const teamMemberSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email address'), role: z.nativeEnum(TeamMemberRole).default('MEMBER'), title: z.string().optional(), }) const applicationSchema = z.object({ competitionCategory: z.nativeEnum(CompetitionCategory), contactName: z.string().min(2, 'Full name is required'), contactEmail: z.string().email('Invalid email address'), contactPhone: z.string().min(5, 'Phone number is required'), country: z.string().min(2, 'Country is required'), city: z.string().optional(), projectName: z.string().min(2, 'Project name is required').max(200), teamName: z.string().optional(), description: z.string().min(20, 'Description must be at least 20 characters'), oceanIssue: z.nativeEnum(OceanIssue), teamMembers: z.array(teamMemberSchema).optional(), institution: z.string().optional(), startupCreatedDate: z.string().optional(), wantsMentorship: z.boolean().default(false), referralSource: z.string().optional(), gdprConsent: z.boolean().refine((val) => val === true, { message: 'You must agree to the data processing terms', }), }) type ApplicationFormData = z.infer const STEPS = [ { id: 'welcome', title: 'Category', fields: ['competitionCategory'] }, { id: 'contact', title: 'Contact', fields: ['contactName', 'contactEmail', 'contactPhone', 'country'] }, { id: 'project', title: 'Project', fields: ['projectName', 'description', 'oceanIssue'] }, { id: 'team', title: 'Team', fields: [] }, { id: 'additional', title: 'Details', fields: [] }, { id: 'review', title: 'Review', fields: ['gdprConsent'] }, ] export default function ApplyWizardPage() { const params = useParams() const router = useRouter() const slug = params.slug as string const [currentStep, setCurrentStep] = useState(0) const [direction, setDirection] = useState(0) const [submitted, setSubmitted] = useState(false) const [submissionMessage, setSubmissionMessage] = useState('') const { data: config, isLoading, error } = trpc.application.getConfig.useQuery( { roundSlug: slug }, { retry: false } ) const submitMutation = trpc.application.submit.useMutation({ onSuccess: (result) => { setSubmitted(true) setSubmissionMessage(result.message) }, onError: (error) => { toast.error(error.message) }, }) const form = useForm({ resolver: zodResolver(applicationSchema), defaultValues: { competitionCategory: undefined, contactName: '', contactEmail: '', contactPhone: '', country: '', city: '', projectName: '', teamName: '', description: '', oceanIssue: undefined, teamMembers: [], institution: '', startupCreatedDate: '', wantsMentorship: false, referralSource: '', gdprConsent: false, }, mode: 'onChange', }) const { watch, trigger, handleSubmit } = form const competitionCategory = watch('competitionCategory') const isBusinessConcept = competitionCategory === 'BUSINESS_CONCEPT' const isStartup = competitionCategory === 'STARTUP' const validateCurrentStep = async () => { const currentFields = STEPS[currentStep].fields as (keyof ApplicationFormData)[] if (currentFields.length === 0) return true return await trigger(currentFields) } const nextStep = async () => { const isValid = await validateCurrentStep() if (isValid && currentStep < STEPS.length - 1) { setDirection(1) setCurrentStep((prev) => prev + 1) } } const prevStep = () => { if (currentStep > 0) { setDirection(-1) setCurrentStep((prev) => prev - 1) } } const onSubmit = async (data: ApplicationFormData) => { if (!config) return await submitMutation.mutateAsync({ roundId: config.round.id, data, }) } // Handle keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && currentStep < STEPS.length - 1) { e.preventDefault() nextStep() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [currentStep]) // Loading state if (isLoading) { return (
Loading application...
) } // Error state if (error) { return (

Application Not Available

{error.message}

) } // Applications closed state if (config && !config.round.isOpen) { return (

Applications Closed

The application period for {config.program.name} {config.program.year} has ended. {config.round.submissionEndDate && ( Submissions closed on{' '} {new Date(config.round.submissionEndDate).toLocaleDateString('en-US', { dateStyle: 'long', })} )}

) } // Success state if (submitted) { return (

Application Submitted!

{submissionMessage}

) } if (!config) return null const progress = ((currentStep + 1) / STEPS.length) * 100 const variants = { enter: (direction: number) => ({ x: direction > 0 ? 50 : -50, opacity: 0, }), center: { x: 0, opacity: 1, }, exit: (direction: number) => ({ x: direction < 0 ? 50 : -50, opacity: 0, }), } return (
{/* Header */}

{config.program.name}

{config.program.year} Application

Step {currentStep + 1} of {STEPS.length}
{/* Progress bar */}
{/* Step indicators */}
{STEPS.map((step, index) => ( ))}
{/* Main content */}
{currentStep === 0 && ( form.setValue('competitionCategory', value)} /> )} {currentStep === 1 && } {currentStep === 2 && } {currentStep === 3 && } {currentStep === 4 && ( )} {currentStep === 5 && ( )}
{/* Navigation buttons */}
{currentStep < STEPS.length - 1 ? ( ) : ( )}
{/* Footer with deadline info */} {config.round.submissionEndDate && ( )}
) }