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:
1 parent
3a74e16bb8
commit
8be8b3d879
15 files changed
+248
-59
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user