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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
104aa1a67b
commit
568ccf9b09
24 files changed
+1938
-1
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { api } from '../lib/api.js';
|
||||
import { getAccess, setAccess } from '../lib/session.js';
|
||||
|
||||
function useCode() {
|
||||
const { code } = useParams();
|
||||
const [info, setInfo] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
useEffect(() => {
|
||||
api('/access', { method: 'POST', body: { code } }).then(setInfo).catch((e) => setError(e.message));
|
||||
}, [code]);
|
||||
return { code, info, error };
|
||||
}
|
||||
|
||||
export function JoinPage() {
|
||||
const { code, info, error } = useCode();
|
||||
const navigate = useNavigate();
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [steamId, setSteamId] = useState('');
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!info) return;
|
||||
if (info.access === 'spectator') navigate(`/watch/${code}`, { replace: true });
|
||||
// Schon beigetreten? Dann direkt zur Lobby.
|
||||
else if (getAccess(info.matchId)) navigate(`/m/${info.matchId}`, { replace: true });
|
||||
}, [info, code, navigate]);
|
||||
|
||||
if (error) return <AccessError message={error} />;
|
||||
if (!info) return <p className="muted">Prüfe Code …</p>;
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setSubmitError(null);
|
||||
try {
|
||||
const res = await api(`/matches/${info.matchId}/join`, { method: 'POST', body: { code, nickname, steamId } });
|
||||
setAccess(info.matchId, { token: res.playerToken, kind: 'player' });
|
||||
navigate(`/m/${info.matchId}`);
|
||||
} catch (err) {
|
||||
setSubmitError(err.message);
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={`join-card team-${info.team}`} onSubmit={submit}>
|
||||
<p className="muted">{info.title}</p>
|
||||
<h1>Beitreten als {info.teamName}</h1>
|
||||
<label className="field">
|
||||
<span>Spielername</span>
|
||||
<input value={nickname} onChange={(e) => setNickname(e.target.value)} required minLength={2} maxLength={24} autoFocus />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>SteamID64 (optional)</span>
|
||||
<input value={steamId} onChange={(e) => setSteamId(e.target.value)} inputMode="numeric" placeholder="7656119…" pattern="\d{17}" />
|
||||
<small className="hint">Hilft dem Organisator, dich auf dem Server zuzuordnen.</small>
|
||||
</label>
|
||||
{submitError && <div className="alert alert-error">{submitError}</div>}
|
||||
<button className="btn btn-primary btn-lg" disabled={busy}>{busy ? 'Trete bei …' : `${info.teamName} beitreten`}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function WatchPage() {
|
||||
const { code, info, error } = useCode();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!info) return;
|
||||
if (info.access !== 'spectator') {
|
||||
navigate(`/join/${code}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
// Spieler oder Organisator behalten ihren stärkeren Zugang.
|
||||
if (!getAccess(info.matchId)) setAccess(info.matchId, { token: code, kind: 'spectator' });
|
||||
navigate(`/m/${info.matchId}`, { replace: true });
|
||||
}, [info, code, navigate]);
|
||||
if (error) return <AccessError message={error} />;
|
||||
return <p className="muted">Öffne Zuschauerzugang …</p>;
|
||||
}
|
||||
|
||||
function AccessError({ message }) {
|
||||
return (
|
||||
<div className="empty">
|
||||
<h2>Code nicht gültig</h2>
|
||||
<p className="muted">{message}. Prüfe den Link oder frag den Organisator nach einem neuen Code.</p>
|
||||
<Link to="/" className="btn">Zur Übersicht</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user