add ActualResultsForm component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:16:58 +01:00
parent f390c21903
commit ef0f88551d

View File

@@ -0,0 +1,159 @@
import { useState } from "react"
import type { Entry, ActualResults } from "@celebrate-esc/shared"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
type SlotKey = "winner" | "second" | "third" | "last"
const SLOTS: { key: SlotKey; label: string }[] = [
{ key: "winner", label: "Winner" },
{ key: "second", label: "2nd Place" },
{ key: "third", label: "3rd Place" },
{ key: "last", label: "Last Place" },
]
function formatEntry(entry: Entry): string {
return `${entry.country.flag} ${entry.artist}${entry.song}`
}
interface ActualResultsFormProps {
entries: Entry[]
existingResults: ActualResults | null
onSubmit: (results: { winner: string; second: string; third: string; last: string }) => void
}
export function ActualResultsForm({ entries, existingResults, onSubmit }: ActualResultsFormProps) {
const [slots, setSlots] = useState<Record<SlotKey, string | null>>(() => {
if (existingResults) {
return {
winner: existingResults.winner,
second: existingResults.second,
third: existingResults.third,
last: existingResults.last,
}
}
return { winner: null, second: null, third: null, last: null }
})
const [pickerForEntry, setPickerForEntry] = useState<string | null>(null)
const assignedCodes = new Set(Object.values(slots).filter(Boolean))
const emptySlots = SLOTS.filter((s) => !slots[s.key])
const allFilled = SLOTS.every((s) => slots[s.key])
function findEntry(code: string): Entry | undefined {
return entries.find((e) => e.country.code === code)
}
function assignToSlot(entryCode: string, slotKey: SlotKey) {
setSlots((prev) => ({ ...prev, [slotKey]: entryCode }))
setPickerForEntry(null)
}
function removeFromSlot(slotKey: SlotKey) {
setSlots((prev) => ({ ...prev, [slotKey]: null }))
}
return (
<Card>
<CardHeader>
<CardTitle>Actual Results</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{SLOTS.map((slot) => {
const code = slots[slot.key]
const entry = code ? findEntry(code) : null
return (
<div
key={slot.key}
className={`flex items-center justify-between rounded-md border p-2 ${
code ? "border-primary/30 bg-primary/5" : "border-dashed"
}`}
>
<div className="flex items-center gap-2">
<span className="w-20 text-xs font-medium text-muted-foreground">{slot.label}</span>
{entry ? (
<span className="text-sm">{formatEntry(entry)}</span>
) : (
<span className="text-sm text-muted-foreground">Tap an entry below</span>
)}
</div>
{code && (
<button
type="button"
onClick={() => removeFromSlot(slot.key)}
className="text-muted-foreground hover:text-foreground"
aria-label={`Remove ${slot.label}`}
>
</button>
)}
</div>
)
})}
</div>
{allFilled && (
<Button
onClick={() =>
onSubmit({
winner: slots.winner!,
second: slots.second!,
third: slots.third!,
last: slots.last!,
})
}
>
{existingResults ? "Update Results" : "Submit Results"}
</Button>
)}
<div className="flex flex-col gap-1">
<h4 className="text-sm font-medium text-muted-foreground">Entries</h4>
{entries.map((entry) => {
const isAssigned = assignedCodes.has(entry.country.code)
const isPickerOpen = pickerForEntry === entry.country.code
return (
<div key={entry.country.code}>
<button
type="button"
disabled={isAssigned}
onClick={() => {
if (emptySlots.length === 1) {
assignToSlot(entry.country.code, emptySlots[0]!.key)
} else {
setPickerForEntry(isPickerOpen ? null : entry.country.code)
}
}}
className={`w-full rounded-md border px-3 py-2 text-left text-sm transition-colors ${
isAssigned
? "border-transparent bg-muted/50 text-muted-foreground line-through opacity-50"
: isPickerOpen
? "border-primary bg-primary/5"
: "hover:bg-muted"
}`}
>
{formatEntry(entry)}
</button>
{isPickerOpen && !isAssigned && (
<div className="mt-1 ml-4 flex gap-1">
{emptySlots.map((slot) => (
<button
type="button"
key={slot.key}
onClick={() => assignToSlot(entry.country.code, slot.key)}
className="rounded-md border px-2 py-1 text-xs hover:bg-primary hover:text-primary-foreground"
>
{slot.label}
</button>
))}
</div>
)}
</div>
)
})}
</div>
</CardContent>
</Card>
)
}