Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m23s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m23s
- Phase 1: 7 round config sub-components covering all ~65 Zod schema fields across INTAKE, FILTERING, EVALUATION, SUBMISSION, MENTORING, LIVE_FINAL, DELIBERATION - Phase 2: Replace Competitions nav with Rounds + add Juries; new /admin/rounds and /admin/rounds/[roundId] pages with tabbed detail (Config, Projects, Windows, Documents, Awards) - Phase 3: Top-level /admin/juries with list + detail pages (members table, settings panel, self-service review) - Phase 4: File requirements editor in round config; project detail per-requirement upload slots replacing generic drop zone - Phase 5: Awards edit page with source round dropdown, eligibility mode, auto-tag rules builder; round detail Awards tab; specialAward router enhanced with evaluationRoundId/eligibilityMode fields - Phase 6: Evaluation page rewrite supporting all 3 scoring modes (criteria/global/binary) with config-driven behavior; live voting UI polish - Phase 7: UI design polish across admin pages — consistent headers, cards, hover transitions, empty states, brand colors - Bulk upload page for admin project imports - File router enhanced with admin upload and submission window procedures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,248 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, 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 { IntakeConfig } from '@/components/admin/rounds/config/intake-config'
|
||||
import { FilteringConfig } from '@/components/admin/rounds/config/filtering-config'
|
||||
import { EvaluationConfig } from '@/components/admin/rounds/config/evaluation-config'
|
||||
import { SubmissionConfig } from '@/components/admin/rounds/config/submission-config'
|
||||
import { MentoringConfig } from '@/components/admin/rounds/config/mentoring-config'
|
||||
import { LiveFinalConfig } from '@/components/admin/rounds/config/live-final-config'
|
||||
import { DeliberationConfig } from '@/components/admin/rounds/config/deliberation-config'
|
||||
|
||||
type RoundConfigFormProps = {
|
||||
roundType: string
|
||||
config: Record<string, unknown>
|
||||
onChange: (config: Record<string, unknown>) => void
|
||||
juryGroups?: Array<{ id: string; name: string }>
|
||||
}
|
||||
|
||||
export function RoundConfigForm({ roundType, config, onChange }: RoundConfigFormProps) {
|
||||
const updateConfig = (key: string, value: unknown) => {
|
||||
onChange({ ...config, [key]: value })
|
||||
export function RoundConfigForm({ roundType, config, onChange, juryGroups }: RoundConfigFormProps) {
|
||||
switch (roundType) {
|
||||
case 'INTAKE':
|
||||
return <IntakeConfig config={config} onChange={onChange} />
|
||||
case 'FILTERING':
|
||||
return <FilteringConfig config={config} onChange={onChange} />
|
||||
case 'EVALUATION':
|
||||
return <EvaluationConfig config={config} onChange={onChange} />
|
||||
case 'SUBMISSION':
|
||||
return <SubmissionConfig config={config} onChange={onChange} />
|
||||
case 'MENTORING':
|
||||
return <MentoringConfig config={config} onChange={onChange} />
|
||||
case 'LIVE_FINAL':
|
||||
return <LiveFinalConfig config={config} onChange={onChange} />
|
||||
case 'DELIBERATION':
|
||||
return <DeliberationConfig config={config} onChange={onChange} juryGroups={juryGroups} />
|
||||
default:
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
Unknown round type: {roundType}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (roundType === 'INTAKE') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Intake Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="allowDrafts">Allow Drafts</Label>
|
||||
<Switch
|
||||
id="allowDrafts"
|
||||
checked={(config.allowDrafts as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('allowDrafts', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="draftExpiryDays">Draft Expiry (days)</Label>
|
||||
<Input
|
||||
id="draftExpiryDays"
|
||||
type="number"
|
||||
min={1}
|
||||
value={(config.draftExpiryDays as number) ?? 30}
|
||||
onChange={(e) => updateConfig('draftExpiryDays', parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxFileSizeMB">Max File Size (MB)</Label>
|
||||
<Input
|
||||
id="maxFileSizeMB"
|
||||
type="number"
|
||||
min={1}
|
||||
value={(config.maxFileSizeMB as number) ?? 50}
|
||||
onChange={(e) => updateConfig('maxFileSizeMB', parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="publicFormEnabled">Public Form Enabled</Label>
|
||||
<Switch
|
||||
id="publicFormEnabled"
|
||||
checked={(config.publicFormEnabled as boolean) ?? false}
|
||||
onCheckedChange={(checked) => updateConfig('publicFormEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (roundType === 'FILTERING') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Filtering Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="aiScreeningEnabled">AI Screening</Label>
|
||||
<Switch
|
||||
id="aiScreeningEnabled"
|
||||
checked={(config.aiScreeningEnabled as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('aiScreeningEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="duplicateDetectionEnabled">Duplicate Detection</Label>
|
||||
<Switch
|
||||
id="duplicateDetectionEnabled"
|
||||
checked={(config.duplicateDetectionEnabled as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('duplicateDetectionEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="manualReviewEnabled">Manual Review</Label>
|
||||
<Switch
|
||||
id="manualReviewEnabled"
|
||||
checked={(config.manualReviewEnabled as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('manualReviewEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="batchSize">Batch Size</Label>
|
||||
<Input
|
||||
id="batchSize"
|
||||
type="number"
|
||||
min={1}
|
||||
value={(config.batchSize as number) ?? 20}
|
||||
onChange={(e) => updateConfig('batchSize', parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (roundType === 'EVALUATION') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Evaluation Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="requiredReviews">Required Reviews per Project</Label>
|
||||
<Input
|
||||
id="requiredReviews"
|
||||
type="number"
|
||||
min={1}
|
||||
value={(config.requiredReviewsPerProject as number) ?? 3}
|
||||
onChange={(e) => updateConfig('requiredReviewsPerProject', parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="scoringMode">Scoring Mode</Label>
|
||||
<Select
|
||||
value={(config.scoringMode as string) ?? 'criteria'}
|
||||
onValueChange={(value) => updateConfig('scoringMode', value)}
|
||||
>
|
||||
<SelectTrigger id="scoringMode">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="criteria">Criteria-based</SelectItem>
|
||||
<SelectItem value="global">Global score</SelectItem>
|
||||
<SelectItem value="binary">Binary (pass/fail)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="requireFeedback">Require Feedback</Label>
|
||||
<Switch
|
||||
id="requireFeedback"
|
||||
checked={(config.requireFeedback as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('requireFeedback', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="coiRequired">COI Declaration Required</Label>
|
||||
<Switch
|
||||
id="coiRequired"
|
||||
checked={(config.coiRequired as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('coiRequired', checked)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (roundType === 'LIVE_FINAL') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Live Final Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="juryVotingEnabled">Jury Voting</Label>
|
||||
<Switch
|
||||
id="juryVotingEnabled"
|
||||
checked={(config.juryVotingEnabled as boolean) ?? true}
|
||||
onCheckedChange={(checked) => updateConfig('juryVotingEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="audienceVotingEnabled">Audience Voting</Label>
|
||||
<Switch
|
||||
id="audienceVotingEnabled"
|
||||
checked={(config.audienceVotingEnabled as boolean) ?? false}
|
||||
onCheckedChange={(checked) => updateConfig('audienceVotingEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(config.audienceVotingEnabled as boolean) && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="audienceVoteWeight">Audience Vote Weight (0-1)</Label>
|
||||
<Input
|
||||
id="audienceVoteWeight"
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
value={(config.audienceVoteWeight as number) ?? 0}
|
||||
onChange={(e) => updateConfig('audienceVoteWeight', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="presentationDuration">Presentation Duration (min)</Label>
|
||||
<Input
|
||||
id="presentationDuration"
|
||||
type="number"
|
||||
min={1}
|
||||
value={(config.presentationDurationMinutes as number) ?? 15}
|
||||
onChange={(e) => updateConfig('presentationDurationMinutes', parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Default view for other types
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{roundType} Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configuration UI for {roundType} rounds is not yet implemented.
|
||||
</p>
|
||||
<pre className="mt-4 p-3 bg-muted rounded text-xs overflow-auto">
|
||||
{JSON.stringify(config, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user