Map-Pools: Overpass, Vertigo und Cache für Competitive, Map-Auswahl im Admin, Veto-Ablauf passend zur Map-Anzahl
This commit is contained in:
1 parent
e3834bb071
commit
2d65634d6e
13 files changed
+459
-45
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user