Map-Pools: Overpass, Vertigo und Cache für Competitive, Map-Auswahl im Admin, Veto-Ablauf passend zur Map-Anzahl

This commit is contained in:
mregosar committed 2026-10-29 21:03:29 +01:00
1 parent e3834bb071
commit 2d65634d6e
13 files changed
+459 -45

No files matched your search

+95
View File
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { api } from '../lib/api.js';
import { FORMAT_LABEL, serverLabel } from '../lib/labels.js';
import CopyField from '../components/CopyField.jsx';
import MapImage from '../components/MapImage.jsx';
const KEY = 'cs2mv:admin';
const FILTER_KEY = 'cs2mv:admin:filter';
@@ -146,6 +147,7 @@ export default function AdminPage() {
<button className={`admin-tab ${tab === 'settings' ? 'is-active' : ''}`} onClick={() => setTab('settings')}>Einstellungen</button>
</nav>
{tab === 'settings' && token && <MapPoolsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <ServerSettingsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <DockerHostsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <NotificationSettings token={token} setNotice={setNotice} />}
@@ -905,3 +907,96 @@ function KnifePanel({ token, setNotice }) {
</section>
);
}
const STEP_LABEL = { ban: 'Ban', pick: 'Pick', side: 'Seite' };
/** Veto-Ablauf als kurze Schrittfolge; X = Team, das das Veto beginnt, Y = das andere Team. */
function FlowSteps({ flow }) {
return (
<ol className="flow-steps">
{flow.map((raw, i) => {
if (raw === 'decider') return <li key={i} className="flow-decider">Entscheidung</li>;
const [type, actor] = raw.split(':');
return <li key={i} className={`flow-${type}`}>{STEP_LABEL[type]} {actor === 'X' ? '1' : '2'}</li>;
})}
</ol>
);
}
/** Welche Maps gespielt werden, pro Modus. Gilt für neue Matches, laufende behalten ihre Maps. */
function MapPoolsPanel({ token, setNotice }) {
const [pools, setPools] = useState(null);
const [selection, setSelection] = useState({});
const [busy, setBusy] = useState(false);
const apply = (res) => {
setPools(res.pools);
setSelection(Object.fromEntries(res.pools.map((p) => [p.mode, p.maps.filter((m) => m.active).map((m) => m.id)])));
};
useEffect(() => {
api('/admin/map-pools', { token }).then(apply).catch((err) => setNotice({ tone: 'error', text: err.message }));
}, [token, setNotice]);
if (!pools) return null;
const toggle = (mode, id) => {
const cur = new Set(selection[mode]);
if (cur.has(id)) cur.delete(id);
else cur.add(id);
setSelection({ ...selection, [mode]: [...cur] });
};
const save = async (mode) => {
setBusy(true);
try {
apply(await api('/admin/map-pools', { method: 'PUT', body: { mode, maps: selection[mode] }, token }));
setNotice({ tone: 'ok', text: 'Maps gespeichert – gilt für neue Matches' });
} catch (err) {
setNotice({ tone: 'error', text: err.message });
} finally {
setBusy(false);
}
};
return (
<section className="admin-settings">
<h2>Map-Pools</h2>
<p className="small muted">
Welche Maps im Veto stehen. Der Veto-Ablauf passt sich der Anzahl an: Für jede weitere Map gibt es einen Ban mehr.
Laufende Matches behalten ihre Maps. „1“ ist das Team, das das Veto beginnt.
</p>
{pools.map((p) => {
const chosen = new Set(selection[p.mode]);
const saved = new Set(p.maps.filter((m) => m.active).map((m) => m.id));
const dirty = chosen.size !== saved.size || [...chosen].some((id) => !saved.has(id));
return (
<div key={p.mode} className="setting-group">
<h3>{p.label} <span className="muted small">{chosen.size} von {p.maps.length} Maps</span></h3>
<div className="pool-maps">
{p.maps.map((m) => (
<label key={m.id} className={`pool-map ${chosen.has(m.id) ? 'is-on' : ''}`}>
<input type="checkbox" checked={chosen.has(m.id)} onChange={() => toggle(p.mode, m.id)} />
<MapImage map={m} className="pool-map-art" />
<span className="pool-map-name">{m.name}</span>
</label>
))}
</div>
{!dirty && (
<div className="pool-flows">
{Object.entries(p.formats).map(([f, info]) => (
<div key={f} className="pool-flow">
<strong>{f.toUpperCase()}</strong>
{info.supported ? <FlowSteps flow={info.flow} /> : <span className="muted small">{info.reason}</span>}
</div>
))}
</div>
)}
<div className="admin-actions">
<button className="btn btn-sm btn-primary" disabled={busy || !dirty || chosen.size < 2} onClick={() => save(p.mode)}>Speichern</button>
{dirty && <button className="btn btn-sm" disabled={busy} onClick={() => setSelection({ ...selection, [p.mode]: [...saved] })}>Verwerfen</button>}
{chosen.size < 2 && <span className="small tone-error">Mindestens 2 Maps</span>}
{dirty && chosen.size >= 2 && <span className="small muted">Nach dem Speichern steht hier der neue Veto-Ablauf.</span>}
</div>
</div>
);
})}
</section>
);
}