Startseite neu: Spieler zuerst mit Code-Eingabe, eigenem Match und den vier Schritten

This commit is contained in:
mregosar committed 2026-10-29 22:27:06 +01:00
1 parent 7b6ce01d05
commit c62fe4ba1e
2 files changed
+231 -40

No files matched your search

+155 -40
View File
@@ -6,53 +6,145 @@ import { getAccess } from '../lib/session.js';
import { useOverview } from '../lib/useMatch.js';
import MapImage from '../components/MapImage.jsx';
const GROUPS = [
{ key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' },
{ key: 'running', title: 'Laufend', empty: 'Gerade läuft kein Match.' },
{ key: 'closed', title: 'Abgeschlossen', empty: 'Noch keine abgeschlossenen Matches.' },
];
export default function Overview() {
const { matches } = useOverview();
const mine = (matches ?? []).filter((m) => m.group !== 'closed' && getAccess(m.id));
const step = mine.length ? Math.max(...mine.map((m) => nextStep(m, getAccess(m.id)).step)) : 0;
const running = (matches ?? []).filter((m) => m.group === 'running');
const open = (matches ?? []).filter((m) => m.group === 'open');
const closed = (matches ?? []).filter((m) => m.group === 'closed');
return (
<div className="overview">
<section className="overview-head">
<div>
<h1>Match-Lobbys</h1>
<p className="lead">Teams treten mit ihrem Einladungscode bei, die Captains führen das Map-Veto, der Server wird automatisch eingerichtet.</p>
<div className="overview home">
<section className={`home-hero ${mine.length ? 'has-mine' : ''}`}>
<div className="home-intro">
<h1>Mit deinem Team ins Match</h1>
<p className="lead">
Den Teamlink postet der Organisator im Match-Chat auf maxlan.de oder in Discord. Öffne den Link deines Teams.
Hast du nur den Code, gib ihn hier ein.
</p>
<CodeTicket />
</div>
<CodeEntry />
{mine.length > 0 && <MyMatches matches={mine} />}
</section>
<Steps current={step} />
<KnifeServers />
{matches === null ? (
<p className="muted">Lade Matches …</p>
) : (
<div className="groups">
{GROUPS.map((g) => {
const list = matches.filter((m) => m.group === g.key);
return (
<section key={g.key} className={`group group-${g.key}`}>
<h2>{g.title} <span className="count">{list.length}</span></h2>
{list.length === 0 ? <p className="muted small">{g.empty}</p> : g.key === 'running' ? (
<div className="run-grid">
{list.map((m) => <RunningCard key={m.id} match={m} />)}
</div>
) : (
<ul className="match-list">
{list.map((m) => <MatchRow key={m.id} match={m} />)}
</ul>
)}
</section>
);
})}
</div>
<>
<section className="group home-live">
<h2>Laufend <span className="count">{running.length}</span></h2>
{running.length === 0 ? (
<p className="muted small">Gerade läuft kein Match.</p>
) : (
<div className="run-grid">
{running.map((m) => <RunningCard key={m.id} match={m} />)}
</div>
)}
</section>
<div className="home-lists">
<section className="group">
<h2>Offene Lobbys <span className="count">{open.length}</span></h2>
{open.length === 0 ? (
<p className="muted small">Keine offene Lobby. Sobald der Organisator ein Match erstellt, steht es hier.</p>
) : (
<ul className="match-list">{open.map((m) => <MatchRow key={m.id} match={m} />)}</ul>
)}
</section>
<section className="group">
<h2>Ergebnisse <span className="count">{closed.length}</span></h2>
{closed.length === 0 ? (
<p className="muted small">Noch kein Match abgeschlossen.</p>
) : (
<ul className="match-list">{closed.slice(0, 12).map((m) => <MatchRow key={m.id} match={m} />)}</ul>
)}
</section>
</div>
</>
)}
</div>
);
}
const STEPS = [
{ title: 'Teamlink öffnen', text: 'Link aus dem Match-Chat, Namen eingeben' },
{ title: 'Bereit klicken', text: 'In der Lobby auf „Ich bin bereit“' },
{ title: 'Map-Veto', text: 'Die Captains bannen und wählen' },
{ title: 'Auf den Server', text: 'Befehl kopieren oder im LAN-Reiter' },
];
/** Was als Nächstes zu tun ist, je nach Stand des Matches und Rolle. step = Schritt in STEPS (1–4). */
function nextStep(m, access) {
const organizer = access?.kind === 'organizer';
switch (m.status) {
case 'lobby':
return organizer
? { step: 2, text: 'Lobby offen: Veto starten, sobald beide Teams bereit sind', action: 'Zur Lobby' }
: { step: 2, text: 'Lobby offen: Klick auf „Ich bin bereit“', action: 'Zur Lobby' };
case 'veto':
return { step: 3, text: 'Das Map-Veto läuft', action: 'Zum Veto' };
case 'server_setup':
return { step: 4, text: 'Der Server wird eingerichtet, gleich geht es los', action: 'Zum Match' };
default:
return organizer
? { step: 4, text: 'Match läuft: Aufwärmphase beenden, wenn alle da sind', action: 'Zum Match' }
: { step: 4, text: 'Match läuft: Verbindung steht auf der Match-Seite', action: 'Zum Server' };
}
}
/** Matches, für die dieser Browser einen Zugang hat, mit dem nächsten Schritt. */
function MyMatches({ matches }) {
return (
<aside className="home-mine" aria-label="Deine Matches">
<h2>{matches.length === 1 ? 'Dein Match' : 'Deine Matches'}</h2>
<ul>
{matches.map((m) => {
const access = getAccess(m.id);
const next = nextStep(m, access);
return (
<li key={m.id}>
<Link to={`/m/${m.id}`} className="mine-card">
<span className="mine-teams">
<span className="team-a">{m.teamA}</span> <span className="vs">vs.</span> <span className="team-b">{m.teamB}</span>
</span>
<span className="mine-meta">
{m.modeLabel}, {FORMAT_LABEL[m.format]}
{access.kind === 'organizer' ? ', du bist Organisator' : access.kind === 'spectator' ? ', als Zuschauer' : ''}
</span>
<span className="mine-next">{next.text}</span>
<span className="btn btn-primary btn-sm mine-go">{next.action}</span>
</Link>
</li>
);
})}
</ul>
</aside>
);
}
/** Die vier Schritte vom Teamlink bis auf den Server. current = Schritt des eigenen Matches (0 = keins). */
function Steps({ current }) {
return (
<ol className="home-steps">
{STEPS.map((st, i) => {
const n = i + 1;
const state = current === 0 ? '' : n < current ? 'is-done' : n === current ? 'is-current' : '';
return (
<li key={st.title} className={state} aria-current={n === current ? 'step' : undefined}>
<span className="step-n">{n}</span>
<span className="step-title">{st.title}</span>
<span className="step-text">{st.text}</span>
</li>
);
})}
</ol>
);
}
function MatchRow({ match: m }) {
const access = getAccess(m.id);
const server = serverLabel(m.server);
@@ -114,31 +206,54 @@ function RunningCard({ match: m }) {
);
}
function CodeEntry() {
/** Code in die Form ABCD-EFGH bringen (Großbuchstaben, Bindestrich nach vier Zeichen). */
function formatCode(raw) {
const clean = raw.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8);
return clean.length > 4 ? `${clean.slice(0, 4)}-${clean.slice(4)}` : clean;
}
/** Code-Eingabe im Stil einer Eintrittskarte. */
function CodeTicket() {
const [code, setCode] = useState('');
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const navigate = useNavigate();
const complete = code.replace(/-/g, '').length === 8;
const submit = async (e) => {
e.preventDefault();
setError(null);
setBusy(true);
try {
const res = await api('/access', { method: 'POST', body: { code } });
navigate(res.access === 'spectator' ? `/watch/${code.trim()}` : `/join/${code.trim()}`);
navigate(res.access === 'spectator' ? `/watch/${code}` : `/join/${code}`);
} catch (err) {
setError(err.message);
setBusy(false);
}
};
return (
<form className="code-entry" onSubmit={submit}>
<label htmlFor="code">Einladungs- oder Zuschauercode</label>
<div className="row">
<input id="code" value={code} onChange={(e) => setCode(e.target.value)} placeholder="ABCD-EFGH" autoComplete="off" spellCheck="false" />
<button className="btn" disabled={code.replace(/[^a-z0-9]/gi, '').length < 8}>Beitreten</button>
</div>
{error && <p className="field-error">{error}</p>}
<form className="ticket" onSubmit={submit}>
<label className="ticket-stub" htmlFor="code">Code</label>
<input
id="code"
className="ticket-code"
value={code}
onChange={(e) => setCode(formatCode(e.target.value))}
placeholder="ABCD-EFGH"
autoComplete="off"
autoCapitalize="characters"
spellCheck="false"
inputMode="text"
aria-describedby={error ? 'code-error' : 'code-hint'}
/>
<button className="btn btn-primary ticket-go" disabled={!complete || busy}>{busy ? 'Prüfe …' : 'Beitreten'}</button>
{error
? <p id="code-error" className="field-error ticket-msg">{error}</p>
: <p id="code-hint" className="muted small ticket-msg">Teamcode oder Zuschauercode, acht Zeichen.</p>}
</form>
);
}
/** Offene Messer-Server für die Seitenwahl, zum direkten Verbinden. */
function KnifeServers() {
const [servers, setServers] = useState([]);