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) => )}
) : (
)}
);
})}
)}
);
}
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 (
);
}
/** 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.
{servers.map((k) => (
{k.name}
{k.modeShort && {k.modeShort} }
{k.mapName}
{k.playerCount ? `${k.playerCount} Spieler` : 'frei'}
{k.connect}
Verbinden
))}
);
}