updated
This commit is contained in:
parent
728b5cb6f6
commit
990c73beef
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user