166 lines
6.0 KiB
TypeScript
166 lines
6.0 KiB
TypeScript
|
|
'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>
|
|||
|
|
)
|
|||
|
|
}
|