diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index a399610..5f70d23 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -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 ( +
+ setOpen(true)} + onChange={(e) => { onChange(e.target.value); setOpen(true); }} + onKeyDown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') setOpen(false); }} + /> + { e.preventDefault(); setOpen((o) => !o); }}>▾ + {open && filtered.length > 0 && ( +
+ {filtered.map(([id, label]) => ( + + ))} +
+ )} +
+ ); +} + 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 }) {
setServerDraft({ ...serverDraft, name: e.target.value })} /> - setServerDraft({ ...serverDraft, type: e.target.value })} + onChange={(type) => setServerDraft({ ...serverDraft, type })} /> - - {GAME_PRESETS.map(([id, label]) => )} - {serverNeedsUrl ? ( setServerDraft({ ...serverDraft, query_url: e.target.value })} /> ) : ( diff --git a/frontend/src/style.css b/frontend/src/style.css index 76afeab..1e62351 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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-thumb { width: 76px; height: 76px; object-fit: cover; border-radius: 6px; flex-shrink: 0; } .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); }