// 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 (
setState({ view: "dashboard" })}
style={{ paddingLeft: 0 }}>← BACK TO PIT
{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.`)}
setState({ view: "dashboard" })}>
← ZURÜCK
);
}
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}
setOpen(o => !o)} disabled={locked}>
{open ? "ZU" : (selected ? "ÄNDERN" : "WÄHLEN")}
{selected && !open && (
{cat.kind === "driver" ? (
{selected.first} {selected.last}
{selected.active === false && (
✕ KEIN COCKPIT
)}
{getTeam(selected.team).name.toUpperCase()}
) : (
)}
)}
{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 (
setState({ view: "races" })}>← KALENDER
{/* 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."}
setState({ view: "dashboard" })}>
← Zurück zum Pit Wall
{/* 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 (
{locked ? (
⚠ GELOCKT
) : (
)}
{locked && lockMsg && (
{lockMsg}
)}
{sel && !open && (
{sel.first} {sel.last}
{sel.active === false && (
✕ KEIN COCKPIT
)}
{getTeam(sel.team).name.toUpperCase()}
{!locked &&
setOpen(true)}>ÄNDERN }
)}
{(!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 (
{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 });