Admin dashboard & round management UX overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
- Extract round detail monolith (2900→600 lines) into 13 standalone components - Add shared round/status config (round-config.ts) replacing 4 local copies - Delete 12 legacy competition-scoped pages, merge project pool into projects page - Add round-type-specific dashboard stat panels (submission, mentoring, live final, deliberation, summary) - Add contextual header quick actions based on active round type - Improve pipeline visualization: progress bars, checkmarks, chevron connectors, overflow fix - Add config tab completion dots (green/amber/red) and inline validation warnings - Enhance juries page with round assignments, member avatars, and cap mode badges - Add context-aware project list (recent submissions vs active evaluations) - Move competition settings into Manage Editions page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
165
src/components/admin/program/competition-settings.tsx
Normal file
165
src/components/admin/program/competition-settings.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Loader2, Save, Settings } from 'lucide-react'
|
||||
|
||||
type CompetitionSettingsProps = {
|
||||
competitionId: string
|
||||
initialSettings: {
|
||||
categoryMode: string
|
||||
startupFinalistCount: number
|
||||
conceptFinalistCount: number
|
||||
notifyOnRoundAdvance: boolean
|
||||
notifyOnDeadlineApproach: boolean
|
||||
deadlineReminderDays: number[]
|
||||
}
|
||||
}
|
||||
|
||||
export function CompetitionSettings({ competitionId, initialSettings }: CompetitionSettingsProps) {
|
||||
const [settings, setSettings] = useState(initialSettings)
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
const updateMutation = trpc.competition.update.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Competition settings saved')
|
||||
setDirty(false)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
function update<K extends keyof typeof settings>(key: K, value: (typeof settings)[K]) {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }))
|
||||
setDirty(true)
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
updateMutation.mutate({ id: competitionId, ...settings })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Competition Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Category mode, finalist targets, and notification preferences
|
||||
</CardDescription>
|
||||
</div>
|
||||
{dirty && (
|
||||
<Button onClick={handleSave} disabled={updateMutation.isPending} size="sm">
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Category Mode</Label>
|
||||
<Select value={settings.categoryMode} onValueChange={(v) => update('categoryMode', v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="SHARED">Shared Pool</SelectItem>
|
||||
<SelectItem value="SEPARATE">Separate Tracks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Startup Finalist Count</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={settings.startupFinalistCount}
|
||||
onChange={(e) => update('startupFinalistCount', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Concept Finalist Count</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={settings.conceptFinalistCount}
|
||||
onChange={(e) => update('conceptFinalistCount', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">Notifications</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>Notify on Round Advance</Label>
|
||||
<p className="text-xs text-muted-foreground">Email applicants when their project advances</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.notifyOnRoundAdvance}
|
||||
onCheckedChange={(v) => update('notifyOnRoundAdvance', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>Notify on Deadline Approach</Label>
|
||||
<p className="text-xs text-muted-foreground">Send reminders before deadlines</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.notifyOnDeadlineApproach}
|
||||
onCheckedChange={(v) => update('notifyOnDeadlineApproach', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Reminder Days Before Deadline</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
{settings.deadlineReminderDays.map((day, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="gap-1">
|
||||
{day}d
|
||||
<button
|
||||
className="ml-1 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
const next = settings.deadlineReminderDays.filter((_, i) => i !== idx)
|
||||
update('deadlineReminderDays', next)
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Add..."
|
||||
className="w-20 h-7 text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = parseInt((e.target as HTMLInputElement).value)
|
||||
if (val > 0 && !settings.deadlineReminderDays.includes(val)) {
|
||||
update('deadlineReminderDays', [...settings.deadlineReminderDays, val].sort((a, b) => b - a))
|
||||
;(e.target as HTMLInputElement).value = ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user