Config statt Setup: Bereiche gruppiert, Suche, Status als Einstieg
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Die Seite hieß Setup und listete 15 Bereiche flach untereinander. Wer
nicht wusste, dass Geburtstage unter „Community" stecken und Willkommens-
Texte unter „Support", hat geklickt bis er es fand.

Die Seitenleiste gruppiert jetzt nach Überblick, Auftritt, Inhalte,
Community, Technik und Zugang. Darüber steht ein Suchfeld, das nicht nur
Beschriftungen durchsucht, sondern auch Stichwörter je Bereich — „geburtstag"
führt zu Community, „ticket" zu Support. Enter springt zum ersten Treffer.

Der aktive Bereich steht in der Adresse (/settings#texte). Damit überlebt er
das Neuladen, und man kann jemandem einen Link auf genau die Stelle schicken.

Der Status-Bereich war die leerste Seite im ganzen Panel: sechs Zahlen. Er ist
jetzt der Einstieg und beantwortet die Frage, die man beim Öffnen wirklich hat
— was läuft noch nicht? Eingeschaltete Module, denen ein Kanal oder eine Rolle
fehlt, stehen dort mit einem Knopf, der direkt an die richtige Stelle springt.
Ist alles eingerichtet, sagt die Seite genau das.

Umbenannt in „Config", weil „Setup" nach einmaliger Einrichtung klingt — die
Seite ist aber der Ort, an dem man dauerhaft alles einstellt.

Schmale Bildschirme: die Leiste wird zur umbrechenden Zeile ohne
Gruppen-Überschriften, das Suchfeld nimmt die volle Breite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:13:33 +02:00
co-authored by Claude Fable 5
parent dadd75d452
commit 2ce274707a
15 changed files with 280 additions and 78 deletions
+168 -40
View File
@@ -6,6 +6,7 @@ import {
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
IconSearch,
} from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
@@ -16,22 +17,54 @@ const API_SCOPES = [
{ id: 'read', label: 'read', hint: 'Member-Infos & Statistiken lesen' },
];
// Bereiche der Seitenleiste — bei 15 Tabs findet man ohne Gruppierung nichts mehr.
const TAB_SECTIONS = [
{ id: 'ueberblick', label: 'Überblick' },
{ id: 'auftritt', label: 'Auftritt' },
{ id: 'inhalte', label: 'Inhalte' },
{ id: 'community', label: 'Community' },
{ id: 'technik', label: 'Technik' },
{ id: 'zugang', label: 'Zugang' },
];
// `find` sind zusätzliche Suchbegriffe: was jemand tippt, wenn er den Tab-Namen
// nicht kennt („geburtstag" → Community, „willkommen" → Support).
const TABS = [
{ id: 'status', Icon: IconStatus, label: 'Status' },
{ id: 'module', Icon: IconLayers, label: 'Module' },
{ id: 'texte', Icon: IconMessage, label: 'Texte' },
{ id: 'brand', Icon: IconBrand, label: 'Brand' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds' },
{ id: 'community', Icon: IconCommunity, label: 'Community' },
{ id: 'rollen', Icon: IconRoles, label: 'Rollen' },
{ id: 'support', Icon: IconSupport, label: 'Support' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen' },
{ id: 'composer', Icon: IconComposer, label: 'Composer' },
{ id: 'seiten', Icon: IconPages, label: 'Seiten' },
{ id: 'server', Icon: IconServer, label: 'Server' },
{ id: 'system', Icon: IconSystem, label: 'System' },
{ id: 'api', Icon: IconApi, label: 'API' },
{ id: 'team', Icon: IconTeam, label: 'Team' },
{ id: 'status', Icon: IconStatus, label: 'Status', section: 'ueberblick',
find: 'uptime laufzeit datenbank backup version zahlen übersicht' },
{ id: 'module', Icon: IconLayers, label: 'Module', section: 'ueberblick',
find: 'funktionen an aus schalter aktivieren deaktivieren' },
{ id: 'texte', Icon: IconMessage, label: 'Texte', section: 'ueberblick',
find: 'nachrichten vorlagen platzhalter begrüßung wortlaut' },
{ id: 'brand', Icon: IconBrand, label: 'Brand', section: 'auftritt',
find: 'avatar banner farbe logo name footer aussehen embed' },
{ id: 'seiten', Icon: IconPages, label: 'Seiten', section: 'auftritt',
find: 'regeln über uns faq markdown menü impressum' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds', section: 'inhalte',
find: 'devlog commit release changelog rückblick threads ping' },
{ id: 'composer', Icon: IconComposer, label: 'Composer', section: 'inhalte',
find: 'nachricht senden embed bauen tags trigger geplant zeitplan' },
{ id: 'community', Icon: IconCommunity, label: 'Community', section: 'community',
find: 'level xp starboard galerie geburtstag playtester alpha keys voting wünsche autorolle' },
{ id: 'rollen', Icon: IconRoles, label: 'Rollen', section: 'community',
find: 'rollen menü selbstbedienung buttons reaction roles' },
{ id: 'support', Icon: IconSupport, label: 'Support', section: 'community',
find: 'ticket modmail willkommen protokoll modlog moderation warnung' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen', section: 'community',
find: 'formular fragen bewerbung prüfen annehmen ablehnen' },
{ id: 'server', Icon: IconServer, label: 'Server', section: 'technik',
find: 'game server monitor fivem minecraft status spieler gamedig' },
{ id: 'system', Icon: IconSystem, label: 'System', section: 'technik',
find: 'backup gitea watchdog twitch youtube events sprachkanäle impressum datenschutz' },
{ id: 'api', Icon: IconApi, label: 'API', section: 'zugang',
find: 'schlüssel key token zugriff sso webhook' },
{ id: 'team', Icon: IconTeam, label: 'Team', section: 'zugang',
find: 'mitarbeiter rechte bereiche protokoll audit zugriff' },
];
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
@@ -125,7 +158,15 @@ function fmtBytes(b) {
}
export default function Settings({ me }) {
const [tab, setTab] = useState('status');
// Tab steht in der Adresse (#texte), damit Neuladen und Links den Bereich behalten
const [tab, setTabState] = useState(
() => TABS.find((t) => t.id === window.location.hash.slice(1))?.id ?? 'status'
);
const setTab = (id) => {
setTabState(id);
window.history.replaceState(null, '', `#${id}`);
};
const [tabSearch, setTabSearch] = useState('');
const [data, setData] = useState(null);
const [form, setForm] = useState(null);
const [error, setError] = useState(null);
@@ -202,6 +243,12 @@ export default function Settings({ me }) {
return need ? me.scopes?.includes(need) : false;
};
const visibleTabs = TABS.filter((t) => canTab(t.id));
// Suche greift auf Beschriftung und Stichwörter — wer „geburtstag" tippt,
// landet bei Community, ohne den Tab-Namen zu kennen.
const query = tabSearch.trim().toLowerCase();
const foundTabs = query
? visibleTabs.filter((t) => `${t.label} ${t.find}`.toLowerCase().includes(query))
: visibleTabs;
// Tags + geplante Posts
const [tags, setTags] = useState([]);
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
@@ -666,11 +713,11 @@ export default function Settings({ me }) {
const header = (
<section className="page-header">
<div className="page-bg-text">SETUP</div>
<div className="page-bg-text">CONFIG</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Admin</div>
<h1 className="page-title">Setup</h1>
<h1 className="page-title">Config</h1>
<p className="page-subtitle">Alles an einem Ort Änderungen greifen sofort.</p>
</div>
</section>
@@ -706,21 +753,66 @@ export default function Settings({ me }) {
/* ── Tab-Inhalte ───────────────────────────────── */
// Eingeschaltete Module, denen noch etwas fehlt — die tun bis dahin nichts
const activeModules = modules.filter((m) => m.enabled);
const unfinished = activeModules.filter((m) => m.missing.length > 0);
const tabStatus = (
<div className="settings-section">
<h2 className="settings-title">// Status</h2>
<div className="stats">
<div className="stat"><span className="stat-value">{data.status.botTag ?? '—'}</span><span className="stat-label">Bot</span></div>
<div className="stat"><span className="stat-value">{fmtUptime(data.status.uptimeSeconds)}</span><span className="stat-label">Uptime</span></div>
<div className="stat"><span className="stat-value">{data.status.devlogs}</span><span className="stat-label">Devlogs</span></div>
<div className="stat"><span className="stat-value">{data.status.commits}</span><span className="stat-label">Commits</span></div>
<div className="stat"><span className="stat-value">{fmtBytes(data.status.dbSizeBytes)}</span><span className="stat-label">Datenbank</span></div>
<div className="stat">
<span className="stat-value">{data.status.lastBackup ? data.status.lastBackup.slice(0, 10) : '—'}</span>
<span className="stat-label">Letztes Backup</span>
<>
<div className="settings-section">
<h2 className="settings-title">// Status</h2>
<div className="stats">
<div className="stat"><span className="stat-value">{data.status.botTag ?? '—'}</span><span className="stat-label">Bot</span></div>
<div className="stat"><span className="stat-value">{fmtUptime(data.status.uptimeSeconds)}</span><span className="stat-label">Uptime</span></div>
{modules.length > 0 && (
<div className="stat">
<span className="stat-value">{activeModules.length}<span className="stat-of">/{modules.length}</span></span>
<span className="stat-label">Module an</span>
</div>
)}
<div className="stat"><span className="stat-value">{data.status.devlogs}</span><span className="stat-label">Devlogs</span></div>
<div className="stat"><span className="stat-value">{data.status.commits}</span><span className="stat-label">Commits</span></div>
<div className="stat"><span className="stat-value">{fmtBytes(data.status.dbSizeBytes)}</span><span className="stat-label">Datenbank</span></div>
<div className="stat">
<span className="stat-value">{data.status.lastBackup ? data.status.lastBackup.slice(0, 10) : '—'}</span>
<span className="stat-label">Letztes Backup</span>
</div>
</div>
</div>
</div>
{modules.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Noch einzurichten</h2>
{unfinished.length === 0 ? (
<p className="section-intro todo-done with-icon">
<IconCheck size={15} /> Alles eingerichtet jedes eingeschaltete Modul hat, was es braucht.
</p>
) : (
<>
<p className="section-intro">
Diese Module sind eingeschaltet, aber es fehlt ihnen noch etwas
bis dahin tun sie nichts.
</p>
<div className="key-list">
{unfinished.map((m) => (
<div className="key-row" key={m.id}>
<span className="key-name">{m.name}</span>
<span className="todo-missing with-icon">
<IconWarning size={13} /> {m.missing.join(', ')}
</span>
{m.tab && (
<button className="btn-mini" onClick={() => setTab(m.tab)}>
Einrichten
</button>
)}
</div>
))}
</div>
</>
)}
</div>
)}
</>
);
const statusDot = { online: '#23a55a', idle: '#f0b232', dnd: '#f23f43' }[form.bot_presence_status] ?? '#23a55a';
@@ -2089,16 +2181,52 @@ export default function Settings({ me }) {
<section className="content content-wide">
<div className="settings-layout">
<nav className="settings-nav">
{visibleTabs.map((t) => (
<button
key={t.id}
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
<span className="settings-nav-icon"><t.Icon /></span>
{t.label}
</button>
))}
<div className="settings-search">
<IconSearch size={15} />
<input
type="text"
value={tabSearch}
placeholder="Suchen "
aria-label="Bereich suchen"
onChange={(e) => setTabSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && foundTabs[0]) {
setTab(foundTabs[0].id);
setTabSearch('');
}
if (e.key === 'Escape') setTabSearch('');
}}
/>
{tabSearch && (
<button className="settings-search-clear" aria-label="Suche leeren" onClick={() => setTabSearch('')}>
<IconX size={13} />
</button>
)}
</div>
{foundTabs.length === 0 && (
<p className="settings-nav-empty">Nichts gefunden.</p>
)}
{TAB_SECTIONS.map((section) => {
const items = foundTabs.filter((t) => t.section === section.id);
if (items.length === 0) return null;
return (
<div className="settings-nav-group" key={section.id}>
<span className="settings-nav-label">{section.label}</span>
{items.map((t) => (
<button
key={t.id}
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
<span className="settings-nav-icon"><t.Icon /></span>
{t.label}
</button>
))}
</div>
);
})}
</nav>
<div className="settings-content">