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.
This commit is contained in:
mregosar committed 2026-10-25 17:02:18 +01:00
1 parent f08297dd0d
commit f7520a6ca7
24 files changed
+1938 -1

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }) {
const isOwnTeam = viewer.team === side;
const inLobby = state.status === 'lobby';
const me = team.players.find((p) => p.isYou);
const emptySlots = Math.max(0, team.teamSize - team.players.length);
const canManageCaptain = isOrg || (viewer.role === 'captain' && isOwnTeam);
const { readiness } = team;
return (
<section className={`team-card team-${side} ${readiness.ready ? 'is-ready' : ''}`} aria-label={`Team ${team.name}`}>
<header>
<h2>{team.name}</h2>
<span className={`ready-state ${readiness.ready ? 'on' : ''}`}>
{readiness.ready ? 'Bereit' : `${readiness.count}/${team.teamSize}`}
</span>
</header>
<ul className="roster">
{team.players.map((p) => (
<li key={p.id} className={`${p.isYou ? 'is-you' : ''}`}>
<span className={`presence ${p.online ? 'on' : ''}`} title={p.online ? 'Online' : 'Offline'} />
<span className="nick">
{p.nickname}
{p.isYou && <small> (du)</small>}
</span>
{p.isCaptain && <span className="captain" title="Captain – führt das Veto">Captain</span>}
{inLobby && <span className={`ready-dot ${p.ready ? 'on' : ''}`}>{p.ready ? 'bereit' : 'wartet'}</span>}
{(canManageCaptain && !p.isCaptain) || (isOrg && inLobby) ? (
<span className="row-actions">
{canManageCaptain && !p.isCaptain && (
<button className="link-btn" disabled={!!busy} onClick={() => act('captain', { playerId: p.id }, `${p.nickname} ist jetzt Captain`)}>
Zum Captain machen
</button>
)}
{isOrg && inLobby && (
<button className="link-btn danger" disabled={!!busy} onClick={() => act('kick', { playerId: p.id }, `${p.nickname} entfernt`)}>
Entfernen
</button>
)}
</span>
) : null}
</li>
))}
{Array.from({ length: emptySlots }, (_, i) => (
<li key={`empty-${i}`} className="slot-empty">Freier Platz</li>
))}
</ul>
{me && inLobby && (
<div className="team-actions">
<button className={`btn ${me.ready ? '' : 'btn-primary'}`} disabled={!!busy}
onClick={() => act('ready', { ready: !me.ready })}>
{me.ready ? 'Doch nicht bereit' : 'Ich bin bereit'}
</button>
<button className="link-btn" disabled={!!busy}
onClick={() => window.confirm('Lobby wirklich verlassen?') && act('leave', null, 'Du hast das Team verlassen')}>
Team verlassen
</button>
</div>
)}
{inLobby && !readiness.enough && <p className="small muted">Es fehlen noch {team.teamSize - readiness.count} Spieler.</p>}
</section>
);
}