// Auth + Dashboard screens const { useState: useStateA, useEffect: useEffectA } = React; // ---- AuthScreen --------------------------------------------------------- function AuthScreen() { const [mode, setMode] = useStateA("magic"); // "magic" | "password" const [email, setEmail] = useStateA(""); const [password, setPassword] = useStateA(""); const [showPw, setShowPw] = useStateA(false); const [status, setStatus] = useStateA("idle"); // idle | loading | sent | error const [errorMsg, setErrorMsg] = useStateA(""); const authError = useStore(s => s.authError); useEffectA(() => { if (authError === 'not_registered') { setStatus("error"); setErrorMsg("Diese E-Mail ist nicht fΓΌr das Tippspiel registriert. Frag den Admin."); setState({ authError: null }); } else if (authError === 'sign_in_failed') { setStatus("error"); setErrorMsg("Login fehlgeschlagen. Bitte erneut versuchen."); setState({ authError: null }); } }, [authError]); const switchMode = (m) => { setMode(m); setStatus("idle"); setErrorMsg(""); setPassword(""); }; const sendMagicLink = async (e) => { e.preventDefault(); const trimmed = email.trim(); if (!trimmed) return; setStatus("loading"); setErrorMsg(""); const redirectTo = window.location.href.split('#')[0].split('?')[0]; const { error } = await window._supabase.auth.signInWithOtp({ email: trimmed, options: { emailRedirectTo: redirectTo }, }); if (error) { setStatus("error"); setErrorMsg(error.message); } else setStatus("sent"); }; const signInWithPassword = async (e) => { e.preventDefault(); const trimmed = email.trim(); if (!trimmed || !password) return; setStatus("loading"); setErrorMsg(""); const withTimeout = (p, ms) => Promise.race([ p, new Promise((_, rej) => setTimeout(() => rej(new Error('__timeout__')), ms)), ]); const onErr = (err) => { setStatus("error"); setErrorMsg(err?.message === '__timeout__' ? 'ZeitΓΌberschreitung. Bitte erneut versuchen.' : (err?.message || 'Login fehlgeschlagen. Bitte erneut versuchen.')); }; try { const { data, error } = await withTimeout( window._supabase.auth.signInWithPassword({ email: trimmed, password }), 15000 ); if (error) { onErr(error); return; } await withTimeout(window.processSignIn(data.session), 15000); } catch (err) { onErr(err); } }; const resend = async () => { setStatus("loading"); const redirectTo = window.location.href.split('#')[0].split('?')[0]; const { error } = await window._supabase.auth.signInWithOtp({ email: email.trim(), options: { emailRedirectTo: redirectTo }, }); if (error) { setStatus("error"); setErrorMsg(error.message); } else setStatus("sent"); }; return (
{/* Hintergrund-Streifen */}
{/* Roter Glow-Kreis */}
{/* Logo */}
{/* Roter Top-Stripe */}
{status === "sent" ? (
πŸ“¬
LOGIN LINK GESENDET

Check dein Postfach

Wir haben einen Login-Link an
{email}
geschickt. Klick auf den Link – kein Passwort nΓΆtig.
Kein Link da? PrΓΌf den Spam-Ordner oder:
) : ( <>

Login

{[["magic", "Login Link"], ["password", "Passwort"]].map(([m, label]) => ( ))}
{status === "error" && (
{errorMsg}
)}
setEmail(e.target.value)} placeholder="du@example.com" disabled={status === "loading"} required autoFocus />
{mode === "password" && (
setPassword(e.target.value)} placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" disabled={status === "loading"} required style={{ paddingRight: 40 }} />
)} {mode === "magic" && (
Du erhΓ€ltst einen einmaligen Login-Link per E-Mail.
)}
)}
{/* end padding-wrapper */}
{/* end card */}
PRIVATE Β· SEASON {window.SEED?.season || 2026} Β· INVITE ONLY
); } // ---- LoadingScreen ------------------------------------------------------ function LoadingScreen() { return (
VERBINDE...
); } // ---- Dashboard ---------------------------------------------------------- function Dashboard() { const { raceState, racePicks, user, scoring } = useStore(); const announcement = scoring?.announcement || null; const isDismissed = React.useMemo(() => { try { return localStorage.getItem('f1_dismissed_msg') === announcement?.id; } catch { return false; } }, [announcement?.id]); const [dismissed, setDismissed] = React.useState(isDismissed); const dismiss = () => { try { localStorage.setItem('f1_dismissed_msg', announcement.id); } catch {} setDismissed(true); }; const SEED = window.SEED; const nextRaceId = SEED.calendar.find(r => raceState[r.id] !== 'finished')?.id || SEED.calendar[SEED.calendar.length - 1]?.id; const race = getRace(nextRaceId); const dl = race ? getDeadlines(race) : null; const myPicks = race && racePicks[nextRaceId]?.[user.id]; const seasonPicks = useStore(s => s.seasonPicks[user.id]); const seasonComplete = seasonPicks && Object.values(seasonPicks).filter(Boolean).length >= 6; const { totals } = computeScores(); const ranked = SEED.players.map(p => ({ p, ...totals[p.id] })).sort((a,b) => b.total - a.total); const myRank = ranked.findIndex(r => r.p.id === user.id) + 1; const myPts = totals[user.id]?.total || 0; const leaderPts = ranked[0]?.total || 0; const lastFinished = [...SEED.calendar].reverse().find(r => raceState[r.id] === "finished"); const goRace = () => setState({ view: "race", currentRaceId: nextRaceId }); // Race-Week ab Montag: Rennen liegt in den nΓ€chsten 6 Tagen (und ist noch nicht vorbei) const raceTs = race ? new Date(race.date + "T15:00:00Z").getTime() : null; const msUntilRace = raceTs ? raceTs - Date.now() : null; const isRaceWeek = msUntilRace !== null && msUntilRace > 0 && msUntilRace <= 6 * 24 * 3600 * 1000; if (!race) return (
Kalender wird geladen…
); const rdNum = String(SEED.calendar.indexOf(race) + 1).padStart(2, "0"); const rdTotal = SEED.calendar.length; return (
{/* ── Greeting ─────────────────────────────────────────────────── */}
PIT WALL Β· SAISON {SEED.season || 2026} {isRaceWeek && RACE WEEK}

Hallo, {user.name}.

{/* ── Announcement Banner ─────────────────────────────────────── */} {announcement && !dismissed && (
πŸ“’
{announcement.text}
)} {/* ── Hero-Card: NΓ€chstes Rennen ───────────────────────────────── */}
{/* Oberer Bereich: Renninfo */}
{/* Dekorativer Kreis rechts */}
NΓ„CHSTES RENNEN Β· RD {rdNum} / {rdTotal} {race.sprint && Β· SPRINT}

{race.name}

{race.circuit} {formatDate(race.date)}
{/* Trennlinie + Countdown */}
⏱ DEADLINE · FP1
{/* Pick-Status-Leiste */}
{[ { label: "Pole", done: !!myPicks?.pole }, { label: "Sieger", done: !!myPicks?.winner }, ...(race.sprint ? [{ label: "S-Pole", done: !!myPicks?.sprintPole }] : []), ...(race.sprint ? [{ label: "Sprint", done: !!myPicks?.sprintWinner }] : []), { label: "Finisher", done: myPicks?.finishers != null }, ].map(({ label, done }) => ( {done ? `βœ“ ${label}` : `β—‹ ${label}`} ))}
{/* ── Zwei Kacheln: Championship + Saison ─────────────────────── */}
{/* Championship */}
Championship
{myRank > 0 ? `P${myRank}` : "β€”"}
{myPts}PKT
{myPts > 0 && myPts >= leaderPts ? "πŸ† FΓΌhrung" : `–${Math.max(0, leaderPts - myPts)} zur Spitze`}
{/* Progress bar */}
{/* Saison-Tipps */}
Saison-Tipps
{seasonComplete ? "βœ“ Komplett" : "Offen"}
{seasonComplete ? (
{[ { key: "WDC", val: driverName(seasonPicks.driverChampion) }, { key: "WCC", val: teamName(seasonPicks.constructor) }, { key: "Destructor", val: driverName(seasonPicks.destructor) }, ].map(({ key, val }) => ( {key} {val} ))}
) : (
6 Tipps Β· bis zu{" "} {['seasonChampion','seasonConstructor','seasonDestructor','seasonDriverKicked','seasonTeamBossKicked','seasonSurprise'].reduce((sum, k) => sum + ((scoring || SEED.scoring)[k] || 0), 0)} {" "} Punkte mΓΆglich.
)}
{/* ── Letztes Rennen ───────────────────────────────────────────── */} {lastFinished && (
setState({ view: "race", currentRaceId: lastFinished.id })}> Detail β†’ } />
)} {/* ── Form-Kurve ───────────────────────────────────────────────── */}
); } function LastRaceSummary({ raceId }) { const race = getRace(raceId); const res = useStore(s => s.results[raceId]); const { totals } = computeScores(); const me = totals[getState().user.id]; const myPts = me?.byRace[raceId] || 0; return (
{/* Header-Zeile */}
R{window.SEED.calendar.indexOf(race) + 1}
{race.name}
{formatDate(race.date)} Β· {race.circuit}
0 ? "var(--good)" : "var(--ink-3)", }}>+{myPts}
Punkte
{/* Ergebnis-Grid */}
{race.sprint && }
); } function ResultCell({ label, driverId, value }) { const d = driverId ? getDriver(driverId) : null; const t = d ? getTeam(d.team) : null; return (
{label}
{d ? (
{d.last}
{t?.short}
) : (
{value || "β€”"}
)}
); } function FormSpark() { const SEED = window.SEED; const { totals } = computeScores(); const me = totals[getState().user.id]; const finished = SEED.calendar.filter(r => getState().raceState[r.id] === "finished"); const points = finished.map(r => me?.byRace[r.id] || 0); const maxP = Math.max(1, ...points); const totalPts = points.reduce((a, b) => a + b, 0); const avgPts = finished.length ? (totalPts / finished.length).toFixed(1) : "β€”"; return (
{/* Stats-Header */}
{[ { label: "RENNEN", value: finished.length }, { label: "GESAMT", value: totalPts + " PKT" }, { label: "Ø PRO RENNEN", value: avgPts }, ].map(({ label, value }) => (
{label}
{value}
))}
{/* Balken-Chart */}
{finished.length === 0 ? (
Noch keine abgeschlossenen Rennen.
) : (
{finished.map((r, i) => (
{points[i] > 0 && (
{points[i]}
)}
0 ? "var(--accent)" : "var(--bg-3)", opacity: points[i] > 0 ? 1 : 0.4, transition: "height 0.3s ease", minHeight: 4, }} />
{r.country?.slice(0, 3).toUpperCase()}
))}
)}
); } // ---- ProfileScreen ------------------------------------------------------- const AVATAR_OPTIONS = [ '🏎','🏁','πŸ”΄','⚑','🦁','🐺','🦊','🐯','πŸ¦…','πŸ‰', '🌢','πŸ”₯','πŸ’Ž','⭐','πŸŒ™','🎯','πŸ†','πŸ‘‘','🎸','πŸ€–', ]; function ProfileScreen() { const { user } = useStore(); const sb = window._supabase; const [name, setName] = useStateA(user?.name || ""); const [avatar, setAvatar] = useStateA(user?.avatar || '🏎'); const [notifyEmail, setNotifyEmail] = useStateA(user?.notifyEmail || false); const [pw, setPw] = useStateA(""); const [pw2, setPw2] = useStateA(""); const [saving, setSaving] = useStateA(false); const [msg, setMsg] = useStateA(null); // { type: 'ok'|'err', text } const save = async (e) => { e.preventDefault(); if (!name.trim()) return; if (pw && pw !== pw2) { setMsg({ type: 'err', text: 'PasswΓΆrter stimmen nicht ΓΌberein.' }); return; } setSaving(true); setMsg(null); if (sb) { const { error: playerErr } = await sb .from('players') .update({ name: name.trim(), avatar, notify_email: notifyEmail }) .eq('user_id', user.id); if (playerErr) { setMsg({ type: 'err', text: 'Fehler beim Speichern: ' + playerErr.message }); setSaving(false); return; } if (pw) { const { error: pwErr } = await sb.auth.updateUser({ password: pw }); if (pwErr) { setMsg({ type: 'err', text: 'Passwort-Fehler: ' + pwErr.message }); setSaving(false); return; } } } setState(s => ({ ...s, user: { ...s.user, name: name.trim(), avatar, notifyEmail } })); setPw(""); setPw2(""); setMsg({ type: 'ok', text: pw ? 'Profil + Passwort gespeichert.' : 'Profil gespeichert.' }); setSaving(false); }; return (
{/* Profile hero β€” current avatar + name */}
{avatar}
{name || user?.name || "β€”"}
{user?.email}
{/* Avatar picker */}
AVATAR WΓ„HLEN
{AVATAR_OPTIONS.map(em => ( ))}
{/* Name */}
ANZEIGENAME
setName(e.target.value)} placeholder="Dein Name" maxLength={30} />
{/* Passwort */}
PASSWORT Γ„NDERN
{sb ? 'Leer lassen, um das Passwort nicht zu Γ€ndern.' : 'Nur im Live-Modus verfΓΌgbar.'}
setPw(e.target.value)} placeholder="Neues Passwort" disabled={!sb} /> setPw2(e.target.value)} placeholder="Passwort wiederholen" disabled={!sb} />
{/* E-Mail-Benachrichtigungen */}
RENN-ERINNERUNGEN
E-Mail 3 Tage vor jedem Rennwochenende
{!sb && (
Nur im Live-Modus verfΓΌgbar.
)}
{msg && (
{msg.text}
)}
); } Object.assign(window, { AuthScreen, LoadingScreen, Dashboard, ProfileScreen });