Reopen rounds, file type buttons, checklist live-update
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m1s
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user