From ef0f88551dcad9b24d96f64295ef85cb71cbf0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 12 Mar 2026 22:16:58 +0100 Subject: [PATCH] add ActualResultsForm component Co-Authored-By: Claude Opus 4.6 --- .../src/components/actual-results-form.tsx | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 packages/client/src/components/actual-results-form.tsx diff --git a/packages/client/src/components/actual-results-form.tsx b/packages/client/src/components/actual-results-form.tsx new file mode 100644 index 0000000..79afdab --- /dev/null +++ b/packages/client/src/components/actual-results-form.tsx @@ -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>(() => { + 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(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 ( + + + Actual Results + + +
+ {SLOTS.map((slot) => { + const code = slots[slot.key] + const entry = code ? findEntry(code) : null + return ( +
+
+ {slot.label} + {entry ? ( + {formatEntry(entry)} + ) : ( + Tap an entry below + )} +
+ {code && ( + + )} +
+ ) + })} +
+ + {allFilled && ( + + )} + +
+

Entries

+ {entries.map((entry) => { + const isAssigned = assignedCodes.has(entry.country.code) + const isPickerOpen = pickerForEntry === entry.country.code + return ( +
+ + {isPickerOpen && !isAssigned && ( +
+ {emptySlots.map((slot) => ( + + ))} +
+ )} +
+ ) + })} +
+
+
+ ) +}