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
+1 -1
View File
@@ -7,7 +7,7 @@
<meta name="description" content="D4RKST3R — Devlog-Archiv, Changelog & Community." />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#0a0a0a" />
<meta name="theme-color" content="#131316" />
</head>
<body>
<div id="root"></div>
+8 -18
View File
@@ -8,9 +8,8 @@
"name": "d4rkbot-frontend",
"version": "0.1.0",
"dependencies": {
"@fontsource/barlow-condensed": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
"@fontsource/chakra-petch": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"lucide-react": "^1.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -745,28 +744,19 @@
"node": ">=18"
}
},
"node_modules/@fontsource/barlow-condensed": {
"node_modules/@fontsource/chakra-petch": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/barlow-condensed/-/barlow-condensed-5.3.0.tgz",
"integrity": "sha512-RA7MmIx0v3eQ6Gcf74qLVLOwLB0GpPOzx+eU9iG/AmCnSJGcIWYeU8y6CEMGZ210MGwGg5CZhzLkkvyZdTTk1g==",
"resolved": "https://registry.npmjs.org/@fontsource/chakra-petch/-/chakra-petch-5.3.0.tgz",
"integrity": "sha512-QZvbM/q2R6n1pg2x+TJPftZmBNS+ADU37L4Z4L/6UJIz+JZ/SRaEf8usPFRiP7Ht28sIideMu/Bm1SnGlSrwNw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/bebas-neue": {
"node_modules/@fontsource/ibm-plex-mono": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/bebas-neue/-/bebas-neue-5.3.0.tgz",
"integrity": "sha512-nCxMEb9Bg9sjNWm9TIOsvHKDMLRUfEZPWpLgYu6m9KavZZVSA/WoHzI5wROwqA+eNwEi/PNBAcB5Co7HA3S0Cw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/share-tech-mono": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/share-tech-mono/-/share-tech-mono-5.3.0.tgz",
"integrity": "sha512-hVEzVCzEid/fBfhggMyvkfDzn9ODrMihxC0EiOeQK3BuQNvdjmith2ERnoDNUkbW8zhufbKNuCgaSoT8LUNpuQ==",
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
+2 -3
View File
@@ -9,9 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource/barlow-condensed": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
"@fontsource/chakra-petch": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"lucide-react": "^1.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+15 -44
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
import { apiGet } from './api.js';
import Devlogs from './pages/Devlogs.jsx';
@@ -62,35 +62,9 @@ function DiscordMark() {
);
}
/* Aufsteigende Neon-Partikel wie auf der Platform-Seite */
function Particles() {
const particles = useMemo(
() =>
Array.from({ length: 24 }, (_, i) => ({
id: i,
left: `${Math.random() * 100}%`,
dur: `${6 + Math.random() * 8}s`,
delay: `${Math.random() * 8}s`,
})),
[]
);
return (
<div className="particles-wrap" aria-hidden="true">
{particles.map((p) => (
<span
key={p.id}
className="particle"
style={{ left: p.left, '--dur': p.dur, '--delay': p.delay, bottom: 0 }}
/>
))}
</div>
);
}
export default function App() {
// Login-Status einmal beim Laden holen
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
const [scrolled, setScrolled] = useState(false);
const [drawer, setDrawer] = useState(false);
const location = useLocation();
const { t } = useT();
@@ -116,18 +90,12 @@ export default function App() {
if (gated) apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {});
}, [gated]);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div className="app">
<Particles />
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
{/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
<header className="nav">
<NavLink to="/" className="nav-logo">
<img className="nav-mark" src="/brand/mark.png" alt="" />
D4RKST<span>3R</span>
</NavLink>
@@ -238,18 +206,21 @@ export default function App() {
</main>
<footer className="footer">
<span className="footer-logo">D4RKST<span>3R</span></span>
<span className="footer-sep">//</span>
<span className="footer-logo">
<img className="footer-mark" src="/brand/mark.png" alt="" />
D4RKST<span>3R</span>
</span>
<a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
git.d4rkst3r.de
</a>
<span className="footer-sep">//</span>
<NavLink to="/status">{t('footer.status')}</NavLink>
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
{pages.map((p) => (
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
))}
<span className="footer-right">
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
{pages.map((p) => (
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
))}
</span>
</footer>
</div>
);
+17 -19
View File
@@ -1,4 +1,4 @@
// Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe.
// Rahmen der Bot-Produktseite — eigene Navigation, gleiche Marke wie der Hub.
// Das Dashboard ist dieselbe Config-Seite wie bisher, nur unter /dashboard.
import { useEffect, useState } from 'react';
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
@@ -14,7 +14,6 @@ import { useT, LangSwitch } from './i18n.jsx';
export default function BotApp() {
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
const [scrolled, setScrolled] = useState(false);
const [drawer, setDrawer] = useState(false);
const [hub, setHub] = useState(null);
// Seiten wie die Nutzungsbedingungen liegen auf dem Hub — verlinkt
@@ -33,19 +32,15 @@ export default function BotApp() {
apiGet('/api/pages').then((d) => setPages(d.pages)).catch(() => {});
}, []);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
const canDashboard = me.admin || me.scopes?.length > 0;
return (
<div className="app site-bot">
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
<div className="app">
{/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
<header className="nav">
<NavLink to="/" className="nav-logo">
D4RKBOT<span>.</span>
<img className="nav-mark" src="/brand/mark.png" alt="" />
D4RKBOT<span>_</span>
</NavLink>
<nav className="nav-links">
@@ -105,18 +100,21 @@ export default function BotApp() {
</main>
<footer className="footer">
<span className="footer-logo">D4RKBOT<span>.</span></span>
<span className="footer-sep">//</span>
<span className="footer-logo">
<img className="footer-mark" src="/brand/mark.png" alt="" />
D4RKBOT<span>_</span>
</span>
<a href="https://git.d4rkst3r.de/D4rkst3r/d4rkbot" target="_blank" rel="noreferrer">
{t('footer.source')}
</a>
<span className="footer-sep">//</span>
{hub && <a href={`${hub}/status`}>{t('footer.status')}</a>}
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
{hub && pages.map((p) => (
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
))}
<span className="footer-right">
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
{hub && pages.map((p) => (
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
))}
</span>
</footer>
</div>
);
+27
View File
@@ -0,0 +1,27 @@
// Betonte Stelle in einer Überschrift.
//
// Welches Wort — oder welche Silbe — den Marken-Verlauf bekommt, steht im
// Wörterbuch: der Teil in eckigen Klammern. So entscheidet jede Sprache
// selbst, wo sich die Überschrift teilen lässt („Funk[tionen]", aber
// „Fea[tures]"), ohne dass die Seite darüber etwas wissen muss.
//
// Mit `outline` bleibt der Rest als Kontur stehen statt gefüllt — so sieht
// die zweite Zeile des Hero auf der Produktseite aus.
const MARKE = /^(.*?)\[([^\]]*)\](.*)$/s;
export function Accent({ text, outline = false }) {
const treffer = MARKE.exec(text ?? '');
if (!treffer) return <>{text}</>;
const [, davor, betont, danach] = treffer;
const rest = outline ? 'outline-text' : undefined;
return (
<>
{davor && <span className={rest}>{davor}</span>}
<span className="grad-text">{betont}</span>
{danach && <span className={rest}>{danach}</span>}
</>
);
}
+1 -1
View File
@@ -162,7 +162,7 @@ export default function EmbedComposer({ channelOptions, flash }) {
return (
<div className="settings-section">
<h2 className="settings-title">// Post verfassen</h2>
<h2 className="settings-title">Post verfassen</h2>
<div className="field">
<div className="field-row">
+40 -36
View File
@@ -44,7 +44,7 @@ export default {
'common.monthsAgo': 'vor %s Monaten',
// ── Startseite (Hub) ──────────────────────────────
'home.tag': '// Community-Hub',
'home.tag': 'Community-Hub',
'home.welcome': 'Willkommen',
'home.at': 'bei',
'home.subtitle': 'Devlogs, Roadmap, Server-Status und alles aus der Community — an einem Ort, direkt aus dem Discord.',
@@ -53,8 +53,8 @@ export default {
'home.members': 'Member im Discord',
'home.serversOnline': 'Server online',
'home.playersNow': 'Spieler gerade drauf',
'home.services': '// Dienste',
'home.latestDevlog': '// Neuestes Devlog',
'home.services': 'Dienste',
'home.latestDevlog': 'Neuestes Devlog',
'home.more': 'Weiterlesen',
'home.tile.devlog': 'Entwicklungs-Updates, frisch aus dem Editor',
'home.tile.roadmap': 'Meilensteine + Community-Wünsche zum Abstimmen',
@@ -64,7 +64,7 @@ export default {
'home.tile.events': 'Playtests, Streams und was sonst ansteht',
'nav.status': 'Status',
'status.tag': '// Betriebslage',
'status.tag': 'Betriebslage',
'status.subtitle': 'Erreichbarkeit der Dienste und Game-Server — alle 60 Sekunden aktualisiert.',
'status.error': 'Der Status konnte nicht geladen werden.',
'status.allOk': 'Alle Systeme betriebsbereit',
@@ -72,9 +72,9 @@ export default {
'status.majorOutage': 'Größere Störung',
'status.waiting': 'Wird geprüft',
'status.nothing': 'Nichts eingerichtet',
'status.services': '// Dienste',
'status.gameservers': '// Game-Server',
'status.incidents': '// Störungen der letzten 90 Tage',
'status.services': 'Dienste',
'status.gameservers': 'Game-Server',
'status.incidents': 'Störungen der letzten 90 Tage',
'status.noIncidents': 'Keine Störungen aufgezeichnet.',
'status.ok': 'erreichbar',
'status.down': 'nicht erreichbar',
@@ -82,7 +82,7 @@ export default {
'status.minutes': '%s min',
'status.days90': 'vor 90 Tagen',
// ── Devlogs ───────────────────────────────────────
'devlogs.tag': '// Archiv',
'devlogs.tag': 'Archiv',
'devlogs.subtitle': 'Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.',
'devlogs.all': 'Alle',
'devlogs.search': 'Devlogs durchsuchen …',
@@ -98,7 +98,7 @@ export default {
'devlogs.screenshot': 'Devlog-Screenshot',
// ── Devlog-Permalink ──────────────────────────────
'devlog.tag': '// Devlog',
'devlog.tag': 'Devlog',
'devlog.project': 'Projekt: %s',
'devlog.notFound': 'Devlog nicht gefunden —',
'devlog.backToArchive': 'zurück zum Archiv',
@@ -107,17 +107,17 @@ export default {
'devlog.linkCopied': 'Link kopiert',
// ── Roadmap ───────────────────────────────────────
'roadmap.tag': '// Wohin die Reise geht',
'roadmap.tag': 'Wohin die Reise geht',
'roadmap.subtitle': 'Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.',
'roadmap.error': 'Roadmap konnte nicht geladen werden.',
'roadmap.noMilestones': 'Noch keine Meilensteine angelegt.',
'roadmap.active': '// In Arbeit',
'roadmap.planned': '// Geplant',
'roadmap.done': '// Fertig',
'roadmap.active': 'In Arbeit',
'roadmap.planned': 'Geplant',
'roadmap.done': 'Fertig',
'roadmap.nothingActive': 'Gerade nichts in Arbeit.',
'roadmap.due': 'Ziel: %s',
'roadmap.tasks': '%s / %s Aufgaben',
'roadmap.activity': '// Aktivität — %s Commits in 12 Monaten',
'roadmap.activity': 'Aktivität — %s Commits in 12 Monaten',
'roadmap.commitsOn': '%s: %s Commits',
'roadmap.commitOn': '%s: 1 Commit',
'roadmap.sort': 'Sortierung',
@@ -127,13 +127,13 @@ export default {
'roadmap.status': 'Stand',
'roadmap.area': 'Bereich',
'roadmap.areaPick': '— Bereich —',
'wunsch.tag': '// Idee',
'wunsch.tag': 'Idee',
'wunsch.back': 'Zurück zur Roadmap',
'wunsch.notFound': 'Diese Idee gibt es nicht (mehr).',
'wunsch.by': 'von %s',
'wunsch.teamSays': 'Vom Team',
'wunsch.comments': '// %s Beiträge',
'wunsch.oneComment': '// 1 Beitrag',
'wunsch.comments': '%s Beiträge',
'wunsch.oneComment': '1 Beitrag',
'wunsch.noComments': 'Noch nichts dazu geschrieben — mach den Anfang im Thread.',
'wunsch.joinThread': 'Im Discord mitreden',
'wish.alle': 'Alle',
@@ -144,7 +144,7 @@ export default {
'wish.abgelehnt': 'Nicht geplant',
'roadmap.noneInFilter': 'Hier steht gerade nichts.',
'roadmap.alreadyThere': 'Gibt es schon — stimm lieber dafür:',
'roadmap.wishes': '// Community-Wünsche',
'roadmap.wishes': 'Community-Wünsche',
'roadmap.unvote': 'Vote zurücknehmen',
'roadmap.vote': 'Dafür stimmen',
'roadmap.ideaPlaceholder': 'Deine Idee für den Server …',
@@ -157,7 +157,7 @@ export default {
'roadmap.submitFailed': '✗ Konnte den Wunsch nicht einreichen.',
// ── Server ────────────────────────────────────────
'server.tag': '// Live-Status',
'server.tag': 'Live-Status',
'server.subtitle': 'Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.',
'server.error': 'Server-Status konnte nicht geladen werden.',
'server.none': 'Noch keine Server eingetragen.',
@@ -166,7 +166,7 @@ export default {
'server.waiting': 'Warte auf Check',
'server.players24h': 'Spielerzahl · letzte 24h',
// ── Mods eines LS-Servers ─────────────────────────
'mods.tag': '// Modliste',
'mods.tag': 'Modliste',
'mods.title': 'Mods',
'mods.subtitle': 'Alle Mods dieses Servers einzeln zum Laden — statt eines Pakets über alles.',
'mods.error': 'Modliste konnte nicht geladen werden.',
@@ -207,7 +207,7 @@ export default {
'server.modsDownload': 'Mods laden',
// ── Galerie ───────────────────────────────────────
'gallery.tag': '// Community',
'gallery.tag': 'Community',
'gallery.subtitle': 'Screenshots aus der Community — direkt aus dem Discord.',
'gallery.error': 'Galerie konnte nicht geladen werden.',
'gallery.emptyTitle': 'Noch keine Screenshots',
@@ -215,17 +215,17 @@ export default {
'gallery.shotBy': 'Screenshot von %s',
// ── Level ─────────────────────────────────────────
'level.tag': '// Bestenliste',
'level.tag': 'Bestenliste',
'level.subtitle': 'XP gibts fürs Mitreden im Discord — hier stehen die Aktivsten. %s zeigt deinen Platz.',
'level.error': 'Bestenliste konnte nicht geladen werden.',
'level.emptyTitle': 'Noch keine XP vergeben',
'level.emptyText': 'XP gibt es fürs Mitreden im Discord. Sobald geschrieben wird, füllt sich die Bestenliste von selbst.',
'level.lv': 'Lv. %s',
'level.activity': '// Server-Aktivität — %s Nachrichten in 30 Tagen · %s Member',
'level.activity': 'Server-Aktivität — %s Nachrichten in 30 Tagen · %s Member',
'level.barTitle': '%s: %s Nachrichten · +%s/%s Member',
// ── Events ────────────────────────────────────────
'events.tag': '// Termine',
'events.tag': 'Termine',
'events.subtitle': 'Playtests, Streams & Community-Abende — direkt aus dem Discord.',
'events.error': 'Events konnten nicht geladen werden.',
'events.emptyTitle': 'Noch keine Events geplant',
@@ -235,7 +235,7 @@ export default {
'events.viewInDiscord': 'Im Discord ansehen',
// ── Changelog ─────────────────────────────────────
'changelog.tag': '// Releases',
'changelog.tag': 'Releases',
'changelog.subtitle': 'Alle Versionen und Release-Notes.',
'changelog.error': 'Changelog konnte nicht geladen werden.',
'changelog.emptyTitle': 'Noch keine Releases',
@@ -245,18 +245,21 @@ export default {
'changelog.viewInGitea': 'Release in Gitea ansehen',
// ── Bot-Produktseite: Start ───────────────────────
'bot.tag': '// Selbst gebaut, keine Paywall',
'bot.tag': 'Selbst gebaut · keine Paywall',
'bot.title1': 'Ein Discord-Bot,',
'bot.title2': 'der alles macht',
'bot.title2': 'der alles [macht]',
'bot.subtitle': 'Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets — was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem, mit eigenem Webinterface.',
'bot.allFeatures': 'Alle Funktionen',
'bot.viewCommands': 'Befehle ansehen',
'bot.statFeatures': 'Funktionen, einzeln schaltbar',
'bot.statFeatures': 'Funktionen',
'bot.statCommands': 'Slash-Befehle',
'bot.statGames': 'Spiele im Monitor',
'bot.statCost': 'Kosten, keine Premium-Stufen',
'bot.whatItDoes': '// Was er kann',
'bot.forDevs': '// Für Entwickler',
'bot.statCost': 'Kosten',
'bot.online': 'Online',
'bot.hosting': 'Hosting',
'bot.modulesMeta': '%s Module · einzeln schaltbar',
'bot.whatItDoes': 'Was er kann',
'bot.forDevs': 'Für Entwickler',
'bot.runsFor': 'Der Bot läuft für die D4RKST3R-Community',
'bot.runsForWith': 'Der Bot läuft für die D4RKST3R-Community mit %s Mitgliedern',
'bot.toHub': 'zum Community-Hub',
@@ -285,11 +288,12 @@ export default {
'bot.dev.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.',
// ── Bot-Produktseite: Funktionen ──────────────────
'features.tag': '// Alles drin, nichts hinter einer Paywall',
'features.title': 'Funktionen',
'features.tag': 'Alles drin, nichts hinter einer Paywall',
'features.title': 'Funk[tionen]',
'features.subtitleN': '%s Funktionen — jede einzeln ein- und ausschaltbar, jede im Browser einstellbar.',
'features.subtitle': 'Jede Funktion einzeln ein- und ausschaltbar, jede im Browser einstellbar.',
'features.extras': '// Drumherum',
'features.count': '%s Module',
'features.extras': 'Drumherum',
'features.outro': 'Gebaut, weil drei Bots mit je eigenem Abo für eine Community weder übersichtlich noch günstig sind. Der Quelltext liegt offen, der Betrieb kostet einen Container.',
'features.overview': 'Zur Übersicht',
'features.x.brand': 'Webinterface im eigenen Look',
@@ -306,8 +310,8 @@ export default {
'features.x.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.',
// ── Bot-Produktseite: Befehle ─────────────────────
'commands.tag': '// Referenz',
'commands.title': 'Befehle',
'commands.tag': 'Referenz',
'commands.title': 'Be[fehle]',
'commands.subtitle': 'Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe direkt in den Nachrichten.',
'commands.note': 'Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden Discord-Rechten vorbehalten.',
'commands.g.everyone': 'Für alle',
+40 -36
View File
@@ -43,7 +43,7 @@ export default {
'common.monthsAgo': '%s months ago',
// ── Home (hub) ────────────────────────────────────
'home.tag': '// Community hub',
'home.tag': 'Community hub',
'home.welcome': 'Welcome',
'home.at': 'to',
'home.subtitle': 'Devlogs, roadmap, server status and everything from the community — in one place, straight out of Discord.',
@@ -52,8 +52,8 @@ export default {
'home.members': 'members on Discord',
'home.serversOnline': 'servers online',
'home.playersNow': 'players right now',
'home.services': '// Services',
'home.latestDevlog': '// Latest devlog',
'home.services': 'Services',
'home.latestDevlog': 'Latest devlog',
'home.more': 'Read more',
'home.tile.devlog': 'Development updates, fresh from the editor',
'home.tile.roadmap': 'Milestones plus community wishes to vote on',
@@ -63,7 +63,7 @@ export default {
'home.tile.events': 'Playtests, streams and whatever else is coming up',
'nav.status': 'Status',
'status.tag': '// Operational status',
'status.tag': 'Operational status',
'status.subtitle': 'Reachability of services and game servers — refreshed every 60 seconds.',
'status.error': 'The status could not be loaded.',
'status.allOk': 'All systems operational',
@@ -71,9 +71,9 @@ export default {
'status.majorOutage': 'Major outage',
'status.waiting': 'Checking',
'status.nothing': 'Nothing set up',
'status.services': '// Services',
'status.gameservers': '// Game servers',
'status.incidents': '// Incidents in the last 90 days',
'status.services': 'Services',
'status.gameservers': 'Game servers',
'status.incidents': 'Incidents in the last 90 days',
'status.noIncidents': 'No incidents recorded.',
'status.ok': 'operational',
'status.down': 'down',
@@ -81,7 +81,7 @@ export default {
'status.minutes': '%s min',
'status.days90': '90 days ago',
// ── Devlogs ───────────────────────────────────────
'devlogs.tag': '// Archive',
'devlogs.tag': 'Archive',
'devlogs.subtitle': 'Development updates, fresh from the editor — archived automatically.',
'devlogs.all': 'All',
'devlogs.search': 'Search devlogs …',
@@ -97,7 +97,7 @@ export default {
'devlogs.screenshot': 'Devlog screenshot',
// ── Devlog permalink ──────────────────────────────
'devlog.tag': '// Devlog',
'devlog.tag': 'Devlog',
'devlog.project': 'Project: %s',
'devlog.notFound': 'Devlog not found —',
'devlog.backToArchive': 'back to the archive',
@@ -106,17 +106,17 @@ export default {
'devlog.linkCopied': 'Link copied',
// ── Roadmap ───────────────────────────────────────
'roadmap.tag': '// Where this is heading',
'roadmap.tag': 'Where this is heading',
'roadmap.subtitle': 'Milestones straight from development — live from the issue tracker.',
'roadmap.error': 'The roadmap could not be loaded.',
'roadmap.noMilestones': 'No milestones yet.',
'roadmap.active': '// In progress',
'roadmap.planned': '// Planned',
'roadmap.done': '// Done',
'roadmap.active': 'In progress',
'roadmap.planned': 'Planned',
'roadmap.done': 'Done',
'roadmap.nothingActive': 'Nothing in progress right now.',
'roadmap.due': 'Due: %s',
'roadmap.tasks': '%s / %s tasks',
'roadmap.activity': '// Activity — %s commits in 12 months',
'roadmap.activity': 'Activity — %s commits in 12 months',
'roadmap.commitsOn': '%s: %s commits',
'roadmap.commitOn': '%s: 1 commit',
'roadmap.sort': 'Sort',
@@ -126,13 +126,13 @@ export default {
'roadmap.status': 'Status',
'roadmap.area': 'Area',
'roadmap.areaPick': '— Area —',
'wunsch.tag': '// Idea',
'wunsch.tag': 'Idea',
'wunsch.back': 'Back to the roadmap',
'wunsch.notFound': 'This idea does not exist (any more).',
'wunsch.by': 'by %s',
'wunsch.teamSays': 'From the team',
'wunsch.comments': '// %s comments',
'wunsch.oneComment': '// 1 comment',
'wunsch.comments': '%s comments',
'wunsch.oneComment': '1 comment',
'wunsch.noComments': 'Nothing said about this yet — start it off in the thread.',
'wunsch.joinThread': 'Join the thread on Discord',
'wish.alle': 'All',
@@ -143,7 +143,7 @@ export default {
'wish.abgelehnt': 'Not planned',
'roadmap.noneInFilter': 'Nothing here right now.',
'roadmap.alreadyThere': 'Already suggested — vote for it instead:',
'roadmap.wishes': '// Community wishes',
'roadmap.wishes': 'Community wishes',
'roadmap.unvote': 'Take back your vote',
'roadmap.vote': 'Vote for this',
'roadmap.ideaPlaceholder': 'Your idea for the server …',
@@ -156,7 +156,7 @@ export default {
'roadmap.submitFailed': '✗ Could not submit the wish.',
// ── Servers ───────────────────────────────────────
'server.tag': '// Live status',
'server.tag': 'Live status',
'server.subtitle': 'Every game server at a glance — refreshed every 2 minutes.',
'server.error': 'The server status could not be loaded.',
'server.none': 'No servers added yet.',
@@ -165,7 +165,7 @@ export default {
'server.waiting': 'Waiting for check',
'server.players24h': 'Player count · last 24h',
// ── Mods of an FS server ──────────────────────────
'mods.tag': '// Mod list',
'mods.tag': 'Mod list',
'mods.title': 'Mods',
'mods.subtitle': 'Every mod on this server, one by one — instead of one big bundle.',
'mods.error': 'Could not load the mod list.',
@@ -206,7 +206,7 @@ export default {
'server.modsDownload': 'Get mods',
// ── Gallery ───────────────────────────────────────
'gallery.tag': '// Community',
'gallery.tag': 'Community',
'gallery.subtitle': 'Screenshots from the community — straight out of Discord.',
'gallery.error': 'The gallery could not be loaded.',
'gallery.emptyTitle': 'No screenshots yet',
@@ -214,17 +214,17 @@ export default {
'gallery.shotBy': 'Screenshot by %s',
// ── Levels ────────────────────────────────────────
'level.tag': '// Leaderboard',
'level.tag': 'Leaderboard',
'level.subtitle': 'XP comes from joining the conversation on Discord — here are the most active. %s shows your rank.',
'level.error': 'The leaderboard could not be loaded.',
'level.emptyTitle': 'No XP awarded yet',
'level.emptyText': 'XP comes from joining the conversation on Discord. As soon as people start writing, the leaderboard fills itself.',
'level.lv': 'Lv. %s',
'level.activity': '// Server activity — %s messages in 30 days · %s members',
'level.activity': 'Server activity — %s messages in 30 days · %s members',
'level.barTitle': '%s: %s messages · +%s/%s members',
// ── Events ────────────────────────────────────────
'events.tag': '// Dates',
'events.tag': 'Dates',
'events.subtitle': 'Playtests, streams & community nights — straight out of Discord.',
'events.error': 'Events could not be loaded.',
'events.emptyTitle': 'No events planned yet',
@@ -234,7 +234,7 @@ export default {
'events.viewInDiscord': 'View on Discord',
// ── Changelog ─────────────────────────────────────
'changelog.tag': '// Releases',
'changelog.tag': 'Releases',
'changelog.subtitle': 'Every version and its release notes.',
'changelog.error': 'The changelog could not be loaded.',
'changelog.emptyTitle': 'No releases yet',
@@ -244,18 +244,21 @@ export default {
'changelog.viewInGitea': 'View release on Gitea',
// ── Bot product page: landing ─────────────────────
'bot.tag': '// Self-built, no paywall',
'bot.tag': 'Self-built · no paywall',
'bot.title1': 'A Discord bot',
'bot.title2': 'that does it all',
'bot.title2': 'that does it [all]',
'bot.subtitle': 'Devlogs, levels, moderation, server monitoring, giveaways, tickets — what usually takes three subscription bots runs here in one, with its own web interface.',
'bot.allFeatures': 'All features',
'bot.viewCommands': 'View commands',
'bot.statFeatures': 'features, each one switchable',
'bot.statFeatures': 'features',
'bot.statCommands': 'slash commands',
'bot.statGames': 'games in the monitor',
'bot.statCost': 'cost, no premium tiers',
'bot.whatItDoes': '// What it does',
'bot.forDevs': '// For developers',
'bot.statCost': 'cost',
'bot.online': 'Online',
'bot.hosting': 'Hosting',
'bot.modulesMeta': '%s modules · individually switchable',
'bot.whatItDoes': 'What it does',
'bot.forDevs': 'For developers',
'bot.runsFor': 'The bot runs for the D4RKST3R community',
'bot.runsForWith': 'The bot runs for the D4RKST3R community with %s members',
'bot.toHub': 'to the community hub',
@@ -284,11 +287,12 @@ export default {
'bot.dev.host.text': 'Node and SQLite in one container, one volume for the data. Nightly backups included — of the database and every repository.',
// ── Bot product page: features ────────────────────
'features.tag': '// Everything included, nothing behind a paywall',
'features.title': 'Features',
'features.tag': 'Everything included, nothing behind a paywall',
'features.title': 'Fea[tures]',
'features.subtitleN': '%s features — each one switchable, each one configurable in the browser.',
'features.subtitle': 'Every feature switchable on its own, every one configurable in the browser.',
'features.extras': '// Around it',
'features.count': '%s modules',
'features.extras': 'Around it',
'features.outro': 'Built because three bots with three subscriptions for one community is neither tidy nor cheap. The source is open, running it costs one container.',
'features.overview': 'Back to the overview',
'features.x.brand': 'Web interface in your own look',
@@ -395,8 +399,8 @@ export default {
'feat.member_gate.desc': 'Only people on the Discord server can sign in on the web.',
// ── Bot product page: commands ────────────────────
'commands.tag': '// Reference',
'commands.title': 'Commands',
'commands.tag': 'Reference',
'commands.title': 'Com[mands]',
'commands.subtitle': 'Every slash command at a glance. A lot also runs through buttons right inside the messages.',
'commands.note': 'Moderation and setup commands are reserved for members with the matching Discord permissions.',
'commands.g.everyone': 'For everyone',
+6 -5
View File
@@ -5,11 +5,12 @@ import App from './App.jsx';
import BotApp from './BotApp.jsx';
import { IS_BOT } from './site.js';
import { LangProvider } from './i18n.jsx';
import '@fontsource/bebas-neue';
import '@fontsource/barlow-condensed/300.css';
import '@fontsource/barlow-condensed/400.css';
import '@fontsource/barlow-condensed/600.css';
import '@fontsource/share-tech-mono';
import '@fontsource/chakra-petch/400.css';
import '@fontsource/chakra-petch/500.css';
import '@fontsource/chakra-petch/600.css';
import '@fontsource/chakra-petch/700.css';
import '@fontsource/ibm-plex-mono/400.css';
import '@fontsource/ibm-plex-mono/500.css';
import './style.css';
// Zwei Gesichter, eine Anwendung: Der Server sagt über data-site am <body>,
+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>
+851 -599
View File
File diff suppressed because it is too large Load Diff
+71 -53
View File
@@ -18,8 +18,8 @@ const js = (value) => JSON.stringify(String(value ?? ''));
export function registerBrandRoutes(app) {
app.get('/brand.css', async (request, reply) => {
const neon = getSetting('brand_color') || '#f5c518';
const neon2 = getSetting('brand_color2') || '#ff4d00';
const neon = getSetting('brand_color') || '#ffb340';
const neon2 = getSetting('brand_color2') || '#f07800';
return reply
.type('text/css; charset=utf-8')
@@ -27,88 +27,106 @@ export function registerBrandRoutes(app) {
.header('access-control-allow-origin', '*')
.send(`/* D4RKST3R Design-Tokens — erzeugt von ${publicUrl()} */
:root {
/* Leitfarbe: hell = Text und Symbole, dunkel = Ende des Verlaufs */
--accent-hi: ${neon};
--accent-lo: ${neon2};
--accent: ${neon};
--grad: linear-gradient(135deg, ${neon}, ${neon2});
/* Alte Namen bleiben als Zweitname stehen eingebundene Dienste
benutzen sie in ihrem eigenen CSS. */
--neon: ${neon};
--neon2: ${neon2};
--bg: #0a0a0a;
--bg2: #111;
--bg3: #161616;
--text: #e8e0d0;
--muted: #8a8378;
--muted2: #666;
--border: color-mix(in srgb, ${neon} 15%, transparent);
--success: #00ff88;
--bg: #131316;
--bg2: #1b1b20;
--bg3: #1f1f25;
--text: #ececee;
--text2: #c9c9cf;
--muted: #9a9aa3;
--muted2: #66666e;
--border: rgba(255,255,255,.08);
--success: #3ba55d;
--danger: #f23f43;
/* Schriften per @fontsource in der App installieren:
npm i @fontsource/bebas-neue @fontsource/barlow-condensed @fontsource/share-tech-mono */
--display: 'Bebas Neue', Impact, sans-serif;
--body: 'Barlow Condensed', system-ui, sans-serif;
--mono: 'Share Tech Mono', ui-monospace, monospace;
npm i @fontsource/chakra-petch @fontsource/ibm-plex-mono */
--display: 'Chakra Petch', system-ui, sans-serif;
--body: 'Chakra Petch', system-ui, sans-serif;
--mono: 'IBM Plex Mono', ui-monospace, monospace;
}
/* Abgeschrägte Ecken statt Rundungen — Tiefe über --bv */
.d4rk-bevel, .d4rk-card, .d4rk-btn {
clip-path: polygon(
var(--bv, 12px) 0, 100% 0,
100% calc(100% - var(--bv, 12px)), calc(100% - var(--bv, 12px)) 100%,
0 100%, 0 var(--bv, 12px)
);
}
.d4rk-body {
background: var(--bg); color: var(--text);
font-family: var(--body); font-size: 17px; line-height: 1.6;
font-family: var(--body); font-size: 16px; line-height: 1.55;
}
.d4rk-title {
font-family: var(--display); letter-spacing: .03em;
color: var(--text); line-height: .95;
font-family: var(--display); font-weight: 700;
letter-spacing: .02em; text-transform: uppercase;
color: var(--text); line-height: 1.02;
}
.d4rk-tag {
font-family: var(--mono); font-size: .7rem; letter-spacing: .3em;
text-transform: uppercase; color: var(--neon2);
font-family: var(--mono); font-size: .68rem; letter-spacing: .2em;
text-transform: uppercase; color: var(--accent-hi);
}
.d4rk-card {
background: var(--bg2); border: 1px solid rgba(255,255,255,.06);
border-radius: 12px; padding: 1.3rem 1.6rem;
transition: border-color .25s, transform .25s, box-shadow .25s;
background: var(--bg2); border: 1px solid var(--border);
padding: 1.3rem 1.6rem;
transition: border-color .2s, background .2s, transform .2s;
}
.d4rk-card:hover {
border-color: var(--border); transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0,0,0,.45);
border-color: color-mix(in srgb, ${neon} 50%, transparent);
background: var(--bg3); transform: translateY(-3px);
}
.d4rk-btn {
--bv: 10px;
display: inline-flex; align-items: center; gap: .5rem;
font-family: var(--mono); font-size: .7rem;
letter-spacing: .2em; text-transform: uppercase;
color: var(--neon); background: none;
border: 1px solid var(--border); border-radius: 999px;
padding: .5rem 1.2rem; cursor: pointer;
transition: background .2s, border-color .2s, box-shadow .2s;
font-family: var(--mono); font-size: .72rem; font-weight: 500;
letter-spacing: .14em; text-transform: uppercase;
color: var(--bg); background: var(--grad);
border: 0; padding: .55rem 1.3rem; cursor: pointer;
transition: filter .2s;
}
.d4rk-btn:hover {
border-color: var(--neon); background: color-mix(in srgb, var(--neon) 8%, transparent);
box-shadow: 0 0 16px color-mix(in srgb, var(--neon) 15%, transparent);
.d4rk-btn:hover { filter: brightness(1.1); }
/* Zweite Stufe: nur Kontur, für alles neben der Hauptaktion */
.d4rk-btn-ghost {
background: none; color: var(--text2);
border: 1px solid rgba(255,255,255,.18);
}
.d4rk-btn-ghost:hover { background: rgba(255,255,255,.06); color: var(--accent-hi); filter: none; }
/* Navbar, die brand-nav.js einfügt */
.d4rk-nav {
position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; gap: 1.6rem; flex-wrap: wrap;
padding: 1rem 2rem;
background: rgba(10,10,10,.9);
backdrop-filter: blur(14px) saturate(1.2);
-webkit-backdrop-filter: blur(14px) saturate(1.2);
display: flex; align-items: center; gap: 1.8rem; flex-wrap: wrap;
padding: .85rem 2rem;
background: rgba(19,19,22,.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
.d4rk-nav-logo {
font-family: var(--display); font-size: 1.5rem; letter-spacing: .15em;
color: var(--neon); text-decoration: none; line-height: 1;
font-family: var(--display); font-weight: 700;
font-size: 1.12rem; letter-spacing: .06em;
color: var(--text); text-decoration: none; line-height: 1;
}
.d4rk-nav-logo span { color: var(--neon2); }
.d4rk-nav-logo span { color: var(--accent); }
.d4rk-nav a:not(.d4rk-nav-logo) {
position: relative;
font-family: var(--mono); font-size: .7rem;
letter-spacing: .25em; text-transform: uppercase;
color: var(--muted); text-decoration: none; padding: .3rem 0;
transition: color .2s;
font-family: var(--mono); font-size: .72rem;
letter-spacing: .14em; text-transform: uppercase;
color: var(--muted); text-decoration: none;
border-bottom: 2px solid transparent; padding-bottom: .2rem;
transition: color .2s, border-color .2s;
}
.d4rk-nav a:not(.d4rk-nav-logo)::after {
content: ''; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
background: linear-gradient(90deg, var(--neon), var(--neon2)); border-radius: 2px;
transform: scaleX(0); transform-origin: left; transition: transform .25s ease;
.d4rk-nav a:not(.d4rk-nav-logo):hover {
color: var(--accent-hi); border-bottom-color: var(--accent);
}
.d4rk-nav a:not(.d4rk-nav-logo):hover { color: var(--text); }
.d4rk-nav a:not(.d4rk-nav-logo):hover::after { transform: scaleX(1); }
.d4rk-nav-spacer { flex: 1; }
@media (max-width: 640px) {
.d4rk-nav { padding: .8rem 1rem; gap: 1rem; }
@@ -149,7 +167,7 @@ export function registerBrandRoutes(app) {
a.href = link.url;
a.textContent = link.name;
// aktuelle Seite hervorheben
if (location.href.indexOf(link.url) === 0) a.style.color = 'var(--neon)';
if (location.href.indexOf(link.url) === 0) a.style.color = 'var(--accent-hi)';
nav.appendChild(a);
});
+2 -1
View File
@@ -196,7 +196,8 @@ export async function startWebServer(client) {
`<meta property="og:description" content="${esc(description)}">`,
`<meta property="og:url" content="${esc(base + path)}">`,
`<meta property="og:type" content="website">`,
`<meta name="theme-color" content="${isBotSite ? '#ff4d00' : '#f5c518'}">`,
// Beide Auftritte teilen sich seit dem Redesign eine Palette
`<meta name="theme-color" content="#131316">`,
`<meta name="description" content="${esc(description)}">`,
`<meta property="og:image" content="${esc(image)}">`,
`<meta name="twitter:card" content="summary_large_image">`,