Compare commits

..
2 Commits
Author SHA1 Message Date
mregosar 35bdfc1f67 Merge feature/einladungslinks-bei-teams in main 2026-11-01 20:09:36 +01:00
mregosar f64da5ac86 Lobby: Einladungslinks unter dem jeweiligen Team
Der Organisator sieht Code, Link, Kopieren und Neuer Code jetzt unten in der Team-Karte. Im Bereich Einladungen bleiben Zuschauer-, Stream- und Organisator-Links.
2026-11-01 20:09:35 +01:00
3 changed files with 44 additions and 26 deletions

No files matched your search

+38 -26
View File
@@ -1,40 +1,52 @@
import CopyField from './CopyField.jsx'; import CopyField from './CopyField.jsx';
function regenerate(act, target, label) {
if (!window.confirm(`Neuen Code für ${label} erzeugen? Der alte Link funktioniert dann nicht mehr. Bereits beigetretene Spieler bleiben im Team.`)) return;
act('codes/regenerate', { target }, `Neuer Code für ${label}`);
}
/** Einladungslink eines Teams, steht für den Organisator unten in der Team-Karte. */
export function TeamInvite({ state, side, act, busy }) {
const { codes, links } = state.organizer;
const open = !['finished', 'cancelled'].includes(state.status);
const name = state.teams[side].name;
return (
<div className={`invite team-invite team-${side}`}>
<div className="invite-head">
<strong>Einladung</strong>
<span className="invite-code">{codes[side]}</span>
{open && <button className="link-btn" disabled={!!busy} onClick={() => regenerate(act, side, name)}>Neuer Code</button>}
</div>
<CopyField value={links[side]} />
<p className="small muted">Führt nur in {name}. Nur an dieses Team weitergeben.</p>
</div>
);
}
export default function OrganizerPanel({ state, act, busy, matchId, token }) { export default function OrganizerPanel({ state, act, busy, matchId, token }) {
const { codes, links } = state.organizer; const { codes, links } = state.organizer;
const open = !['finished', 'cancelled'].includes(state.status); const open = !['finished', 'cancelled'].includes(state.status);
const orgLink = `${window.location.origin}/m/${matchId}#org=${token}`; const orgLink = `${window.location.origin}/m/${matchId}#org=${token}`;
const label = 'Zuschauer (Stream, Kommentatoren)';
const regen = (target, label) =>
window.confirm(`Neuen Code für ${label} erzeugen? Der alte Link funktioniert dann nicht mehr. Bereits beigetretene Spieler bleiben im Team.`) &&
act('codes/regenerate', { target }, `Neuer Code für ${label}`);
return ( return (
<section className="panel organizer-panel"> <section className="panel organizer-panel">
<h2>Einladungen</h2> <h2>Einladungen</h2>
<p className="small muted">Jeder Link führt nur in das jeweilige Team. Gib ihn ausschließlich an dieses Team weiter.</p> <p className="small muted">Die Einladungslinks der Teams stehen unten in der jeweiligen Team-Karte.</p>
{[ <div className="invite spectator">
['A', state.teams.A.name, 'team-A'], <div className="invite-head">
['B', state.teams.B.name, 'team-B'], <strong>{label}</strong>
['spectator', 'Zuschauer (Stream, Kommentatoren)', 'spectator'], <span className="invite-code">{codes.spectator}</span>
].map(([key, label, cls]) => ( {open && <button className="link-btn" disabled={!!busy} onClick={() => regenerate(act, 'spectator', label)}>Neuer Code</button>}
<div key={key} className={`invite ${cls}`}>
<div className="invite-head">
<strong>{label}</strong>
<span className="invite-code">{codes[key]}</span>
{open && <button className="link-btn" disabled={!!busy} onClick={() => regen(key, label)}>Neuer Code</button>}
</div>
<CopyField value={links[key]} />
{key === 'spectator' && (
<div className="stream-link">
<CopyField label="Stream-Übersicht (als Browserquelle in OBS, 1920×1080)" value={links.stream} />
<a className="btn btn-sm" href={links.stream} target="_blank" rel="noreferrer">Stream-Ansicht öffnen</a>
<CopyField label="Match-Übersicht (nur dieses Match, ohne Zuschauercode)" value={links.overview} />
<a className="btn btn-sm" href={links.overview} target="_blank" rel="noreferrer">Match-Übersicht öffnen</a>
</div>
)}
</div> </div>
))} <CopyField value={links.spectator} />
<div className="stream-link">
<CopyField label="Stream-Übersicht (als Browserquelle in OBS, 1920×1080)" value={links.stream} />
<a className="btn btn-sm" href={links.stream} target="_blank" rel="noreferrer">Stream-Ansicht öffnen</a>
<CopyField label="Match-Übersicht (nur dieses Match, ohne Zuschauercode)" value={links.overview} />
<a className="btn btn-sm" href={links.overview} target="_blank" rel="noreferrer">Match-Übersicht öffnen</a>
</div>
</div>
<details className="org-link"> <details className="org-link">
<summary>Organisator-Zugang auf einem anderen Gerät öffnen</summary> <summary>Organisator-Zugang auf einem anderen Gerät öffnen</summary>
<p className="small muted">Dieser Link gibt volle Kontrolle über das Match. Nicht weitergeben.</p> <p className="small muted">Dieser Link gibt volle Kontrolle über das Match. Nicht weitergeben.</p>
+2
View File
@@ -1,3 +1,4 @@
import { TeamInvite } from './OrganizerPanel.jsx';
export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }) { export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }) {
const isOwnTeam = viewer.team === side; const isOwnTeam = viewer.team === side;
const inLobby = state.status === 'lobby'; const inLobby = state.status === 'lobby';
@@ -59,6 +60,7 @@ export default function TeamCard({ team, side, state, viewer, isOrg, act, busy }
</div> </div>
)} )}
{inLobby && !readiness.enough && <p className="small muted">Es fehlen noch {team.teamSize - readiness.count} Spieler.</p>} {inLobby && !readiness.enough && <p className="small muted">Es fehlen noch {team.teamSize - readiness.count} Spieler.</p>}
{isOrg && state.organizer && <TeamInvite state={state} side={side} act={act} busy={busy} />}
</section> </section>
); );
} }
+4
View File
@@ -775,3 +775,7 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
.knife-connect { display: flex; flex-wrap: wrap; align-items: end; gap: 8px 12px; margin-top: 10px; } .knife-connect { display: flex; flex-wrap: wrap; align-items: end; gap: 8px 12px; margin-top: 10px; }
.knife-connect > :first-child { flex: 1 1 260px; } .knife-connect > :first-child { flex: 1 1 260px; }
.knife-actions { margin-top: 10px; gap: 8px; flex-wrap: wrap; } .knife-actions { margin-top: 10px; gap: 8px; flex-wrap: wrap; }
.team-invite { margin-top: 12px; padding-right: 0; }
.team-invite .copy-row { flex-wrap: wrap; }
.team-invite .copy-row code { flex: 1 1 100%; font-size: .78rem; white-space: normal; word-break: break-all; overflow: visible; }
.team-invite p { margin: 6px 0 0; }