import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { api } from '../lib/api.js'; import { FORMAT_LABEL, serverLabel, timeAgo } from '../lib/labels.js'; import { getAccess } from '../lib/session.js'; import { useOverview } from '../lib/useMatch.js'; import MapImage from '../components/MapImage.jsx'; const GROUPS = [ { key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' }, { key: 'running', title: 'Laufend', empty: 'Gerade läuft kein Match.' }, { key: 'closed', title: 'Abgeschlossen', empty: 'Noch keine abgeschlossenen Matches.' }, ]; export default function Overview() { const { matches } = useOverview(); return (

Match-Lobbys

Teams treten mit ihrem Einladungscode bei, die Captains führen das Map-Veto, der Server wird automatisch eingerichtet.

{matches === null ? (

Lade Matches …

) : (
{GROUPS.map((g) => { const list = matches.filter((m) => m.group === g.key); return (

{g.title} {list.length}

{list.length === 0 ?

{g.empty}

: g.key === 'running' ? (
{list.map((m) => )}
) : (
    {list.map((m) => )}
)}
); })}
)}
); } function MatchRow({ match: m }) { const access = getAccess(m.id); const server = serverLabel(m.server); return (
  • {m.teamA} {m.seriesScore && m.format !== 'bo1' ? ( {m.seriesScore.A}:{m.seriesScore.B} ) : ( vs. )} {m.teamB}
    {m.currentMap && ( {m.currentMap.finished ? '' : Live} {m.currentMap.name} {m.currentMap.score.A}:{m.currentMap.score.B} )} {m.modeLabel} {FORMAT_LABEL[m.format]} {m.group === 'open' && {m.players.A + m.players.B}/{m.players.teamSize * 2} Spieler} {m.statusLabel} {access && Dein Zugang} {timeAgo(m.createdAt)}
  • ); } /** Karte für ein laufendes Match: Map als Hintergrund, Stand groß, Serie und Status darunter. */ function RunningCard({ match: m }) { const server = serverLabel(m.server); const map = m.currentMap; const live = map && !map.finished; const bo1 = m.format === 'bo1'; return ( {map?.image && }
    {m.modeLabel}, {FORMAT_LABEL[m.format]} {live ? Live : {m.statusLabel}}
    {m.teamA} {map ? <>{map.score.A}:{map.score.B} : vs.} {m.teamB}
    {map ? `Map ${map.number}: ${map.name}` : 'Veto läuft'} {!bo1 && m.seriesScore && Serie {m.seriesScore.A}:{m.seriesScore.B}}
    ); } function CodeEntry() { const [code, setCode] = useState(''); const [error, setError] = useState(null); const navigate = useNavigate(); const submit = async (e) => { e.preventDefault(); setError(null); try { const res = await api('/access', { method: 'POST', body: { code } }); navigate(res.access === 'spectator' ? `/watch/${code.trim()}` : `/join/${code.trim()}`); } catch (err) { setError(err.message); } }; return (
    setCode(e.target.value)} placeholder="ABCD-EFGH" autoComplete="off" spellCheck="false" />
    {error &&

    {error}

    }
    ); } /** Offene Messer-Server für die Seitenwahl, zum direkten Verbinden. */ function KnifeServers() { const [servers, setServers] = useState([]); useEffect(() => { let alive = true; const load = () => api('/knife-servers').then((res) => alive && setServers(res.servers)).catch(() => {}); load(); const t = setInterval(load, 10_000); return () => { alive = false; clearInterval(t); }; }, []); if (servers.length === 0) return null; return (

    Messer-Server für die Seitenwahl {servers.length}

    Ohne Passwort, auch im LAN-Reiter von Steam. Nach eurer Messerrunde setzt sich der Server selbst zurück.

    ); }