-
Match-Lobbys
-
Teams treten mit ihrem Einladungscode bei, die Captains führen das Map-Veto, der Server wird automatisch eingerichtet.
+
+
+
+
Mit deinem Team ins Match
+
+ 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.
+
+
-
+ {mine.length > 0 && }
+
+
{matches === null ? (
Lade Matches …
) : (
-
- {GROUPS.map((g) => {
- const list = matches.filter((m) => m.group === g.key);
- return (
-
- {g.title} {list.length}
- {list.length === 0 ? {g.empty}
: g.key === 'running' ? (
-
- {list.map((m) => )}
-
- ) : (
-
- )}
-
- );
- })}
-
+ <>
+
+ Laufend {running.length}
+ {running.length === 0 ? (
+ Gerade läuft kein Match.
+ ) : (
+
+ {running.map((m) => )}
+
+ )}
+
+
+
+ Offene Lobbys {open.length}
+ {open.length === 0 ? (
+ Keine offene Lobby. Sobald der Organisator ein Match erstellt, steht es hier.
+ ) : (
+
+ )}
+
+
+ Ergebnisse {closed.length}
+ {closed.length === 0 ? (
+ Noch kein Match abgeschlossen.
+ ) : (
+ {closed.slice(0, 12).map((m) => )}
+ )}
+
+
+ >
)}
);
}
+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 (
+
+ {matches.length === 1 ? 'Dein Match' : 'Deine Matches'}
+
+ {matches.map((m) => {
+ const access = getAccess(m.id);
+ const next = nextStep(m, access);
+ return (
+
+
+
+ {m.teamA} vs. {m.teamB}
+
+
+ {m.modeLabel}, {FORMAT_LABEL[m.format]}
+ {access.kind === 'organizer' ? ', du bist Organisator' : access.kind === 'spectator' ? ', als Zuschauer' : ''}
+
+ {next.text}
+ {next.action}
+
+
+ );
+ })}
+
+
+ );
+}
+
+/** Die vier Schritte vom Teamlink bis auf den Server. current = Schritt des eigenen Matches (0 = keins). */
+function Steps({ current }) {
+ return (
+
+ {STEPS.map((st, i) => {
+ const n = i + 1;
+ const state = current === 0 ? '' : n < current ? 'is-done' : n === current ? 'is-current' : '';
+ return (
+
+ {n}
+ {st.title}
+ {st.text}
+
+ );
+ })}
+
+ );
+}
+
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 (
-
);
}
+
/** Offene Messer-Server für die Seitenwahl, zum direkten Verbinden. */
function KnifeServers() {
const [servers, setServers] = useState([]);
diff --git a/client/src/styles.css b/client/src/styles.css
index c20b91d..4607bda 100644
--- a/client/src/styles.css
+++ b/client/src/styles.css
@@ -645,3 +645,79 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
.settings-nav-item small { display: none; }
.settings-nav-item.is-active { border-color: var(--primary); }
}
+
+/* ---------- Startseite: Spieler zuerst ---------- */
+.home-hero { display: grid; grid-template-columns: minmax(0, 1fr); gap: 32px; align-items: start; margin: 8px 0 28px; }
+.home-hero.has-mine { grid-template-columns: minmax(0, 1.25fr) minmax(280px, 1fr); }
+.home-intro h1 { font: 700 clamp(2.6rem, 6vw, 4.4rem)/.95 var(--display); letter-spacing: -.01em; margin: 0 0 14px; max-width: 14ch; }
+.home-intro .lead { max-width: 58ch; margin: 0 0 22px; }
+
+/* Code-Eingabe als Eintrittskarte: Abriss links, Code groß in der Mitte */
+.ticket {
+ --notch: 11px;
+ display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; column-gap: 0;
+ max-width: 620px; background: var(--surface-2); border: 1px solid var(--line-strong);
+ clip-path: polygon(var(--chamfer) 0, 100% 0, 100% calc(100% - var(--chamfer)), calc(100% - var(--chamfer)) 100%, 0 100%, 0 var(--chamfer));
+}
+.ticket-stub {
+ align-self: stretch; display: flex; align-items: center; padding: 0 18px 0 22px;
+ font: 700 1.05rem var(--display); color: var(--muted); letter-spacing: .02em;
+ border-right: 2px dashed var(--line-strong); background: color-mix(in srgb, var(--brand-red) 14%, transparent);
+}
+.ticket-code {
+ min-width: 0; width: 100%; border: 0; background: transparent; color: var(--text); outline: none;
+ font: 700 clamp(1.9rem, 4.6vw, 2.75rem)/1 var(--display); letter-spacing: .14em; padding: 16px 18px;
+}
+.ticket-code::placeholder { color: color-mix(in srgb, var(--muted) 45%, transparent); }
+.ticket:focus-within { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
+.ticket-go { margin: 0 14px 0 0; }
+.ticket-msg { grid-column: 1 / -1; margin: 0; padding: 8px 22px 12px; border-top: 1px solid var(--line); }
+
+/* Eigene Matches */
+.home-mine h2 { font-size: 1.15rem; margin: 6px 0 10px; }
+.home-mine ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
+.mine-card {
+ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 14px; align-items: center;
+ padding: 14px 16px; text-decoration: none; color: var(--text);
+ background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--primary); border-radius: var(--radius);
+}
+.mine-card:hover { border-color: var(--line-strong); border-left-color: var(--primary-hover); }
+.mine-card:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
+.mine-teams { font: 700 1.35rem/1.1 var(--display); grid-column: 1; }
+.mine-meta { grid-column: 1; font-size: .85rem; color: var(--muted); }
+.mine-next { grid-column: 1; font-weight: 600; margin-top: 4px; }
+.mine-go { grid-column: 2; grid-row: 1 / span 3; }
+
+/* Vier Schritte */
+.home-steps { list-style: none; margin: 0 0 36px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); }
+.home-steps li { position: relative; display: grid; grid-template-columns: auto 1fr; align-content: start; column-gap: 12px; row-gap: 2px; padding: 14px 16px 4px 0; }
+.step-n {
+ grid-row: 1; align-self: center; width: 34px; height: 34px; display: grid; place-items: center;
+ font: 700 1.2rem var(--display); color: var(--muted); border: 1px solid var(--line-strong); border-radius: 50%;
+}
+.step-title { font-weight: 700; align-self: center; }
+.step-text { grid-column: 2; font-size: .85rem; color: var(--muted); }
+.home-steps li.is-done .step-n { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 60%, transparent); }
+.home-steps li.is-current::before { content: ''; position: absolute; top: -2px; left: 0; right: 16px; height: 3px; background: var(--primary); }
+.home-steps li.is-current .step-n { color: #fff; background: var(--primary); border-color: var(--primary); }
+
+.home-live { margin-bottom: 36px; }
+.home-lists { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; }
+.home-lists .match-row-meta { flex-wrap: wrap; }
+
+@media (max-width: 900px) {
+ .home-hero.has-mine { grid-template-columns: minmax(0, 1fr); }
+ .home-hero.has-mine .home-mine { order: -1; }
+ .home-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .home-lists { grid-template-columns: minmax(0, 1fr); }
+}
+@media (max-width: 520px) {
+ .ticket { grid-template-columns: minmax(0, 1fr) auto; }
+ .ticket-stub { grid-column: 1 / -1; border-right: 0; border-bottom: 2px dashed var(--line-strong); padding: 8px 18px; }
+ .ticket-code { letter-spacing: .08em; padding: 12px 14px; }
+ .home-steps { grid-template-columns: minmax(0, 1fr); }
+ .home-steps li { padding: 10px 0 10px 12px; }
+ .home-steps li.is-current::before { top: 8px; bottom: 8px; left: 0; right: auto; width: 3px; height: auto; }
+ .mine-card { grid-template-columns: minmax(0, 1fr); }
+ .mine-go { grid-column: 1; grid-row: auto; justify-self: start; margin-top: 6px; }
+}