Wichtige Funktionen direkt in der Seitenleiste
Nach dem Umbau lagen alle Funktionen hinter dem Raster — für die vier, fünf, die man dauernd anfasst, ist das ein Umweg zu viel. Oben in der Seitenleiste steht jetzt „Meine Funktionen": ein Klick öffnet die Seite direkt. Welche dort stehen, entscheidet der Stern auf der Modul-Karte oder auf der Seite selbst. Bewusst nicht alle 35: Deckel bei acht, sonst ist der Gewinn gegenüber dem Raster wieder weg. Der Standard ist eine Vermutung (Devlogs, Willkommen, Tickets, Starboard, Galerie) und ausdrücklich zum Ändern gedacht. Die Auswahl liegt als Einstellung in der Datenbank, nicht im Browser — sie gilt damit auf jedem Gerät und fürs ganze Team. Eine geleerte Auswahl wird als solche gespeichert, sonst käme beim nächsten Laden der Standard zurück. Der Stern ist ungeheftet nur angedeutet und wird erst beim Überfahren deutlich — 35 Karten mit vollen Sternen sähen aus wie ein Sternenhimmel. Nebenbei: bei geöffneter Modul-Seite war sowohl „Module" als auch die Funktion in der Leiste hervorgehoben. Jetzt nur noch die Funktion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,11 @@ statt alles über vier Bereiche verteilt. Für „Willkommen" also Kanal, Karte,
|
||||
Farben, Vorschau und Begrüßungstext an einem Ort. Die Adresse merkt sich die
|
||||
Seite (`/settings#module/welcome`).
|
||||
|
||||
Die Funktionen, die man dauernd anfasst, stehen zusätzlich **oben in der
|
||||
Seitenleiste** unter *Meine Funktionen* — ein Klick statt Umweg übers Raster.
|
||||
Welche das sind, entscheidet der Stern auf der Modul-Karte (höchstens acht,
|
||||
damit die Leiste kurz bleibt).
|
||||
|
||||
Gebaut wird sie aus den Registern: [`src/modules.js`](src/modules.js) kennt die
|
||||
Felder, [`src/templates.js`](src/templates.js) die Texte, [`src/tuning.js`](src/tuning.js)
|
||||
die Werte — jeweils über `module` verknüpft. Eine neue Funktion bekommt ihre
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet, apiPut, apiPost } from '../api.js';
|
||||
import { moduleIcon } from '../module-icons.jsx';
|
||||
import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown } from '../icons.jsx';
|
||||
import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown, IconStar } from '../icons.jsx';
|
||||
|
||||
/** Ein Feld aus dem Register — Art entscheidet über die Darstellung */
|
||||
function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) {
|
||||
@@ -64,7 +64,7 @@ function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) {
|
||||
*/
|
||||
export default function ModulePage(props) {
|
||||
const { id, onBack, onToggle, form, setForm, PickSelect, channelItems, roleItems,
|
||||
welcomePreviewUrl, flash, goTab, reloadSettings } = props;
|
||||
welcomePreviewUrl, flash, goTab, reloadSettings, favorites, onToggleFavorite } = props;
|
||||
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [fehler, setFehler] = useState(false);
|
||||
@@ -141,6 +141,15 @@ export default function ModulePage(props) {
|
||||
<h2 className="modpage-name">{detail.name}</h2>
|
||||
<p className="modpage-desc">{detail.desc}</p>
|
||||
</div>
|
||||
<button
|
||||
className={`mod-fav${favorites?.includes(detail.id) ? ' on' : ''}`}
|
||||
title={favorites?.includes(detail.id)
|
||||
? 'Aus der Seitenleiste nehmen'
|
||||
: 'In die Seitenleiste heften'}
|
||||
onClick={() => onToggleFavorite(detail.id)}
|
||||
>
|
||||
<IconStar size={17} />
|
||||
</button>
|
||||
<button
|
||||
className={`mod-switch${detail.enabled ? ' on' : ''}`}
|
||||
title={detail.enabled ? 'Ausschalten' : 'Einschalten'}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
|
||||
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
|
||||
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
|
||||
IconSearch, IconSystem as IconTune,
|
||||
IconSearch, IconSystem as IconTune, IconStar,
|
||||
} from '../icons.jsx';
|
||||
import { moduleIcon } from '../module-icons.jsx';
|
||||
import ModulePage from '../components/ModulePage.jsx';
|
||||
@@ -315,6 +315,9 @@ export default function Settings({ me }) {
|
||||
window.history.replaceState(null, '', id ? `#module/${id}` : '#module');
|
||||
};
|
||||
const [creating, setCreating] = useState(null); // Einstellung, die gerade angelegt wird
|
||||
// Funktionen, die in der Seitenleiste stehen
|
||||
const [favorites, setFavorites] = useState([]);
|
||||
const [maxFavorites, setMaxFavorites] = useState(8);
|
||||
// Grundgerüst-Assistent
|
||||
const [plan, setPlan] = useState(null); // Vorschau, null = zu
|
||||
const [setupRunning, setSetupRunning] = useState(false);
|
||||
@@ -425,6 +428,8 @@ export default function Settings({ me }) {
|
||||
apiGet('/api/modules').then((d) => {
|
||||
setModules(d.modules);
|
||||
setModuleGroups(d.groups);
|
||||
setFavorites(d.favorites ?? []);
|
||||
setMaxFavorites(d.maxFavorites ?? 8);
|
||||
}).catch(() => {});
|
||||
apiGet('/api/texts').then((d) => {
|
||||
setTemplates(d.templates);
|
||||
@@ -526,6 +531,21 @@ export default function Settings({ me }) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Funktion in der Seitenleiste an- oder abheften */
|
||||
async function toggleFavorite(id) {
|
||||
const dabei = favorites.includes(id);
|
||||
const neu = dabei ? favorites.filter((f) => f !== id) : [...favorites, id];
|
||||
const vorher = favorites;
|
||||
setFavorites(neu); // sofort umschalten
|
||||
try {
|
||||
const res = await apiPut('/api/module-favorites', { ids: neu });
|
||||
setFavorites(res.favorites);
|
||||
} catch (error) {
|
||||
setFavorites(vorher); // bei Ablehnung zurück
|
||||
flash(`✗ ${error.body?.error ?? 'Fehlgeschlagen'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vorschau holen: was würde der Assistent anlegen? */
|
||||
async function loadPlan() {
|
||||
try {
|
||||
@@ -2387,6 +2407,8 @@ export default function Settings({ me }) {
|
||||
flash={flash}
|
||||
goTab={setTab}
|
||||
reloadSettings={reloadSettings}
|
||||
favorites={favorites}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
) : (
|
||||
<div className="settings-section">
|
||||
@@ -2431,6 +2453,15 @@ export default function Settings({ me }) {
|
||||
<span className="mod-group">{groupLabel(m.group)}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`mod-fav${favorites.includes(m.id) ? ' on' : ''}`}
|
||||
title={favorites.includes(m.id)
|
||||
? 'Aus der Seitenleiste nehmen'
|
||||
: 'In die Seitenleiste heften'}
|
||||
onClick={() => toggleFavorite(m.id)}
|
||||
>
|
||||
<IconStar size={15} />
|
||||
</button>
|
||||
<button
|
||||
className={`mod-switch${m.enabled ? ' on' : ''}`}
|
||||
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
|
||||
@@ -2907,6 +2938,30 @@ export default function Settings({ me }) {
|
||||
<p className="settings-nav-empty">Nichts gefunden.</p>
|
||||
)}
|
||||
|
||||
{/* Angeheftete Funktionen — direkt erreichbar, ohne Umweg
|
||||
übers Raster. Bei aktiver Suche ausgeblendet, dort
|
||||
zählen Treffer. */}
|
||||
{!query && canTab('module') && favorites.length > 0 && (
|
||||
<div className="settings-nav-group">
|
||||
<span className="settings-nav-label">Meine Funktionen</span>
|
||||
{favorites.map((id) => {
|
||||
const m = modules.find((x) => x.id === id);
|
||||
if (!m) return null;
|
||||
const Icon = moduleIcon(id);
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
className={`settings-nav-item ${openModule === id ? 'active' : ''}`}
|
||||
onClick={() => { setTabState('module'); setOpenModule(id); }}
|
||||
>
|
||||
<span className="settings-nav-icon"><Icon /></span>
|
||||
{m.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{TAB_SECTIONS.map((section) => {
|
||||
const items = foundTabs.filter((t) => t.section === section.id);
|
||||
if (items.length === 0) return null;
|
||||
@@ -2916,7 +2971,7 @@ export default function Settings({ me }) {
|
||||
{items.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
|
||||
className={`settings-nav-item ${tab === t.id && !(t.id === 'module' && openModule) ? 'active' : ''}`}
|
||||
onClick={() => setTab(t.id)}
|
||||
>
|
||||
<span className="settings-nav-icon"><t.Icon /></span>
|
||||
|
||||
@@ -726,6 +726,19 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
||||
.btn-create:hover:not(:disabled) { background: rgba(245, 197, 24, .1); }
|
||||
.btn-create:disabled { opacity: .4; cursor: default; }
|
||||
|
||||
/* Stern: heftet eine Funktion in die Seitenleiste. Ungeheftet nur angedeutet,
|
||||
damit die Karten nicht wie ein Sternenhimmel aussehen. */
|
||||
.mod-fav {
|
||||
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
||||
background: none; border: 0; padding: .15rem;
|
||||
color: var(--muted2); opacity: .35; cursor: pointer;
|
||||
transition: color .15s, opacity .15s;
|
||||
}
|
||||
.mod-card:hover .mod-fav, .modpage-head .mod-fav { opacity: .75; }
|
||||
.mod-fav:hover { color: var(--neon); opacity: 1; }
|
||||
.mod-fav.on { color: var(--neon); opacity: 1; }
|
||||
.mod-fav.on svg { fill: currentColor; }
|
||||
|
||||
/* ── MODUL-SEITE ───────────────────────────────────── */
|
||||
/* Name und Symbol auf der Karte öffnen die Seite der Funktion */
|
||||
.mod-open {
|
||||
|
||||
@@ -282,6 +282,44 @@ export const MODULES = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Funktionen, die in der Seitenleiste stehen — die paar, die man dauernd
|
||||
* anfasst. Der Rest bleibt im Raster.
|
||||
*
|
||||
* Der Standard ist eine Vermutung, kein Gesetz: geändert wird er über den
|
||||
* Stern auf der Modul-Karte. Deckel bei acht, damit die Leiste kurz bleibt —
|
||||
* sonst ist der Gewinn gegenüber dem Raster wieder weg.
|
||||
*/
|
||||
export const DEFAULT_FAVORITES = ['devlogs', 'welcome', 'tickets', 'starboard', 'gallery'];
|
||||
export const MAX_FAVORITES = 8;
|
||||
|
||||
/** Aktuelle Auswahl — nur existierende Module, in gespeicherter Reihenfolge */
|
||||
export function moduleFavorites() {
|
||||
const gespeichert = String(getSetting('module_favorites') ?? '').trim();
|
||||
if (!gespeichert) return DEFAULT_FAVORITES.filter((id) => MODULES.some((m) => m.id === id));
|
||||
// Ein einzelner Bindestrich heißt „bewusst keine" — sonst käme der Standard zurück
|
||||
if (gespeichert === '-') return [];
|
||||
return gespeichert
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((id) => MODULES.some((m) => m.id === id))
|
||||
.slice(0, MAX_FAVORITES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auswahl setzen.
|
||||
* @returns {{ ok: true, favorites: string[] } | { ok: false, error: string }}
|
||||
*/
|
||||
export function setModuleFavorites(ids) {
|
||||
if (!Array.isArray(ids)) return { ok: false, error: 'Liste erwartet' };
|
||||
const gefiltert = [...new Set(ids.map(String))].filter((id) => MODULES.some((m) => m.id === id));
|
||||
if (gefiltert.length > MAX_FAVORITES) {
|
||||
return { ok: false, error: `Höchstens ${MAX_FAVORITES} Funktionen in der Seitenleiste` };
|
||||
}
|
||||
setSetting('module_favorites', gefiltert.length ? gefiltert.join(',') : '-');
|
||||
return { ok: true, favorites: gefiltert };
|
||||
}
|
||||
|
||||
/** Einstellungs-Name des Schalters für ein Modul */
|
||||
function toggleKey(mod) {
|
||||
return mod.setting ?? `module_${mod.id}`;
|
||||
|
||||
+16
-2
@@ -21,7 +21,7 @@ import {
|
||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||
} from '../db.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { moduleStates, moduleDetail, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js';
|
||||
import { moduleStates, moduleDetail, setModuleEnabled, moduleFavorites, setModuleFavorites, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
||||
import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
||||
import { tuningStates, setTuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
@@ -190,7 +190,21 @@ export function registerApiRoutes(app, client) {
|
||||
|
||||
app.get('/api/modules', async (request, reply) => {
|
||||
if (requireAnyScope(request, reply)) return;
|
||||
return { modules: moduleStates(), groups: MODULE_GROUPS };
|
||||
return {
|
||||
modules: moduleStates(),
|
||||
groups: MODULE_GROUPS,
|
||||
favorites: moduleFavorites(),
|
||||
maxFavorites: MAX_FAVORITES,
|
||||
};
|
||||
});
|
||||
|
||||
// Welche Funktionen in der Seitenleiste stehen
|
||||
app.put('/api/module-favorites', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const res = setModuleFavorites(request.body?.ids ?? []);
|
||||
if (!res.ok) return reply.code(400).send({ error: res.error });
|
||||
logAudit(getSessionUser(request), 'seitenleiste geändert', res.favorites.join(', ') || 'leer');
|
||||
return { ok: true, favorites: res.favorites };
|
||||
});
|
||||
|
||||
// Alles zu einer Funktion an einem Ort: ihre Kanäle und Felder, ihre Texte,
|
||||
|
||||
Reference in New Issue
Block a user