Stream-Übersicht: Matches anklickbar, Einzelansicht pro Match
Karten und Listeneinträge der Stream-Übersicht führen zur Übersicht des jeweiligen Matches (/stream/match/:matchId). Der Zuschauercode bleibt geheim, daher wird nur die Match-ID verlinkt. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82239c8447
commit
fde3baafb5
3 files changed
+44
-11
No files matched your search
@@ -14,6 +14,7 @@ export default function App() {
|
||||
<Routes>
|
||||
{/* Stream-Übersicht ohne Kopfzeile, als Vollbild bzw. OBS-Browserquelle */}
|
||||
<Route path="/stream/:code" element={<StreamPage />} />
|
||||
<Route path="/stream/match/:matchId" element={<StreamOverviewPage />} />
|
||||
<Route path="/stream" element={<StreamOverviewPage />} />
|
||||
<Route path="*" element={<MainLayout />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { api } from '../lib/api.js';
|
||||
import { FORMAT_LABEL } from '../lib/labels.js';
|
||||
import { getSocket } from '../lib/useMatch.js';
|
||||
@@ -14,11 +15,13 @@ const PAGE_SECONDS = 12;
|
||||
/**
|
||||
* Stream-Übersicht aller laufenden Vetos und Matches: /stream
|
||||
* Mit STREAM_KEY: /stream?key=<schlüssel>. Optional ?bg=transparent.
|
||||
* Ein einzelnes Match: /stream/match/<matchId>, daraus führen die Karten der Übersicht.
|
||||
*/
|
||||
export default function StreamOverviewPage() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const key = params.get('key') ?? '';
|
||||
const transparent = params.get('bg') === 'transparent';
|
||||
const { matchId } = useParams();
|
||||
const { data, error } = useStreamOverview(key);
|
||||
const now = useClock();
|
||||
const [page, setPage] = useState(0);
|
||||
@@ -29,8 +32,10 @@ export default function StreamOverviewPage() {
|
||||
return () => document.documentElement.classList.remove('stream-mode', 'stream-transparent');
|
||||
}, [transparent]);
|
||||
|
||||
const activeMatches = (data?.active ?? []).filter((m) => !matchId || m.id === matchId);
|
||||
|
||||
// Mehr Matches als auf eine Seite passen: automatisch weiterblättern
|
||||
const pages = data ? Math.max(1, Math.ceil(data.active.length / PER_PAGE)) : 1;
|
||||
const pages = Math.max(1, Math.ceil(activeMatches.length / PER_PAGE));
|
||||
useEffect(() => {
|
||||
if (pages <= 1) { setPage(0); return undefined; }
|
||||
const t = setInterval(() => setPage((p) => (p + 1) % pages), PAGE_SECONDS * 1000);
|
||||
@@ -49,7 +54,7 @@ export default function StreamOverviewPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const active = data?.active ?? [];
|
||||
const active = activeMatches;
|
||||
const shown = active.slice(page * PER_PAGE, page * PER_PAGE + PER_PAGE);
|
||||
const liveCount = active.filter((m) => m.status === 'live').length;
|
||||
const vetoCount = active.filter((m) => m.status === 'veto').length;
|
||||
@@ -73,11 +78,24 @@ export default function StreamOverviewPage() {
|
||||
{!data && <div className="so-empty">Lade Matches …</div>}
|
||||
{data && shown.length === 0 && (
|
||||
<div className="so-empty">
|
||||
<h2>Gerade läuft kein Match</h2>
|
||||
<p>{data.upcoming.length ? 'Die nächsten Lobbys stehen unten.' : 'Sobald ein Veto beginnt, erscheint es hier.'}</p>
|
||||
{matchId ? (
|
||||
<>
|
||||
<h2>Dieses Match läuft gerade nicht</h2>
|
||||
<p>Es erscheint hier, sobald das Veto oder das Match beginnt.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2>Gerade läuft kein Match</h2>
|
||||
<p>{data.upcoming.length ? 'Die nächsten Lobbys stehen unten.' : 'Sobald ein Veto beginnt, erscheint es hier.'}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{shown.map((m) => <MatchCard key={m.id} m={m} />)}
|
||||
{shown.map((m) => (
|
||||
<a key={m.id} className="so-link" href={matchHref(m.id, key)}>
|
||||
<MatchCard m={m} />
|
||||
</a>
|
||||
))}
|
||||
</main>
|
||||
|
||||
<footer className="so-bottom">
|
||||
@@ -87,8 +105,10 @@ export default function StreamOverviewPage() {
|
||||
<ul>
|
||||
{data.upcoming.map((m) => (
|
||||
<li key={m.id}>
|
||||
<span className="a">{m.teams.A}</span> <span className="vs">vs</span> <span className="b">{m.teams.B}</span>
|
||||
<small>{FORMAT_LABEL[m.format]}{m.ready.A && m.ready.B ? ', bereit' : ''}</small>
|
||||
<a className="so-link" href={matchHref(m.id, key)}>
|
||||
<span className="a">{m.teams.A}</span> <span className="vs">vs</span> <span className="b">{m.teams.B}</span>
|
||||
<small>{FORMAT_LABEL[m.format]}{m.ready.A && m.ready.B ? ', bereit' : ''}</small>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -100,9 +120,11 @@ export default function StreamOverviewPage() {
|
||||
<ul>
|
||||
{data.results.map((m) => (
|
||||
<li key={m.id}>
|
||||
<span className={`a ${winner(m) === 'A' ? 'won' : ''}`}>{m.teams.A}</span>
|
||||
<b>{resultScore(m)}</b>
|
||||
<span className={`b ${winner(m) === 'B' ? 'won' : ''}`}>{m.teams.B}</span>
|
||||
<a className="so-link" href={matchHref(m.id, key)}>
|
||||
<span className={`a ${winner(m) === 'A' ? 'won' : ''}`}>{m.teams.A}</span>
|
||||
<b>{resultScore(m)}</b>
|
||||
<span className={`b ${winner(m) === 'B' ? 'won' : ''}`}>{m.teams.B}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -114,6 +136,11 @@ export default function StreamOverviewPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Link zur Stream-Übersicht eines einzelnen Matches. Der Zuschauercode bleibt geheim, daher nur die Match-ID. */
|
||||
function matchHref(id, key) {
|
||||
return `/stream/match/${encodeURIComponent(id)}${key ? `?key=${encodeURIComponent(key)}` : ''}`;
|
||||
}
|
||||
|
||||
function useStreamOverview(key) {
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
@@ -145,7 +172,6 @@ function winner(m) {
|
||||
const [a, b] = resultScore(m).split(':').map(Number);
|
||||
return a === b ? null : a > b ? 'A' : 'B';
|
||||
}
|
||||
|
||||
function MatchCard({ m }) {
|
||||
const current = m.series.find((s) => s.number === m.currentMapNumber);
|
||||
const isBo1 = m.format === 'bo1';
|
||||
|
||||
@@ -114,3 +114,9 @@
|
||||
.so-strip li b { font-variant-numeric: tabular-nums; }
|
||||
.so-strip li .won { text-decoration: underline; text-underline-offset: 5px; }
|
||||
.so-none { color: var(--muted); font-size: 22px; margin: 0; }
|
||||
|
||||
/* Klickbare Karten und Listeneinträge führen zur Übersicht des jeweiligen Matches */
|
||||
.so-link { display: block; color: inherit; text-decoration: none; }
|
||||
.so-strip li .so-link { display: flex; align-items: baseline; gap: 10px; color: inherit; text-decoration: none; }
|
||||
.so-link:hover { filter: brightness(1.15); }
|
||||
.so-link:focus-visible { outline: 3px solid var(--brand-red); outline-offset: 4px; }
|
||||
Reference in new issue
Block a user