updated
5
.env
@ -22,4 +22,7 @@ PTERO_PANEL_URL=https://panel.ironieopen.de
|
|||||||
PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022
|
PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022
|
||||||
PTERO_SERVER_SFTP_USER=army.37a11489
|
PTERO_SERVER_SFTP_USER=army.37a11489
|
||||||
PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
||||||
PTERO_SERVER_ID=37a11489
|
PTERO_SERVER_ID=37a11489
|
||||||
|
NEXT_PUBLIC_CS2_WS_URL=wss://cs2.ironieopen.de:8081/telemetry
|
||||||
|
NEXT_PUBLIC_CS2_WS_HOST=cs2.ironieopen.de
|
||||||
|
NEXT_PUBLIC_CS2_WS_PORT=8081
|
||||||
BIN
public/assets/img/radar/de_ancient/de_ancient_radar_psd.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/assets/img/radar/de_ancient/de_ancient_v1_radar_psd.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/assets/img/radar/de_anubis/de_anubis_radar_psd.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/assets/img/radar/de_dust2/de_dust2_radar_psd.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
public/assets/img/radar/de_inferno/de_inferno_radar_psd.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/assets/img/radar/de_mirage/de_mirage_radar_psd.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
public/assets/img/radar/de_nuke/de_nuke_lower_radar_psd.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
public/assets/img/radar/de_nuke/de_nuke_radar_psd.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/assets/img/radar/de_overpass/de_overpass_radar_psd.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
public/assets/img/radar/de_train/de_train_lower_radar_psd.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
public/assets/img/radar/de_train/de_train_radar_psd.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 121 KiB |
BIN
public/assets/img/radar/de_vertigo/de_vertigo_radar_psd.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
202
src/app/components/LiveRadar.tsx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// /app/components/LiveRadar.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import Button from './Button'
|
||||||
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
|
||||||
|
type Props = { matchId: string }
|
||||||
|
|
||||||
|
type ApiStep = { action: 'ban'|'pick'|'decider', map?: string | null }
|
||||||
|
type ApiResponse = {
|
||||||
|
steps: ApiStep[]
|
||||||
|
mapVisuals?: Record<string, { label: string; bg: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LiveRadar({ matchId }: Props) {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [data, setData] = useState<ApiResponse | null>(null)
|
||||||
|
|
||||||
|
// --- WS Status für Header ---
|
||||||
|
const [wsStatus, setWsStatus] = useState<'idle'|'connecting'|'open'|'closed'|'error'>('idle')
|
||||||
|
|
||||||
|
// 1) MapVote laden (welche Map wurde gewählt?)
|
||||||
|
useEffect(() => {
|
||||||
|
let canceled = false
|
||||||
|
const load = async () => {
|
||||||
|
setLoading(true); setError(null)
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/matches/${matchId}/mapvote`, { cache: 'no-store' })
|
||||||
|
if (!r.ok) {
|
||||||
|
const j = await r.json().catch(() => ({}))
|
||||||
|
throw new Error(j?.message || 'Laden fehlgeschlagen')
|
||||||
|
}
|
||||||
|
const json = await r.json()
|
||||||
|
if (!Array.isArray(json?.steps)) throw new Error('Ungültige Serverantwort (steps fehlt)')
|
||||||
|
if (!canceled) setData(json)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!canceled) setError(e?.message ?? 'Unbekannter Fehler')
|
||||||
|
} finally {
|
||||||
|
if (!canceled) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
return () => { canceled = true }
|
||||||
|
}, [matchId])
|
||||||
|
|
||||||
|
// 2) Beim Betreten des Radars mit dem CS2-WS-Server verbinden und alles loggen
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
// Priorität: explizite URL > Host/Port > Fallback auf aktuelle Hostname:8081
|
||||||
|
const explicit = process.env.NEXT_PUBLIC_CS2_WS_URL
|
||||||
|
const host = process.env.NEXT_PUBLIC_CS2_WS_HOST || window.location.hostname
|
||||||
|
const port = process.env.NEXT_PUBLIC_CS2_WS_PORT || '8081'
|
||||||
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
const url = explicit || `${proto}://${host}:${port}`
|
||||||
|
|
||||||
|
let alive = true
|
||||||
|
let ws: WebSocket | null = null
|
||||||
|
let retryTimer: number | null = null
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
if (!alive) return
|
||||||
|
setWsStatus('connecting')
|
||||||
|
ws = new WebSocket(url)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
setWsStatus('open')
|
||||||
|
console.info('[cs2-ws] connected →', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
// Rohdaten + JSON-parsed in der Konsole anzeigen
|
||||||
|
const raw = ev.data
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw as string)
|
||||||
|
console.log('[cs2-ws] message (json):', parsed)
|
||||||
|
} catch {
|
||||||
|
console.log('[cs2-ws] message (raw):', raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
setWsStatus('error')
|
||||||
|
console.error('[cs2-ws] error:', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = (ev) => {
|
||||||
|
console.warn('[cs2-ws] closed:', ev.code, ev.reason)
|
||||||
|
setWsStatus('closed')
|
||||||
|
if (alive) {
|
||||||
|
// simpler Reconnect nach 2s
|
||||||
|
retryTimer = window.setTimeout(connect, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
alive = false
|
||||||
|
if (retryTimer) window.clearTimeout(retryTimer)
|
||||||
|
try { ws?.close(1000, 'radar unmounted') } catch {}
|
||||||
|
}
|
||||||
|
}, []) // nur einmal beim Mount auf /radar verbinden
|
||||||
|
|
||||||
|
// Erste gespielte Map (Pick/Decider in Anzeige-Reihenfolge)
|
||||||
|
const firstMapKey = useMemo(() => {
|
||||||
|
const chosen = (data?.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||||
|
return chosen[0]?.map ?? null
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const mapLabel = useMemo(() => {
|
||||||
|
if (!firstMapKey) return 'Unbekannte Map'
|
||||||
|
return data?.mapVisuals?.[firstMapKey]?.label ?? firstMapKey
|
||||||
|
}, [data, firstMapKey])
|
||||||
|
|
||||||
|
// Radar-Datei(en)
|
||||||
|
const { folderKey, candidates } = useMemo(() => {
|
||||||
|
if (!firstMapKey) return { folderKey: null as string | null, candidates: [] as string[] }
|
||||||
|
const key = firstMapKey.startsWith('de_') ? firstMapKey.slice(3) : firstMapKey
|
||||||
|
const base = `/assets/img/radar/${key}`
|
||||||
|
const list = [
|
||||||
|
`${base}/de_${key}_radar_psd.png`,
|
||||||
|
`${base}/de_${key}_lower_radar_psd.png`,
|
||||||
|
`${base}/de_${key}_v1_radar_psd.png`,
|
||||||
|
`${base}/de_${key}_radar.png`, // optionaler Fallback
|
||||||
|
]
|
||||||
|
return { folderKey: key, candidates: list }
|
||||||
|
}, [firstMapKey])
|
||||||
|
|
||||||
|
const [srcIdx, setSrcIdx] = useState(0)
|
||||||
|
useEffect(() => { setSrcIdx(0) }, [folderKey])
|
||||||
|
const currentSrc = candidates[srcIdx]
|
||||||
|
|
||||||
|
// kleines Badge für WS-Status
|
||||||
|
const WsDot = ({ status }: { status: typeof wsStatus }) => {
|
||||||
|
const color =
|
||||||
|
status === 'open' ? 'bg-green-500' :
|
||||||
|
status === 'connecting' ? 'bg-amber-500' :
|
||||||
|
status === 'error' ? 'bg-red-500' :
|
||||||
|
'bg-neutral-400'
|
||||||
|
const label =
|
||||||
|
status === 'open' ? 'verbunden' :
|
||||||
|
status === 'connecting' ? 'verbinde…' :
|
||||||
|
status === 'error' ? 'Fehler' :
|
||||||
|
status === 'closed' ? 'getrennt' : '—'
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs opacity-80">
|
||||||
|
<span className={`inline-block w-2.5 h-2.5 rounded-full ${color}`} />
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold">Live Radar</h2>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-sm opacity-80">{mapLabel}</div>
|
||||||
|
<WsDot status={wsStatus} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-8 flex justify-center"><LoadingSpinner /></div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="p-4 text-red-600">{error}</div>
|
||||||
|
) : !firstMapKey ? (
|
||||||
|
<div className="p-4 rounded bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100">
|
||||||
|
Noch keine gespielte Map gefunden.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* großes Radar-Bild */}
|
||||||
|
<div className="relative w-full max-w-5xl mx-auto rounded-lg overflow-hidden border border-neutral-300 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800">
|
||||||
|
{currentSrc ? (
|
||||||
|
<img
|
||||||
|
key={currentSrc}
|
||||||
|
src={currentSrc}
|
||||||
|
alt={mapLabel}
|
||||||
|
className="w-full h-auto block"
|
||||||
|
onError={() => {
|
||||||
|
if (srcIdx < candidates.length - 1) setSrcIdx(i => i + 1)
|
||||||
|
else setError('Radar-Grafik nicht gefunden.')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="p-6 text-center">Keine Radar-Grafik gefunden.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-xs opacity-70 text-center">
|
||||||
|
Quelle: <code className="px-1">{currentSrc ?? '—'}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -78,14 +78,12 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
if (lastEvent.payload?.matchId !== match.id) return
|
if (lastEvent.payload?.matchId !== match.id) return
|
||||||
|
|
||||||
const fm = lastEvent.payload?.firstMap ?? {}
|
const fm = lastEvent.payload?.firstMap ?? {}
|
||||||
showWithDelay(
|
showWithDelay({
|
||||||
{
|
matchId: match.id,
|
||||||
matchId: match.id,
|
mapLabel: fm?.label ?? 'Erste Map',
|
||||||
mapLabel: fm?.label ?? 'Erste Map',
|
mapBg: fm?.bg ?? '/assets/img/maps/cs2.webp',
|
||||||
mapBg: fm?.bg ?? '/assets/img/maps/cs2.webp',
|
nextHref: `/match-details/${match.id}/radar`,
|
||||||
},
|
}, 3000)
|
||||||
3000
|
|
||||||
)
|
|
||||||
}, [lastEvent, match.id, showWithDelay])
|
}, [lastEvent, match.id, showWithDelay])
|
||||||
|
|
||||||
/* -------- Data load (initial + SSE refresh) -------- */
|
/* -------- Data load (initial + SSE refresh) -------- */
|
||||||
|
|||||||
@ -1,37 +1,39 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/app/lib/useSSEStore'
|
||||||
import MatchReadyOverlay from './MatchReadyOverlay'
|
import MatchReadyOverlay from './MatchReadyOverlay'
|
||||||
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
|
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erwartet ein SSE-Event 'map-vote-updated' bzw. 'match-ready' mit payload:
|
* Erwartet SSE-Events:
|
||||||
* { matchId: string, locked: boolean, bestOf: number, steps: [...], mapVisuals: { [k]: {label, bg} } }
|
* - 'match-ready' { matchId, firstMap:{label,bg}, participants:string[] }
|
||||||
*
|
* - 'map-vote-updated' { matchId, locked, bestOf, steps, mapVisuals, teams:{teamA,teamB} }
|
||||||
* Falls dein Backend leichter anpassbar ist: schicke einfach ein schlankes 'match-ready' Event mit:
|
|
||||||
* { matchId, firstMap: { key, label, bg }, participants: string[] }
|
|
||||||
* Dann sparst du dir den Client-seitigen Ableitungscode unten.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function ReadyOverlayHost() {
|
export default function ReadyOverlayHost() {
|
||||||
|
const router = useRouter()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const mySteamId = session?.user?.steamId ?? null
|
const mySteamId = session?.user?.steamId ?? null
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
|
|
||||||
const { open, data, showWithDelay, hide } = useReadyOverlayStore()
|
const { open, data, showWithDelay, hide } = useReadyOverlayStore()
|
||||||
|
|
||||||
// lokal gemerkte "bereits akzeptiert"-Flags pro Match
|
// --- LocalStorage-Flag: pro Match nur einmal anzeigen ---
|
||||||
const isAccepted = (matchId: string) =>
|
const isAccepted = (matchId: string) =>
|
||||||
typeof window !== 'undefined' && window.localStorage.getItem(`match:${matchId}:readyAccepted`) === '1'
|
typeof window !== 'undefined' &&
|
||||||
|
window.localStorage.getItem(`match:${matchId}:readyAccepted`) === '1'
|
||||||
|
|
||||||
const markAccepted = (matchId: string) => {
|
const markAccepted = (matchId: string) => {
|
||||||
if (typeof window !== 'undefined') window.localStorage.setItem(`match:${matchId}:readyAccepted`, '1')
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(`match:${matchId}:readyAccepted`, '1')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// –– Helfer: aus einem map-vote-updated Payload die erste Map & Teilnehmer ableiten
|
// --- Ableitung firstMap + participants aus 'map-vote-updated' ---
|
||||||
function deriveReadySummary(payload: any) {
|
function deriveReadySummary(payload: any) {
|
||||||
// payload: { matchId, locked, bestOf, steps, mapVisuals, teams: { teamA:{players,leader}, teamB:{players,leader} } }
|
|
||||||
const matchId: string | undefined = payload?.matchId
|
const matchId: string | undefined = payload?.matchId
|
||||||
if (!matchId) return null
|
if (!matchId) return null
|
||||||
|
|
||||||
@ -40,7 +42,6 @@ export default function ReadyOverlayHost() {
|
|||||||
const steps: any[] = Array.isArray(payload?.steps) ? payload.steps : []
|
const steps: any[] = Array.isArray(payload?.steps) ? payload.steps : []
|
||||||
const mapVisuals = payload?.mapVisuals ?? {}
|
const mapVisuals = payload?.mapVisuals ?? {}
|
||||||
|
|
||||||
// Teilnehmer ermitteln (aus votes-Teams oder Fallbacks)
|
|
||||||
const playersA = payload?.teams?.teamA?.players ?? []
|
const playersA = payload?.teams?.teamA?.players ?? []
|
||||||
const playersB = payload?.teams?.teamB?.players ?? []
|
const playersB = payload?.teams?.teamB?.players ?? []
|
||||||
const participants: string[] = [
|
const participants: string[] = [
|
||||||
@ -48,39 +49,44 @@ export default function ReadyOverlayHost() {
|
|||||||
...playersB.map((p: any) => p?.steamId).filter(Boolean),
|
...playersB.map((p: any) => p?.steamId).filter(Boolean),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Picks + Decider mit Map
|
const chosen = steps.filter(
|
||||||
const chosen = steps.filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
(s) => (s.action === 'pick' || s.action === 'decider') && s.map
|
||||||
|
)
|
||||||
const allChosen = locked && chosen.length >= bestOf
|
const allChosen = locked && chosen.length >= bestOf
|
||||||
|
|
||||||
if (!allChosen) return null
|
if (!allChosen) return null
|
||||||
|
|
||||||
const first = chosen[0]
|
const first = chosen[0]
|
||||||
const key = first?.map
|
const key = first?.map
|
||||||
const label = key ? (mapVisuals?.[key]?.label ?? key) : '?'
|
const label = key ? (mapVisuals?.[key]?.label ?? key) : '?'
|
||||||
const bg = key ? (mapVisuals?.[key]?.bg ?? `/assets/img/maps/${key}/1.jpg`) : '/assets/img/maps/cs2.webp'
|
const bg = key
|
||||||
|
? (mapVisuals?.[key]?.bg ?? `/assets/img/maps/${key}/1.jpg`)
|
||||||
|
: '/assets/img/maps/cs2.webp'
|
||||||
|
|
||||||
return {
|
return { matchId, firstMap: { key, label, bg }, participants }
|
||||||
matchId,
|
|
||||||
firstMap: { key, label, bg },
|
|
||||||
participants,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// –– SSE-Events beobachten (map-vote-updated oder match-ready)
|
// --- SSE: 'match-ready' / 'map-vote-updated' öffnen das Overlay ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent || !mySteamId) return
|
if (!lastEvent || !mySteamId) return
|
||||||
|
|
||||||
console.log("Empfangen: ", lastEvent);
|
|
||||||
|
|
||||||
if (lastEvent.type === 'match-ready') {
|
if (lastEvent.type === 'match-ready') {
|
||||||
const m = lastEvent.payload?.matchId
|
const m = lastEvent.payload?.matchId
|
||||||
const participants: string[] = lastEvent.payload?.participants ?? []
|
const participants: string[] = lastEvent.payload?.participants ?? []
|
||||||
if (!m || !participants.includes(mySteamId)) return
|
if (!m || !participants.includes(mySteamId) || isAccepted(m)) return
|
||||||
if (isAccepted(m)) return
|
|
||||||
const label = lastEvent.payload?.firstMap?.label ?? '?'
|
const label = lastEvent.payload?.firstMap?.label ?? '?'
|
||||||
const bg = lastEvent.payload?.firstMap?.bg ?? '/assets/img/maps/cs2.webp'
|
const bg = lastEvent.payload?.firstMap?.bg ?? '/assets/img/maps/cs2.webp'
|
||||||
// 3s Delay:
|
|
||||||
showWithDelay({ matchId: m, mapLabel: label, mapBg: bg }, 3000)
|
// 3s Delay + Zielroute mitgeben:
|
||||||
|
showWithDelay(
|
||||||
|
{
|
||||||
|
matchId: m,
|
||||||
|
mapLabel: label,
|
||||||
|
mapBg: bg,
|
||||||
|
nextHref: `/match-details/${m}/radar`, // <- WICHTIG
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,14 +94,21 @@ export default function ReadyOverlayHost() {
|
|||||||
const summary = deriveReadySummary(lastEvent.payload)
|
const summary = deriveReadySummary(lastEvent.payload)
|
||||||
if (!summary) return
|
if (!summary) return
|
||||||
const { matchId: m, firstMap, participants } = summary
|
const { matchId: m, firstMap, participants } = summary
|
||||||
if (!participants.includes(mySteamId)) return
|
if (!participants.includes(mySteamId) || isAccepted(m)) return
|
||||||
if (isAccepted(m)) return
|
|
||||||
// 3s Delay:
|
showWithDelay(
|
||||||
showWithDelay({ matchId: m, mapLabel: firstMap.label, mapBg: firstMap.bg }, 3000)
|
{
|
||||||
|
matchId: m,
|
||||||
|
mapLabel: firstMap.label,
|
||||||
|
mapBg: firstMap.bg,
|
||||||
|
nextHref: `/match-details/${m}/radar`, // <- WICHTIG
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [lastEvent, mySteamId, showWithDelay])
|
}, [lastEvent, mySteamId, showWithDelay])
|
||||||
|
|
||||||
// Falls Voting zurückgesetzt wird: Overlay schließen + Flag löschen
|
// --- Reset-Event: Overlay schließen & Flag löschen ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return
|
if (!lastEvent) return
|
||||||
if (lastEvent.type === 'map-vote-reset') {
|
if (lastEvent.type === 'map-vote-reset') {
|
||||||
@ -115,10 +128,11 @@ export default function ReadyOverlayHost() {
|
|||||||
mapLabel={data.mapLabel}
|
mapLabel={data.mapLabel}
|
||||||
mapBg={data.mapBg}
|
mapBg={data.mapBg}
|
||||||
onAccept={async () => {
|
onAccept={async () => {
|
||||||
// optional: Backend benachrichtigen
|
// optional: Backend informieren
|
||||||
// await fetch(`/api/matches/${data.matchId}/ready-accept`, { method: 'POST' }).catch(() => {})
|
// await fetch(`/api/matches/${data.matchId}/ready-accept`, { method: 'POST' }).catch(() => {})
|
||||||
markAccepted(data.matchId)
|
markAccepted(data.matchId)
|
||||||
hide()
|
hide()
|
||||||
|
router.push(data.nextHref ?? `/match-details/${data.matchId}/radar`) // <- WICHTIG
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,12 +6,13 @@ type ReadyOverlayData = {
|
|||||||
matchId: string
|
matchId: string
|
||||||
mapLabel: string
|
mapLabel: string
|
||||||
mapBg: string
|
mapBg: string
|
||||||
|
nextHref?: string // Zielroute nach "ACCEPT"
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
open: boolean
|
open: boolean
|
||||||
data: ReadyOverlayData | null
|
data: ReadyOverlayData | null
|
||||||
showAt?: number | null // für 3s Delay
|
showAt?: number | null
|
||||||
showWithDelay: (data: ReadyOverlayData, delayMs: number) => void
|
showWithDelay: (data: ReadyOverlayData, delayMs: number) => void
|
||||||
hide: () => void
|
hide: () => void
|
||||||
}
|
}
|
||||||
@ -23,7 +24,6 @@ export const useReadyOverlayStore = create<State>((set) => ({
|
|||||||
showWithDelay: (data, delayMs) => {
|
showWithDelay: (data, delayMs) => {
|
||||||
const showAt = Date.now() + Math.max(0, delayMs)
|
const showAt = Date.now() + Math.max(0, delayMs)
|
||||||
set({ data, showAt })
|
set({ data, showAt })
|
||||||
// kleiner Ticker um nach delayMs zu öffnen (ohne extra setTimeout-Leak)
|
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const t = Date.now()
|
const t = Date.now()
|
||||||
if (t >= showAt) {
|
if (t >= showAt) {
|
||||||
|
|||||||
5
src/app/match-details/[matchId]/radar/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import LiveRadar from '@/app/components/LiveRadar'
|
||||||
|
|
||||||
|
export default function RadarPage({ params }: { params: { matchId: string } }) {
|
||||||
|
return <LiveRadar matchId={params.matchId} />
|
||||||
|
}
|
||||||