From 4489c774e51da283543b96e17eff74b6d3ec6107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 12 Mar 2026 17:39:47 +0100 Subject: [PATCH] rewrite predictions form as tap-to-assign with 4 ranked slots Co-Authored-By: Claude Opus 4.6 --- .../src/components/predictions-form.tsx | 262 ++++++++++++------ 1 file changed, 181 insertions(+), 81 deletions(-) diff --git a/packages/client/src/components/predictions-form.tsx b/packages/client/src/components/predictions-form.tsx index 71d0d79..259fd43 100644 --- a/packages/client/src/components/predictions-form.tsx +++ b/packages/client/src/components/predictions-form.tsx @@ -1,19 +1,58 @@ import { useState } from "react" -import type { Country, Prediction } from "@celebrate-esc/shared" +import type { Entry, Prediction } from "@celebrate-esc/shared" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -interface PredictionsFormProps { - countries: Country[] - existingPrediction: Prediction | null - locked: boolean - onSubmit: (prediction: { predictedWinner: string; top3: string[]; nulPointsPick: string }) => void +type SlotKey = "first" | "second" | "third" | "last" + +const SLOTS: { key: SlotKey; label: string }[] = [ + { key: "first", label: "1st Place" }, + { 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}` } -export function PredictionsForm({ countries, existingPrediction, locked, onSubmit }: PredictionsFormProps) { - const [winner, setWinner] = useState(existingPrediction?.predictedWinner ?? "") - const [top3, setTop3] = useState(existingPrediction?.top3 ?? []) - const [nulPoints, setNulPoints] = useState(existingPrediction?.nulPointsPick ?? "") +interface PredictionsFormProps { + entries: Entry[] + existingPrediction: Prediction | null + locked: boolean + onSubmit: (prediction: { first: string; second: string; third: string; last: string }) => void +} + +export function PredictionsForm({ entries, existingPrediction, locked, onSubmit }: PredictionsFormProps) { + const [slots, setSlots] = useState>(() => { + if (existingPrediction) { + return { + first: existingPrediction.first, + second: existingPrediction.second, + third: existingPrediction.third, + last: existingPrediction.last, + } + } + return { first: 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 })) + } if (locked) { if (!existingPrediction) { @@ -30,97 +69,158 @@ export function PredictionsForm({ countries, existingPrediction, locked, onSubmi Your Predictions (locked) - -

- Winner:{" "} - {countries.find((c) => c.code === existingPrediction.predictedWinner)?.name} -

-

- Top 3:{" "} - {existingPrediction.top3.map((code) => countries.find((c) => c.code === code)?.name).join(", ")} -

-

- Nul Points:{" "} - {countries.find((c) => c.code === existingPrediction.nulPointsPick)?.name} -

+ + {SLOTS.map((slot) => { + const entry = findEntry(existingPrediction[slot.key]) + return ( +
+ {slot.label} + {entry ? formatEntry(entry) : "—"} +
+ ) + })}
) } - function toggleTop3(code: string) { - setTop3((prev) => { - if (prev.includes(code)) return prev.filter((c) => c !== code) - if (prev.length >= 3) return prev - return [...prev, code] - }) + // Already submitted — show read-only with option to change + if (existingPrediction && allFilled) { + const hasChanges = SLOTS.some((s) => slots[s.key] !== existingPrediction[s.key]) + if (!hasChanges) { + return ( + + + Your Predictions (submitted) + + + {SLOTS.map((slot) => { + const entry = findEntry(existingPrediction[slot.key]) + return ( +
+
+ {slot.label} + {entry ? formatEntry(entry) : "—"} +
+ +
+ ) + })} +
+
+ ) + } } - const canSubmit = winner && top3.length === 3 && nulPoints && !top3.includes(winner) - return ( Predictions -
- - + {/* Slot cards */} +
+ {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 && ( + + )} +
+ ) + })}
-
- -
- {countries - .filter((c) => c.code !== winner) - .map((c) => ( + {/* Submit button */} + {allFilled && ( + + )} + + {/* Entry list */} +
+

Entries

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