diff --git a/src/components/admin/settings/edition-settings-tab.tsx b/src/components/admin/settings/edition-settings-tab.tsx new file mode 100644 index 0000000..10a1300 --- /dev/null +++ b/src/components/admin/settings/edition-settings-tab.tsx @@ -0,0 +1,230 @@ +'use client' + +import { useEffect, useState } from 'react' +import { trpc } from '@/lib/trpc/client' +import { useEdition } from '@/contexts/edition-context' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { Skeleton } from '@/components/ui/skeleton' +import { Loader2, Salad, ScrollText, Stamp, Users } from 'lucide-react' +import { toast } from 'sonner' + +function NumberField({ + id, + label, + hint, + value, + onCommit, + disabled, + min, + max, +}: { + id: string + label: string + hint?: string + value: number | null + onCommit: (next: number) => void + disabled?: boolean + min?: number + max?: number +}) { + const [draft, setDraft] = useState(value != null ? String(value) : '') + useEffect(() => { + setDraft(value != null ? String(value) : '') + }, [value]) + + return ( +
+ + setDraft(e.target.value)} + onBlur={() => { + const parsed = Number(draft) + if (!Number.isFinite(parsed) || parsed === value) return + if (min !== undefined && parsed < min) return + if (max !== undefined && parsed > max) return + onCommit(parsed) + }} + className="max-w-[12rem]" + /> + {hint &&

{hint}

} +
+ ) +} + +export function EditionSettingsTab() { + const { currentEdition } = useEdition() + const programId = currentEdition?.id ?? null + + const utils = trpc.useUtils() + const { data, isLoading } = trpc.program.getEditionSettings.useQuery( + { programId: programId ?? '' }, + { enabled: !!programId }, + ) + const update = trpc.program.updateEditionSettings.useMutation({ + onSuccess: () => { + if (programId) utils.program.getEditionSettings.invalidate({ programId }) + toast.success('Edition settings updated') + }, + onError: (e) => toast.error(e.message), + }) + + if (!programId) { + return ( +

+ Select an edition from the sidebar dropdown to manage settings. +

+ ) + } + if (isLoading || !data) { + return ( +
+ + +
+ ) + } + + const noLiveFinalRound = data.liveFinalRoundId == null + + return ( +
+ {/* Grand-finale logistics */} + + + +
+ +
+ Grand-finale logistics +
+ + Per-edition limits and deadlines that drive finalist confirmation, attendee + editing, and visa visibility. + +
+ + + update.mutate({ programId, defaultAttendeeCap: next }) + } + /> + + update.mutate({ programId, confirmationWindowHours: next }) + } + /> + + update.mutate({ programId, attendeeEditCutoffHours: next }) + } + /> + +
+ + {/* Visa */} + + + +
+ +
+ Visa +
+ + Visa documents are exchanged over email and never stored on the platform — + we track only process metadata. Choose whether teams see their own status. + +
+ +
+
+ +

+ When on, attendees with needsVisa=true see their status on the + applicant dashboard. When off, only admins see the workflow. +

+
+ + update.mutate({ programId, visaStatusVisibleToMembers: v }) + } + /> +
+
+
+ + {/* Coming soon */} + + + Coming soon + + Lunch-event configuration and editable email templates land in upcoming + updates and will surface here. + + + +
+ Lunch event — dishes, allergies, RSVP deadline +
+
+ Email templates — editable subject + body + for confirmation, decline-cascade, mentor onboarding, etc. +
+
+
+ + {update.isPending && ( +
+ Saving… +
+ )} +
+ ) +} diff --git a/src/components/settings/settings-content.tsx b/src/components/settings/settings-content.tsx index afcac06..2c76fea 100644 --- a/src/components/settings/settings-content.tsx +++ b/src/components/settings/settings-content.tsx @@ -25,7 +25,9 @@ import { ShieldAlert, Webhook, MessageCircle, + Trophy, } from 'lucide-react' +import { EditionSettingsTab } from '@/components/admin/settings/edition-settings-tab' import Link from 'next/link' import { AnimatedCard } from '@/components/shared/animated-container' import { AISettingsForm } from './ai-settings-form' @@ -179,6 +181,10 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin Defaults + + + Edition + Branding @@ -252,6 +258,10 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin Defaults + + + Edition + Branding @@ -464,6 +474,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin )} + + + + + +