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.
This commit is contained in:
mregosar committed 2026-10-25 17:53:39 +01:00
1 parent 3a74e16bb8
commit 8be8b3d879
15 files changed
+248 -59

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { api } from '../lib/api.js';
/** Organisator tritt im selben Browser selbst einem Team bei. */
export default function SelfJoin({ state, matchId, onJoined }) {
const [nickname, setNickname] = useState('');
const [busyTeam, setBusyTeam] = useState(null);
const [error, setError] = useState(null);
const { codes } = state.organizer;
const join = async (team) => {
setBusyTeam(team);
setError(null);
try {
const res = await api(`/matches/${matchId}/join`, { method: 'POST', body: { code: codes[team], nickname } });
onJoined(res.playerToken);
} catch (err) {
setError(err.message);
} finally {
setBusyTeam(null);
}
};
const valid = nickname.trim().length >= 2;
return (
<div className="self-join">
<h3>Selbst mitspielen?</h3>
<p className="small muted">Tritt hier einem Team bei. Du bleibst dabei Organisator, ein zweiter Browser ist nicht nötig.</p>
<label className="field">
<span>Dein Spielername</span>
<input value={nickname} onChange={(e) => setNickname(e.target.value)} maxLength={24} placeholder="z. B. MrEgosar" />
</label>
<div className="self-join-buttons">
{['A', 'B'].map((key) => {
const team = state.teams[key];
const full = team.players.length >= team.teamSize;
return (
<button key={key} type="button" className={`btn self-join-${key}`} disabled={!valid || full || !!busyTeam}
onClick={() => join(key)}>
{busyTeam === key ? 'Trete bei …' : full ? `${team.name} ist voll` : `${team.name} beitreten`}
</button>
);
})}
</div>
{error && <p className="field-error">{error}</p>}
</div>
);
}
+1 -1
View File
@@ -59,7 +59,7 @@ export default function ServerPanel({ state, isOrg, act, busy }) {
</p>
</div>
)}
{state.viewer.role === 'spectator' && server?.status === 'configured' && !server.gotv && (
{state.viewer.isSpectator && !state.viewer.isOrganizer && server?.status === 'configured' && !server.gotv && (
<p className="small muted">CSTV ist auf diesem Server deaktiviert.</p>
)}
+3 -3
View File
@@ -3,7 +3,7 @@ export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }
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 canManageCaptain = isOrg || (viewer.isCaptain && isOwnTeam);
const { readiness } = team;
return (
@@ -25,14 +25,14 @@ export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }
</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) ? (
{(canManageCaptain && !p.isCaptain) || (isOrg && inLobby && !p.isYou) ? (
<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 && (
{isOrg && inLobby && !p.isYou && (
<button className="link-btn danger" disabled={!!busy} onClick={() => act('kick', { playerId: p.id }, `${p.nickname} entfernt`)}>
Entfernen
</button>
+2 -2
View File
@@ -12,7 +12,7 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
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 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;
@@ -34,7 +34,7 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
? 'Du bist am Zug.'
: viewer.team === current.team
? 'Dein Captain entscheidet.'
: viewer.role === 'spectator' || viewer.role === 'organizer'
: !viewer.team
? 'Live-Ansicht – nur Captains können wählen.'
: 'Das andere Team ist am Zug.'}
</span>