Spiel-Auswahl im Server-Tab: eigene Combobox statt nativer datalist

Die Browser-datalist ließ sich nicht stylen und fiel aus dem Design.
Jetzt eine GameSelect-Combobox im D4RKST3R-Look: filtert Presets beim
Tippen (ID + Label), zeigt Icon-Label plus gamedig-ID, Klick wählt aus,
freie gamedig-IDs bleiben weiterhin möglich.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 02:27:45 +02:00
co-authored by Claude Fable 5
parent 00249f9d8b
commit 3bfa559f2e
2 changed files with 74 additions and 8 deletions
+52 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
import EmbedComposer from '../components/EmbedComposer.jsx';
@@ -51,6 +51,54 @@ const TEAM_SCOPE_LABELS = {
};
/** Sekunden → "2d 4h 13m" */
/** Combobox für die Spiel-Auswahl — datalist ist nicht stylebar, das hier schon.
* Filtert die Presets beim Tippen, freie gamedig-IDs bleiben erlaubt. */
function GameSelect({ presets, value, onChange }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
useEffect(() => {
function onDocClick(e) {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
}
document.addEventListener('mousedown', onDocClick);
return () => document.removeEventListener('mousedown', onDocClick);
}, []);
const q = value.trim().toLowerCase();
const filtered = presets.filter(([id, label]) =>
!q || id.includes(q) || label.toLowerCase().includes(q)
);
return (
<div className="gamesel" ref={wrapRef} style={{ flex: 'none', width: '13rem' }}>
<input
type="text" placeholder="Spiel (gamedig-ID)"
title="Vorschläge oder beliebige gamedig-Spiel-ID"
value={value}
onFocus={() => setOpen(true)}
onChange={(e) => { onChange(e.target.value); setOpen(true); }}
onKeyDown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') setOpen(false); }}
/>
<span className="gamesel-caret" onMouseDown={(e) => { e.preventDefault(); setOpen((o) => !o); }}></span>
{open && filtered.length > 0 && (
<div className="gamesel-list">
{filtered.map(([id, label]) => (
<button
type="button" key={id}
className={`gamesel-item${id === value ? ' active' : ''}`}
onMouseDown={(e) => { e.preventDefault(); onChange(id); setOpen(false); }}
>
<span className="gamesel-label">{label}</span>
<span className="gamesel-id">{id}</span>
</button>
))}
</div>
)}
</div>
);
}
function fmtUptime(s) {
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
@@ -1318,15 +1366,11 @@ export default function Settings({ me }) {
<label>{serverDraft.id ? `Server #${serverDraft.id} bearbeiten` : 'Neuer Server'}</label>
<div className="field-row">
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Name, z. B. FiveM" value={serverDraft.name} onChange={(e) => setServerDraft({ ...serverDraft, name: e.target.value })} />
<input
type="text" list="game-presets" style={{ flex: 'none', width: '13rem' }}
placeholder="Spiel (gamedig-ID)" title="Vorschläge oder beliebige gamedig-Spiel-ID"
<GameSelect
presets={GAME_PRESETS}
value={serverDraft.type}
onChange={(e) => setServerDraft({ ...serverDraft, type: e.target.value })}
onChange={(type) => setServerDraft({ ...serverDraft, type })}
/>
<datalist id="game-presets">
{GAME_PRESETS.map(([id, label]) => <option key={id} value={id}>{label}</option>)}
</datalist>
{serverNeedsUrl ? (
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
) : (