import { useCallback, useEffect, useState } from 'react'; import { FORMAT_LABEL } from '../lib/labels.js'; import MapImage from '../components/MapImage.jsx'; import SideIcon from '../components/SideIcon.jsx'; import { ScaledStage, useClock } from './StreamPage.jsx'; import { useStreamOverview } from './StreamOverviewPage.jsx'; import '../stream.css'; import '../stream-overview.css'; import '../beamer.css'; const DEFAULT_SECONDS = 15; /** * Diashow der laufenden Matches für den Beamer: /beamer * Ein Match pro Folie, schaltet automatisch weiter. Am Ende eine Folie mit Lobbys und Ergebnissen. * Optional ?sekunden=20 (5 bis 120) und ?key=. * Tasten: ← → blättern, Leertaste anhalten, F Vollbild. Klick schaltet Vollbild um. */ export default function BeamerPage() { const params = new URLSearchParams(window.location.search); const key = params.get('key') ?? ''; const seconds = Math.max(5, Math.min(120, Number.parseInt(params.get('sekunden'), 10) || DEFAULT_SECONDS)); const { data, error } = useStreamOverview(key); const now = useClock(); const [index, setIndex] = useState(0); const [paused, setPaused] = useState(false); useEffect(() => { document.documentElement.classList.add('stream-mode'); return () => document.documentElement.classList.remove('stream-mode'); }, []); const slides = buildSlides(data); const count = slides.length; const current = slides[count ? index % count : 0]; // Folie über die ID merken, damit sich bei neuen Daten nicht die angezeigte Folie verschiebt const currentId = current?.id; const go = useCallback((step) => setIndex((i) => (count ? (i + step + count) % count : 0)), [count]); useEffect(() => { if (paused || count <= 1) return undefined; const t = setTimeout(() => go(1), seconds * 1000); return () => clearTimeout(t); }, [index, paused, count, seconds, go, currentId]); useEffect(() => { const onKey = (e) => { if (e.key === 'ArrowRight') go(1); else if (e.key === 'ArrowLeft') go(-1); else if (e.key === ' ') { e.preventDefault(); setPaused((p) => !p); } else if (e.key === 'f' || e.key === 'F') toggleFullscreen(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [go]); if (error) { return (
Maxlan e.V.

Diashow nicht verfügbar

{error}

); } const live = (data?.active ?? []).filter((m) => m.status === 'live').length; return (
{current?.type === 'match' && }
Maxlan e.V.
Counter-Strike 2 {live > 0 && {live} live} {count > 1 && {(index % count) + 1} / {count}} {paused && angehalten}
{!data &&
Lade Matches …
} {current?.type === 'match' && } {current?.type === 'summary' && }
{count > 1 && (
{slides.map((s, i) => )}
)} {count > 1 && !paused &&
}
); } /** Ein Match pro Folie, danach eine Übersicht mit Lobbys und Ergebnissen (wenn es welche gibt). */ function buildSlides(data) { if (!data) return []; const slides = data.active.map((m) => ({ id: m.id, type: 'match', match: m })); if (data.upcoming.length || data.results.length || slides.length === 0) slides.push({ id: 'summary', type: 'summary' }); return slides; } function toggleFullscreen() { if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {}); else document.documentElement.requestFullscreen?.().catch(() => {}); } function currentMap(m) { return m.series.find((s) => s.number === m.currentMapNumber); } function Backdrop({ m }) { const map = currentMap(m); if (m.status === 'veto' || !map) return null; return ; } function MatchSlide({ m }) { const map = currentMap(m); const isBo1 = m.format === 'bo1'; const playing = m.status === 'live' && map?.live?.status === 'live' && map.status !== 'done'; const badge = m.status === 'veto' ? 'Map-Veto' : m.status === 'server_setup' ? 'Server startet' : playing ? 'Live' : map?.status === 'done' ? 'Map beendet' : 'Aufwärmphase'; return (
{m.modeLabel}, {FORMAT_LABEL[m.format]} {badge}

{m.teams.A}

{!isBo1 && m.status !== 'veto' ? ( <> {m.seriesScore.A}:{m.seriesScore.B} Maps ) : vs}

{m.teams.B}

{m.status === 'veto' && } {m.status === 'server_setup' && map &&

Server wird eingerichtet: Map {map.number}, {map.name}

} {m.status === 'live' && map && }
); } function LivePart({ m, map }) { const sides = map.live?.sides; const sideOf = (t) => (sides ? (sides.CT === t ? 'CT' : 'T') : null); const finished = map.status === 'done'; const rounds = map.live?.roundsPlayed ?? 0; return ( <>
{map.name} {finished ? 'Endstand' : map.live?.status === 'live' ? `Runde ${rounds + 1}` : 'Aufwärmphase'}
{sideOf('A') && } {map.score.A} : {map.score.B} {sideOf('B') && }
{m.series.length > 1 && (
    {m.series.map((s) => (
  1. {s.name} {(s.status === 'done' || s.number === map.number) && {s.score.A}:{s.score.B}}
  2. ))}
)} ); } function VetoPart({ m }) { const { veto, teams } = m; const c = veto.current; const action = c && ({ ban: 'bannt eine Map', pick: 'wählt eine Map', side: 'wählt die Seite' })[c.type]; return ( <>

{c ? <>{teams[c.team]} {action} : 'Veto abgeschlossen'} Schritt {Math.min(veto.stepsDone + 1, veto.stepsTotal)} von {veto.stepsTotal}

{veto.maps.map((x) => (
{x.name}
))}
); } function SummarySlide({ data }) { const score = (m) => (m.format === 'bo1' && m.series[0] ? m.series[0].score : m.seriesScore); return (
{data.active.length === 0 &&

Gerade läuft kein Match

}

Gleich

{data.upcoming.length ? (
    {data.upcoming.map((m) => (
  • {m.teams.A}vs{m.teams.B} {FORMAT_LABEL[m.format]}{m.ready.A && m.ready.B ? ', bereit' : ''}
  • ))}
) :

Keine offenen Lobbys

}

Ergebnisse

{data.results.length ? (
    {data.results.map((m) => { const s = score(m); return (
  • s.B ? 'won' : ''}`}>{m.teams.A} {s.A}:{s.B} s.A ? 'won' : ''}`}>{m.teams.B}
  • ); })}
) :

Noch keine Ergebnisse

}
); }