'use client' import { useState, useEffect } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { SidebarStepper } from '@/components/ui/sidebar-stepper' import type { StepConfig } from '@/components/ui/sidebar-stepper' import { ArrowLeft } from 'lucide-react' import { BasicsSection } from '@/components/admin/competition/sections/basics-section' import { RoundsSection } from '@/components/admin/competition/sections/rounds-section' import { JuryGroupsSection } from '@/components/admin/competition/sections/jury-groups-section' import { ReviewSection } from '@/components/admin/competition/sections/review-section' import { useEdition } from '@/contexts/edition-context' type WizardRound = { tempId: string name: string slug: string roundType: string sortOrder: number configJson: Record } type WizardJuryGroup = { tempId: string name: string slug: string defaultMaxAssignments: number defaultCapMode: string sortOrder: number } type WizardState = { programId: string name: string slug: string categoryMode: string startupFinalistCount: number conceptFinalistCount: number notifyOnRoundAdvance: boolean notifyOnDeadlineApproach: boolean deadlineReminderDays: number[] rounds: WizardRound[] juryGroups: WizardJuryGroup[] } const defaultRounds: WizardRound[] = [ { tempId: crypto.randomUUID(), name: 'Intake', slug: 'intake', roundType: 'INTAKE', sortOrder: 0, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Filtering', slug: 'filtering', roundType: 'FILTERING', sortOrder: 1, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Evaluation (Jury 1)', slug: 'evaluation-jury-1', roundType: 'EVALUATION', sortOrder: 2, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Submission', slug: 'submission', roundType: 'SUBMISSION', sortOrder: 3, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Evaluation (Jury 2)', slug: 'evaluation-jury-2', roundType: 'EVALUATION', sortOrder: 4, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Mentoring', slug: 'mentoring', roundType: 'MENTORING', sortOrder: 5, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Live Final', slug: 'live-final', roundType: 'LIVE_FINAL', sortOrder: 6, configJson: {}, }, { tempId: crypto.randomUUID(), name: 'Deliberation', slug: 'deliberation', roundType: 'DELIBERATION', sortOrder: 7, configJson: {}, }, ] export default function NewCompetitionPage() { const router = useRouter() const searchParams = useSearchParams() const { currentEdition } = useEdition() const paramProgramId = searchParams.get('programId') const programId = paramProgramId || currentEdition?.id || '' const [currentStep, setCurrentStep] = useState(0) const [isDirty, setIsDirty] = useState(false) const [state, setState] = useState({ programId, name: '', slug: '', categoryMode: 'SHARED', startupFinalistCount: 3, conceptFinalistCount: 3, notifyOnRoundAdvance: true, notifyOnDeadlineApproach: true, deadlineReminderDays: [7, 3, 1], rounds: defaultRounds, juryGroups: [], }) useEffect(() => { if (programId) { setState((prev) => ({ ...prev, programId })) } }, [programId]) useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (isDirty) { e.preventDefault() e.returnValue = '' } } window.addEventListener('beforeunload', handleBeforeUnload) return () => window.removeEventListener('beforeunload', handleBeforeUnload) }, [isDirty]) const utils = trpc.useUtils() const createCompetitionMutation = trpc.competition.create.useMutation() const createRoundMutation = trpc.round.create.useMutation() const createJuryGroupMutation = trpc.juryGroup.create.useMutation() const handleStateChange = (updates: Partial) => { setState((prev) => ({ ...prev, ...updates })) setIsDirty(true) // Auto-generate slug from name if name changed if (updates.name !== undefined && updates.slug === undefined) { const autoSlug = updates.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') setState((prev) => ({ ...prev, slug: autoSlug })) } } const handleSubmit = async () => { if (!state.name.trim()) { toast.error('Competition name is required') setCurrentStep(0) return } if (!state.slug.trim()) { toast.error('Competition slug is required') setCurrentStep(0) return } if (state.rounds.length === 0) { toast.error('At least one round is required') setCurrentStep(1) return } try { // Create competition const competition = await createCompetitionMutation.mutateAsync({ programId: state.programId, name: state.name, slug: state.slug, categoryMode: state.categoryMode, startupFinalistCount: state.startupFinalistCount, conceptFinalistCount: state.conceptFinalistCount, notifyOnRoundAdvance: state.notifyOnRoundAdvance, notifyOnDeadlineApproach: state.notifyOnDeadlineApproach, deadlineReminderDays: state.deadlineReminderDays, }) // Create rounds for (const round of state.rounds) { await createRoundMutation.mutateAsync({ competitionId: competition.id, name: round.name, slug: round.slug, roundType: round.roundType as any, sortOrder: round.sortOrder, configJson: round.configJson, }) } // Create jury groups for (const group of state.juryGroups) { await createJuryGroupMutation.mutateAsync({ competitionId: competition.id, name: group.name, slug: group.slug, defaultMaxAssignments: group.defaultMaxAssignments, defaultCapMode: group.defaultCapMode as any, sortOrder: group.sortOrder, }) } toast.success('Competition created successfully') setIsDirty(false) utils.competition.list.invalidate() router.push(`/admin/competitions/${competition.id}` as Route) } catch (err: any) { toast.error(err.message || 'Failed to create competition') } } const steps: StepConfig[] = [ { title: 'Basics', description: 'Name and settings', isValid: !!state.name && !!state.slug, }, { title: 'Rounds', description: 'Configure rounds', isValid: state.rounds.length > 0, }, { title: 'Jury Groups', description: 'Add jury groups', isValid: true, // Optional }, { title: 'Review', description: 'Confirm and create', isValid: !!state.name && !!state.slug && state.rounds.length > 0, }, ] const canSubmit = steps.every((s) => s.isValid) return (
{/* Header */}

New Competition

Create a multi-round competition workflow

{/* Wizard */} handleStateChange({ rounds })} /> handleStateChange({ juryGroups })} />
) }