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
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { api } from '../lib/api.js';
|
||||
import { FORMAT_LABEL, ROLE_LABEL, serverLabel } from '../lib/labels.js';
|
||||
import { clearAccess, getToken, setAccess } from '../lib/session.js';
|
||||
import { clearAccess, getPlayerToken, getToken, setAccess, setPlayerToken } from '../lib/session.js';
|
||||
import { useMatch } from '../lib/useMatch.js';
|
||||
import TeamCard from '../components/TeamCard.jsx';
|
||||
import VetoBoard from '../components/VetoBoard.jsx';
|
||||
@@ -11,6 +11,7 @@ import OrganizerPanel from '../components/OrganizerPanel.jsx';
|
||||
import SeriesList from '../components/SeriesList.jsx';
|
||||
import EventLog from '../components/EventLog.jsx';
|
||||
import MapImage from '../components/MapImage.jsx';
|
||||
import SelfJoin from '../components/SelfJoin.jsx';
|
||||
|
||||
export default function MatchPage() {
|
||||
const { id } = useParams();
|
||||
@@ -24,7 +25,9 @@ export default function MatchPage() {
|
||||
}
|
||||
return getToken(id);
|
||||
});
|
||||
const { state, error, connected } = useMatch(id, token);
|
||||
// Spieler-Token des Organisators, wenn er im selben Browser selbst mitspielt
|
||||
const [playerToken, setPlayerTokenState] = useState(() => getPlayerToken(id));
|
||||
const { state, error, connected } = useMatch(id, token, playerToken);
|
||||
const [busy, setBusy] = useState(null);
|
||||
const [toast, setToast] = useState(null);
|
||||
|
||||
@@ -38,6 +41,26 @@ export default function MatchPage() {
|
||||
}
|
||||
}, [lostAccess, id]);
|
||||
|
||||
// Organisator war Spieler, ist es aber nicht mehr (Team verlassen oder entfernt)
|
||||
// justJoined: bis der erste Zustand mit Spieler ankommt, ist ein Zustand ohne Spieler noch veraltet
|
||||
const [justJoined, setJustJoined] = useState(false);
|
||||
useEffect(() => {
|
||||
if (state?.viewer.playerId) setJustJoined(false);
|
||||
}, [state]);
|
||||
const lostPlayer = state && playerToken && !justJoined && state.viewer.isOrganizer && !state.viewer.playerId;
|
||||
useEffect(() => {
|
||||
if (lostPlayer) {
|
||||
setPlayerToken(id, null);
|
||||
setPlayerTokenState(null);
|
||||
}
|
||||
}, [lostPlayer, id]);
|
||||
|
||||
const joinAsOrganizer = useCallback((newPlayerToken) => {
|
||||
setJustJoined(true);
|
||||
setPlayerToken(id, newPlayerToken);
|
||||
setPlayerTokenState(newPlayerToken);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toast) return undefined;
|
||||
const t = setTimeout(() => setToast(null), 6000);
|
||||
@@ -49,14 +72,14 @@ export default function MatchPage() {
|
||||
if (busy) return;
|
||||
setBusy(path);
|
||||
try {
|
||||
await api(`/matches/${id}/${path}`, { method: 'POST', body: body ?? {}, token });
|
||||
await api(`/matches/${id}/${path}`, { method: 'POST', body: body ?? {}, token, playerToken });
|
||||
if (success) setToast({ tone: 'ok', text: success });
|
||||
} catch (err) {
|
||||
setToast({ tone: 'error', text: err.message });
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}, [busy, id, token]);
|
||||
}, [busy, id, token, playerToken]);
|
||||
|
||||
if (error && !state) {
|
||||
return (
|
||||
@@ -70,9 +93,9 @@ export default function MatchPage() {
|
||||
if (!state) return <p className="muted">Verbinde mit Lobby …</p>;
|
||||
|
||||
const { viewer } = state;
|
||||
const isOrg = viewer.role === 'organizer';
|
||||
const isOrg = viewer.isOrganizer;
|
||||
const server = serverLabel(state.server);
|
||||
const ctx = { state, act, busy, isOrg, viewer };
|
||||
const ctx = { state, act, busy, isOrg, viewer, matchId: id, onJoined: joinAsOrganizer };
|
||||
|
||||
return (
|
||||
<div className="match">
|
||||
@@ -92,7 +115,7 @@ export default function MatchPage() {
|
||||
<div><dt>Serie</dt><dd>{FORMAT_LABEL[state.format]}</dd></div>
|
||||
<div><dt>Status</dt><dd>{state.statusLabel}</dd></div>
|
||||
<div><dt>Server</dt><dd className={`tone-${server.tone}`}>{server.text}</dd></div>
|
||||
<div><dt>Du bist</dt><dd>{ROLE_LABEL[viewer.role]}{viewer.team && `, ${state.teams[viewer.team].name}`}</dd></div>
|
||||
<div><dt>Du bist</dt><dd>{describeViewer(viewer, state.teams)}</dd></div>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
@@ -109,7 +132,7 @@ export default function MatchPage() {
|
||||
{viewer.role !== 'public' && (
|
||||
<div className="match-lower">
|
||||
<ServerPanel {...ctx} />
|
||||
{isOrg && <OrganizerPanel {...ctx} matchId={id} token={token} />}
|
||||
{isOrg && <OrganizerPanel {...ctx} token={token} />}
|
||||
<EventLog events={state.events} />
|
||||
</div>
|
||||
)}
|
||||
@@ -117,6 +140,14 @@ export default function MatchPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** z. B. "Organisator, Captain bei Nordlicht" oder "Spieler bei Hafenkante" */
|
||||
function describeViewer(viewer, teams) {
|
||||
const team = viewer.team ? teams[viewer.team].name : null;
|
||||
const asPlayer = team ? `${viewer.isCaptain ? 'Captain' : 'Spieler'} bei ${team}` : null;
|
||||
if (viewer.isOrganizer) return asPlayer ? `Organisator, ${asPlayer}` : 'Organisator';
|
||||
return asPlayer ?? ROLE_LABEL[viewer.role];
|
||||
}
|
||||
|
||||
function Stage(ctx) {
|
||||
const { state, isOrg, act, busy } = ctx;
|
||||
if (state.viewer.role === 'public') {
|
||||
@@ -148,6 +179,7 @@ function Stage(ctx) {
|
||||
: 'Sobald beide Teams bereit sind, startet der Organisator das Veto.'}
|
||||
</p>
|
||||
)}
|
||||
{isOrg && !state.viewer.playerId && <SelfJoin state={state} matchId={ctx.matchId} onJoined={ctx.onJoined} />}
|
||||
<MapPoolPreview maps={state.maps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user