Webseiten-Redesign: Graphit + Orange, abgeschraegte Ecken
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s

Umsetzung des Entwurfs aus design_handoff_redesign/ -- beide Auftritte
(Produktseite und Community-Hub) plus Dashboard.

  Farbe    #0a0a0a + Neon-Gelb   ->  #131316 + Orange-Verlauf aus dem Logo
  Schrift  Bebas/Barlow/ShareTech ->  Chakra Petch + IBM Plex Mono
  Form     border-radius          ->  clip-path, oben links + unten rechts

Rauschen, Scanlines, Hintergrundschrift, Kopf-Raster und die Partikel sind
raus; der Schein von oben rechts ersetzt sie. Die Abschraegung ist ein
System und keine sechzig Einzelregeln: eine clip-path-Regel mit --bv, jede
Komponente setzt nur die Tiefe.

Der Farbdurchstich ging durch alle ~55 Abschnitte der style.css, nicht nur
durch die fuenf gezeichneten Seiten -- sonst waeren Galerie, Level,
Composer und Emoji-Auswahl im alten Gelb stehen geblieben. Von --neon ist
nichts uebrig.

Die Schriften kommen weiter self-hosted ueber @fontsource. Der Entwurf
schlaegt einen Google-Fonts-Link vor; das waere eine Fremdanfrage mehr, wo
der Aufbau bisher ohne auskam.

ZWEI FEHLER unterwegs gefunden, beide aelter als dieser Umbau:

1. .btn stand in der Datei NACH .btn-save und .btn-ghost. Bei gleicher
   Spezifitaet gewinnt die spaetere Regel, also hat ihr `background: none`
   jeden Verlauf ueberschrieben -- der Speichern-Knopf sah aus wie ein
   Ghost-Knopf, schon in der gelben Fassung. Die Grundform steht jetzt
   vor allen Abwandlungen.

2. .nav-logo ist ein Flex-Container, und `gap` legte sich zwischen die
   Textknoten "D4RKST" und <span>3R</span>. In der Leiste stand
   "D4RKST 3R". Der Abstand haengt jetzt am Bild statt am Container.

DREI ABWEICHUNGEN vom Entwurf, bewusst:

- Die Statuszeile im Hero zeigt nur "ONLINE", nicht "38 MS · UPTIME
  99,2 %". Antwortzeit und Erreichbarkeit des Bots liegen ausschliesslich
  hinter /api/settings und damit hinter der Anmeldung; oeffentlich gibt es
  sie nirgends. Dafuer braeuchte es einen neuen Endpunkt -- erfunden wird
  hier nichts.
- Keine Aura am Hauptknopf: clip-path schneidet den Schatten des eigenen
  Elements mit weg. Im Referenz-HTML stehen box-shadow und clip-path am
  selben Element, dort ist sie also ebenso wenig zu sehen. Tote
  Deklaration weggelassen statt mitgeschleppt.
- Die Config-Bereiche bleiben scharfkantig statt abgeschraegt. In ihnen
  klappen Auswahllisten aus dem Panel heraus, die clip-path abschneiden
  wuerde. Deckt sich mit der Vorgabe, dass die uebrigen Tabs nur die
  Token uebernehmen; die Dashboard-Panels selbst sind abgeschraegt.

Dazu: .content von 860 auf 1160 px, weil die dreispaltigen Raster es
brauchen -- die Devlog-Timeline behaelt 880 px Lesebreite, sonst waeren
die Zeilen zu lang. Der Punkt an den Timeline-Karten ist entfallen: er
sass ausserhalb der Karte und waere von der Abschraegung weggeschnitten
worden.

Welches Wort in einer Ueberschrift den Verlauf bekommt, steht jetzt im
Woerterbuch -- der Teil in eckigen Klammern ("Funk[tionen]", aber
"Fea[tures]"). So entscheidet jede Sprache selbst, wo sich das Wort
teilen laesst.

Mitgezogen: brand.js, das geteilte Design-System fuer die eingebundenen
Dienste. Neue Tokens und Abschraegungen, aber --neon/--neon2 bleiben als
Zweitname stehen -- fremde Apps benutzen sie in ihrem eigenen CSS.

Marken-Dateien nach frontend/public/brand/, verkleinert auf die Groessen,
in denen sie wirklich dargestellt werden: 2,4 MB -> 97 KB.

Geprueft: Build laeuft. Im Browser durchgesehen -- Landing, Funktionen,
Befehle, Hub-Start, Devlogs, Server, Roadmap, Dashboard.

Ungeprueft: das Dashboard mit echter Anmeldung. Eine Sitzung war hier
nicht moeglich, gesehen habe ich es ueber gestubbte API-Antworten. Ebenso
ungeprueft, wie sich die Schriftumstellung auf schmalen Schirmen macht.

