// Shared helpers, store, components used across screens // Exports to window: store, useStore, fmt, Avatar, DriverPick, TeamPick, Logo, Countdown, etc. const { useState, useEffect, useMemo, useRef, useCallback } = React; // ---- Supabase client (null = Demo-Modus ohne Backend) ------------------- const _supabase = (window.SUPABASE_URL && window.SUPABASE_ANON_KEY && window.supabase) ? window.supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY) : null; // Expose for other files (screens-1, screens-3) window._supabase = _supabase; // ---- Persistent cache (localStorage) ------------------------------------ const LIVE_CACHE_KEY = "f1live_cache_v1"; function _hasStoredSession() { try { return Object.keys(localStorage).some(k => k.startsWith('sb-') && k.endsWith('-auth-token')); } catch { return false; } } function _saveCache(state) { try { localStorage.setItem(LIVE_CACHE_KEY, JSON.stringify({ user: state.user, currentRaceId: state.currentRaceId, seasonPicks: state.seasonPicks, racePicks: state.racePicks, results: state.results, raceState: state.raceState, scoring: state.scoring, players: window.SEED.players, drivers: window.SEED.drivers, // inkl. active-Flag aus API-Sync calendar: window.SEED.calendar, // aktueller Kalender aus API-Sync season: window.SEED.season, })); } catch {} } function _loadCache() { try { return JSON.parse(localStorage.getItem(LIVE_CACHE_KEY)); } catch { return null; } } function _clearCache() { try { localStorage.removeItem(LIVE_CACHE_KEY); } catch {} } const defaultState = () => ({ user: null, // { id, name, email, isAdmin } view: _hasStoredSession() ? "loading" : "auth", currentRaceId: null, seasonPicks: {}, // { [userId]: { driverChampion, constructor, ... } } racePicks: {}, // { [raceId]: { [userId]: { pole, winner, ... } } } results: {}, // { [raceId]: { pole, winner, sprintWinner, finishers } } raceState: {}, // { [raceId]: 'upcoming' | 'pre-race' | 'finished' } scoring: null, // null = SEED.scoring Defaults authError: null, // 'not_registered' | null }); // ---- Lightweight store --------------------------------------------------- function _deriveMissingRaceStates(raceState) { // Rennstatus aus Datum ableiten / korrigieren: // - Vergangene Rennen (> 6h nach Rennstart) → immer 'finished', auch wenn Cache veraltet ist // - Zukünftige Rennen ohne Eintrag → aus Datum ableiten // - Zukünftige Rennen mit bestehendem Eintrag → unverändert lassen const now = Date.now(); const updated = { ...raceState }; (window.SEED?.calendar || []).forEach(race => { const raceTs = new Date(race.date + 'T15:00:00Z').getTime(); const preTs = raceTs - 2 * 24 * 3600 * 1000; if (now > raceTs + 6 * 3600 * 1000) { updated[race.id] = 'finished'; // Rennen liegt eindeutig in der Vergangenheit } else if (!updated[race.id]) { updated[race.id] = now >= preTs ? 'pre-race' : 'upcoming'; } }); return updated; } function _initLiveState() { if (!_hasStoredSession()) return { ...defaultState(), view: 'auth' }; const cache = _loadCache(); if (cache?.user) { // Sofortiger Restore aus Cache – Supabase läuft im Hintergrund nach if (cache.players) window.SEED.players = cache.players; if (cache.drivers) window.SEED.drivers = cache.drivers; if (cache.calendar && cache.calendar.length > 0) window.SEED.calendar = cache.calendar; if (cache.season) window.SEED.season = cache.season; const raceState = _deriveMissingRaceStates(cache.raceState || {}); const cal = window.SEED?.calendar || []; const currentRaceId = cal.find(r => raceState[r.id] !== 'finished')?.id || cache.currentRaceId || cal[cal.length - 1]?.id; return { ...defaultState(), user: cache.user, currentRaceId, seasonPicks: cache.seasonPicks || {}, racePicks: cache.racePicks || {}, results: cache.results || {}, raceState, scoring: cache.scoring || null, view: 'dashboard', }; } return { ...defaultState() }; } let _state = _initLiveState(); const _subs = new Set(); function getState() { return _state; } function setState(updater) { const prev = _state; const next = typeof updater === "function" ? updater(prev) : { ...prev, ...updater }; _state = next; _subs.forEach(fn => fn(next)); if (next.user) { _syncToSupabase(prev, next); // Cache sofort aktualisieren wenn Picks geändert — sonst gehen sie beim Reload verloren if (next.racePicks !== prev.racePicks || next.seasonPicks !== prev.seasonPicks) { _saveCache(next); } } } function useStore(selector = s => s) { const [, force] = useState(0); useEffect(() => { const fn = () => force(x => x + 1); _subs.add(fn); return () => _subs.delete(fn); }, []); return selector(_state); } // ---- Supabase: Daten nach DB schreiben (fire-and-forget) ---------------- function _syncToSupabase(prev, next) { if (!next.user) return; const userId = next.user.id; // Saison-Tipps const nextSP = next.seasonPicks?.[userId]; if (nextSP && nextSP !== prev.seasonPicks?.[userId]) { _supabase.from('season_picks').upsert({ user_id: userId, driver_champion: nextSP.driverChampion || null, constructor: Object.prototype.hasOwnProperty.call(nextSP, 'constructor') ? (nextSP.constructor || null) : null, destructor: nextSP.destructor || null, driver_kicked: nextSP.driverKicked || null, team_boss_kicked: nextSP.teamBossKicked || null, surprise: nextSP.surprise || null, updated_at: new Date().toISOString(), }).then(({ error }) => error && console.error('[sync] season_picks:', error)); } // Renn-Tipps (nur eigene) Object.entries(next.racePicks || {}).forEach(([raceId, picks]) => { const nextRP = picks?.[userId]; if (nextRP && nextRP !== prev.racePicks?.[raceId]?.[userId]) { _supabase.from('race_picks').upsert({ user_id: userId, race_id: raceId, pole: nextRP.pole || null, winner: nextRP.winner || null, sprint_pole: nextRP.sprintPole || null, sprint_winner: nextRP.sprintWinner || null, finishers: nextRP.finishers ?? null, updated_at: new Date().toISOString(), }).then(({ error }) => error && console.error('[sync] race_picks:', error)); } }); // Admin-only: Ergebnisse, Rennstatus, Scoring if (!next.user.isAdmin) return; Object.entries(next.results || {}).forEach(([raceId, res]) => { if (res && res !== prev.results?.[raceId]) { _supabase.from('results').upsert({ race_id: raceId, pole: res.pole || null, winner: res.winner || null, sprint_pole: res.sprintPole || null, sprint_winner: res.sprintWinner || null, finishers: res.finishers ?? null, updated_at: new Date().toISOString(), }).then(({ error }) => error && console.error('[sync] results:', error)); } }); Object.entries(next.raceState || {}).forEach(([raceId, state]) => { if (state !== prev.raceState?.[raceId]) { _supabase.from('race_states').upsert({ race_id: raceId, state, updated_at: new Date().toISOString(), }).then(({ error }) => error && console.error('[sync] race_states:', error)); } }); if (next.scoring !== prev.scoring && next.scoring) { _supabase.from('scoring_config').upsert({ id: 1, config: next.scoring, updated_at: new Date().toISOString(), }).then(({ error }) => error && console.error('[sync] scoring_config:', error)); } } // ---- Supabase: Alle Daten laden ----------------------------------------- async function _loadAllData(userId, myPlayer) { const [ { data: playersData }, { data: seasonPicksData }, { data: racePicksData }, { data: resultsData }, { data: raceStatesData }, { data: scoringRow }, ] = await Promise.all([ _supabase.from('players').select('*'), _supabase.from('season_picks').select('*'), _supabase.from('race_picks').select('*'), _supabase.from('results').select('*'), _supabase.from('race_states').select('*'), _supabase.from('scoring_config').select('config').eq('id', 1).maybeSingle(), window.f1Api?.syncCalendar().catch(() => null), window.f1Api?.syncDrivers().catch(() => null), ]); // Spieler-Array aufbauen und SEED.players überschreiben const players = (playersData || []) .filter(p => p.user_id) // nur bereits eingeloggte Spieler .map(p => ({ id: p.user_id, name: p.name, avatar: p.avatar || '🏎', email: p.email, isAdmin: p.is_admin, you: p.email === myPlayer.email, })); window.SEED.players = players; // bestehenden UI-Code unverändert lassen // Saison-Tipps const seasonPicks = {}; (seasonPicksData || []).forEach(sp => { seasonPicks[sp.user_id] = { driverChampion: sp.driver_champion, constructor: sp.constructor, destructor: sp.destructor, driverKicked: sp.driver_kicked, teamBossKicked: sp.team_boss_kicked, surprise: sp.surprise, }; }); // Renn-Tipps const racePicks = {}; (racePicksData || []).forEach(rp => { if (!racePicks[rp.race_id]) racePicks[rp.race_id] = {}; racePicks[rp.race_id][rp.user_id] = { pole: rp.pole, winner: rp.winner, sprintPole: rp.sprint_pole, sprintWinner: rp.sprint_winner, finishers: rp.finishers, }; }); // Ergebnisse const results = {}; (resultsData || []).forEach(r => { results[r.race_id] = { pole: r.pole, winner: r.winner, sprintPole: r.sprint_pole, sprintWinner: r.sprint_winner, finishers: r.finishers, }; }); // Renn-Status aus DB const raceState = {}; (raceStatesData || []).forEach(rs => { raceState[rs.race_id] = rs.state; }); // Fehlende Rennstatus aus Datum ableiten (DB-Einträge haben Vorrang) Object.assign(raceState, _deriveMissingRaceStates(raceState)); // Aktuelles Rennen = erstes, das nicht "finished" ist const cal = window.SEED.calendar; const currentRaceId = cal.find(r => raceState[r.id] !== 'finished')?.id || cal[cal.length - 1].id; _state = { ...defaultState(), user: { id: userId, name: myPlayer.name, email: myPlayer.email, avatar: myPlayer.avatar || '🏎', isAdmin: myPlayer.is_admin, notifyEmail: myPlayer.notify_email || false, }, seasonPicks, racePicks, results, raceState, scoring: scoringRow?.config || null, view: 'dashboard', currentRaceId, authError: null, }; _saveCache(_state); _subs.forEach(fn => fn(_state)); // Auto-API-Sync: Saison-Ergebnisse nur für Admins (Kalender+Fahrer bereits oben geladen) if (window.f1Api && myPlayer.is_admin) { window.f1Api.syncSeason() .then(apiData => { if (!apiData) return; setState(s => { const raceState = _deriveMissingRaceStates({ ...s.raceState, ...(apiData.raceState || {}), }); const cal = window.SEED.calendar; const currentRaceId = cal.find(r => raceState[r.id] !== 'finished')?.id || cal[cal.length - 1]?.id; return { ...s, raceState, currentRaceId, results: { ...s.results, ...apiData.results } }; }); _saveCache(getState()); console.log('[f1] Admin-Sync abgeschlossen'); }) .catch(err => console.warn('[f1] Admin-Sync fehlgeschlagen:', err)); } } // ---- Supabase: Login verarbeiten ---------------------------------------- async function _handleSignIn(session) { // Prüfen ob E-Mail in der players-Tabelle steht const { data: player, error: playerError } = await _supabase .from('players') .select('*') .eq('email', session.user.email) .maybeSingle(); if (playerError) throw new Error('players query: ' + playerError.message); if (!player) { // Nicht registriert → sofort ausloggen _clearCache(); await _supabase.auth.signOut(); _state = { ...defaultState(), view: 'auth', authError: 'not_registered' }; _subs.forEach(fn => fn(_state)); return; } // Beim ersten Login: user_id in players-Tabelle eintragen if (!player.user_id) { await _supabase.from('players') .update({ user_id: session.user.id }) .eq('email', session.user.email); } await _loadAllData(session.user.id, { ...player, user_id: session.user.id }); } // ---- Supabase: Session verarbeiten (öffentlich, für direkten Aufruf) ---- async function processSignIn(session) { try { await _handleSignIn(session); } catch (err) { console.error('[f1] processSignIn failed:', err); _state = { ...defaultState(), view: 'auth', authError: 'sign_in_failed' }; _subs.forEach(fn => fn(_state)); throw err; } } // ---- Supabase: Stille Daten-Aktualisierung (nach Cache-Restore) --------- // Lädt Picks, Ergebnisse und Rennstatus frisch aus der DB, ohne View zu ändern. async function _silentRefresh() { try { const [ { data: racePicksData }, { data: seasonPicksData }, { data: resultsData }, { data: raceStatesData }, { data: playersData }, { data: scoringRow }, ] = await Promise.all([ _supabase.from('race_picks').select('*'), _supabase.from('season_picks').select('*'), _supabase.from('results').select('*'), _supabase.from('race_states').select('*'), _supabase.from('players').select('*'), _supabase.from('scoring_config').select('config').eq('id', 1).maybeSingle(), window.f1Api?.syncCalendar().catch(() => null), ]); const racePicks = {}; (racePicksData || []).forEach(rp => { if (!racePicks[rp.race_id]) racePicks[rp.race_id] = {}; racePicks[rp.race_id][rp.user_id] = { pole: rp.pole, winner: rp.winner, sprintPole: rp.sprint_pole, sprintWinner: rp.sprint_winner, finishers: rp.finishers, }; }); const seasonPicks = {}; (seasonPicksData || []).forEach(sp => { seasonPicks[sp.user_id] = { driverChampion: sp.driver_champion, constructor: Object.prototype.hasOwnProperty.call(sp, 'constructor') ? sp.constructor : null, destructor: sp.destructor, driverKicked: sp.driver_kicked, teamBossKicked: sp.team_boss_kicked, surprise: sp.surprise, }; }); const results = {}; (resultsData || []).forEach(r => { results[r.race_id] = { pole: r.pole, winner: r.winner, sprintPole: r.sprint_pole, sprintWinner: r.sprint_winner, finishers: r.finishers, }; }); const raceStateFromDb = {}; (raceStatesData || []).forEach(rs => { raceStateFromDb[rs.race_id] = rs.state; }); const raceState = _deriveMissingRaceStates(raceStateFromDb); if (playersData) { const myEmail = getState().user?.email; window.SEED.players = playersData .filter(p => p.user_id) .map(p => ({ id: p.user_id, name: p.name, avatar: p.avatar || '🏎', email: p.email, isAdmin: p.is_admin, you: p.email === myEmail, })); } const cal = window.SEED?.calendar || []; const currentRaceId = cal.find(r => raceState[r.id] !== 'finished')?.id || cal[cal.length - 1]?.id; const scoring = scoringRow?.config !== undefined ? scoringRow.config : getState().scoring; setState(s => ({ ...s, racePicks, seasonPicks, results, raceState, currentRaceId, scoring })); _saveCache(getState()); console.log('[f1] Silent refresh abgeschlossen'); } catch (err) { console.warn('[f1] Silent refresh fehlgeschlagen:', err); } } // ---- Supabase: Session beim Start laden + Auth-Events ------------------- // Primär: getSession() direkt – zuverlässiger als onAuthStateChange INITIAL_SESSION _supabase.auth.getSession().then(async ({ data: { session } }) => { if (getState().view !== 'loading') { // Cache hat bereits geladen — Daten still aus DB aktualisieren (Picks, Ergebnisse) if (session) _silentRefresh(); return; } if (session) { try { await _handleSignIn(session); } catch (err) { console.error('[f1] getSession handleSignIn failed:', err); _state = { ...defaultState(), view: 'auth', authError: 'sign_in_failed' }; _subs.forEach(fn => fn(_state)); } } else { _state = { ...defaultState(), view: 'auth' }; _subs.forEach(fn => fn(_state)); } }).catch(err => { console.error('[f1] getSession failed:', err); if (getState().view === 'loading') { _state = { ...defaultState(), view: 'auth' }; _subs.forEach(fn => fn(_state)); } }); // Sicherheits-Timeout: falls nach 10s noch im loading-State, auf auth fallen setTimeout(() => { if (getState().view === 'loading') { _state = { ...defaultState(), view: 'auth' }; _subs.forEach(fn => fn(_state)); } }, 10000); // Periodische Rennstatus-Korrektur: verhindert veraltete Zustände bei langem Tab-Betrieb. // Läuft alle 15 Minuten und korrigiert raceState + currentRaceId auf Basis der aktuellen Zeit. // (Beispiel: Monaco wurde "pre-race" gespeichert, Tab blieb offen → nächster Tick → 'finished') setInterval(() => { const s = getState(); if (!s.user) return; const raceState = _deriveMissingRaceStates(s.raceState); const cal = window.SEED?.calendar || []; const currentRaceId = cal.find(r => raceState[r.id] !== 'finished')?.id || cal[cal.length - 1]?.id; setState(prev => ({ ...prev, raceState, currentRaceId })); }, 15 * 60 * 1000); // Backup: Magic-Link-Redirect feuert SIGNED_IN nach URL-Hash-Verarbeitung _supabase.auth.onAuthStateChange(async (event, session) => { if (event === 'SIGNED_IN' && session && !getState().user) { try { await _handleSignIn(session); } catch (err) { console.error('[f1] onAuthStateChange handleSignIn failed:', err); _state = { ...defaultState(), view: 'auth', authError: 'sign_in_failed' }; _subs.forEach(fn => fn(_state)); } } }); // ---- Öffentliche Auth-Funktion ------------------------------------------ async function signOut() { _clearCache(); await _supabase.auth.signOut(); _state = { ...defaultState(), view: 'auth' }; _subs.forEach(fn => fn(_state)); } // ---- Util --------------------------------------------------------------- // Gibt immer ein Objekt zurück (Fallback für gekickte/unbekannte Fahrer) function getDriver(id) { return window.SEED.drivers.find(d => d.id === id) || (id ? { id, num: '?', first: '', last: id, team: null, code: id.slice(0,3).toUpperCase(), active: false, _unknown: true } : null); } function getTeam(id) { return window.SEED.teams.find(t => t.id === id) || { id: id || '?', name: id || '?', short: '?', color: 'var(--bg-3)', accent: 'var(--ink-2)' }; } function getRace(id) { return window.SEED.calendar.find(r => r.id === id); } function driverName(id, short) { const d = getDriver(id); if (!d) return "—"; return short ? `${d.last}` : `${d.first} ${d.last}`; } function teamName(id) { const t = getTeam(id); return t ? t.name : "—"; } function formatDate(iso) { const d = new Date(iso); const months = ["JAN","FEB","MAR","APR","MAI","JUN","JUL","AUG","SEP","OKT","NOV","DEZ"]; const day = String(d.getDate()).padStart(2, "0"); return `${day} ${months[d.getMonth()]}`; } function getDeadlines(race) { const raceTs = new Date(race.date + "T15:00:00Z").getTime(); // FP1 aus API wenn vorhanden, Fallback: Freitag 11:30 UTC (≈ 2 Tage vor Rennen) const fp1Ts = race.fp1 ? new Date(race.fp1).getTime() : raceTs - 2 * 24 * 3600 * 1000 - 3.5 * 3600 * 1000; return { fp1: fp1Ts }; } // ---- useNow: gibt aktuelle Zeit zurück, re-rendert alle N ms ------------- function useNow(ms) { const [now, setNow] = React.useState(Date.now); React.useEffect(() => { const id = setInterval(() => setNow(Date.now()), ms || 30000); return () => clearInterval(id); }, [ms]); return now; } // ---- Toast: globale Benachrichtigung ------------------------------------- let _toastFn = null; function showToast(msg, type) { if (_toastFn) _toastFn(msg, type || 'ok'); } function Toast() { const [item, setItem] = React.useState(null); const timerRef = React.useRef(null); React.useEffect(() => { _toastFn = (msg, type) => { setItem({ msg, type }); clearTimeout(timerRef.current); timerRef.current = setTimeout(() => setItem(null), 2500); }; return () => { _toastFn = null; }; }, []); if (!item) return null; const color = item.type === 'err' ? 'var(--warn)' : 'var(--good)'; return (
{item.msg}
); } // ---- Visual atoms ------------------------------------------------------- function Logo({ size = 140, styleOverride = {}, className = "" }) { if (window.F1_LOGO_SRC) { const style = className ? { display: "block", ...styleOverride } : { width: size, height: "auto", display: "block", ...styleOverride }; return F1 Tippspiel; } // Fallback — Bebas Neue hat kein Italic/Bold, daher ohne fontStyle/fontWeight return (
F1
TIPPSPIEL BOX/26 · PRIVATE GROUP
); } function TeamLogo({ teamId, size = 24 }) { const t = getTeam(teamId); if (!t) return null; return (
{t.short}
); } function DriverNum({ num, teamId, size = 36 }) { const t = getTeam(teamId); return (
{num}
); } function Avatar({ player, size = 32 }) { return (
{player.avatar}
); } function FlagDot({ country }) { return ( {country} ); } function Countdown({ ts, compact }) { const [now, setNow] = useState(Date.now()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, []); const diff = Math.max(0, ts - now); const d = Math.floor(diff / 86400000); const h = Math.floor((diff % 86400000) / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); const expired = diff <= 0; if (compact) { return ( {expired ? "GESCHLOSSEN" : `${d}T ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`} ); } return (
{String(d).padStart(2,"0")}
Tage
{String(h).padStart(2,"0")}
Std
{String(m).padStart(2,"0")}
Min
{String(s).padStart(2,"0")}
Sek
); } function DriverPick({ driver, selected, onClick, disabled, showTeam = true }) { const t = getTeam(driver.team); return ( ); } function TeamPick({ team, selected, onClick, disabled }) { return ( ); } function SectionH({ title, sub, right }) { return (

{title}

{sub &&
{sub}
}
{right}
); } // ---- Score-Berechnung --------------------------------------------------- function computeScores() { const s = getState(); const SEED = window.SEED; const scoring = s.scoring || SEED.scoring; const totals = {}; SEED.players.forEach(p => totals[p.id] = { total: 0, byRace: {}, byRaceBreakdown: {}, season: 0, seasonBreakdown: {} }); // Renn-Punkte Object.entries(s.results).forEach(([raceId, res]) => { const picks = s.racePicks[raceId] || {}; SEED.players.forEach(p => { const pk = picks[p.id]; if (!pk) return; const bk = {}; if (res.pole && pk.pole === res.pole) bk.pole = scoring.pole; if (res.winner && pk.winner === res.winner) bk.raceWinner = scoring.raceWinner; if (res.sprintWinner && pk.sprintWinner === res.sprintWinner) bk.sprintWinner = scoring.sprintWinner; if (res.sprintPole && pk.sprintPole === res.sprintPole) bk.sprintPole = scoring.sprintPole || 0; if (res.finishers != null && pk.finishers != null) { if (pk.finishers === res.finishers) bk.finishers = scoring.finishers; else if (Math.abs(pk.finishers - res.finishers) === 1) bk.finishersClose = scoring.finishersClose; } const pts = Object.values(bk).reduce((sum, v) => sum + v, 0); totals[p.id].byRace[raceId] = pts; totals[p.id].byRaceBreakdown[raceId] = bk; totals[p.id].total += pts; }); }); // Saison-Punkte (einmalig, sobald Admin Saison-Ergebnisse einträgt) // Object.create(null) statt {} verhindert, dass prototype-Properties wie .constructor fälschlicherweise matchen const sr = scoring.seasonResults || Object.create(null); SEED.players.forEach(p => { const sp = s.seasonPicks[p.id] || Object.create(null); const sbk = {}; if (sr.driverChampion && sp.driverChampion === sr.driverChampion) sbk.driverChampion = scoring.seasonChampion; if (sr.constructor && sp.constructor === sr.constructor) sbk.constructor = scoring.seasonConstructor; if (sr.destructor && sp.destructor === sr.destructor) sbk.destructor = scoring.seasonDestructor; if (sr.driverKicked && sp.driverKicked === sr.driverKicked) sbk.driverKicked = scoring.seasonDriverKicked; if (sr.teamBossKicked && sp.teamBossKicked === sr.teamBossKicked) sbk.teamBossKicked = scoring.seasonTeamBossKicked; if (sr.surprise && sp.surprise === sr.surprise) sbk.surprise = scoring.seasonSurprise; const pts = Object.values(sbk).reduce((sum, v) => sum + v, 0); totals[p.id].season = pts; totals[p.id].seasonBreakdown = sbk; totals[p.id].total += pts; }); return { scoring, totals }; } class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { error: null }; } static getDerivedStateFromError(error) { return { error }; } componentDidCatch(error, info) { console.error('[ErrorBoundary]', error, info?.componentStack); } render() { if (this.state.error) { return (
!
FEHLER IN DIESER ANSICHT
Etwas ist schiefgelaufen. Du kannst es erneut versuchen oder zu einer anderen Seite wechseln.
{this.state.error?.message || "Unbekannter Fehler"}
); } return this.props.children; } } Object.assign(window, { // store getState, setState, useStore, signOut, processSignIn, // util getDriver, getTeam, getRace, driverName, teamName, formatDate, getDeadlines, // hooks + toast useNow, showToast, Toast, // atoms Logo, TeamLogo, DriverNum, Avatar, FlagDot, Countdown, DriverPick, TeamPick, SectionH, // scoring computeScores, // error handling ErrorBoundary, });