updated
This commit is contained in:
parent
728b5cb6f6
commit
990c73beef
@ -1,4 +1,3 @@
|
|||||||
// /app/components/MapVotePanel.tsx
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
||||||
@ -36,20 +35,6 @@ const fmtCountdown = (ms: number) => {
|
|||||||
return `${h}:${pad(m)}:${pad(s)}`
|
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 =================== */
|
/* =================== Component =================== */
|
||||||
|
|
||||||
export default function MapVotePanel({ match }: Props) {
|
export default function MapVotePanel({ match }: Props) {
|
||||||
@ -65,13 +50,18 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [adminEditMode, setAdminEditMode] = useState(false)
|
const [adminEditMode, setAdminEditMode] = useState(false)
|
||||||
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
|
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
|
||||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
|
|
||||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const matchBaseTs = useMemo(() => {
|
/* -------- Timers / open window -------- */
|
||||||
const raw = match.matchDate ?? match.demoDate ?? Date.now();
|
const opensAtTs = useMemo(() => {
|
||||||
return new Date(raw).getTime();
|
const base = new Date(match.matchDate ?? match.demoDate ?? Date.now())
|
||||||
}, [match.matchDate, match.demoDate]);
|
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 -------- */
|
/* -------- Overlay integration -------- */
|
||||||
const overlayIsForThisMatch = overlayData?.matchId === match.id
|
const overlayIsForThisMatch = overlayData?.matchId === match.id
|
||||||
@ -117,115 +107,14 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
}, [match.id])
|
}, [match.id])
|
||||||
|
|
||||||
useEffect(() => { load() }, [load, match.matchDate, match.demoDate])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpensAtOverride(null)
|
if (!lastEvent) return
|
||||||
}, [match.matchDate, match.demoDate])
|
if (!MATCH_EVENTS.has(lastEvent.type)) return
|
||||||
|
if (lastEvent.payload?.matchId !== match.id) return
|
||||||
// 🔔 SSE: wie in MatchDetails — opensAt/leadMinutes direkt aus dem Event übernehmen
|
load()
|
||||||
useEffect(() => {
|
}, [lastEvent, match.id, load])
|
||||||
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
|
|
||||||
|
|
||||||
/* -------- Admin-Edit Mirror -------- */
|
/* -------- Admin-Edit Mirror -------- */
|
||||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||||
@ -236,6 +125,14 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
||||||
|
|
||||||
/* -------- Derived flags & memoized maps -------- */
|
/* -------- 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 me = session?.user
|
||||||
const isAdmin = !!me?.isAdmin
|
const isAdmin = !!me?.isAdmin
|
||||||
const mySteamId = me?.steamId
|
const mySteamId = me?.steamId
|
||||||
@ -580,7 +477,7 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Countdown / Status */}
|
{/* Countdown / Status */}
|
||||||
<div className="mb-4" key={openTs}>
|
<div className="mb-4">
|
||||||
<div className="mx-auto w-full max-w-xl">
|
<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="grid grid-cols-[max-content_1fr_max-content] items-center gap-2">
|
||||||
<div className="w-10 h-10" />
|
<div className="w-10 h-10" />
|
||||||
@ -622,11 +519,8 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<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">
|
||||||
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)}
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
Öffnet in {mounted ? fmtCountdown(msToOpen) : '–:–:–'}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user