Reopen rounds, file type buttons, checklist live-update
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m1s

- Add reopenRound() to round engine (CLOSED → ACTIVE) with auto-pause of subsequent active rounds
- Add reopen endpoint to roundEngine router and UI button on round detail page
- Replace free-text MIME type input with toggle-only badge buttons in file requirements editor
- Enable refetchOnWindowFocus and shorter polling intervals for readiness checklist queries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-16 12:06:07 +01:00
parent de73a6f080
commit 079468d2ca
4 changed files with 242 additions and 53 deletions

View File

@@ -49,7 +49,7 @@ type FileRequirementsEditorProps = {
type FormState = {
name: string
description: string
acceptedMimeTypes: string
acceptedMimeTypes: string[]
maxSizeMB: string
isRequired: boolean
}
@@ -57,21 +57,27 @@ type FormState = {
const emptyForm: FormState = {
name: '',
description: '',
acceptedMimeTypes: '',
acceptedMimeTypes: [],
maxSizeMB: '',
isRequired: true,
}
const COMMON_MIME_PRESETS: { label: string; value: string }[] = [
{ label: 'PDF only', value: 'application/pdf' },
{ label: 'Images', value: 'image/png, image/jpeg, image/webp' },
{ label: 'Video', value: 'video/mp4, video/quicktime, video/webm' },
{ label: 'Documents', value: 'application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
{ label: 'Spreadsheets', value: 'application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/csv' },
{ label: 'Presentations', value: 'application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation' },
{ label: 'Any file', value: '' },
const MIME_TYPE_OPTIONS: { label: string; value: string }[] = [
{ label: 'PDF', value: 'application/pdf' },
{ label: 'Images', value: 'image/*' },
{ label: 'Video', value: 'video/*' },
{ label: 'Word', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
{ label: 'Excel', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
{ label: 'PowerPoint', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
]
function getMimeLabel(mime: string): string {
const preset = MIME_TYPE_OPTIONS.find((p) => p.value === mime)
if (preset) return preset.label
if (mime.endsWith('/*')) return mime.replace('/*', '')
return mime
}
export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }: FileRequirementsEditorProps) {
const [dialogOpen, setDialogOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
@@ -123,7 +129,7 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
setForm({
name: req.name,
description: req.description || '',
acceptedMimeTypes: (req.acceptedMimeTypes || []).join(', '),
acceptedMimeTypes: req.acceptedMimeTypes || [],
maxSizeMB: req.maxSizeMB?.toString() || '',
isRequired: req.isRequired ?? true,
})
@@ -131,12 +137,16 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
setDialogOpen(true)
}
const handleSubmit = () => {
const mimeTypes = form.acceptedMimeTypes
.split(',')
.map((s) => s.trim())
.filter(Boolean)
const toggleMimeType = (mime: string) => {
setForm((prev) => ({
...prev,
acceptedMimeTypes: prev.acceptedMimeTypes.includes(mime)
? prev.acceptedMimeTypes.filter((m) => m !== mime)
: [...prev.acceptedMimeTypes, mime],
}))
}
const handleSubmit = () => {
const maxSize = form.maxSizeMB ? parseInt(form.maxSizeMB, 10) : undefined
if (editingId) {
@@ -144,7 +154,7 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
id: editingId,
name: form.name,
description: form.description || null,
acceptedMimeTypes: mimeTypes,
acceptedMimeTypes: form.acceptedMimeTypes,
maxSizeMB: maxSize ?? null,
isRequired: form.isRequired,
})
@@ -153,7 +163,7 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
roundId,
name: form.name,
description: form.description || undefined,
acceptedMimeTypes: mimeTypes,
acceptedMimeTypes: form.acceptedMimeTypes,
maxSizeMB: maxSize,
isRequired: form.isRequired,
sortOrder: (requirements?.length ?? 0),
@@ -258,25 +268,22 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
{req.description && (
<p className="text-xs text-muted-foreground mt-0.5">{req.description}</p>
)}
<div className="flex flex-wrap gap-2 mt-1.5">
<div className="flex flex-wrap gap-1 mt-1.5">
{req.acceptedMimeTypes?.length > 0 ? (
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
{req.acceptedMimeTypes.map((t: string) => {
if (t === 'application/pdf') return 'PDF'
if (t.startsWith('image/')) return t.replace('image/', '').toUpperCase()
if (t.startsWith('video/')) return t.replace('video/', '').toUpperCase()
return t.split('/').pop()?.toUpperCase() || t
}).join(', ')}
</span>
req.acceptedMimeTypes.map((t: string) => (
<Badge key={t} variant="outline" className="text-[10px]">
{getMimeLabel(t)}
</Badge>
))
) : (
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
<Badge variant="outline" className="text-[10px]">
Any file type
</span>
</Badge>
)}
{req.maxSizeMB && (
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
<Badge variant="outline" className="text-[10px]">
Max {req.maxSizeMB} MB
</span>
</Badge>
)}
</div>
</div>
@@ -347,23 +354,21 @@ export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }:
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Accepted File Types</label>
<Input
placeholder="application/pdf, image/png (leave empty for any)"
value={form.acceptedMimeTypes}
onChange={(e) => setForm((f) => ({ ...f, acceptedMimeTypes: e.target.value }))}
/>
<div className="flex flex-wrap gap-1.5">
{COMMON_MIME_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => setForm((f) => ({ ...f, acceptedMimeTypes: preset.value }))}
className="text-[10px] px-2 py-1 rounded-full border hover:bg-muted transition-colors"
<div className="flex flex-wrap gap-2">
{MIME_TYPE_OPTIONS.map((opt) => (
<Badge
key={opt.value}
variant={form.acceptedMimeTypes.includes(opt.value) ? 'default' : 'outline'}
className="cursor-pointer select-none"
onClick={() => toggleMimeType(opt.value)}
>
{preset.label}
</button>
{opt.label}
</Badge>
))}
</div>
<p className="text-xs text-muted-foreground">
Select one or more file types. Leave empty to accept any file type.
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Max File Size (MB)</label>