Files
Cs2-Mapveto/client/src/components/VetoBoard.jsx
T
mregosar 8be8b3d879 Organisator kann im selben Browser selbst mitspielen
Ein Aufrufer darf Organisator- und Spieler-Zugang kombinieren
(Authorization + X-Player-Token). Berechtigungen prüfen jetzt einzelne
Merkmale statt einer Rangstufe. In der Lobby gibt es „Selbst mitspielen?“,
Teamlinks funktionieren auch im Organisator-Browser. Online-Status wird
direkt nach dem Beitritt aktualisiert.
2026-10-25 17:53:39 +01:00

132 lines
5.6 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';
import MapImage from './MapImage.jsx';
import SideIcon from './SideIcon.jsx';
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.isCaptain && 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.team
? '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)}`}
>
<MapImage map={meta} />
<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">
<MapImage map={sideMap} className="side-choice-art" />
<div className="side-choice-body">
<p>Auf welcher Seite startet <strong>{teams[viewer.team].name}</strong> auf <strong>{sideMap?.name}</strong>?</p>
<div className="side-options">
<button className="side-option side-option-ct" disabled={!!busy} onClick={() => submitSide('CT')}>
<SideIcon side="CT" size={44} />
<span className="side-option-name">Counter-Terrorists</span>
<span className="side-option-sub">Start auf CT, Gegner beginnt als T</span>
</button>
<button className="side-option side-option-t" disabled={!!busy} onClick={() => submitSide('T')}>
<SideIcon side="T" size={44} />
<span className="side-option-name">Terrorists</span>
<span className="side-option-sub">Start auf T, Gegner beginnt als CT</span>
</button>
</div>
</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>
<small>{s.team ? teams[s.team].name : 'automatisch'}</small>
</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';
}
}