This commit is contained in:
Linrador 2025-09-04 11:49:59 +02:00
parent 728b5cb6f6
commit 990c73beef

View File

@ -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<string | null>(null)
const [adminEditMode, setAdminEditMode] = useState(false)
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
const [leadOverride, setLeadOverride] = useState<number | null>(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<number>(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) {
</div>
{/* Countdown / Status */}
<div className="mb-4" key={openTs}>
<div className="mb-4">
<div className="mx-auto w-full max-w-xl">
<div className="grid grid-cols-[max-content_1fr_max-content] items-center gap-2">
<div className="w-10 h-10" />
@ -622,11 +519,8 @@ export default function MapVotePanel({ match }: Props) {
</span>
)
) : (
<span
className="block text-sm sm:text-base md:text-lg leading-tight whitespace-normal font-semibold px-2 py-1 sm:px-3 sm:py-2 rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 text-center"
suppressHydrationWarning
>
Öffnet in {mounted ? fmtCountdown(msToOpen) : '::'}
<span className="block text-sm sm:text-base md:text-lg leading-tight whitespace-normal font-semibold px-2 py-1 sm:px-3 sm:py-2 rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 text-center">
Öffnet in {fmtCountdown(msToOpen)}
</span>
)}
</div>