// Saison-Tipps + Rennen-Liste + Rennen-Detail screens function SeasonScreen() { const { user, seasonPicks, scoring } = useStore(); const SEED = window.SEED; const myPicks = seasonPicks[user.id] || {}; const locked = scoring?.seasonLocked === true; const setPick = (key, val) => { setState(s => ({ ...s, seasonPicks: { ...s.seasonPicks, [user.id]: { ...(s.seasonPicks[user.id] || {}), [key]: val } } })); window.showToast("✓ Gespeichert"); }; const cats = [ { key: "driverChampion", label: "Fahrer-Weltmeister", points: (scoring || SEED.scoring).seasonChampion, hint: "Wer holt 2026 die WDC-Krone?", kind: "driver" }, { key: "constructor", label: "Constructors Champion", points: (scoring || SEED.scoring).seasonConstructor, hint: "Welches Team gewinnt die Konstrukteurs-WM?", kind: "team" }, { key: "destructor", label: "Destructor", points: (scoring || SEED.scoring).seasonDestructor, hint: "Häufigster Crasher / Schadens-Champion", kind: "driver" }, { key: "driverKicked", label: "Fahrer wird gekickt", points: (scoring || SEED.scoring).seasonDriverKicked, hint: "Wer verliert während der Saison sein Cockpit?", kind: "driver" }, { key: "teamBossKicked", label: "Teamchef-Wechsel", points: (scoring || SEED.scoring).seasonTeamBossKicked, hint: "Welches Team trennt sich von seinem Teamchef?", kind: "team" }, { key: "surprise", label: "Biggest Surprise", points: (scoring || SEED.scoring).seasonSurprise, hint: "Größter positiver Sprung an Punkten/Platzierungen", kind: "driver" }, ]; const filled = cats.filter(c => myPicks[c.key]).length; return (
{filled}/6 ABGEGEBEN} />
DEADLINE
{locked ? "Saison-Tipps sind vom Admin gesperrt. Keine Änderungen mehr möglich." : "Saison-Tipps können bis zur Sperrung durch den Admin geändert werden."}
{locked ? ⚠ GELOCKT : OFFEN}
{cats.map(c => ( setPick(c.key, v)} locked={locked} /> ))}
STATUS
{locked ? (filled === 6 ? "Alle Saison-Tipps abgegeben." : `${filled}/6 Tipps abgegeben — Deadline vorbei.`) : (filled === 6 ? "Alle Saison-Tipps abgegeben. Du kannst sie noch ändern." : `Noch ${6 - filled} Tipp${6-filled === 1 ? "" : "s"} offen.`)}
); } function SeasonCard({ cat, value, onChange, locked }) { const [open, setOpen] = React.useState(false); const SEED = window.SEED; // Picker: nur aktive Fahrer zeigen; bereits gewählte inaktive Fahrer immer einschließen const list = cat.kind === "driver" ? SEED.drivers.filter(d => d.active !== false || d.id === value) : SEED.teams; const selected = value ? (cat.kind === "driver" ? getDriver(value) : getTeam(value)) : null; return (
+{cat.points} PKT {value && ✓ GETIPPT}
{cat.label}
{cat.hint}
{selected && !open && (
{cat.kind === "driver" ? (
{selected.first} {selected.last} {selected.active === false && ( ✕ KEIN COCKPIT )}
{getTeam(selected.team).name.toUpperCase()}
) : (
{selected.name}
)}
)} {open && (
{list.map(item => ( cat.kind === "driver" ? ( { onChange(item.id); setOpen(false); }} /> ) : ( { onChange(item.id); setOpen(false); }} /> ) ))}
)}
); } // -- Race list ----------------------------------------------------------- function RacesScreen() { const { raceState, racePicks, results, user } = useStore(); const SEED = window.SEED; const nextRaceId = SEED.calendar.find(r => (raceState[r.id] || "upcoming") !== "finished")?.id; const finished = SEED.calendar.filter(r => raceState[r.id] === "finished").length; return (
{SEED.calendar.map((r, i) => { const st = raceState[r.id] || "upcoming"; const myPicks = racePicks[r.id]?.[user.id]; const hasPicks = myPicks && (myPicks.pole || myPicks.winner); const isNext = r.id === nextRaceId; const res = results[r.id]; return (
setState({ view: "race", currentRaceId: r.id })} style={{ cursor: "pointer" }} >
); })}
); } function RaceCard({ race, idx, st, hasPicks, isNext, res }) { const finished = st === "finished"; const pole = res?.pole ? getDriver(res.pole) : null; const winner = res?.winner ? getDriver(res.winner) : null; return (
{ e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 32px rgba(0,0,0,0.5)"; }} onMouseLeave={e => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = ""; }} > {/* Karten-Header — dunkles Bild-Ersatz mit Rundenummer */}
{/* Große Rundenummer als Dekorativ-Element */}
{String(idx + 1).padStart(2, "0")}
{/* Status-Badge + Sprint oben */}
RD {String(idx + 1).padStart(2, "0")}
{race.sprint && ( SPRINT )} {isNext && ( NÄCHSTES )}
{/* Land + Name */}
{race.name}
{formatDate(race.date)}
{/* Karten-Body */}
{race.circuit}
{/* Abgeschlossen: Top-Ergebnisse zeigen */} {finished && res ? (
{pole && (
POLE {pole.last}
)} {winner && (
P1 {winner.last}
)}
) : ( /* Noch nicht ausgewertet: Pick-Status */
{hasPicks ? ✓ Tipp abgegeben : ○ Tipp offen }
)}
{/* Footer-Zeile */}
{finished ? "Abgeschlossen" : st === "pre-race" ? "Deadline läuft" : "Upcoming"}
); } // -- Race detail (tip + after-deadline view) ----------------------------- function RaceScreen() { const { currentRaceId, raceState, racePicks, results, user, scoring } = useStore(); const sc = scoring || SEED.scoring; const SEED = window.SEED; const race = getRace(currentRaceId); if (!race) return null; const st = raceState[race.id] || "upcoming"; const dl = getDeadlines(race); const myPicks = racePicks[race.id]?.[user.id] || {}; const now = useNow(30000); // Alle Tipps haben dieselbe Deadline: vor FP1 (Zeit-basiert + raceState als Fallback) const allLocked = st === "finished" || now >= dl.fp1; const setPick = (key, val) => { setState(s => ({ ...s, racePicks: { ...s.racePicks, [race.id]: { ...(s.racePicks[race.id] || {}), [user.id]: { ...(s.racePicks[race.id]?.[user.id] || {}), [key]: val } } } })); window.showToast("✓ Gespeichert"); }; return (
{/* Hero */}
{/* Decorative round number */}
{String(SEED.calendar.indexOf(race) + 1).padStart(2, "0")}
RD {SEED.calendar.indexOf(race) + 1} · {race.country}

{race.name}

{race.circuit} · {formatDate(race.date)}
{race.sprint && ( SPRINT WEEKEND )}
{st === "finished" ? ( ) : ( <> {/* Pole */} setPick("pole", v)} lockMsg={allLocked ? "Wochenende hat begonnen. Tipp gelockt." : null} /> {/* Sprint (if applicable) */} {race.sprint && ( setPick("sprintPole", v)} /> )} {race.sprint && ( setPick("sprintWinner", v)} /> )} {/* Race winner */} setPick("winner", v)} /> {/* Finishers slider */} setPick("finishers", v)} locked={allLocked} deadline={dl.fp1} scoring={sc} /> {/* Submit / Status */}
{allLocked ? "⏱" : "✎"}
{allLocked ? "Tipps gesperrt" : "Tipps abgeben"}
{allLocked ? "Die FP1-Deadline ist abgelaufen. Ergebnisse werden nach dem Rennen eingetragen." : "Tipps werden automatisch gespeichert. Alle Tipps sind für alle Spieler sichtbar."}
{/* Always show all picks — visibility before deadline is intentional */} )}
); } function PickSection({ title, sub, deadline, locked, value, onChange, lockMsg }) { const [open, setOpen] = React.useState(!value); const SEED = window.SEED; const sel = value ? getDriver(value) : null; return (
{sub}
{title}
{locked ? ( ⚠ GELOCKT ) : ( )}
{locked && lockMsg && (
{lockMsg}
)} {sel && !open && (
{sel.first} {sel.last} {sel.active === false && ( ✕ KEIN COCKPIT )}
{getTeam(sel.team).name.toUpperCase()}
{!locked && }
)} {(!sel || open) && !locked && (
{SEED.drivers.filter(d => d.active !== false || value === d.id).map(d => ( { onChange(d.id); setOpen(false); }} /> ))}
)}
); } function FinisherSection({ value, onChange, locked, deadline, scoring }) { const v = value == null ? 16 : value; return (
+{scoring.finishers} PKT EXAKT · +{scoring.finishersClose} ±1 · DEADLINE: VOR FP1
Anzahl Finisher
{locked ? ⚠ GELOCKT : }
{String(v).padStart(2,"0")}
VON 22
Autos beenden Rennen
onChange(parseInt(e.target.value, 10))} style={{ width: "100%", accentColor: "var(--accent)" }} />
0 (DNF-FEST) 22 (CLEAN RACE)
); } function AfterDeadlinePicksView({ race, allLocked }) { const { racePicks } = useStore(); const picks = racePicks[race.id] || {}; const SEED = window.SEED; return (
{SEED.players.map((p, i) => { const pk = picks[p.id] || {}; const hasAny = pk.pole || pk.winner || pk.sprintWinner || pk.finishers != null; return (
{p.name} {p.you && YOU}
{hasAny ? [ `POLE ${pk.pole ? (getDriver(pk.pole)?.code ?? "?") : "—"}`, `RACE ${pk.winner ? (getDriver(pk.winner)?.code ?? "?") : "—"}`, race.sprint && pk.sprintPole ? `SPOLE ${getDriver(pk.sprintPole)?.code ?? "?"}` : null, race.sprint && pk.sprintWinner ? `SPR ${getDriver(pk.sprintWinner)?.code ?? "?"}` : null, pk.finishers != null ? `FIN ${pk.finishers}` : null, ].filter(Boolean).join(" · ") : "KEINE TIPPS ABGEGEBEN"}
); })}
); } function AllSeasonPicksView({ cats }) { const { seasonPicks } = useStore(); const SEED = window.SEED; return (
{/* Header-Zeile */}
SPIELER
{cats.map(c => (
{c.label.toUpperCase()}
))}
{SEED.players.map((p, i) => { const pk = seasonPicks[p.id] || Object.create(null); return (
{p.name} {p.you && DU}
{cats.map(c => { const val = pk[c.key]; const item = val ? (c.kind === "driver" ? getDriver(val) : getTeam(val)) : null; return (
{item ? ( c.kind === "driver" ? (
{item.code}
) : (
{item.short}
) ) : ( )}
); })}
); })}
); } // -- After-race result view ---------------------------------------------- function RaceResultView({ race }) { const { results, racePicks, user } = useStore(); const res = results[race.id]; const picks = racePicks[race.id] || {}; const SEED = window.SEED; const { totals, scoring } = computeScores(); if (!res) { return (
KEINE ERGEBNISSE
Für dieses Rennen sind noch keine Ergebnisse verfügbar.
); } const playerRows = SEED.players.map(p => { const pk = picks[p.id] || {}; let pts = 0; const breakdown = []; if (res.pole && pk.pole === res.pole) { pts += scoring.pole; breakdown.push(`+${scoring.pole} POLE`); } if (res.winner && pk.winner === res.winner) { pts += scoring.raceWinner; breakdown.push(`+${scoring.raceWinner} SIEGER`); } if (race.sprint && res.sprintPole && pk.sprintPole === res.sprintPole) { const sp = scoring.sprintPole || 0; pts += sp; breakdown.push(`+${sp} S-POLE`); } if (race.sprint && res.sprintWinner && pk.sprintWinner === res.sprintWinner) { pts += scoring.sprintWinner; breakdown.push(`+${scoring.sprintWinner} SPRINT`); } if (res.finishers != null && pk.finishers != null) { if (pk.finishers === res.finishers) { pts += scoring.finishers; breakdown.push(`+${scoring.finishers} FINISHER`); } else if (Math.abs(pk.finishers - res.finishers) === 1) { pts += scoring.finishersClose; breakdown.push(`+${scoring.finishersClose} FINISHER ±1`); } } return { p, pk, pts, breakdown }; }).sort((a,b) => b.pts - a.pts); return ( <>
OFFIZIELLES ERGEBNIS
{race.sprint && } {race.sprint && }
{playerRows.map((row, i) => (
{String(i+1).padStart(2,"0")}
{row.p.name}{row.p.you && YOU}
POLE {row.pk.pole ? (getDriver(row.pk.pole)?.code ?? "?") : "—"} · RACE {row.pk.winner ? (getDriver(row.pk.winner)?.code ?? "?") : "—"} {race.sprint && row.pk.sprintPole ? ` · SPOLE ${getDriver(row.pk.sprintPole)?.code ?? "?"}` : ""} {race.sprint && row.pk.sprintWinner ? ` · SPR ${getDriver(row.pk.sprintWinner)?.code ?? "?"}` : ""} {row.pk.finishers != null ? ` · FIN ${row.pk.finishers}` : ""}
0 ? "var(--good)" : "var(--ink-3)" }}>+{row.pts}
{row.breakdown.length > 0 && (
{row.breakdown.join(" · ")}
)}
))}
); } Object.assign(window, { SeasonScreen, RacesScreen, RaceScreen });