Files
Cs2-Mapveto/client/src/components/VetoBoard.jsx
T
SPP UserandClaude Opus 5.5 568ccf9b09 Frontend, README und Konfigurationsvorlage
React-Oberfläche mit Übersicht, Match-Erstellung, Beitritt über Team- und
Zuschauercodes, Teamkarten, visuellem Map-Veto, Serie und Serverpanel.
README beschreibt Einrichtung, Umgebungsvariablen, Rollen, Veto-Abläufe,
Serveradapter und CSTV-Zuschauerzugang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:02:18 +01:00

118 lines
4.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { clock } from '../lib/labels.js';
const STEP_SHORT = { ban: 'Ban', pick: 'Pick', side: 'Seite', decider: 'Decider' };
export default function VetoBoard({ state, viewer, act, busy, compact = false }) {
const { veto, teams } = state;
const { current } = veto;
const [selected, setSelected] = useState(null);
useEffect(() => setSelected(null), [current?.index]);
const mapsById = Object.fromEntries(state.maps.map((m) => [m.id, m]));
const yourTurn = !!current && viewer.role === 'captain' && viewer.team === current.team;
const choosingMap = yourTurn && (current.type === 'ban' || current.type === 'pick');
const sideMap = current?.type === 'side' ? mapsById[current.map] : null;
const submitMap = () => act('veto/action', { stepIndex: current.index, map: selected });
const submitSide = (side) => act('veto/action', { stepIndex: current.index, side });
return (
<div className={`veto ${compact ? 'veto-compact' : ''}`}>
{current && (
<div className={`turn turn-${current.team} ${yourTurn ? 'is-you' : ''}`} aria-live="polite">
<span className="turn-team">{teams[current.team].name}</span>
<span className="turn-action">
{current.type === 'ban' && 'bannt eine Map'}
{current.type === 'pick' && 'wählt eine Map'}
{current.type === 'side' && `wählt die Startseite auf ${sideMap?.name}`}
</span>
<span className="turn-hint">
{yourTurn
? 'Du bist am Zug.'
: viewer.team === current.team
? 'Dein Captain entscheidet.'
: viewer.role === 'spectator' || viewer.role === 'organizer'
? 'Live-Ansicht – nur Captains können wählen.'
: 'Das andere Team ist am Zug.'}
</span>
</div>
)}
{!current && veto.done && !compact && <div className="turn turn-done">Veto abgeschlossen</div>}
<div className="map-grid" role="list">
{veto.maps.map((m) => {
const meta = mapsById[m.id];
const selectable = choosingMap && m.status === 'available';
const isSel = selected === m.id;
return (
<button
key={m.id}
type="button"
role="listitem"
className={`map-tile status-${m.status} ${m.by ? `by-${m.by}` : ''} ${isSel ? `is-selected sel-${current.type}` : ''} ${sideMap?.id === m.id ? 'is-side' : ''}`}
style={{ '--map': meta?.color ?? '#556' }}
disabled={!selectable || !!busy}
onClick={() => setSelected(isSel ? null : m.id)}
aria-pressed={isSel}
aria-label={`${meta.name}: ${statusText(m, teams)}`}
>
<span className="map-art" aria-hidden="true" />
<span className="map-name">{meta.name}</span>
<span className="map-status">{statusText(m, teams)}</span>
</button>
);
})}
</div>
{choosingMap && (
<div className="veto-confirm">
<button className={`btn btn-lg ${current.type === 'ban' ? 'btn-danger' : 'btn-primary'}`} disabled={!selected || !!busy} onClick={submitMap}>
{selected
? `${mapsById[selected].name} ${current.type === 'ban' ? 'bannen' : 'wählen'}`
: `Map zum ${current.type === 'ban' ? 'Bannen' : 'Wählen'} antippen`}
</button>
</div>
)}
{yourTurn && current.type === 'side' && (
<div className="side-choice">
<p>Auf welcher Seite startet {teams[viewer.team].name} auf {sideMap?.name}?</p>
<div className="row">
<button className="btn btn-ct btn-lg" disabled={!!busy} onClick={() => submitSide('CT')}>Counter-Terrorists</button>
<button className="btn btn-t btn-lg" disabled={!!busy} onClick={() => submitSide('T')}>Terrorists</button>
</div>
</div>
)}
<ol className="step-track" aria-label="Veto-Ablauf">
{veto.steps.map((s) => (
<li key={s.index} className={`step step-${s.type} ${s.team ? `step-${s.team}` : ''} ${s.done ? 'done' : ''} ${current?.index === s.index ? 'now' : ''}`}
title={s.team ? `${teams[s.team].name}: ${STEP_SHORT[s.type]}` : 'Übrig gebliebene Map'}>
<span>{STEP_SHORT[s.type]}</span>
</li>
))}
</ol>
{veto.history.length > 0 && !compact && (
<ol className="veto-history">
{veto.history.map((h) => (
<li key={h.index} className={`hist-${h.type} ${h.team ? `hist-${h.team}` : ''}`}>
<time>{clock(h.at)}</time> {h.text}
</li>
))}
</ol>
)}
</div>
);
}
function statusText(m, teams) {
switch (m.status) {
case 'banned': return `Gebannt von ${teams[m.by].name}`;
case 'picked': return `Gewählt von ${teams[m.by].name}`;
case 'decider': return 'Entscheidungsmap';
default: return 'Verfügbar';
}
}