// 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}
)}
>
)}
{/* end padding-wrapper */}
{/* end card */}
PRIVATE Β· SEASON {window.SEED?.season || 2026} Β· INVITE ONLY
);
}
// ---- LoadingScreen ------------------------------------------------------
function LoadingScreen() {
return (
);
}
// ---- 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 (
);
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 */}
{/* 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 ? (
) : (
{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 }) => (
))}
{/* 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}
);
}
Object.assign(window, { AuthScreen, LoadingScreen, Dashboard, ProfileScreen });