Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ee009063 |
No files matched your search
@@ -127,7 +127,7 @@ export default function MatchPage() {
|
|||||||
|
|
||||||
{viewer.role === 'public' && <PublicNotice />}
|
{viewer.role === 'public' && <PublicNotice />}
|
||||||
|
|
||||||
<div className={`match-grid ${state.status !== 'lobby' && viewer.role !== 'public' ? 'stage-wide' : ''}`}>
|
<div className={`match-grid ${state.status !== 'lobby' ? 'stage-wide' : ''}`}>
|
||||||
<TeamCard team={state.teams.A} side="A" {...ctx} />
|
<TeamCard team={state.teams.A} side="A" {...ctx} />
|
||||||
<section className="stage">
|
<section className="stage">
|
||||||
<Stage {...ctx} />
|
<Stage {...ctx} />
|
||||||
@@ -156,8 +156,8 @@ function describeViewer(viewer, teams) {
|
|||||||
|
|
||||||
function Stage(ctx) {
|
function Stage(ctx) {
|
||||||
const { state, isOrg, act, busy } = ctx;
|
const { state, isOrg, act, busy } = ctx;
|
||||||
if (state.viewer.role === 'public') {
|
if (state.viewer.role === 'public' && state.status === 'lobby') {
|
||||||
return <div className="stage-note"><h2>{state.statusLabel}</h2><p className="muted">Details sehen nur Teilnehmer.</p></div>;
|
return <div className="stage-note"><h2>{state.statusLabel}</h2><p className="muted">Das Veto und der Spielstand erscheinen hier, sobald das Match startet.</p></div>;
|
||||||
}
|
}
|
||||||
if (state.status === 'lobby') {
|
if (state.status === 'lobby') {
|
||||||
const { A, B } = state.teams;
|
const { A, B } = state.teams;
|
||||||
@@ -230,8 +230,8 @@ function MapPoolPreview({ maps }) {
|
|||||||
function PublicNotice() {
|
function PublicNotice() {
|
||||||
return (
|
return (
|
||||||
<div className="alert">
|
<div className="alert">
|
||||||
Du siehst diese Lobby ohne Zugang. Spieler treten über den Einladungslink ihres Teams bei,
|
Du siehst dieses Match ohne Zugang. Veto, Serienstand und Ergebnisse sind öffentlich.
|
||||||
Zuschauer über den Zuschauerlink. Beides bekommst du vom Organisator.
|
Teamdetails und Verbindungsdaten gibt es nur für Teilnehmer.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { api } from '../lib/api.js';
|
|||||||
import { FORMAT_LABEL, serverLabel, timeAgo } from '../lib/labels.js';
|
import { FORMAT_LABEL, serverLabel, timeAgo } from '../lib/labels.js';
|
||||||
import { getAccess } from '../lib/session.js';
|
import { getAccess } from '../lib/session.js';
|
||||||
import { useOverview } from '../lib/useMatch.js';
|
import { useOverview } from '../lib/useMatch.js';
|
||||||
import MapImage from '../components/MapImage.jsx';
|
|
||||||
|
|
||||||
const GROUPS = [
|
const GROUPS = [
|
||||||
{ key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' },
|
{ key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' },
|
||||||
@@ -33,11 +32,7 @@ export default function Overview() {
|
|||||||
return (
|
return (
|
||||||
<section key={g.key} className={`group group-${g.key}`}>
|
<section key={g.key} className={`group group-${g.key}`}>
|
||||||
<h2>{g.title} <span className="count">{list.length}</span></h2>
|
<h2>{g.title} <span className="count">{list.length}</span></h2>
|
||||||
{list.length === 0 ? <p className="muted small">{g.empty}</p> : g.key === 'running' ? (
|
{list.length === 0 ? <p className="muted small">{g.empty}</p> : (
|
||||||
<div className="run-grid">
|
|
||||||
{list.map((m) => <RunningCard key={m.id} match={m} />)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ul className="match-list">
|
<ul className="match-list">
|
||||||
{list.map((m) => <MatchRow key={m.id} match={m} />)}
|
{list.map((m) => <MatchRow key={m.id} match={m} />)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -84,34 +79,6 @@ function MatchRow({ match: m }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Karte für ein laufendes Match: Map als Hintergrund, Stand groß, Serie und Status darunter. */
|
|
||||||
function RunningCard({ match: m }) {
|
|
||||||
const server = serverLabel(m.server);
|
|
||||||
const map = m.currentMap;
|
|
||||||
const live = map && !map.finished;
|
|
||||||
const bo1 = m.format === 'bo1';
|
|
||||||
return (
|
|
||||||
<Link to={`/m/${m.id}`} className={`run-card ${live ? 'is-live' : ''}`} style={{ '--map': map?.color ?? '#3a4150' }}>
|
|
||||||
{map?.image && <MapImage map={{ image: map.image, color: map.color, name: map.name }} className="run-art" />}
|
|
||||||
<div className="run-top">
|
|
||||||
<span className="run-mode">{m.modeLabel}, {FORMAT_LABEL[m.format]}</span>
|
|
||||||
{live ? <span className="live-badge run-live">Live</span> : <span className={`run-status tone-${server.tone}`}>{m.statusLabel}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="run-teams">
|
|
||||||
<span className="run-team a">{m.teamA}</span>
|
|
||||||
<span className="run-score">
|
|
||||||
{map ? <>{map.score.A}<i>:</i>{map.score.B}</> : <i className="run-vs">vs.</i>}
|
|
||||||
</span>
|
|
||||||
<span className="run-team b">{m.teamB}</span>
|
|
||||||
</div>
|
|
||||||
<div className="run-foot">
|
|
||||||
<span>{map ? `Map ${map.number}: ${map.name}` : 'Veto läuft'}</span>
|
|
||||||
{!bo1 && m.seriesScore && <span className="run-series">Serie {m.seriesScore.A}:{m.seriesScore.B}</span>}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeEntry() {
|
function CodeEntry() {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|||||||
@@ -534,23 +534,3 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
|
|||||||
.admin-badge { display: inline-block; margin-top: 6px; padding: 2px 10px; border-radius: 999px; font-size: .8rem; font-weight: 600; }
|
.admin-badge { display: inline-block; margin-top: 6px; padding: 2px 10px; border-radius: 999px; font-size: .8rem; font-weight: 600; }
|
||||||
.admin-badge.is-paused { background: color-mix(in srgb, var(--pending) 22%, transparent); color: var(--pending); border: 1px solid var(--pending); }
|
.admin-badge.is-paused { background: color-mix(in srgb, var(--pending) 22%, transparent); color: var(--pending); border: 1px solid var(--pending); }
|
||||||
.btn-warn { border-color: var(--pending); color: var(--pending); }
|
.btn-warn { border-color: var(--pending); color: var(--pending); }
|
||||||
/* Laufende Matches in der Übersicht */
|
|
||||||
.run-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
|
||||||
.run-card { position: relative; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-height: 190px; padding: 16px 18px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--surface); color: var(--text); text-decoration: none; overflow: hidden; isolation: isolate; transition: transform .15s ease, border-color .15s ease; }
|
|
||||||
.run-card:hover { transform: translateY(-2px); border-color: var(--map); }
|
|
||||||
.run-card.is-live { box-shadow: inset 3px 0 0 var(--brand-red); }
|
|
||||||
.run-art { position: absolute; inset: 0; z-index: -1; opacity: .28; }
|
|
||||||
.run-art::after { background: linear-gradient(180deg, rgba(0,0,0,.2), rgba(0,0,0,.92)); }
|
|
||||||
.run-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: .8rem; color: var(--muted); }
|
|
||||||
.run-live { animation: run-pulse 1.6s ease-in-out infinite; }
|
|
||||||
@keyframes run-pulse { 50% { opacity: .55; } }
|
|
||||||
.run-status { font-weight: 600; }
|
|
||||||
.run-teams { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; text-align: center; }
|
|
||||||
.run-team { font: 700 1.25rem var(--display); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.run-team.a { color: var(--team-a); text-align: right; }
|
|
||||||
.run-team.b { color: var(--team-b); text-align: left; }
|
|
||||||
.run-score { font: 700 2.25rem var(--display); font-variant-numeric: tabular-nums; }
|
|
||||||
.run-score i { font-style: normal; color: var(--muted); padding: 0 4px; }
|
|
||||||
.run-vs { font-size: 1rem; color: var(--muted); font-style: normal; }
|
|
||||||
.run-foot { display: flex; justify-content: space-between; gap: 8px; font-size: .875rem; color: var(--muted); }
|
|
||||||
.run-series { font-variant-numeric: tabular-nums; }
|
|
||||||
@@ -791,14 +791,7 @@ export class MatchService extends EventEmitter {
|
|||||||
return {
|
return {
|
||||||
seriesScore: maps.length ? seriesScore(maps) : null,
|
seriesScore: maps.length ? seriesScore(maps) : null,
|
||||||
currentMap: current && (current.status === 'live' || current.status === 'done')
|
currentMap: current && (current.status === 'live' || current.status === 'done')
|
||||||
? {
|
? { number: current.number, name: this.mapName(m, current.map), score: { A: current.score_a, B: current.score_b }, finished: current.status === 'done' }
|
||||||
number: current.number,
|
|
||||||
name: this.mapName(m, current.map),
|
|
||||||
image: JSON.parse(m.maps).find((x) => x.id === current.map)?.image ?? null,
|
|
||||||
color: JSON.parse(m.maps).find((x) => x.id === current.map)?.color ?? null,
|
|
||||||
score: { A: current.score_a, B: current.score_b },
|
|
||||||
finished: current.status === 'done',
|
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
id: m.id,
|
id: m.id,
|
||||||
teamA: m.team_a_name,
|
teamA: m.team_a_name,
|
||||||
@@ -892,7 +885,7 @@ export class MatchService extends EventEmitter {
|
|||||||
requireFullTeams: this.config.requireFullTeams,
|
requireFullTeams: this.config.requireFullTeams,
|
||||||
autoAdvanceSeconds: this.config.autoAdvanceSeconds,
|
autoAdvanceSeconds: this.config.autoAdvanceSeconds,
|
||||||
maps: pool.maps,
|
maps: pool.maps,
|
||||||
veto: publicView
|
veto: publicView && match.status === 'lobby'
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
started: match.status !== 'lobby',
|
started: match.status !== 'lobby',
|
||||||
|
|||||||
Reference in new issue
Block a user