NICHT angefasst: die Embed-Farben in runtime-settings.js stehen weiter auf
dem alten Gelb -- die wirken in Discord, nicht auf der Webseite. Und ist
brand_color in der Datenbank gesetzt, behalten die eingebundenen Dienste
ihre bisherige Farbe, unabhaengig von den neuen Vorgabewerten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:15:37 +02:00
co-authored by Claude Opus 5
parent 5394d5c0c6
commit 25d36c7269
36 changed files with 1345 additions and 1068 deletions
+8 -5
View File
@@ -1,6 +1,7 @@
// Übersicht aller Slash-Befehle des Bots.
import { useEffect } from 'react';
import { IconLock } from '../icons.jsx';
import { Accent } from '../components/Accent.jsx';
import { useT } from '../i18n.jsx';
// Befehlsname bleibt wie er ist — nur die Erklärung wird übersetzt.
@@ -40,11 +41,9 @@ export default function BotCommands() {
return (
<>
<section className="page-header">
<div className="page-bg-text">BEFEHLE</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('commands.tag')}</div>
<h1 className="page-title">{t('commands.title')}</h1>
<h1 className="page-title"><Accent text={t('commands.title')} /></h1>
<p className="page-subtitle">{t('commands.subtitle')}</p>
</div>
</section>
@@ -53,8 +52,12 @@ export default function BotCommands() {
{GROUPS.map(([groupKey, restricted, commands]) => (
<div className="roadmap-group" key={groupKey}>
<h2 className="settings-title">
// {t(groupKey)}
{restricted && <IconLock size={13} />}
{/* Schloss gehört zum Titel, nicht als eigene
Spalte daneben — deshalb in einer Hülle */}
<span className="settings-title-text">
{t(groupKey)}
{restricted && <IconLock size={15} className="cmd-lock" />}
</span>
</h2>
<div className="cmd-list">
{commands.map(([name, descKey]) => (
+16 -11
View File
@@ -12,6 +12,7 @@ import {
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
IconBot, IconArrowRight,
} from '../icons.jsx';
import { Accent } from '../components/Accent.jsx';
import { moduleIcon } from '../module-icons.jsx';
// Was kein Discord-Modul ist, sondern zum Drumherum gehört
@@ -38,11 +39,9 @@ export default function BotFeatures() {
return (
<>
<section className="page-header">
<div className="page-bg-text">FEATURES</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('features.tag')}</div>
<h1 className="page-title">{t('features.title')}</h1>
<h1 className="page-title"><Accent text={t('features.title')} /></h1>
<p className="page-subtitle">
{total ? t('features.subtitleN', total) : t('features.subtitle')}
</p>
@@ -57,17 +56,20 @@ export default function BotFeatures() {
if (items.length === 0) return null;
return (
<div className="feature-group" key={group.id}>
<h2 className="settings-title">// {tOr(`fgroup.${group.id}`, group.label)}</h2>
<h2 className="settings-title">
{tOr(`fgroup.${group.id}`, group.label)}
<span className="section-meta">{t('features.count', items.length)}</span>
</h2>
<div className="feature-grid">
{items.map((f) => {
const Icon = moduleIcon(f.id);
return (
<div className="card feature-card" key={f.id}>
<span className="feature-icon"><Icon size={20} /></span>
<div>
<div className="feature-head">
<span className="feature-icon"><Icon size={16} /></span>
<span className="feature-name">{tOr(`feat.${f.id}`, f.name)}</span>
<p className="feature-desc">{tOr(`feat.${f.id}.desc`, f.desc)}</p>
</div>
<p className="feature-desc">{tOr(`feat.${f.id}.desc`, f.desc)}</p>
</div>
);
})}
@@ -78,15 +80,18 @@ export default function BotFeatures() {
{data && (
<div className="feature-group">
<h2 className="settings-title">{t('features.extras')}</h2>
<h2 className="settings-title">
{t('features.extras')}
<span className="section-meta">{t('features.count', EXTRAS.length)}</span>
</h2>
<div className="feature-grid">
{EXTRAS.map(([Icon, key]) => (
<div className="card feature-card" key={key}>
<span className="feature-icon"><Icon size={20} /></span>
<div>
<div className="feature-head">
<span className="feature-icon"><Icon size={16} /></span>
<span className="feature-name">{t(key)}</span>
<p className="feature-desc">{t(`${key}.text`)}</p>
</div>
<p className="feature-desc">{t(`${key}.text`)}</p>
</div>
))}
</div>
+63 -47
View File
@@ -4,9 +4,10 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../api.js';
import { useT } from '../i18n.jsx';
import { Accent } from '../components/Accent.jsx';
import {
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot,
IconBirthday, IconShield, IconMagic, IconArrowRight, IconBot,
} from '../icons.jsx';
const HIGHLIGHTS = [
@@ -38,15 +39,14 @@ export default function BotLanding() {
return (
<>
<section className="page-header bot-hero">
<div className="page-bg-text">BOT</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('bot.tag')}</div>
<h1 className="page-title">
{t('bot.title1')}<br />{t('bot.title2')}
<section className="hero hero-bot">
<div className="hero-text">
<div className="page-tag dot">{t('bot.tag')}</div>
<h1 className="hero-title">
{t('bot.title1')}<br />
<Accent text={t('bot.title2')} outline />
</h1>
<p className="page-subtitle">{t('bot.subtitle')}</p>
<p className="hero-sub">{t('bot.subtitle')}</p>
<div className="home-cta">
<Link className="btn btn-save with-icon" to="/features">
{t('bot.allFeatures')} <IconArrowRight size={15} />
@@ -56,63 +56,79 @@ export default function BotLanding() {
</Link>
</div>
</div>
{/* Karte mit Logo, Zustand und den vier Kennzahlen. Der versetzte
Rahmen liegt als eigenes Element dahinter — als Pseudo-Element
am Panel würde ihn dessen Abschrägung wegschneiden. */}
<div className="hero-card">
<span className="hero-frame" aria-hidden="true" />
<div className="hero-panel">
<div className="hero-lockup">
<img src="/brand/lockup.png" alt="D4RKST3R" />
</div>
{/* Diese Seite liefert der Bot selbst aus — wenn sie
ankommt, läuft er. Antwortzeit und Erreichbarkeit
stehen bisher nur im Panel, nicht öffentlich. */}
<div className="hero-state">
<span className="hero-dot" />
{t('bot.online')}
</div>
<div className="hero-stats">
<div className="hero-stat">
<span className="hero-stat-n">{featureCount ?? '—'}</span>
<span className="hero-stat-label">{t('bot.statFeatures')}</span>
</div>
<div className="hero-stat">
<span className="hero-stat-n">18</span>
<span className="hero-stat-label">{t('bot.statCommands')}</span>
</div>
<div className="hero-stat">
<span className="hero-stat-n">300+</span>
<span className="hero-stat-label">{t('bot.statGames')}</span>
</div>
<div className="hero-stat">
<span className="hero-stat-n">0 </span>
<span className="hero-stat-label">{t('bot.statCost')}</span>
</div>
</div>
</div>
</div>
</section>
<section className="content">
<div className="srv-summary">
{featureCount && (
<div className="srv-sum-item">
<span className="srv-sum-n">{featureCount}</span>
<span className="srv-sum-label">{t('bot.statFeatures')}</span>
</div>
)}
<div className="srv-sum-item">
<span className="srv-sum-n">18</span>
<span className="srv-sum-label">{t('bot.statCommands')}</span>
</div>
<div className="srv-sum-item">
<span className="srv-sum-n">300<span className="srv-sum-of">+</span></span>
<span className="srv-sum-label">{t('bot.statGames')}</span>
</div>
<div className="srv-sum-item">
<span className="srv-sum-n">0<span className="srv-sum-of"> </span></span>
<span className="srv-sum-label">{t('bot.statCost')}</span>
</div>
</div>
<h2 className="settings-title" style={{ marginTop: '2.5rem' }}>{t('bot.whatItDoes')}</h2>
<h2 className="settings-title">
{t('bot.whatItDoes')}
{featureCount && <span className="section-meta">{t('bot.modulesMeta', featureCount)}</span>}
</h2>
<div className="home-tiles">
{HIGHLIGHTS.map(([Icon, key]) => (
<div className="card home-tile" key={key}>
<span className="home-tile-icon"><Icon size={24} /></span>
<span className="home-tile-icon"><Icon size={18} /></span>
<span className="home-tile-title">{t(key)}</span>
<span className="home-tile-desc">{t(`${key}.text`)}</span>
</div>
))}
</div>
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">{t('bot.forDevs')}</h2>
<div className="home-tiles">
<div className="card home-tile">
<span className="home-tile-icon"><IconKey size={24} /></span>
<span className="home-tile-title">{t('bot.dev.api')}</span>
<span className="home-tile-desc">{t('bot.dev.api.text')}</span>
</div>
<div className="card home-tile">
<span className="home-tile-icon"><IconServer size={24} /></span>
<span className="home-tile-title">{t('bot.dev.host')}</span>
<span className="home-tile-desc">{t('bot.dev.host.text')}</span>
</div>
<div className="info-panels">
<div className="info-panel">
<span className="info-panel-tag">{t('bot.forDevs')}</span>
<h3 className="info-panel-title">{t('bot.dev.api')}</h3>
<p>{t('bot.dev.api.text')}</p>
</div>
<div className="info-panel warm">
<span className="info-panel-tag">{t('bot.hosting')}</span>
<h3 className="info-panel-title">{t('bot.dev.host')}</h3>
<p>{t('bot.dev.host.text')}</p>
</div>
</div>
{hub && (
<p className="notice" style={{ marginTop: '2rem' }}>
<p className="footnote">
{status?.members
? t('bot.runsForWith', status.members)
: t('bot.runsFor')}
{' '}<a href={hub} style={{ textDecoration: 'underline' }}>{t('bot.toHub')}</a>.
{' '}<a href={hub}>{t('bot.toHub')}</a>.
</p>
)}
</section>
-2
View File
@@ -27,8 +27,6 @@ export default function Changelog() {
return (
<>
<section className="page-header">
<div className="page-bg-text">CHANGELOG</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('changelog.tag')}</div>
<h1 className="page-title">{t('nav.changelog')}</h1>
+1 -3
View File
@@ -20,10 +20,8 @@ export default function Commits({ me }) {
const header = (
<section className="page-header">
<div className="page-bg-text">COMMITS</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Privat</div>
<div className="page-tag">Privat</div>
<h1 className="page-title">Commits</h1>
{data && <p className="page-subtitle">{data.total} archivierte Commits über alle Repos.</p>}
</div>
+12 -14
View File
@@ -17,10 +17,8 @@ export default function Datenschutz() {
return (
<>
<section className="page-header">
<div className="page-bg-text">LEGAL</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// DSGVO</div>
<div className="page-tag">DSGVO</div>
<h1 className="page-title">Datenschutz</h1>
<p className="page-subtitle">
Welche Daten hier verarbeitet werden, warum und wie du sie wieder loswirst.
@@ -29,7 +27,7 @@ export default function Datenschutz() {
</section>
<section className="content legal-text">
<h2 className="settings-title">// 1. Verantwortlicher</h2>
<h2 className="settings-title">1. Verantwortlicher</h2>
<p>
Verantwortlich für die Datenverarbeitung auf dieser Webseite im Sinne der
Datenschutz-Grundverordnung (DSGVO) ist:<br /><br />
@@ -40,7 +38,7 @@ export default function Datenschutz() {
{legal.email && <>E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a></>}
</p>
<h2 className="settings-title">// 2. Aufruf der Webseite (Server-Logs)</h2>
<h2 className="settings-title">2. Aufruf der Webseite (Server-Logs)</h2>
<p>
Beim Aufruf dieser Seite verarbeitet der Server technische Zugriffsdaten:
IP-Adresse, Zeitpunkt der Anfrage, aufgerufene Adresse, HTTP-Statuscode und
@@ -51,7 +49,7 @@ export default function Datenschutz() {
zusammengeführt und nach kurzer Zeit im Rahmen der üblichen Rotation gelöscht.
</p>
<h2 className="settings-title">// 3. Cookies</h2>
<h2 className="settings-title">3. Cookies</h2>
<p>
Diese Seite verwendet ausschließlich technisch notwendige Cookies. Konkret
wird nach einem Login ein signiertes Sitzungs-Cookie gesetzt, damit du
@@ -64,7 +62,7 @@ export default function Datenschutz() {
Verbindung zu Dritten.
</p>
<h2 className="settings-title">// 4. Login mit Discord</h2>
<h2 className="settings-title">4. Login mit Discord</h2>
<p>
Für den Mitglieder-Bereich kannst du dich freiwillig mit deinem
Discord-Konto anmelden (OAuth2, Berechtigung <code>identify</code>). Dabei
@@ -85,7 +83,7 @@ export default function Datenschutz() {
Datenschutzerklärung von Discord.
</p>
<h2 className="settings-title">// 5. Community-Funktionen</h2>
<h2 className="settings-title">5. Community-Funktionen</h2>
<p>
{brand} betreibt einen eigenen Discord-Bot, der zur Verwaltung der Community
Daten speichert. Verarbeitet werden je nachdem, welche Funktionen du nutzt:
@@ -133,7 +131,7 @@ export default function Datenschutz() {
am Betrieb einer funktionierenden Community) für den technischen Betrieb.
</p>
<h2 className="settings-title">// 6. Moderation und Protokolle</h2>
<h2 className="settings-title">6. Moderation und Protokolle</h2>
<p>
Zur Durchsetzung der Serverregeln werden Verwarnungen mit Benutzer-ID, Grund
und Zeitpunkt gespeichert. Änderungen im Administrationsbereich der Webseite
@@ -142,7 +140,7 @@ export default function Datenschutz() {
Abs. 1 lit. f DSGVO.
</p>
<h2 className="settings-title">// 7. Speicherdauer</h2>
<h2 className="settings-title">7. Speicherdauer</h2>
<p>
Wir speichern personenbezogene Daten so lange, wie du Mitglied der Community
bist oder die jeweilige Funktion nutzt. Verlässt du den Discord-Server, kannst
@@ -152,14 +150,14 @@ export default function Datenschutz() {
werden.
</p>
<h2 className="settings-title">// 8. Weitergabe an Dritte</h2>
<h2 className="settings-title">8. Weitergabe an Dritte</h2>
<p>
Eine Weitergabe deiner Daten zu Werbezwecken findet nicht statt. Ein Verkauf
von Daten erfolgt nicht. Übermittlungen erfolgen nur an Discord, soweit dies
für die Nutzung des Discord-Servers und des Logins technisch notwendig ist.
</p>
<h2 className="settings-title">// 9. Deine Rechte</h2>
<h2 className="settings-title">9. Deine Rechte</h2>
<p>
Du hast jederzeit das Recht auf Auskunft über die zu deiner Person
gespeicherten Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), auf Löschung
@@ -179,14 +177,14 @@ export default function Datenschutz() {
{' '}{legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : 'die im Impressum genannte Adresse'}.
</p>
<h2 className="settings-title">// 10. Beschwerderecht</h2>
<h2 className="settings-title">10. Beschwerderecht</h2>
<p>
Wenn du der Ansicht bist, dass die Verarbeitung deiner Daten gegen die DSGVO
verstößt, kannst du dich bei einer Datenschutz-Aufsichtsbehörde beschweren —
etwa bei der für deinen Wohnsitz zuständigen Landesdatenschutzbehörde.
</p>
<h2 className="settings-title">// 11. Änderungen</h2>
<h2 className="settings-title">11. Änderungen</h2>
<p>
Wir passen diese Datenschutzerklärung an, wenn sich die angebotenen Funktionen
ändern. Es gilt jeweils die hier veröffentlichte Fassung.
-2
View File
@@ -41,8 +41,6 @@ export default function DevlogDetail() {
return (
<>
<section className="page-header">
<div className="page-bg-text">DEVLOG</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('devlog.tag')}</div>
<h1 className="page-title">{item ? datum(item.posted_at, DATUM) : t('nav.devlog')}</h1>
-2
View File
@@ -101,8 +101,6 @@ export default function Devlogs({ me }) {
return (
<>
<section className="page-header">
<div className="page-bg-text">DEVLOG</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('devlogs.tag')}</div>
<h1 className="page-title">{t('nav.devlog')}</h1>
-2
View File
@@ -23,8 +23,6 @@ export default function Events() {
return (
<>
<section className="page-header">
<div className="page-bg-text">EVENTS</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('events.tag')}</div>
<h1 className="page-title">{t('nav.events')}</h1>
-2
View File
@@ -30,8 +30,6 @@ export default function Galerie() {
return (
<>
<section className="page-header">
<div className="page-bg-text">GALERIE</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('gallery.tag')}</div>
<h1 className="page-title">{t('nav.gallery')}</h1>
+38 -33
View File
@@ -51,56 +51,61 @@ export default function Home() {
return (
<>
<section className="page-header home-hero">
<div className="page-bg-text">D4RKST3R</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('home.tag')}</div>
<h1 className="page-title">
{t('home.welcome')}<br />{t('home.at')} D4RKST<span className="home-accent">3</span>R
<section className="hero hero-hub">
<div className="hero-text">
<div className="page-tag dot">{t('home.tag')}</div>
<h1 className="hero-title">
{t('home.welcome')} {t('home.at')}<br />
<span className="grad-text">D4RKST3R</span>
</h1>
<p className="page-subtitle">{t('home.subtitle')}</p>
<p className="hero-sub">{t('home.subtitle')}</p>
<div className="home-cta">
{invite && (
<a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer">
<IconMessage /> {t('home.discord')}
</a>
)}
<Link className="btn btn-ghost home-cta-alt with-icon" to="/devlogs">
<Link className="btn btn-ghost with-icon" to="/devlogs">
<IconDevlog /> {t('home.readDevlogs')}
</Link>
</div>
{(stats || servers) && (
<div className="hero-figures">
{stats?.members > 0 && (
<div>
<span className="hero-figure-n">{zahl(stats.members)}</span>
<span className="hero-figure-label">{t('home.members')}</span>
</div>
)}
{servers?.length > 0 && (
<div>
<span className="hero-figure-n">
{online.length}<span className="hero-figure-of"> / {servers.length}</span>
</span>
<span className="hero-figure-label">{t('home.serversOnline')}</span>
</div>
)}
{online.length > 0 && (
<div>
<span className="hero-figure-n">{players}</span>
<span className="hero-figure-label">{t('home.playersNow')}</span>
</div>
)}
</div>
)}
</div>
<div className="hero-logo">
<img src="/brand/lockup.png" alt="D4RKST3R" />
</div>
</section>
<section className="content">
{(stats || servers) && (
<div className="srv-summary home-stats">
{stats?.members > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{zahl(stats.members)}</span>
<span className="srv-sum-label">{t('home.members')}</span>
</div>
)}
{servers?.length > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{online.length}<span className="srv-sum-of"> / {servers.length}</span></span>
<span className="srv-sum-label">{t('home.serversOnline')}</span>
</div>
)}
{online.length > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{players}</span>
<span className="srv-sum-label">{t('home.playersNow')}</span>
</div>
)}
</div>
)}
<div className="home-tiles">
{TILES.map(([to, Icon, titleKey, descKey]) => (
<Link className="card home-tile" to={to} key={to}>
<span className="home-tile-icon"><Icon size={26} /></span>
<span className="home-tile-icon"><Icon size={18} /></span>
<span className="home-tile-title">{t(titleKey)}</span>
<span className="home-tile-desc">{t(descKey)}</span>
</Link>
+8 -10
View File
@@ -17,10 +17,8 @@ export default function Impressum() {
return (
<>
<section className="page-header">
<div className="page-bg-text">LEGAL</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Pflichtangaben</div>
<div className="page-tag">Pflichtangaben</div>
<h1 className="page-title">Impressum</h1>
<p className="page-subtitle">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p>
</div>
@@ -34,7 +32,7 @@ export default function Impressum() {
</p>
)}
<h2 className="settings-title">// Diensteanbieter</h2>
<h2 className="settings-title">Diensteanbieter</h2>
<p>
{legal.name || '—'}<br />
{(legal.address || '').split('\n').map((line, i) => (
@@ -42,25 +40,25 @@ export default function Impressum() {
))}
</p>
<h2 className="settings-title">// Kontakt</h2>
<h2 className="settings-title">Kontakt</h2>
<p>
E-Mail: {legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : '—'}
</p>
{legal.extra && (
<>
<h2 className="settings-title">// Weitere Angaben</h2>
<h2 className="settings-title">Weitere Angaben</h2>
<p>{legal.extra.split('\n').map((line, i) => <span key={i}>{line}<br /></span>)}</p>
</>
)}
<h2 className="settings-title">// Verantwortlich für den Inhalt</h2>
<h2 className="settings-title">Verantwortlich für den Inhalt</h2>
<p>
Verantwortlich für journalistisch-redaktionelle Inhalte nach § 18 Abs. 2 MStV:
{' '}{legal.name || '—'}{legal.address ? `, ${legal.address.replace(/\n/g, ', ')}` : ''}
</p>
<h2 className="settings-title">// Haftung für Inhalte</h2>
<h2 className="settings-title">Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet,
@@ -73,7 +71,7 @@ export default function Impressum() {
entfernen wir diese Inhalte umgehend.
</p>
<h2 className="settings-title">// Haftung für Links</h2>
<h2 className="settings-title">Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte
wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch
@@ -82,7 +80,7 @@ export default function Impressum() {
von Rechtsverletzungen entfernen wir derartige Links umgehend.
</p>
<h2 className="settings-title">// Urheberrecht</h2>
<h2 className="settings-title">Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
unterliegen dem deutschen Urheberrecht. Beiträge Dritter insbesondere
-2
View File
@@ -60,8 +60,6 @@ export default function Level() {
return (
<>
<section className="page-header">
<div className="page-bg-text">LEVEL</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('level.tag')}</div>
<h1 className="page-title">{t('nav.level')}</h1>
-2
View File
@@ -49,8 +49,6 @@ export default function Mods() {
return (
<>
<section className="page-header">
<div className="page-bg-text">MODS</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('mods.tag')}</div>
<h1 className="page-title">{daten?.server?.name ?? t('mods.title')}</h1>
+5 -7
View File
@@ -72,7 +72,7 @@ export default function Profil({ me }) {
return (
<section className="page-header">
<div className="page-header-content">
<div className="page-tag">// Profil</div>
<div className="page-tag">Profil</div>
<h1 className="page-title">Nicht eingeloggt</h1>
<p className="page-subtitle">
<a href="/auth/login" style={{ textDecoration: 'underline' }}>Mit Discord einloggen</a>, um dein Profil zu sehen.
@@ -87,10 +87,8 @@ export default function Profil({ me }) {
return (
<>
<section className="page-header">
<div className="page-bg-text">PROFIL</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Member</div>
<div className="page-tag">Member</div>
<h1 className="page-title">Profil</h1>
<p className="page-subtitle">Dein Stand auf dem Server Rang, Rollen und Keys.</p>
</div>
@@ -145,7 +143,7 @@ export default function Profil({ me }) {
{/* Alpha-Key */}
{profile.alphaKey && (
<div className="roadmap-group">
<h2 className="settings-title">// Dein Alpha-Key</h2>
<h2 className="settings-title">Dein Alpha-Key</h2>
<div className="field-row">
<code className="alpha-key">{profile.alphaKey}</code>
<button className="btn btn-mini" onClick={copyKey}>
@@ -158,7 +156,7 @@ export default function Profil({ me }) {
{/* Rollen-Selfservice */}
{menus.length > 0 && (
<div className="roadmap-group">
<h2 className="settings-title">// Rollen wählen</h2>
<h2 className="settings-title">Rollen wählen</h2>
{menus.map((m) => (
<div className="rolemenu-box" key={m.id}>
<h3 className="rolemenu-title">{m.title}</h3>
@@ -183,7 +181,7 @@ export default function Profil({ me }) {
{/* DSGVO Art. 17: eigene Daten löschen */}
<div className="roadmap-group danger-zone">
<h2 className="settings-title">// Meine Daten</h2>
<h2 className="settings-title">Meine Daten</h2>
<p className="field-hint">
Du kannst jederzeit alle Daten löschen, die hier zu dir gespeichert
sind: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen,
-2
View File
@@ -191,8 +191,6 @@ export default function Roadmap() {
return (
<>
<section className="page-header">
<div className="page-bg-text">ROADMAP</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('roadmap.tag')}</div>
<h1 className="page-title">{t('nav.roadmap')}</h1>
+2 -4
View File
@@ -21,7 +21,7 @@ export default function Seite() {
return (
<section className="page-header">
<div className="page-header-content">
<div className="page-tag">// 404</div>
<div className="page-tag">404</div>
<h1 className="page-title">Seite nicht gefunden</h1>
<p className="page-subtitle">
<Link to="/" style={{ textDecoration: 'underline' }}>Zurück zur Startseite</Link>
@@ -34,10 +34,8 @@ export default function Seite() {
return (
<>
<section className="page-header">
<div className="page-bg-text">{(page?.title ?? '').slice(0, 12).toUpperCase()}</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// {page?.published === false ? 'Entwurf' : 'Info'}</div>
<div className="page-tag">{page?.published === false ? 'Entwurf' : 'Info'}</div>
<h1 className="page-title">{page?.title ?? ' '}</h1>
</div>
</section>
-2
View File
@@ -251,8 +251,6 @@ export default function Server() {
return (
<>
<section className="page-header">
<div className="page-bg-text">SERVER</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('server.tag')}</div>
<h1 className="page-title">{t('nav.server')}</h1>
+111 -96
View File
@@ -1248,14 +1248,19 @@ export default function Settings({ me }) {
entries: menuDraft.entries.map((e, j) => (j === i ? { ...e, ...patch } : e)),
});
// Kopf des Panels: eine Zeile statt eines eigenen Seitenkopfs — darunter
// beginnt sofort die Arbeitsfläche. Die Antwortzeit steht nur hier, weil
// sie aus der Panel-API kommt und nicht öffentlich ist.
const header = (
<section className="page-header">
<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">Config</h1>
<p className="page-subtitle">Alles an einem Ort Änderungen greifen sofort.</p>
<section className="page-header wide">
<div className="page-header-content dash-title-row">
<h1 className="page-title dash-title">Dash<span className="grad-text">board</span></h1>
<span className="dash-meta">Alles an einem Ort Änderungen greifen sofort</span>
{data?.status && (
<span className="status-pill">
Bot online{data.status.ping != null && ` · ${data.status.ping} ms`}
</span>
)}
</div>
</section>
);
@@ -1337,11 +1342,11 @@ export default function Settings({ me }) {
const tabStatus = (
<>
<div className="settings-section">
<div className="settings-section dash-panel">
<div className="dash-head">
{data.status.botAvatar
? <img className="dash-avatar" src={data.status.botAvatar} alt="" />
: <span className="dash-avatar leer">?</span>}
: <img className="dash-avatar mark" src="/brand/mark.png" alt="" />}
<div className="dash-ident">
<span className="dash-name">
{data.status.botName ?? 'Bot'}
@@ -1402,55 +1407,65 @@ export default function Settings({ me }) {
</div>
</div>
{aktivitaet && aktivitaet.gesamt > 0 && (
<div className="settings-section">
<h2 className="settings-title">
// Aktivität — {aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten in 30 Tagen
</h2>
<div className="dash-spark">
{aktivitaet.tage.map((t) => (
<span
key={t.key}
className="dash-bar"
style={{ height: `${Math.max(3, (t.n / aktivitaet.max) * 100)}%` }}
title={`${t.key}: ${t.n} Nachrichten`}
/>
))}
{/* Verlauf und Modul-Verteilung stehen nebeneinander — beides sind
Übersichten, keine Formulare */}
<div className="dash-cols">
{aktivitaet && aktivitaet.gesamt > 0 && (
<div className="settings-section dash-panel">
<h2 className="settings-title">
Aktivität
<span className="section-meta">
{aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten / 30 Tage
</span>
</h2>
<div className="dash-spark">
{aktivitaet.tage.map((t) => (
<span
key={t.key}
className="dash-bar"
style={{ height: `${Math.max(3, (t.n / aktivitaet.max) * 100)}%` }}
title={`${t.key}: ${t.n} Nachrichten`}
/>
))}
</div>
</div>
</div>
)}
)}
{modules.length > 0 && moduleGroups.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Module nach Bereich</h2>
<div className="dash-groups">
{moduleGroups.map((g) => {
const drin = modules.filter((m) => m.group === g.id);
if (drin.length === 0) return null;
const an = drin.filter((m) => m.enabled).length;
const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length;
return (
<button
className="dash-group"
key={g.id}
onClick={() => setTab('module')}
title={`${g.label} im Modul-Überblick öffnen`}
>
<span className="dash-group-name">{g.label}</span>
<span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}>
<i style={{ width: `${(an / drin.length) * 100}%` }} />
</span>
<span className="dash-group-n">{an}/{drin.length}</span>
</button>
);
})}
{modules.length > 0 && moduleGroups.length > 0 && (
<div className="settings-section dash-panel">
<h2 className="settings-title">
Module
<span className="section-meta">{activeModules.length}/{modules.length} an</span>
</h2>
<div className="dash-groups">
{moduleGroups.map((g) => {
const drin = modules.filter((m) => m.group === g.id);
if (drin.length === 0) return null;
const an = drin.filter((m) => m.enabled).length;
const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length;
return (
<button
className="dash-group"
key={g.id}
onClick={() => setTab('module')}
title={`${g.label} im Modul-Überblick öffnen`}
>
<span className="dash-group-name">{g.label}</span>
<span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}>
<i style={{ width: `${(an / drin.length) * 100}%` }} />
</span>
<span className="dash-group-n">{an}/{drin.length}</span>
</button>
);
})}
</div>
</div>
</div>
)}
)}
</div>
{modules.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Noch einzurichten</h2>
<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.
@@ -1567,7 +1582,7 @@ export default function Settings({ me }) {
const willkommensKarte = (
<>
<div className="settings-section">
<h2 className="settings-title">// Willkommen</h2>
<h2 className="settings-title">Willkommen</h2>
<div className="field" data-setting="welcome_channel_id">
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label>
<PickSelect
@@ -1580,7 +1595,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Willkommens-Karte</h2>
<h2 className="settings-title">Willkommens-Karte</h2>
<p className="section-intro">
Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau
zeigt sofort, wie es aussieht gespeichert wird erst mit dem Knopf unten.
@@ -1714,7 +1729,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Embeds</h2>
<h2 className="settings-title">Embeds</h2>
<div className="field" data-setting="brand_name">
<label>Brand-Name <span className="field-hint">erscheint in allen Embed-Footern („NAME // DEVLOG")</span></label>
<input type="text" placeholder="D4RKST3R" value={form.brand_name} onChange={(e) => setForm({ ...form, brand_name: e.target.value })} />
@@ -1730,7 +1745,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Aus der Env verlagert</h2>
<h2 className="settings-title">Aus der Env verlagert</h2>
<div className="field" data-setting="discord_guild_id">
<label>Guild-ID <span className="field-hint">für sofortige Slash-Commands; greift beim nächsten Neustart</span></label>
<input type="text" placeholder="Server-ID" value={form.discord_guild_id} onChange={(e) => setForm({ ...form, discord_guild_id: e.target.value })} />
@@ -1751,7 +1766,7 @@ export default function Settings({ me }) {
const tabFeeds = (
<>
<div className="settings-section">
<h2 className="settings-title">// Kanäle</h2>
<h2 className="settings-title">Kanäle</h2>
<div className="field" data-setting="devlog_channel_id">
<label>Devlog-Kanal <span className="field-hint">öffentlich hier postet der Bot die Devlogs</span></label>
<div className="field-row">
@@ -1765,7 +1780,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Twitch &amp; YouTube</h2>
<h2 className="settings-title">Twitch &amp; YouTube</h2>
<div className="field" data-setting="social_announce_channel_id">
<label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + neue YouTube-Videos</span></label>
<PickSelect
@@ -1817,7 +1832,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Devlog</h2>
<h2 className="settings-title">Devlog</h2>
<div className="field" data-setting="devlog_ping_role_id">
<label>Ping-Rolle <span className="field-hint">wird bei jedem Devlog erwähnt; 🔔-Button am Post leer = kein Ping</span></label>
<PickSelect
@@ -1847,7 +1862,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Commit-Feed</h2>
<h2 className="settings-title">Commit-Feed</h2>
<div className="field" data-setting="commit_feed_enabled">
<label className="toggle">
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} />
@@ -1870,7 +1885,7 @@ export default function Settings({ me }) {
const tabCommunity = (
<>
<div className="settings-section">
<h2 className="settings-title">// Wünsche</h2>
<h2 className="settings-title">Wünsche</h2>
<p className="section-intro">
Ein Wunsch ohne sichtbaren Ausgang ist ein Wunsch, den beim nächsten Mal
niemand mehr einreicht. Der Stand steht auf der Roadmap und im Discord-Post;
@@ -1966,7 +1981,7 @@ export default function Settings({ me }) {
)}
<div className="settings-section">
<h2 className="settings-title">// Wunsch-Bereiche</h2>
<h2 className="settings-title">Wunsch-Bereiche</h2>
<p className="section-intro">
Schubladen für Ideen — „Fahrzeuge", Wirtschaft", „Technik". Ohne Bereiche
bleibt die Liste eine Liste; mit ihnen kann man auf der Roadmap filtern, und
@@ -2034,7 +2049,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Geburtstage</h2>
<h2 className="settings-title">Geburtstage</h2>
<div className="field" data-setting="birthday_channel_id">
<label>Geburtstags-Kanal <span className="field-hint">Member tragen sich mit /geburtstag ein; Gratulation morgens ab 09:00 leer = aus</span></label>
<PickSelect
@@ -2056,7 +2071,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Events</h2>
<h2 className="settings-title">Events</h2>
<div className="field" data-setting="events_announce_channel_id">
<label>Event-Ankündigungs-Kanal <span className="field-hint">neue Discord-Events werden hier angekündigt + Events-Seite</span></label>
<PickSelect
@@ -2069,7 +2084,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Starboard & Galerie</h2>
<h2 className="settings-title">Starboard & Galerie</h2>
<div className="field" data-setting="starboard_channel_id">
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug ⭐ landen hier</span></label>
<div className="field-row">
@@ -2103,7 +2118,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Level-System</h2>
<h2 className="settings-title">Level-System</h2>
<div className="field" data-setting="levels_enabled">
<label className="toggle">
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} />
@@ -2125,7 +2140,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Radio</h2>
<h2 className="settings-title">Radio</h2>
{/* Der Schalter steht hier und nicht nur im Modul-Reiter:
wer die Sender pflegt, sucht ihn genau an dieser Stelle. */}
<div className="field" data-setting="radio_enabled">
@@ -2224,7 +2239,7 @@ export default function Settings({ me }) {
const tabRollen = (
<>
<div className="settings-section">
<h2 className="settings-title">// Rollen bei Join</h2>
<h2 className="settings-title">Rollen bei Join</h2>
<div className="field" data-setting="autorole_id">
<label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch — leer = aus</span></label>
<PickSelect
@@ -2247,7 +2262,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Rollen-Menüs</h2>
<h2 className="settings-title">Rollen-Menüs</h2>
<p className="section-intro">
Selbstbedienungs-Rollen wie bei Carl-bot — nur ohne Paywall. Der Bot postet
ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind
@@ -2468,7 +2483,7 @@ export default function Settings({ me }) {
const tabSupport = (
<>
<div className="settings-section">
<h2 className="settings-title">// Ticket-Anliegen</h2>
<h2 className="settings-title">Ticket-Anliegen</h2>
<p className="section-intro">
Ohne Anliegen hat der Ticket-Post einen Knopf und jedes Ticket sieht gleich aus.
Mit Anliegen wird daraus ein Auswahlmenü: der Titel steht im Thread-Namen,
@@ -2563,7 +2578,7 @@ export default function Settings({ me }) {
</div>
{automod && (
<div className="settings-section">
<h2 className="settings-title">// AutoMod</h2>
<h2 className="settings-title">AutoMod</h2>
<p className="section-intro">
Discords eigener Filter — er läuft serverseitig und ohne Umweg über den Bot.
Hier siehst du, was eingerichtet ist, und kannst Regeln schalten.
@@ -2618,7 +2633,7 @@ export default function Settings({ me }) {
</div>
)}
<div className="settings-section">
<h2 className="settings-title">// Moderation &amp; Kontakt</h2>
<h2 className="settings-title">Moderation &amp; Kontakt</h2>
<div className="field" data-setting="modmail_channel_id">
<label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal — DMs an den Bot landen hier als Threads</span></label>
<PickSelect
@@ -2653,7 +2668,7 @@ export default function Settings({ me }) {
const tabPlaytester = (
<>
<div className="settings-section">
<h2 className="settings-title">// Playtester</h2>
<h2 className="settings-title">Playtester</h2>
<p className="section-intro">
Beworben wird sich über ein Formular, angenommen vom Team — wer angenommen
wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt
@@ -2729,7 +2744,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Schlüssel-Vorrat</h2>
<h2 className="settings-title">Schlüssel-Vorrat</h2>
<div className="field-row" style={{ marginBottom: '1rem' }}>
<button className="btn" onClick={() => setKeyDialog(true)}>
<IconKey size={14} /> Schlüssel nachlegen
@@ -2835,7 +2850,7 @@ export default function Settings({ me }) {
const tabBewerbungen = (
<div className="settings-section">
<h2 className="settings-title">// Bewerbungs-Formulare</h2>
<h2 className="settings-title">Bewerbungs-Formulare</h2>
<p className="section-intro">
Button-Post → Discord-Formular (bis 5 Fragen) → Review mit ✅/❌ im Staff-Kanal.
Annahme vergibt optional eine Rolle — z. B. für FiveM-Whitelist oder Team-Bewerbungen.
@@ -2937,7 +2952,7 @@ export default function Settings({ me }) {
<EmbedComposer channelOptions={channelOptions} flash={flash} />
<div className="settings-section">
<h2 className="settings-title">// Geplante Posts</h2>
<h2 className="settings-title">Geplante Posts</h2>
{scheduled.length > 0 && (
<div className="key-list" style={{ marginBottom: '1.2rem' }}>
{scheduled.map((p) => (
@@ -2996,7 +3011,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Triggers</h2>
<h2 className="settings-title">Triggers</h2>
<p className="section-intro">
Auto-Antworten: Enthält eine Nachricht das Keyword, antwortet der Bot (30s-Cooldown pro Kanal).
</p>
@@ -3022,7 +3037,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Tags (/tag)</h2>
<h2 className="settings-title">Tags (/tag)</h2>
<p className="section-intro">
Text-Bausteine für wiederkehrende Antworten — jeder Member kann sie mit <code>/tag name</code> abrufen.
</p>
@@ -3052,7 +3067,7 @@ export default function Settings({ me }) {
const tabSystem = (
<>
<div className="settings-section">
<h2 className="settings-title">// Allgemein</h2>
<h2 className="settings-title">Allgemein</h2>
<div className="field" data-setting="public_url">
<label>Öffentliche URL <span className="field-hint">Basis für Buttons, RSS, OAuth-Redirect</span></label>
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.public_url} onChange={(e) => setForm({ ...form, public_url: e.target.value })} />
@@ -3064,7 +3079,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Zwei Seiten</h2>
<h2 className="settings-title">Zwei Seiten</h2>
<p className="section-intro">
Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange
beide Felder leer sind, läuft alles unverändert auf der öffentlichen URL.
@@ -3095,7 +3110,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Dienste (Portal)</h2>
<h2 className="settings-title">Dienste (Portal)</h2>
<p className="section-intro">
Erscheinen als Kacheln auf der Startseite z. B. Gitea, Kanban oder die Cloud.
</p>
@@ -3135,7 +3150,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Impressum &amp; Datenschutz</h2>
<h2 className="settings-title">Impressum &amp; Datenschutz</h2>
<p className="field-hint" style={{ marginBottom: '.9rem' }}>
Erscheint auf <code>/impressum</code> und <code>/datenschutz</code>. Für § 5 DDG
ist eine ladungsfähige Anschrift nötig (kein Postfach) nur der Owner sieht
@@ -3160,7 +3175,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Web-Login (Member)</h2>
<h2 className="settings-title">Web-Login (Member)</h2>
<div className="field" data-setting="member_gate_enabled">
<label className="toggle">
<input type="checkbox" checked={form.member_gate_enabled} onChange={(e) => setForm({ ...form, member_gate_enabled: e.target.checked })} />
@@ -3175,7 +3190,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Sprachkanäle</h2>
<h2 className="settings-title">Sprachkanäle</h2>
<div className="field" data-setting="tempvoice_channel_id">
<label>Temp-Voice-Hub <span className="field-hint">Join to Create": Beitritt erzeugt eigenen Voice-Kanal (Bot braucht Kanäle verwalten + Member verschieben)</span></label>
<PickSelect
@@ -3188,7 +3203,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Backups &amp; Watchdog</h2>
<h2 className="settings-title">Backups &amp; Watchdog</h2>
<div className="field" data-setting="backup_enabled">
<label className="toggle">
<input type="checkbox" checked={form.backup_enabled} onChange={(e) => setForm({ ...form, backup_enabled: e.target.checked })} />
@@ -3226,7 +3241,7 @@ export default function Settings({ me }) {
{watchdog?.adressen?.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Erreichbarkeit</h2>
<h2 className="settings-title">Erreichbarkeit</h2>
<p className="section-intro">
Ein Strich je Stunde über die letzten 7 Tage grün erreichbar, rot
Ausfall, grau keine Messung. Geprüft wird alle {watchdog.intervall} Minuten.
@@ -3294,7 +3309,7 @@ export default function Settings({ me }) {
{watchdog?.stoerungen?.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Störungen</h2>
<h2 className="settings-title">Störungen</h2>
<div className="st-incidents">
{watchdog.stoerungen.slice(0, 20).map((v) => (
<div className={`st-incident${v.beendet ? '' : ' offen'}`} key={v.id}>
@@ -3314,7 +3329,7 @@ export default function Settings({ me }) {
const tabApi = (
<>
<div className="settings-section">
<h2 className="settings-title">// API-Keys (/api/v1)</h2>
<h2 className="settings-title">API-Keys (/api/v1)</h2>
{keys.length > 0 && (
<div className="key-list">
{keys.map((k) => (
@@ -3363,7 +3378,7 @@ export default function Settings({ me }) {
</div>
<div className="settings-section">
<h2 className="settings-title">// Single Sign-On</h2>
<h2 className="settings-title">Single Sign-On</h2>
<p className="section-intro">
Andere Dienste (Kanban, Platform ) können den Discord-Login mitnutzen: Sie
leiten auf <code>/sso/authorize</code> weiter und tauschen den Token per
@@ -3412,7 +3427,7 @@ export default function Settings({ me }) {
const tabServer = (
<>
<div className="settings-section">
<h2 className="settings-title">// Game-Server</h2>
<h2 className="settings-title">Game-Server</h2>
<p className="section-intro">
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
Spieler-Balken, Map, Adresse) aktualisiert alle 2 Minuten, DiscordGSM-Style.
@@ -3684,7 +3699,7 @@ export default function Settings({ me }) {
/>
) : (
<div className="settings-section">
<h2 className="settings-title">// Module</h2>
<h2 className="settings-title">Module</h2>
<p className="section-intro">
Jede Funktion des Bots lässt sich hier ein- und ausschalten. Ausgeschaltete
Module reagieren auf nichts mehr keine Posts, keine Befehle, keine
@@ -3767,7 +3782,7 @@ export default function Settings({ me }) {
const tabTexte = (
<div className="settings-section">
<h2 className="settings-title">// Texte</h2>
<h2 className="settings-title">Texte</h2>
<p className="section-intro">
Alle Texte am Stück wer nur die einer Funktion sucht, findet sie
auch auf deren Modul-Seite. Zeile anklicken zum Bearbeiten
@@ -3875,7 +3890,7 @@ export default function Settings({ me }) {
const tabWerte = (
<div className="settings-section">
<h2 className="settings-title">// Werte</h2>
<h2 className="settings-title">Werte</h2>
<p className="section-intro">
Alle Werte am Stück; jeder steht zusätzlich auf der Seite seiner
Funktion. Zahlen, die das Verhalten steuern wie viel XP eine Nachricht bringt,
@@ -3954,7 +3969,7 @@ export default function Settings({ me }) {
const tabSeiten = (
<div className="settings-section">
<h2 className="settings-title">// Eigene Seiten</h2>
<h2 className="settings-title">Eigene Seiten</h2>
<p className="section-intro">
Inhalte pflegen, ohne dass jemand Code anfassen muss Regeln, Über uns,
Mitmachen, FAQ. Geschrieben wird in Markdown (wie im Devlog), rechts siehst
@@ -4130,7 +4145,7 @@ export default function Settings({ me }) {
const tabTeam = (
<>
<div className="settings-section">
<h2 className="settings-title">// Team</h2>
<h2 className="settings-title">Team</h2>
<p className="section-intro">
Team-Mitglieder loggen sich ganz normal mit Discord ein und sehen nur ihre
Bereiche. Brand, System, API-Keys und diese Verwaltung bleiben immer dir
@@ -4230,7 +4245,7 @@ export default function Settings({ me }) {
{audit.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Protokoll</h2>
<h2 className="settings-title">Protokoll</h2>
<p className="section-intro">
Wer hat was im Webinterface geändert.
</p>
-2
View File
@@ -49,8 +49,6 @@ export default function Status() {
return (
<>
<section className="page-header">
<div className="page-bg-text">STATUS</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('status.tag')}</div>
<h1 className="page-title">{t('nav.status')}</h1>
-2
View File
@@ -55,8 +55,6 @@ export default function WunschDetail() {
return (
<>
<section className="page-header">
<div className="page-bg-text">IDEE</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">{t('wunsch.tag')}</div>
<h1 className="page-title">{w?.idea ?? '…'}</h1>