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:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
|
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
|
||||||
import EmbedComposer from '../components/EmbedComposer.jsx';
|
import EmbedComposer from '../components/EmbedComposer.jsx';
|
||||||
|
|
||||||
@@ -51,6 +51,54 @@ const TEAM_SCOPE_LABELS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Sekunden → "2d 4h 13m" */
|
/** 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) {
|
function fmtUptime(s) {
|
||||||
const d = Math.floor(s / 86400);
|
const d = Math.floor(s / 86400);
|
||||||
const h = Math.floor((s % 86400) / 3600);
|
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>
|
<label>{serverDraft.id ? `Server #${serverDraft.id} bearbeiten` : 'Neuer Server'}</label>
|
||||||
<div className="field-row">
|
<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" style={{ flex: 'none', width: '11rem' }} placeholder="Name, z. B. FiveM" value={serverDraft.name} onChange={(e) => setServerDraft({ ...serverDraft, name: e.target.value })} />
|
||||||
<input
|
<GameSelect
|
||||||
type="text" list="game-presets" style={{ flex: 'none', width: '13rem' }}
|
presets={GAME_PRESETS}
|
||||||
placeholder="Spiel (gamedig-ID)" title="Vorschläge oder beliebige gamedig-Spiel-ID"
|
|
||||||
value={serverDraft.type}
|
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 ? (
|
{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 })} />
|
<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 })} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1091,3 +1091,25 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
|||||||
.dpv-image { max-width: 100%; border-radius: 6px; margin-top: .6rem; display: block; }
|
.dpv-image { max-width: 100%; border-radius: 6px; margin-top: .6rem; display: block; }
|
||||||
.dpv-thumb { width: 76px; height: 76px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
.dpv-thumb { width: 76px; height: 76px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||||
.dpv-footer { font-size: .72rem; color: #949ba4; margin-top: .55rem; }
|
.dpv-footer { font-size: .72rem; color: #949ba4; margin-top: .55rem; }
|
||||||
|
|
||||||
|
/* ── GameSelect: stylebare Combobox für die Spiel-Auswahl ── */
|
||||||
|
.gamesel { position: relative; }
|
||||||
|
.gamesel input { width: 100%; padding-right: 1.8rem; }
|
||||||
|
.gamesel-caret {
|
||||||
|
position: absolute; right: .6rem; top: 50%; transform: translateY(-50%);
|
||||||
|
color: var(--muted); cursor: pointer; user-select: none; font-size: .8rem;
|
||||||
|
}
|
||||||
|
.gamesel-list {
|
||||||
|
position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; min-width: 100%;
|
||||||
|
max-height: 16rem; overflow-y: auto;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
.gamesel-item {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 1rem;
|
||||||
|
width: 100%; padding: .45rem .7rem; border: 0; background: none; cursor: pointer;
|
||||||
|
color: var(--text); font-family: var(--body); font-size: .95rem; text-align: left;
|
||||||
|
}
|
||||||
|
.gamesel-item:hover, .gamesel-item.active { background: var(--bg3); color: var(--neon); }
|
||||||
|
.gamesel-id { color: var(--muted2); font-family: var(--mono); font-size: .75rem; }
|
||||||
|
.gamesel-item:hover .gamesel-id { color: var(--muted); }
|
||||||
|
|||||||
Reference in New Issue
Block a user