2026-01-30 13:41:32 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react'
|
2026-02-04 14:15:06 +01:00
|
|
|
import { useParams, useRouter } from 'next/navigation'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { useForm } from 'react-hook-form'
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
import { z } from 'zod'
|
2026-02-04 14:15:06 +01:00
|
|
|
import { motion, AnimatePresence } from 'motion/react'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { trpc } from '@/lib/trpc/client'
|
2026-02-04 14:15:06 +01:00
|
|
|
import { toast } from 'sonner'
|
2026-01-30 13:41:32 +01:00
|
|
|
import {
|
2026-02-04 14:15:06 +01:00
|
|
|
Waves,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
Loader2,
|
|
|
|
|
CheckCircle,
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
ArrowRight,
|
|
|
|
|
Clock,
|
|
|
|
|
} from 'lucide-react'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
2026-02-04 14:15:06 +01:00
|
|
|
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<typeof applicationSchema>
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-01-30 13:41:32 +01:00
|
|
|
const params = useParams()
|
2026-02-04 14:15:06 +01:00
|
|
|
const router = useRouter()
|
2026-01-30 13:41:32 +01:00
|
|
|
const slug = params.slug as string
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
const [currentStep, setCurrentStep] = useState(0)
|
|
|
|
|
const [direction, setDirection] = useState(0)
|
2026-01-30 13:41:32 +01:00
|
|
|
const [submitted, setSubmitted] = useState(false)
|
2026-02-04 14:15:06 +01:00
|
|
|
const [submissionMessage, setSubmissionMessage] = useState('')
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const { data: config, isLoading, error } = trpc.application.getConfig.useQuery(
|
|
|
|
|
{ roundSlug: slug },
|
2026-01-30 13:41:32 +01:00
|
|
|
{ retry: false }
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const submitMutation = trpc.application.submit.useMutation({
|
2026-01-30 13:41:32 +01:00
|
|
|
onSuccess: (result) => {
|
|
|
|
|
setSubmitted(true)
|
2026-02-04 14:15:06 +01:00
|
|
|
setSubmissionMessage(result.message)
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
toast.error(error.message)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const form = useForm<ApplicationFormData>({
|
|
|
|
|
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')
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const isBusinessConcept = competitionCategory === 'BUSINESS_CONCEPT'
|
|
|
|
|
const isStartup = competitionCategory === 'STARTUP'
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const validateCurrentStep = async () => {
|
|
|
|
|
const currentFields = STEPS[currentStep].fields as (keyof ApplicationFormData)[]
|
|
|
|
|
if (currentFields.length === 0) return true
|
|
|
|
|
return await trigger(currentFields)
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const nextStep = async () => {
|
|
|
|
|
const isValid = await validateCurrentStep()
|
|
|
|
|
if (isValid && currentStep < STEPS.length - 1) {
|
|
|
|
|
setDirection(1)
|
|
|
|
|
setCurrentStep((prev) => prev + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const prevStep = () => {
|
|
|
|
|
if (currentStep > 0) {
|
|
|
|
|
setDirection(-1)
|
|
|
|
|
setCurrentStep((prev) => prev - 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const onSubmit = async (data: ApplicationFormData) => {
|
|
|
|
|
if (!config) return
|
2026-01-30 13:41:32 +01:00
|
|
|
await submitMutation.mutateAsync({
|
2026-02-04 14:15:06 +01:00
|
|
|
roundId: config.round.id,
|
2026-01-30 13:41:32 +01:00
|
|
|
data,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// 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
|
2026-01-30 13:41:32 +01:00
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
2026-02-04 14:15:06 +01:00
|
|
|
<div className="min-h-screen bg-gradient-to-br from-background to-muted/30 flex items-center justify-center p-4">
|
|
|
|
|
<div className="w-full max-w-2xl space-y-6">
|
|
|
|
|
<div className="flex items-center justify-center gap-3">
|
|
|
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
|
|
|
<span className="text-lg text-muted-foreground">Loading application...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// Error state
|
2026-01-30 13:41:32 +01:00
|
|
|
if (error) {
|
|
|
|
|
return (
|
2026-02-04 14:15:06 +01:00
|
|
|
<div className="min-h-screen bg-gradient-to-br from-background to-muted/30 flex items-center justify-center p-4">
|
|
|
|
|
<div className="w-full max-w-md text-center">
|
|
|
|
|
<AlertCircle className="mx-auto h-16 w-16 text-destructive mb-4" />
|
|
|
|
|
<h1 className="text-2xl font-bold mb-2">Application Not Available</h1>
|
|
|
|
|
<p className="text-muted-foreground mb-6">{error.message}</p>
|
|
|
|
|
<Button variant="outline" onClick={() => router.push('/')}>
|
|
|
|
|
Return Home
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// Applications closed state
|
|
|
|
|
if (config && !config.round.isOpen) {
|
2026-01-30 13:41:32 +01:00
|
|
|
return (
|
2026-02-04 14:15:06 +01:00
|
|
|
<div className="min-h-screen bg-gradient-to-br from-background to-muted/30 flex items-center justify-center p-4">
|
|
|
|
|
<div className="w-full max-w-md text-center">
|
|
|
|
|
<Clock className="mx-auto h-16 w-16 text-muted-foreground mb-4" />
|
|
|
|
|
<h1 className="text-2xl font-bold mb-2">Applications Closed</h1>
|
|
|
|
|
<p className="text-muted-foreground mb-6">
|
|
|
|
|
The application period for {config.program.name} {config.program.year} has ended.
|
|
|
|
|
{config.round.submissionEndDate && (
|
|
|
|
|
<span className="block mt-2">
|
|
|
|
|
Submissions closed on{' '}
|
|
|
|
|
{new Date(config.round.submissionEndDate).toLocaleDateString('en-US', {
|
|
|
|
|
dateStyle: 'long',
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<Button variant="outline" onClick={() => router.push('/')}>
|
|
|
|
|
Return Home
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// Success state
|
|
|
|
|
if (submitted) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-background to-muted/30 flex items-center justify-center p-4">
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
|
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
|
|
|
className="w-full max-w-md text-center"
|
|
|
|
|
>
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ scale: 0 }}
|
|
|
|
|
animate={{ scale: 1 }}
|
|
|
|
|
transition={{ delay: 0.2, type: 'spring' }}
|
|
|
|
|
>
|
|
|
|
|
<CheckCircle className="mx-auto h-20 w-20 text-green-500 mb-6" />
|
|
|
|
|
</motion.div>
|
|
|
|
|
<h1 className="text-3xl font-bold mb-4">Application Submitted!</h1>
|
|
|
|
|
<p className="text-muted-foreground mb-8">{submissionMessage}</p>
|
|
|
|
|
<Button onClick={() => router.push('/')}>
|
|
|
|
|
Return Home
|
|
|
|
|
</Button>
|
|
|
|
|
</motion.div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2026-01-30 13:41:32 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
if (!config) return null
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const progress = ((currentStep + 1) / STEPS.length) * 100
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-background to-muted/30">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
|
|
|
<div className="mx-auto max-w-4xl px-4 py-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
|
|
|
<Waves className="h-5 w-5 text-primary" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="font-semibold">{config.program.name}</h1>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{config.program.year} Application</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
Step {currentStep + 1} of {STEPS.length}
|
|
|
|
|
</span>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
{/* Progress bar */}
|
|
|
|
|
<div className="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
|
|
|
<motion.div
|
|
|
|
|
className="h-full bg-gradient-to-r from-primary to-primary/70"
|
|
|
|
|
initial={{ width: 0 }}
|
|
|
|
|
animate={{ width: `${progress}%` }}
|
|
|
|
|
transition={{ duration: 0.3 }}
|
2026-01-30 13:41:32 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
{/* Step indicators */}
|
|
|
|
|
<div className="mt-3 flex justify-between">
|
|
|
|
|
{STEPS.map((step, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={step.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (index < currentStep) {
|
|
|
|
|
setDirection(index < currentStep ? -1 : 1)
|
|
|
|
|
setCurrentStep(index)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
disabled={index > currentStep}
|
|
|
|
|
className={cn(
|
|
|
|
|
'hidden text-xs font-medium transition-colors sm:block',
|
|
|
|
|
index === currentStep && 'text-primary',
|
|
|
|
|
index < currentStep && 'text-muted-foreground hover:text-foreground cursor-pointer',
|
|
|
|
|
index > currentStep && 'text-muted-foreground/50'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{step.title}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
2026-02-04 14:15:06 +01:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{/* Main content */}
|
|
|
|
|
<main className="mx-auto max-w-4xl px-4 py-8">
|
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
|
|
|
<div className="relative min-h-[500px]">
|
|
|
|
|
<AnimatePresence initial={false} custom={direction} mode="wait">
|
|
|
|
|
<motion.div
|
|
|
|
|
key={currentStep}
|
|
|
|
|
custom={direction}
|
|
|
|
|
variants={variants}
|
|
|
|
|
initial="enter"
|
|
|
|
|
animate="center"
|
|
|
|
|
exit="exit"
|
|
|
|
|
transition={{
|
|
|
|
|
x: { type: 'spring', stiffness: 300, damping: 30 },
|
|
|
|
|
opacity: { duration: 0.2 },
|
|
|
|
|
}}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
{currentStep === 0 && (
|
|
|
|
|
<StepWelcome
|
|
|
|
|
programName={config.program.name}
|
|
|
|
|
programYear={config.program.year}
|
|
|
|
|
value={competitionCategory}
|
|
|
|
|
onChange={(value) => form.setValue('competitionCategory', value)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{currentStep === 1 && <StepContact form={form} />}
|
|
|
|
|
{currentStep === 2 && <StepProject form={form} />}
|
|
|
|
|
{currentStep === 3 && <StepTeam form={form} />}
|
|
|
|
|
{currentStep === 4 && (
|
|
|
|
|
<StepAdditional
|
|
|
|
|
form={form}
|
|
|
|
|
isBusinessConcept={isBusinessConcept}
|
|
|
|
|
isStartup={isStartup}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{currentStep === 5 && (
|
|
|
|
|
<StepReview form={form} programName={config.program.name} />
|
|
|
|
|
)}
|
|
|
|
|
</motion.div>
|
|
|
|
|
</AnimatePresence>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
{/* Navigation buttons */}
|
|
|
|
|
<div className="mt-8 flex items-center justify-between border-t pt-6">
|
2026-01-30 13:41:32 +01:00
|
|
|
<Button
|
2026-02-04 14:15:06 +01:00
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={prevStep}
|
|
|
|
|
disabled={currentStep === 0 || submitMutation.isPending}
|
|
|
|
|
className={cn(currentStep === 0 && 'invisible')}
|
2026-01-30 13:41:32 +01:00
|
|
|
>
|
2026-02-04 14:15:06 +01:00
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back
|
2026-01-30 13:41:32 +01:00
|
|
|
</Button>
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
{currentStep < STEPS.length - 1 ? (
|
|
|
|
|
<Button type="button" onClick={nextStep}>
|
|
|
|
|
Continue
|
|
|
|
|
<ArrowRight className="ml-2 h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button type="submit" disabled={submitMutation.isPending}>
|
|
|
|
|
{submitMutation.isPending ? (
|
|
|
|
|
<>
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
Submitting...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<CheckCircle className="mr-2 h-4 w-4" />
|
|
|
|
|
Submit Application
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
{/* Footer with deadline info */}
|
|
|
|
|
{config.round.submissionEndDate && (
|
|
|
|
|
<footer className="fixed bottom-0 left-0 right-0 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 py-3">
|
|
|
|
|
<div className="mx-auto max-w-4xl px-4 text-center text-sm text-muted-foreground">
|
|
|
|
|
<Clock className="inline-block mr-1 h-4 w-4" />
|
|
|
|
|
Applications due by{' '}
|
|
|
|
|
{new Date(config.round.submissionEndDate).toLocaleDateString('en-US', {
|
|
|
|
|
dateStyle: 'long',
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
)}
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|