diff --git a/src/app/components/MapVotePanel.tsx b/src/app/components/MapVotePanel.tsx index 4e33e07..323fce2 100644 --- a/src/app/components/MapVotePanel.tsx +++ b/src/app/components/MapVotePanel.tsx @@ -1,4 +1,3 @@ -// /app/components/MapVotePanel.tsx 'use client' import { useEffect, useMemo, useState, useCallback, useRef } from 'react' @@ -36,20 +35,6 @@ const fmtCountdown = (ms: number) => { return `${h}:${pad(m)}:${pad(s)}` } -const toMs = (v: unknown): number | null => { - if (v == null) return null - if (typeof v === 'number' && Number.isFinite(v)) return v >= 1e12 ? v : v * 1000 - if (typeof v === 'string') { - const t = Date.parse(v) - return Number.isNaN(t) ? null : t - } - if (v instanceof Date) { - const t = v.getTime() - return Number.isNaN(t) ? null : t - } - return null -} - /* =================== Component =================== */ export default function MapVotePanel({ match }: Props) { @@ -65,13 +50,18 @@ export default function MapVotePanel({ match }: Props) { const [error, setError] = useState(null) const [adminEditMode, setAdminEditMode] = useState(false) const [overlayShownOnce, setOverlayShownOnce] = useState(false) - const [opensAtOverride, setOpensAtOverride] = useState(null); - const [leadOverride, setLeadOverride] = useState(null); - const matchBaseTs = useMemo(() => { - const raw = match.matchDate ?? match.demoDate ?? Date.now(); - return new Date(raw).getTime(); - }, [match.matchDate, match.demoDate]); + /* -------- Timers / open window -------- */ + const opensAtTs = useMemo(() => { + const base = new Date(match.matchDate ?? match.demoDate ?? Date.now()) + return base.getTime() - 60 * 60 * 1000 + }, [match.matchDate, match.demoDate]) + + const [nowTs, setNowTs] = useState(() => Date.now()) + useEffect(() => { + const t = setInterval(() => setNowTs(Date.now()), 1000) + return () => clearInterval(t) + }, []) /* -------- Overlay integration -------- */ const overlayIsForThisMatch = overlayData?.matchId === match.id @@ -117,115 +107,14 @@ export default function MapVotePanel({ match }: Props) { } }, [match.id]) - useEffect(() => { load() }, [load, match.matchDate, match.demoDate]) + useEffect(() => { load() }, [load]) useEffect(() => { - setOpensAtOverride(null) - }, [match.matchDate, match.demoDate]) - - // 🔔 SSE: wie in MatchDetails — opensAt/leadMinutes direkt aus dem Event übernehmen - useEffect(() => { - console.log("lastEvent: ", lastEvent); - if (!lastEvent) return; - - const { type, payload } = lastEvent as any; - const evt = payload ?? lastEvent; - const evtMatchId = evt?.matchId ?? (lastEvent as any)?.matchId; - if (evtMatchId !== match.id) return; - - if (type === 'map-vote-updated' || type === 'match-meta-updated') { - // 1) opensAt aus Event direkt übernehmen (ISO → ms) - if (evt?.opensAt) { - const ts = - typeof evt.opensAt === 'string' - ? new Date(evt.opensAt).getTime() - : new Date(evt.opensAt).getTime(); - if (Number.isFinite(ts)) setOpensAtOverride(ts); - } - - // 2) leadMinutes mitschneiden und ggf. opensAt daraus ableiten, - // falls im Event kein opensAt enthalten war - if (Number.isFinite(evt?.leadMinutes)) { - const lead = Number(evt.leadMinutes); - setLeadOverride(lead); - - if (!evt?.opensAt) { - const base = new Date( - match.matchDate ?? match.demoDate ?? Date.now() - ).getTime(); - setOpensAtOverride(base - lead * 60_000); - } - } - - // WICHTIG: Kein load()/refresh hier – genau wie in MatchDetails - return; - } - - // Refresh bei Events, die Meta/Lineup betreffen – wie in MatchDetails - const REFRESH_TYPES = new Set([ - 'map-vote-reset', - 'map-vote-locked', - 'map-vote-unlocked', - 'match-updated', - 'match-lineup-updated', - 'match-meta-updated', - ]); - if (REFRESH_TYPES.has(type)) { - // analog zu MatchDetails: UI sanft aktualisieren - router.refresh?.(); - return; - } - - // Fallback: bekannte Match-Events → Daten nachladen - if (MATCH_EVENTS.has(type)) load(); - }, [lastEvent, match.id, match.matchDate, match.demoDate, router, load]); - - - // 📅 Öffnungszeit robust ableiten (Server -> SSE-Override -> Lead-Fallback) - const openTs = useMemo(() => { - if (opensAtOverride != null) return opensAtOverride - const srv = toMs(state?.opensAt) - if (srv != null) return srv - const lead = leadOverride ?? (Number.isFinite(state?.leadMinutes) ? (state!.leadMinutes as number) : 60) - return matchBaseTs - lead * 60_000 - }, [opensAtOverride, state?.opensAt, state?.leadMinutes, leadOverride, matchBaseTs]) - - // --- Hydration-Schutz --- - const [mounted, setMounted] = useState(false) - useEffect(() => { setMounted(true) }, []) - - // --- Countdown-State: NICHT mit Date.now() initialisieren --- - const [msLeft, setMsLeft] = useState(0) - - useEffect(() => { - if (!Number.isFinite(openTs)) console.warn('[MapVotePanel] openTs invalid:', openTs, { state, opensAtOverride, leadOverride, matchBaseTs }) - }, [openTs, state, opensAtOverride, leadOverride, matchBaseTs]) - - - // --- Intervall erst nach Mount starten, an Sekundengrenze ausrichten --- - useEffect(() => { - if (!mounted) return - if (!Number.isFinite(openTs)) { setMsLeft(0); return } - - const update = () => setMsLeft(Math.max(openTs - Date.now(), 0)) - update() - - const drift = 1000 - (Date.now() % 1000) - let intervalId: number | null = null - const timeoutId = window.setTimeout(() => { - update() - intervalId = window.setInterval(update, 1000) - }, drift) - - return () => { - window.clearTimeout(timeoutId) - if (intervalId) window.clearInterval(intervalId) - } - }, [openTs, mounted]) - - // --- Ab hier nur noch msLeft benutzen --- - const isOpen = mounted && msLeft <= 0 - const msToOpen = msLeft + if (!lastEvent) return + if (!MATCH_EVENTS.has(lastEvent.type)) return + if (lastEvent.payload?.matchId !== match.id) return + load() + }, [lastEvent, match.id, load]) /* -------- Admin-Edit Mirror -------- */ const adminEditingBy = state?.adminEdit?.by ?? null @@ -236,6 +125,14 @@ export default function MapVotePanel({ match }: Props) { }, [adminEditingEnabled, adminEditingBy, session?.user?.steamId]) /* -------- Derived flags & memoized maps -------- */ + const opensAt = useMemo( + () => (state?.opensAt ? new Date(state.opensAt).getTime() : null), + [state?.opensAt] + ) + const isOpenFromMatch = nowTs >= opensAtTs + const isOpen = opensAt != null ? nowTs >= opensAt : isOpenFromMatch + const msToOpen = Math.max((opensAt ?? opensAtTs) - nowTs, 0) + const me = session?.user const isAdmin = !!me?.isAdmin const mySteamId = me?.steamId @@ -580,7 +477,7 @@ export default function MapVotePanel({ match }: Props) { {/* Countdown / Status */} -
+
@@ -622,11 +519,8 @@ export default function MapVotePanel({ match }: Props) { ) ) : ( - - Öffnet in {mounted ? fmtCountdown(msToOpen) : '–:–:–'} + + Öffnet in {fmtCountdown(msToOpen)} )}