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." /> <meta name="description" content="D4RKST3R — Devlog-Archiv, Changelog & Community." />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#0a0a0a" /> <meta name="theme-color" content="#131316" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+8 -18
View File
@@ -8,9 +8,8 @@
"name": "d4rkbot-frontend", "name": "d4rkbot-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fontsource/barlow-condensed": "^5.3.0", "@fontsource/chakra-petch": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
"lucide-react": "^1.28.0", "lucide-react": "^1.28.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -745,28 +744,19 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@fontsource/barlow-condensed": { "node_modules/@fontsource/chakra-petch": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/barlow-condensed/-/barlow-condensed-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource/chakra-petch/-/chakra-petch-5.3.0.tgz",
"integrity": "sha512-RA7MmIx0v3eQ6Gcf74qLVLOwLB0GpPOzx+eU9iG/AmCnSJGcIWYeU8y6CEMGZ210MGwGg5CZhzLkkvyZdTTk1g==", "integrity": "sha512-QZvbM/q2R6n1pg2x+TJPftZmBNS+ADU37L4Z4L/6UJIz+JZ/SRaEf8usPFRiP7Ht28sIideMu/Bm1SnGlSrwNw==",
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
} }
}, },
"node_modules/@fontsource/bebas-neue": { "node_modules/@fontsource/ibm-plex-mono": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/bebas-neue/-/bebas-neue-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
"integrity": "sha512-nCxMEb9Bg9sjNWm9TIOsvHKDMLRUfEZPWpLgYu6m9KavZZVSA/WoHzI5wROwqA+eNwEi/PNBAcB5Co7HA3S0Cw==", "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
"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==",
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
+2 -3
View File
@@ -9,9 +9,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fontsource/barlow-condensed": "^5.3.0", "@fontsource/chakra-petch": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
"lucide-react": "^1.28.0", "lucide-react": "^1.28.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^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 { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
import { apiGet } from './api.js'; import { apiGet } from './api.js';
import Devlogs from './pages/Devlogs.jsx'; 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() { export default function App() {
// Login-Status einmal beim Laden holen // Login-Status einmal beim Laden holen
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true }); const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
const [scrolled, setScrolled] = useState(false);
const [drawer, setDrawer] = useState(false); const [drawer, setDrawer] = useState(false);
const location = useLocation(); const location = useLocation();
const { t } = useT(); const { t } = useT();
@@ -116,18 +90,12 @@ export default function App() {
if (gated) apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {}); if (gated) apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {});
}, [gated]); }, [gated]);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return ( return (
<div className="app"> <div className="app">
<Particles /> {/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
<header className="nav">
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
<NavLink to="/" className="nav-logo"> <NavLink to="/" className="nav-logo">
<img className="nav-mark" src="/brand/mark.png" alt="" />
D4RKST<span>3R</span> D4RKST<span>3R</span>
</NavLink> </NavLink>
@@ -238,18 +206,21 @@ export default function App() {
</main> </main>
<footer className="footer"> <footer className="footer">
<span className="footer-logo">D4RKST<span>3R</span></span> <span className="footer-logo">
<span className="footer-sep">//</span> <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"> <a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
git.d4rkst3r.de git.d4rkst3r.de
</a> </a>
<span className="footer-sep">//</span>
<NavLink to="/status">{t('footer.status')}</NavLink> <NavLink to="/status">{t('footer.status')}</NavLink>
<NavLink to="/impressum">{t('footer.imprint')}</NavLink> <span className="footer-right">
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink> <NavLink to="/impressum">{t('footer.imprint')}</NavLink>
{pages.map((p) => ( <NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink> {pages.map((p) => (
))} <NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
))}
</span>
</footer> </footer>
</div> </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. // Das Dashboard ist dieselbe Config-Seite wie bisher, nur unter /dashboard.
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom'; import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
@@ -14,7 +14,6 @@ import { useT, LangSwitch } from './i18n.jsx';
export default function BotApp() { export default function BotApp() {
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true }); const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
const [scrolled, setScrolled] = useState(false);
const [drawer, setDrawer] = useState(false); const [drawer, setDrawer] = useState(false);
const [hub, setHub] = useState(null); const [hub, setHub] = useState(null);
// Seiten wie die Nutzungsbedingungen liegen auf dem Hub — verlinkt // 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(() => {}); 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; const canDashboard = me.admin || me.scopes?.length > 0;
return ( return (
<div className="app site-bot"> <div className="app">
<header className={`nav ${scrolled ? 'scrolled' : ''}`}> {/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
<header className="nav">
<NavLink to="/" className="nav-logo"> <NavLink to="/" className="nav-logo">
D4RKBOT<span>.</span> <img className="nav-mark" src="/brand/mark.png" alt="" />
D4RKBOT<span>_</span>
</NavLink> </NavLink>
<nav className="nav-links"> <nav className="nav-links">
@@ -105,18 +100,21 @@ export default function BotApp() {
</main> </main>
<footer className="footer"> <footer className="footer">
<span className="footer-logo">D4RKBOT<span>.</span></span> <span className="footer-logo">
<span className="footer-sep">//</span> <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"> <a href="https://git.d4rkst3r.de/D4rkst3r/d4rkbot" target="_blank" rel="noreferrer">
{t('footer.source')} {t('footer.source')}
</a> </a>
<span className="footer-sep">//</span>
{hub && <a href={`${hub}/status`}>{t('footer.status')}</a>} {hub && <a href={`${hub}/status`}>{t('footer.status')}</a>}
<NavLink to="/impressum">{t('footer.imprint')}</NavLink> <span className="footer-right">
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink> <NavLink to="/impressum">{t('footer.imprint')}</NavLink>
{hub && pages.map((p) => ( <NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a> {hub && pages.map((p) => (
))} <a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
))}
</span>
</footer> </footer>
</div> </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 ( return (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Post verfassen</h2> <h2 className="settings-title">Post verfassen</h2>
<div className="field"> <div className="field">
<div className="field-row"> <div className="field-row">
+40 -36
View File
@@ -44,7 +44,7 @@ export default {
'common.monthsAgo': 'vor %s Monaten', 'common.monthsAgo': 'vor %s Monaten',
// ── Startseite (Hub) ────────────────────────────── // ── Startseite (Hub) ──────────────────────────────
'home.tag': '// Community-Hub', 'home.tag': 'Community-Hub',
'home.welcome': 'Willkommen', 'home.welcome': 'Willkommen',
'home.at': 'bei', 'home.at': 'bei',
'home.subtitle': 'Devlogs, Roadmap, Server-Status und alles aus der Community — an einem Ort, direkt aus dem Discord.', '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.members': 'Member im Discord',
'home.serversOnline': 'Server online', 'home.serversOnline': 'Server online',
'home.playersNow': 'Spieler gerade drauf', 'home.playersNow': 'Spieler gerade drauf',
'home.services': '// Dienste', 'home.services': 'Dienste',
'home.latestDevlog': '// Neuestes Devlog', 'home.latestDevlog': 'Neuestes Devlog',
'home.more': 'Weiterlesen', 'home.more': 'Weiterlesen',
'home.tile.devlog': 'Entwicklungs-Updates, frisch aus dem Editor', 'home.tile.devlog': 'Entwicklungs-Updates, frisch aus dem Editor',
'home.tile.roadmap': 'Meilensteine + Community-Wünsche zum Abstimmen', 'home.tile.roadmap': 'Meilensteine + Community-Wünsche zum Abstimmen',
@@ -64,7 +64,7 @@ export default {
'home.tile.events': 'Playtests, Streams und was sonst ansteht', 'home.tile.events': 'Playtests, Streams und was sonst ansteht',
'nav.status': 'Status', 'nav.status': 'Status',
'status.tag': '// Betriebslage', 'status.tag': 'Betriebslage',
'status.subtitle': 'Erreichbarkeit der Dienste und Game-Server — alle 60 Sekunden aktualisiert.', 'status.subtitle': 'Erreichbarkeit der Dienste und Game-Server — alle 60 Sekunden aktualisiert.',
'status.error': 'Der Status konnte nicht geladen werden.', 'status.error': 'Der Status konnte nicht geladen werden.',
'status.allOk': 'Alle Systeme betriebsbereit', 'status.allOk': 'Alle Systeme betriebsbereit',
@@ -72,9 +72,9 @@ export default {
'status.majorOutage': 'Größere Störung', 'status.majorOutage': 'Größere Störung',
'status.waiting': 'Wird geprüft', 'status.waiting': 'Wird geprüft',
'status.nothing': 'Nichts eingerichtet', 'status.nothing': 'Nichts eingerichtet',
'status.services': '// Dienste', 'status.services': 'Dienste',
'status.gameservers': '// Game-Server', 'status.gameservers': 'Game-Server',
'status.incidents': '// Störungen der letzten 90 Tage', 'status.incidents': 'Störungen der letzten 90 Tage',
'status.noIncidents': 'Keine Störungen aufgezeichnet.', 'status.noIncidents': 'Keine Störungen aufgezeichnet.',
'status.ok': 'erreichbar', 'status.ok': 'erreichbar',
'status.down': 'nicht erreichbar', 'status.down': 'nicht erreichbar',
@@ -82,7 +82,7 @@ export default {
'status.minutes': '%s min', 'status.minutes': '%s min',
'status.days90': 'vor 90 Tagen', 'status.days90': 'vor 90 Tagen',
// ── Devlogs ─────────────────────────────────────── // ── Devlogs ───────────────────────────────────────
'devlogs.tag': '// Archiv', 'devlogs.tag': 'Archiv',
'devlogs.subtitle': 'Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.', 'devlogs.subtitle': 'Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.',
'devlogs.all': 'Alle', 'devlogs.all': 'Alle',
'devlogs.search': 'Devlogs durchsuchen …', 'devlogs.search': 'Devlogs durchsuchen …',
@@ -98,7 +98,7 @@ export default {
'devlogs.screenshot': 'Devlog-Screenshot', 'devlogs.screenshot': 'Devlog-Screenshot',
// ── Devlog-Permalink ────────────────────────────── // ── Devlog-Permalink ──────────────────────────────
'devlog.tag': '// Devlog', 'devlog.tag': 'Devlog',
'devlog.project': 'Projekt: %s', 'devlog.project': 'Projekt: %s',
'devlog.notFound': 'Devlog nicht gefunden —', 'devlog.notFound': 'Devlog nicht gefunden —',
'devlog.backToArchive': 'zurück zum Archiv', 'devlog.backToArchive': 'zurück zum Archiv',
@@ -107,17 +107,17 @@ export default {
'devlog.linkCopied': 'Link kopiert', 'devlog.linkCopied': 'Link kopiert',
// ── Roadmap ─────────────────────────────────────── // ── 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.subtitle': 'Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.',
'roadmap.error': 'Roadmap konnte nicht geladen werden.', 'roadmap.error': 'Roadmap konnte nicht geladen werden.',
'roadmap.noMilestones': 'Noch keine Meilensteine angelegt.', 'roadmap.noMilestones': 'Noch keine Meilensteine angelegt.',
'roadmap.active': '// In Arbeit', 'roadmap.active': 'In Arbeit',
'roadmap.planned': '// Geplant', 'roadmap.planned': 'Geplant',
'roadmap.done': '// Fertig', 'roadmap.done': 'Fertig',
'roadmap.nothingActive': 'Gerade nichts in Arbeit.', 'roadmap.nothingActive': 'Gerade nichts in Arbeit.',
'roadmap.due': 'Ziel: %s', 'roadmap.due': 'Ziel: %s',
'roadmap.tasks': '%s / %s Aufgaben', '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.commitsOn': '%s: %s Commits',
'roadmap.commitOn': '%s: 1 Commit', 'roadmap.commitOn': '%s: 1 Commit',
'roadmap.sort': 'Sortierung', 'roadmap.sort': 'Sortierung',
@@ -127,13 +127,13 @@ export default {
'roadmap.status': 'Stand', 'roadmap.status': 'Stand',
'roadmap.area': 'Bereich', 'roadmap.area': 'Bereich',
'roadmap.areaPick': '— Bereich —', 'roadmap.areaPick': '— Bereich —',
'wunsch.tag': '// Idee', 'wunsch.tag': 'Idee',
'wunsch.back': 'Zurück zur Roadmap', 'wunsch.back': 'Zurück zur Roadmap',
'wunsch.notFound': 'Diese Idee gibt es nicht (mehr).', 'wunsch.notFound': 'Diese Idee gibt es nicht (mehr).',
'wunsch.by': 'von %s', 'wunsch.by': 'von %s',
'wunsch.teamSays': 'Vom Team', 'wunsch.teamSays': 'Vom Team',
'wunsch.comments': '// %s Beiträge', 'wunsch.comments': '%s Beiträge',
'wunsch.oneComment': '// 1 Beitrag', 'wunsch.oneComment': '1 Beitrag',
'wunsch.noComments': 'Noch nichts dazu geschrieben — mach den Anfang im Thread.', 'wunsch.noComments': 'Noch nichts dazu geschrieben — mach den Anfang im Thread.',
'wunsch.joinThread': 'Im Discord mitreden', 'wunsch.joinThread': 'Im Discord mitreden',
'wish.alle': 'Alle', 'wish.alle': 'Alle',
@@ -144,7 +144,7 @@ export default {
'wish.abgelehnt': 'Nicht geplant', 'wish.abgelehnt': 'Nicht geplant',
'roadmap.noneInFilter': 'Hier steht gerade nichts.', 'roadmap.noneInFilter': 'Hier steht gerade nichts.',
'roadmap.alreadyThere': 'Gibt es schon — stimm lieber dafür:', '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.unvote': 'Vote zurücknehmen',
'roadmap.vote': 'Dafür stimmen', 'roadmap.vote': 'Dafür stimmen',
'roadmap.ideaPlaceholder': 'Deine Idee für den Server …', 'roadmap.ideaPlaceholder': 'Deine Idee für den Server …',
@@ -157,7 +157,7 @@ export default {
'roadmap.submitFailed': '✗ Konnte den Wunsch nicht einreichen.', 'roadmap.submitFailed': '✗ Konnte den Wunsch nicht einreichen.',
// ── Server ──────────────────────────────────────── // ── Server ────────────────────────────────────────
'server.tag': '// Live-Status', 'server.tag': 'Live-Status',
'server.subtitle': 'Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.', 'server.subtitle': 'Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.',
'server.error': 'Server-Status konnte nicht geladen werden.', 'server.error': 'Server-Status konnte nicht geladen werden.',
'server.none': 'Noch keine Server eingetragen.', 'server.none': 'Noch keine Server eingetragen.',
@@ -166,7 +166,7 @@ export default {
'server.waiting': 'Warte auf Check', 'server.waiting': 'Warte auf Check',
'server.players24h': 'Spielerzahl · letzte 24h', 'server.players24h': 'Spielerzahl · letzte 24h',
// ── Mods eines LS-Servers ───────────────────────── // ── Mods eines LS-Servers ─────────────────────────
'mods.tag': '// Modliste', 'mods.tag': 'Modliste',
'mods.title': 'Mods', 'mods.title': 'Mods',
'mods.subtitle': 'Alle Mods dieses Servers einzeln zum Laden — statt eines Pakets über alles.', 'mods.subtitle': 'Alle Mods dieses Servers einzeln zum Laden — statt eines Pakets über alles.',
'mods.error': 'Modliste konnte nicht geladen werden.', 'mods.error': 'Modliste konnte nicht geladen werden.',
@@ -207,7 +207,7 @@ export default {
'server.modsDownload': 'Mods laden', 'server.modsDownload': 'Mods laden',
// ── Galerie ─────────────────────────────────────── // ── Galerie ───────────────────────────────────────
'gallery.tag': '// Community', 'gallery.tag': 'Community',
'gallery.subtitle': 'Screenshots aus der Community — direkt aus dem Discord.', 'gallery.subtitle': 'Screenshots aus der Community — direkt aus dem Discord.',
'gallery.error': 'Galerie konnte nicht geladen werden.', 'gallery.error': 'Galerie konnte nicht geladen werden.',
'gallery.emptyTitle': 'Noch keine Screenshots', 'gallery.emptyTitle': 'Noch keine Screenshots',
@@ -215,17 +215,17 @@ export default {
'gallery.shotBy': 'Screenshot von %s', 'gallery.shotBy': 'Screenshot von %s',
// ── Level ───────────────────────────────────────── // ── 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.subtitle': 'XP gibts fürs Mitreden im Discord — hier stehen die Aktivsten. %s zeigt deinen Platz.',
'level.error': 'Bestenliste konnte nicht geladen werden.', 'level.error': 'Bestenliste konnte nicht geladen werden.',
'level.emptyTitle': 'Noch keine XP vergeben', '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.emptyText': 'XP gibt es fürs Mitreden im Discord. Sobald geschrieben wird, füllt sich die Bestenliste von selbst.',
'level.lv': 'Lv. %s', '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', 'level.barTitle': '%s: %s Nachrichten · +%s/%s Member',
// ── Events ──────────────────────────────────────── // ── Events ────────────────────────────────────────
'events.tag': '// Termine', 'events.tag': 'Termine',
'events.subtitle': 'Playtests, Streams & Community-Abende — direkt aus dem Discord.', 'events.subtitle': 'Playtests, Streams & Community-Abende — direkt aus dem Discord.',
'events.error': 'Events konnten nicht geladen werden.', 'events.error': 'Events konnten nicht geladen werden.',
'events.emptyTitle': 'Noch keine Events geplant', 'events.emptyTitle': 'Noch keine Events geplant',
@@ -235,7 +235,7 @@ export default {
'events.viewInDiscord': 'Im Discord ansehen', 'events.viewInDiscord': 'Im Discord ansehen',
// ── Changelog ───────────────────────────────────── // ── Changelog ─────────────────────────────────────
'changelog.tag': '// Releases', 'changelog.tag': 'Releases',
'changelog.subtitle': 'Alle Versionen und Release-Notes.', 'changelog.subtitle': 'Alle Versionen und Release-Notes.',
'changelog.error': 'Changelog konnte nicht geladen werden.', 'changelog.error': 'Changelog konnte nicht geladen werden.',
'changelog.emptyTitle': 'Noch keine Releases', 'changelog.emptyTitle': 'Noch keine Releases',
@@ -245,18 +245,21 @@ export default {
'changelog.viewInGitea': 'Release in Gitea ansehen', 'changelog.viewInGitea': 'Release in Gitea ansehen',
// ── Bot-Produktseite: Start ─────────────────────── // ── Bot-Produktseite: Start ───────────────────────
'bot.tag': '// Selbst gebaut, keine Paywall', 'bot.tag': 'Selbst gebaut · keine Paywall',
'bot.title1': 'Ein Discord-Bot,', '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.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.allFeatures': 'Alle Funktionen',
'bot.viewCommands': 'Befehle ansehen', 'bot.viewCommands': 'Befehle ansehen',
'bot.statFeatures': 'Funktionen, einzeln schaltbar', 'bot.statFeatures': 'Funktionen',
'bot.statCommands': 'Slash-Befehle', 'bot.statCommands': 'Slash-Befehle',
'bot.statGames': 'Spiele im Monitor', 'bot.statGames': 'Spiele im Monitor',
'bot.statCost': 'Kosten, keine Premium-Stufen', 'bot.statCost': 'Kosten',
'bot.whatItDoes': '// Was er kann', 'bot.online': 'Online',
'bot.forDevs': '// Für Entwickler', '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.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.runsForWith': 'Der Bot läuft für die D4RKST3R-Community mit %s Mitgliedern',
'bot.toHub': 'zum Community-Hub', '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.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 ────────────────── // ── Bot-Produktseite: Funktionen ──────────────────
'features.tag': '// Alles drin, nichts hinter einer Paywall', 'features.tag': 'Alles drin, nichts hinter einer Paywall',
'features.title': 'Funktionen', 'features.title': 'Funk[tionen]',
'features.subtitleN': '%s Funktionen — jede einzeln ein- und ausschaltbar, jede im Browser einstellbar.', '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.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.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.overview': 'Zur Übersicht',
'features.x.brand': 'Webinterface im eigenen Look', '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.', '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 ───────────────────── // ── Bot-Produktseite: Befehle ─────────────────────
'commands.tag': '// Referenz', 'commands.tag': 'Referenz',
'commands.title': 'Befehle', 'commands.title': 'Be[fehle]',
'commands.subtitle': 'Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe direkt in den Nachrichten.', '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.note': 'Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden Discord-Rechten vorbehalten.',
'commands.g.everyone': 'Für alle', 'commands.g.everyone': 'Für alle',
+40 -36
View File
@@ -43,7 +43,7 @@ export default {
'common.monthsAgo': '%s months ago', 'common.monthsAgo': '%s months ago',
// ── Home (hub) ──────────────────────────────────── // ── Home (hub) ────────────────────────────────────
'home.tag': '// Community hub', 'home.tag': 'Community hub',
'home.welcome': 'Welcome', 'home.welcome': 'Welcome',
'home.at': 'to', 'home.at': 'to',
'home.subtitle': 'Devlogs, roadmap, server status and everything from the community — in one place, straight out of Discord.', '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.members': 'members on Discord',
'home.serversOnline': 'servers online', 'home.serversOnline': 'servers online',
'home.playersNow': 'players right now', 'home.playersNow': 'players right now',
'home.services': '// Services', 'home.services': 'Services',
'home.latestDevlog': '// Latest devlog', 'home.latestDevlog': 'Latest devlog',
'home.more': 'Read more', 'home.more': 'Read more',
'home.tile.devlog': 'Development updates, fresh from the editor', 'home.tile.devlog': 'Development updates, fresh from the editor',
'home.tile.roadmap': 'Milestones plus community wishes to vote on', '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', 'home.tile.events': 'Playtests, streams and whatever else is coming up',
'nav.status': 'Status', 'nav.status': 'Status',
'status.tag': '// Operational status', 'status.tag': 'Operational status',
'status.subtitle': 'Reachability of services and game servers — refreshed every 60 seconds.', 'status.subtitle': 'Reachability of services and game servers — refreshed every 60 seconds.',
'status.error': 'The status could not be loaded.', 'status.error': 'The status could not be loaded.',
'status.allOk': 'All systems operational', 'status.allOk': 'All systems operational',
@@ -71,9 +71,9 @@ export default {
'status.majorOutage': 'Major outage', 'status.majorOutage': 'Major outage',
'status.waiting': 'Checking', 'status.waiting': 'Checking',
'status.nothing': 'Nothing set up', 'status.nothing': 'Nothing set up',
'status.services': '// Services', 'status.services': 'Services',
'status.gameservers': '// Game servers', 'status.gameservers': 'Game servers',
'status.incidents': '// Incidents in the last 90 days', 'status.incidents': 'Incidents in the last 90 days',
'status.noIncidents': 'No incidents recorded.', 'status.noIncidents': 'No incidents recorded.',
'status.ok': 'operational', 'status.ok': 'operational',
'status.down': 'down', 'status.down': 'down',
@@ -81,7 +81,7 @@ export default {
'status.minutes': '%s min', 'status.minutes': '%s min',
'status.days90': '90 days ago', 'status.days90': '90 days ago',
// ── Devlogs ─────────────────────────────────────── // ── Devlogs ───────────────────────────────────────
'devlogs.tag': '// Archive', 'devlogs.tag': 'Archive',
'devlogs.subtitle': 'Development updates, fresh from the editor — archived automatically.', 'devlogs.subtitle': 'Development updates, fresh from the editor — archived automatically.',
'devlogs.all': 'All', 'devlogs.all': 'All',
'devlogs.search': 'Search devlogs …', 'devlogs.search': 'Search devlogs …',
@@ -97,7 +97,7 @@ export default {
'devlogs.screenshot': 'Devlog screenshot', 'devlogs.screenshot': 'Devlog screenshot',
// ── Devlog permalink ────────────────────────────── // ── Devlog permalink ──────────────────────────────
'devlog.tag': '// Devlog', 'devlog.tag': 'Devlog',
'devlog.project': 'Project: %s', 'devlog.project': 'Project: %s',
'devlog.notFound': 'Devlog not found —', 'devlog.notFound': 'Devlog not found —',
'devlog.backToArchive': 'back to the archive', 'devlog.backToArchive': 'back to the archive',
@@ -106,17 +106,17 @@ export default {
'devlog.linkCopied': 'Link copied', 'devlog.linkCopied': 'Link copied',
// ── Roadmap ─────────────────────────────────────── // ── 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.subtitle': 'Milestones straight from development — live from the issue tracker.',
'roadmap.error': 'The roadmap could not be loaded.', 'roadmap.error': 'The roadmap could not be loaded.',
'roadmap.noMilestones': 'No milestones yet.', 'roadmap.noMilestones': 'No milestones yet.',
'roadmap.active': '// In progress', 'roadmap.active': 'In progress',
'roadmap.planned': '// Planned', 'roadmap.planned': 'Planned',
'roadmap.done': '// Done', 'roadmap.done': 'Done',
'roadmap.nothingActive': 'Nothing in progress right now.', 'roadmap.nothingActive': 'Nothing in progress right now.',
'roadmap.due': 'Due: %s', 'roadmap.due': 'Due: %s',
'roadmap.tasks': '%s / %s tasks', '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.commitsOn': '%s: %s commits',
'roadmap.commitOn': '%s: 1 commit', 'roadmap.commitOn': '%s: 1 commit',
'roadmap.sort': 'Sort', 'roadmap.sort': 'Sort',
@@ -126,13 +126,13 @@ export default {
'roadmap.status': 'Status', 'roadmap.status': 'Status',
'roadmap.area': 'Area', 'roadmap.area': 'Area',
'roadmap.areaPick': '— Area —', 'roadmap.areaPick': '— Area —',
'wunsch.tag': '// Idea', 'wunsch.tag': 'Idea',
'wunsch.back': 'Back to the roadmap', 'wunsch.back': 'Back to the roadmap',
'wunsch.notFound': 'This idea does not exist (any more).', 'wunsch.notFound': 'This idea does not exist (any more).',
'wunsch.by': 'by %s', 'wunsch.by': 'by %s',
'wunsch.teamSays': 'From the team', 'wunsch.teamSays': 'From the team',
'wunsch.comments': '// %s comments', 'wunsch.comments': '%s comments',
'wunsch.oneComment': '// 1 comment', 'wunsch.oneComment': '1 comment',
'wunsch.noComments': 'Nothing said about this yet — start it off in the thread.', 'wunsch.noComments': 'Nothing said about this yet — start it off in the thread.',
'wunsch.joinThread': 'Join the thread on Discord', 'wunsch.joinThread': 'Join the thread on Discord',
'wish.alle': 'All', 'wish.alle': 'All',
@@ -143,7 +143,7 @@ export default {
'wish.abgelehnt': 'Not planned', 'wish.abgelehnt': 'Not planned',
'roadmap.noneInFilter': 'Nothing here right now.', 'roadmap.noneInFilter': 'Nothing here right now.',
'roadmap.alreadyThere': 'Already suggested — vote for it instead:', 'roadmap.alreadyThere': 'Already suggested — vote for it instead:',
'roadmap.wishes': '// Community wishes', 'roadmap.wishes': 'Community wishes',
'roadmap.unvote': 'Take back your vote', 'roadmap.unvote': 'Take back your vote',
'roadmap.vote': 'Vote for this', 'roadmap.vote': 'Vote for this',
'roadmap.ideaPlaceholder': 'Your idea for the server …', 'roadmap.ideaPlaceholder': 'Your idea for the server …',
@@ -156,7 +156,7 @@ export default {
'roadmap.submitFailed': '✗ Could not submit the wish.', 'roadmap.submitFailed': '✗ Could not submit the wish.',
// ── Servers ─────────────────────────────────────── // ── Servers ───────────────────────────────────────
'server.tag': '// Live status', 'server.tag': 'Live status',
'server.subtitle': 'Every game server at a glance — refreshed every 2 minutes.', 'server.subtitle': 'Every game server at a glance — refreshed every 2 minutes.',
'server.error': 'The server status could not be loaded.', 'server.error': 'The server status could not be loaded.',
'server.none': 'No servers added yet.', 'server.none': 'No servers added yet.',
@@ -165,7 +165,7 @@ export default {
'server.waiting': 'Waiting for check', 'server.waiting': 'Waiting for check',
'server.players24h': 'Player count · last 24h', 'server.players24h': 'Player count · last 24h',
// ── Mods of an FS server ────────────────────────── // ── Mods of an FS server ──────────────────────────
'mods.tag': '// Mod list', 'mods.tag': 'Mod list',
'mods.title': 'Mods', 'mods.title': 'Mods',
'mods.subtitle': 'Every mod on this server, one by one — instead of one big bundle.', 'mods.subtitle': 'Every mod on this server, one by one — instead of one big bundle.',
'mods.error': 'Could not load the mod list.', 'mods.error': 'Could not load the mod list.',
@@ -206,7 +206,7 @@ export default {
'server.modsDownload': 'Get mods', 'server.modsDownload': 'Get mods',
// ── Gallery ─────────────────────────────────────── // ── Gallery ───────────────────────────────────────
'gallery.tag': '// Community', 'gallery.tag': 'Community',
'gallery.subtitle': 'Screenshots from the community — straight out of Discord.', 'gallery.subtitle': 'Screenshots from the community — straight out of Discord.',
'gallery.error': 'The gallery could not be loaded.', 'gallery.error': 'The gallery could not be loaded.',
'gallery.emptyTitle': 'No screenshots yet', 'gallery.emptyTitle': 'No screenshots yet',
@@ -214,17 +214,17 @@ export default {
'gallery.shotBy': 'Screenshot by %s', 'gallery.shotBy': 'Screenshot by %s',
// ── Levels ──────────────────────────────────────── // ── 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.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.error': 'The leaderboard could not be loaded.',
'level.emptyTitle': 'No XP awarded yet', '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.emptyText': 'XP comes from joining the conversation on Discord. As soon as people start writing, the leaderboard fills itself.',
'level.lv': 'Lv. %s', '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', 'level.barTitle': '%s: %s messages · +%s/%s members',
// ── Events ──────────────────────────────────────── // ── Events ────────────────────────────────────────
'events.tag': '// Dates', 'events.tag': 'Dates',
'events.subtitle': 'Playtests, streams & community nights — straight out of Discord.', 'events.subtitle': 'Playtests, streams & community nights — straight out of Discord.',
'events.error': 'Events could not be loaded.', 'events.error': 'Events could not be loaded.',
'events.emptyTitle': 'No events planned yet', 'events.emptyTitle': 'No events planned yet',
@@ -234,7 +234,7 @@ export default {
'events.viewInDiscord': 'View on Discord', 'events.viewInDiscord': 'View on Discord',
// ── Changelog ───────────────────────────────────── // ── Changelog ─────────────────────────────────────
'changelog.tag': '// Releases', 'changelog.tag': 'Releases',
'changelog.subtitle': 'Every version and its release notes.', 'changelog.subtitle': 'Every version and its release notes.',
'changelog.error': 'The changelog could not be loaded.', 'changelog.error': 'The changelog could not be loaded.',
'changelog.emptyTitle': 'No releases yet', 'changelog.emptyTitle': 'No releases yet',
@@ -244,18 +244,21 @@ export default {
'changelog.viewInGitea': 'View release on Gitea', 'changelog.viewInGitea': 'View release on Gitea',
// ── Bot product page: landing ───────────────────── // ── Bot product page: landing ─────────────────────
'bot.tag': '// Self-built, no paywall', 'bot.tag': 'Self-built · no paywall',
'bot.title1': 'A Discord bot', '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.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.allFeatures': 'All features',
'bot.viewCommands': 'View commands', 'bot.viewCommands': 'View commands',
'bot.statFeatures': 'features, each one switchable', 'bot.statFeatures': 'features',
'bot.statCommands': 'slash commands', 'bot.statCommands': 'slash commands',
'bot.statGames': 'games in the monitor', 'bot.statGames': 'games in the monitor',
'bot.statCost': 'cost, no premium tiers', 'bot.statCost': 'cost',
'bot.whatItDoes': '// What it does', 'bot.online': 'Online',
'bot.forDevs': '// For developers', '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.runsFor': 'The bot runs for the D4RKST3R community',
'bot.runsForWith': 'The bot runs for the D4RKST3R community with %s members', 'bot.runsForWith': 'The bot runs for the D4RKST3R community with %s members',
'bot.toHub': 'to the community hub', '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.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 ──────────────────── // ── Bot product page: features ────────────────────
'features.tag': '// Everything included, nothing behind a paywall', 'features.tag': 'Everything included, nothing behind a paywall',
'features.title': 'Features', 'features.title': 'Fea[tures]',
'features.subtitleN': '%s features — each one switchable, each one configurable in the browser.', '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.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.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.overview': 'Back to the overview',
'features.x.brand': 'Web interface in your own look', '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.', 'feat.member_gate.desc': 'Only people on the Discord server can sign in on the web.',
// ── Bot product page: commands ──────────────────── // ── Bot product page: commands ────────────────────
'commands.tag': '// Reference', 'commands.tag': 'Reference',
'commands.title': 'Commands', 'commands.title': 'Com[mands]',
'commands.subtitle': 'Every slash command at a glance. A lot also runs through buttons right inside the messages.', '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.note': 'Moderation and setup commands are reserved for members with the matching Discord permissions.',
'commands.g.everyone': 'For everyone', 'commands.g.everyone': 'For everyone',
+6 -5
View File
@@ -5,11 +5,12 @@ import App from './App.jsx';
import BotApp from './BotApp.jsx'; import BotApp from './BotApp.jsx';
import { IS_BOT } from './site.js'; import { IS_BOT } from './site.js';
import { LangProvider } from './i18n.jsx'; import { LangProvider } from './i18n.jsx';
import '@fontsource/bebas-neue'; import '@fontsource/chakra-petch/400.css';
import '@fontsource/barlow-condensed/300.css'; import '@fontsource/chakra-petch/500.css';
import '@fontsource/barlow-condensed/400.css'; import '@fontsource/chakra-petch/600.css';
import '@fontsource/barlow-condensed/600.css'; import '@fontsource/chakra-petch/700.css';
import '@fontsource/share-tech-mono'; import '@fontsource/ibm-plex-mono/400.css';
import '@fontsource/ibm-plex-mono/500.css';
import './style.css'; import './style.css';
// Zwei Gesichter, eine Anwendung: Der Server sagt über data-site am <body>, // 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. // Übersicht aller Slash-Befehle des Bots.
import { useEffect } from 'react'; import { useEffect } from 'react';
import { IconLock } from '../icons.jsx'; import { IconLock } from '../icons.jsx';
import { Accent } from '../components/Accent.jsx';
import { useT } from '../i18n.jsx'; import { useT } from '../i18n.jsx';
// Befehlsname bleibt wie er ist — nur die Erklärung wird übersetzt. // Befehlsname bleibt wie er ist — nur die Erklärung wird übersetzt.
@@ -40,11 +41,9 @@ export default function BotCommands() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('commands.tag')}</div> <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> <p className="page-subtitle">{t('commands.subtitle')}</p>
</div> </div>
</section> </section>
@@ -53,8 +52,12 @@ export default function BotCommands() {
{GROUPS.map(([groupKey, restricted, commands]) => ( {GROUPS.map(([groupKey, restricted, commands]) => (
<div className="roadmap-group" key={groupKey}> <div className="roadmap-group" key={groupKey}>
<h2 className="settings-title"> <h2 className="settings-title">
// {t(groupKey)} {/* Schloss gehört zum Titel, nicht als eigene
{restricted && <IconLock size={13} />} Spalte daneben — deshalb in einer Hülle */}
<span className="settings-title-text">
{t(groupKey)}
{restricted && <IconLock size={15} className="cmd-lock" />}
</span>
</h2> </h2>
<div className="cmd-list"> <div className="cmd-list">
{commands.map(([name, descKey]) => ( {commands.map(([name, descKey]) => (
+16 -11
View File
@@ -12,6 +12,7 @@ import {
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand, IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
IconBot, IconArrowRight, IconBot, IconArrowRight,
} from '../icons.jsx'; } from '../icons.jsx';
import { Accent } from '../components/Accent.jsx';
import { moduleIcon } from '../module-icons.jsx'; import { moduleIcon } from '../module-icons.jsx';
// Was kein Discord-Modul ist, sondern zum Drumherum gehört // Was kein Discord-Modul ist, sondern zum Drumherum gehört
@@ -38,11 +39,9 @@ export default function BotFeatures() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('features.tag')}</div> <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"> <p className="page-subtitle">
{total ? t('features.subtitleN', total) : t('features.subtitle')} {total ? t('features.subtitleN', total) : t('features.subtitle')}
</p> </p>
@@ -57,17 +56,20 @@ export default function BotFeatures() {
if (items.length === 0) return null; if (items.length === 0) return null;
return ( return (
<div className="feature-group" key={group.id}> <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"> <div className="feature-grid">
{items.map((f) => { {items.map((f) => {
const Icon = moduleIcon(f.id); const Icon = moduleIcon(f.id);
return ( return (
<div className="card feature-card" key={f.id}> <div className="card feature-card" key={f.id}>
<span className="feature-icon"><Icon size={20} /></span> <div className="feature-head">
<div> <span className="feature-icon"><Icon size={16} /></span>
<span className="feature-name">{tOr(`feat.${f.id}`, f.name)}</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> </div>
<p className="feature-desc">{tOr(`feat.${f.id}.desc`, f.desc)}</p>
</div> </div>
); );
})} })}
@@ -78,15 +80,18 @@ export default function BotFeatures() {
{data && ( {data && (
<div className="feature-group"> <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"> <div className="feature-grid">
{EXTRAS.map(([Icon, key]) => ( {EXTRAS.map(([Icon, key]) => (
<div className="card feature-card" key={key}> <div className="card feature-card" key={key}>
<span className="feature-icon"><Icon size={20} /></span> <div className="feature-head">
<div> <span className="feature-icon"><Icon size={16} /></span>
<span className="feature-name">{t(key)}</span> <span className="feature-name">{t(key)}</span>
<p className="feature-desc">{t(`${key}.text`)}</p>
</div> </div>
<p className="feature-desc">{t(`${key}.text`)}</p>
</div> </div>
))} ))}
</div> </div>
+63 -47
View File
@@ -4,9 +4,10 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { apiGet } from '../api.js'; import { apiGet } from '../api.js';
import { useT } from '../i18n.jsx'; import { useT } from '../i18n.jsx';
import { Accent } from '../components/Accent.jsx';
import { import {
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway, IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot, IconBirthday, IconShield, IconMagic, IconArrowRight, IconBot,
} from '../icons.jsx'; } from '../icons.jsx';
const HIGHLIGHTS = [ const HIGHLIGHTS = [
@@ -38,15 +39,14 @@ export default function BotLanding() {
return ( return (
<> <>
<section className="page-header bot-hero"> <section className="hero hero-bot">
<div className="page-bg-text">BOT</div> <div className="hero-text">
<div className="page-header-grid" aria-hidden="true" /> <div className="page-tag dot">{t('bot.tag')}</div>
<div className="page-header-content"> <h1 className="hero-title">
<div className="page-tag">{t('bot.tag')}</div> {t('bot.title1')}<br />
<h1 className="page-title"> <Accent text={t('bot.title2')} outline />
{t('bot.title1')}<br />{t('bot.title2')}
</h1> </h1>
<p className="page-subtitle">{t('bot.subtitle')}</p> <p className="hero-sub">{t('bot.subtitle')}</p>
<div className="home-cta"> <div className="home-cta">
<Link className="btn btn-save with-icon" to="/features"> <Link className="btn btn-save with-icon" to="/features">
{t('bot.allFeatures')} <IconArrowRight size={15} /> {t('bot.allFeatures')} <IconArrowRight size={15} />
@@ -56,63 +56,79 @@ export default function BotLanding() {
</Link> </Link>
</div> </div>
</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>
<section className="content"> <section className="content">
<div className="srv-summary"> <h2 className="settings-title">
{featureCount && ( {t('bot.whatItDoes')}
<div className="srv-sum-item"> {featureCount && <span className="section-meta">{t('bot.modulesMeta', featureCount)}</span>}
<span className="srv-sum-n">{featureCount}</span> </h2>
<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>
<div className="home-tiles"> <div className="home-tiles">
{HIGHLIGHTS.map(([Icon, key]) => ( {HIGHLIGHTS.map(([Icon, key]) => (
<div className="card home-tile" key={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-title">{t(key)}</span>
<span className="home-tile-desc">{t(`${key}.text`)}</span> <span className="home-tile-desc">{t(`${key}.text`)}</span>
</div> </div>
))} ))}
</div> </div>
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}> <div className="info-panels">
<h2 className="settings-title">{t('bot.forDevs')}</h2> <div className="info-panel">
<div className="home-tiles"> <span className="info-panel-tag">{t('bot.forDevs')}</span>
<div className="card home-tile"> <h3 className="info-panel-title">{t('bot.dev.api')}</h3>
<span className="home-tile-icon"><IconKey size={24} /></span> <p>{t('bot.dev.api.text')}</p>
<span className="home-tile-title">{t('bot.dev.api')}</span> </div>
<span className="home-tile-desc">{t('bot.dev.api.text')}</span> <div className="info-panel warm">
</div> <span className="info-panel-tag">{t('bot.hosting')}</span>
<div className="card home-tile"> <h3 className="info-panel-title">{t('bot.dev.host')}</h3>
<span className="home-tile-icon"><IconServer size={24} /></span> <p>{t('bot.dev.host.text')}</p>
<span className="home-tile-title">{t('bot.dev.host')}</span>
<span className="home-tile-desc">{t('bot.dev.host.text')}</span>
</div>
</div> </div>
</div> </div>
{hub && ( {hub && (
<p className="notice" style={{ marginTop: '2rem' }}> <p className="footnote">
{status?.members {status?.members
? t('bot.runsForWith', status.members) ? t('bot.runsForWith', status.members)
: t('bot.runsFor')} : t('bot.runsFor')}
{' '}<a href={hub} style={{ textDecoration: 'underline' }}>{t('bot.toHub')}</a>. {' '}<a href={hub}>{t('bot.toHub')}</a>.
</p> </p>
)} )}
</section> </section>
-2
View File
@@ -27,8 +27,6 @@ export default function Changelog() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('changelog.tag')}</div> <div className="page-tag">{t('changelog.tag')}</div>
<h1 className="page-title">{t('nav.changelog')}</h1> <h1 className="page-title">{t('nav.changelog')}</h1>
+1 -3
View File
@@ -20,10 +20,8 @@ export default function Commits({ me }) {
const header = ( const header = (
<section className="page-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-header-content">
<div className="page-tag">// Privat</div> <div className="page-tag">Privat</div>
<h1 className="page-title">Commits</h1> <h1 className="page-title">Commits</h1>
{data && <p className="page-subtitle">{data.total} archivierte Commits über alle Repos.</p>} {data && <p className="page-subtitle">{data.total} archivierte Commits über alle Repos.</p>}
</div> </div>
+12 -14
View File
@@ -17,10 +17,8 @@ export default function Datenschutz() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">// DSGVO</div> <div className="page-tag">DSGVO</div>
<h1 className="page-title">Datenschutz</h1> <h1 className="page-title">Datenschutz</h1>
<p className="page-subtitle"> <p className="page-subtitle">
Welche Daten hier verarbeitet werden, warum und wie du sie wieder loswirst. Welche Daten hier verarbeitet werden, warum und wie du sie wieder loswirst.
@@ -29,7 +27,7 @@ export default function Datenschutz() {
</section> </section>
<section className="content legal-text"> <section className="content legal-text">
<h2 className="settings-title">// 1. Verantwortlicher</h2> <h2 className="settings-title">1. Verantwortlicher</h2>
<p> <p>
Verantwortlich für die Datenverarbeitung auf dieser Webseite im Sinne der Verantwortlich für die Datenverarbeitung auf dieser Webseite im Sinne der
Datenschutz-Grundverordnung (DSGVO) ist:<br /><br /> 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></>} {legal.email && <>E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a></>}
</p> </p>
<h2 className="settings-title">// 2. Aufruf der Webseite (Server-Logs)</h2> <h2 className="settings-title">2. Aufruf der Webseite (Server-Logs)</h2>
<p> <p>
Beim Aufruf dieser Seite verarbeitet der Server technische Zugriffsdaten: Beim Aufruf dieser Seite verarbeitet der Server technische Zugriffsdaten:
IP-Adresse, Zeitpunkt der Anfrage, aufgerufene Adresse, HTTP-Statuscode und 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. zusammengeführt und nach kurzer Zeit im Rahmen der üblichen Rotation gelöscht.
</p> </p>
<h2 className="settings-title">// 3. Cookies</h2> <h2 className="settings-title">3. Cookies</h2>
<p> <p>
Diese Seite verwendet ausschließlich technisch notwendige Cookies. Konkret Diese Seite verwendet ausschließlich technisch notwendige Cookies. Konkret
wird nach einem Login ein signiertes Sitzungs-Cookie gesetzt, damit du wird nach einem Login ein signiertes Sitzungs-Cookie gesetzt, damit du
@@ -64,7 +62,7 @@ export default function Datenschutz() {
Verbindung zu Dritten. Verbindung zu Dritten.
</p> </p>
<h2 className="settings-title">// 4. Login mit Discord</h2> <h2 className="settings-title">4. Login mit Discord</h2>
<p> <p>
Für den Mitglieder-Bereich kannst du dich freiwillig mit deinem Für den Mitglieder-Bereich kannst du dich freiwillig mit deinem
Discord-Konto anmelden (OAuth2, Berechtigung <code>identify</code>). Dabei Discord-Konto anmelden (OAuth2, Berechtigung <code>identify</code>). Dabei
@@ -85,7 +83,7 @@ export default function Datenschutz() {
Datenschutzerklärung von Discord. Datenschutzerklärung von Discord.
</p> </p>
<h2 className="settings-title">// 5. Community-Funktionen</h2> <h2 className="settings-title">5. Community-Funktionen</h2>
<p> <p>
{brand} betreibt einen eigenen Discord-Bot, der zur Verwaltung der Community {brand} betreibt einen eigenen Discord-Bot, der zur Verwaltung der Community
Daten speichert. Verarbeitet werden je nachdem, welche Funktionen du nutzt: 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. am Betrieb einer funktionierenden Community) für den technischen Betrieb.
</p> </p>
<h2 className="settings-title">// 6. Moderation und Protokolle</h2> <h2 className="settings-title">6. Moderation und Protokolle</h2>
<p> <p>
Zur Durchsetzung der Serverregeln werden Verwarnungen mit Benutzer-ID, Grund Zur Durchsetzung der Serverregeln werden Verwarnungen mit Benutzer-ID, Grund
und Zeitpunkt gespeichert. Änderungen im Administrationsbereich der Webseite und Zeitpunkt gespeichert. Änderungen im Administrationsbereich der Webseite
@@ -142,7 +140,7 @@ export default function Datenschutz() {
Abs. 1 lit. f DSGVO. Abs. 1 lit. f DSGVO.
</p> </p>
<h2 className="settings-title">// 7. Speicherdauer</h2> <h2 className="settings-title">7. Speicherdauer</h2>
<p> <p>
Wir speichern personenbezogene Daten so lange, wie du Mitglied der Community Wir speichern personenbezogene Daten so lange, wie du Mitglied der Community
bist oder die jeweilige Funktion nutzt. Verlässt du den Discord-Server, kannst bist oder die jeweilige Funktion nutzt. Verlässt du den Discord-Server, kannst
@@ -152,14 +150,14 @@ export default function Datenschutz() {
werden. werden.
</p> </p>
<h2 className="settings-title">// 8. Weitergabe an Dritte</h2> <h2 className="settings-title">8. Weitergabe an Dritte</h2>
<p> <p>
Eine Weitergabe deiner Daten zu Werbezwecken findet nicht statt. Ein Verkauf Eine Weitergabe deiner Daten zu Werbezwecken findet nicht statt. Ein Verkauf
von Daten erfolgt nicht. Übermittlungen erfolgen nur an Discord, soweit dies von Daten erfolgt nicht. Übermittlungen erfolgen nur an Discord, soweit dies
für die Nutzung des Discord-Servers und des Logins technisch notwendig ist. für die Nutzung des Discord-Servers und des Logins technisch notwendig ist.
</p> </p>
<h2 className="settings-title">// 9. Deine Rechte</h2> <h2 className="settings-title">9. Deine Rechte</h2>
<p> <p>
Du hast jederzeit das Recht auf Auskunft über die zu deiner Person Du hast jederzeit das Recht auf Auskunft über die zu deiner Person
gespeicherten Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), auf Löschung 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'}. {' '}{legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : 'die im Impressum genannte Adresse'}.
</p> </p>
<h2 className="settings-title">// 10. Beschwerderecht</h2> <h2 className="settings-title">10. Beschwerderecht</h2>
<p> <p>
Wenn du der Ansicht bist, dass die Verarbeitung deiner Daten gegen die DSGVO Wenn du der Ansicht bist, dass die Verarbeitung deiner Daten gegen die DSGVO
verstößt, kannst du dich bei einer Datenschutz-Aufsichtsbehörde beschweren — verstößt, kannst du dich bei einer Datenschutz-Aufsichtsbehörde beschweren —
etwa bei der für deinen Wohnsitz zuständigen Landesdatenschutzbehörde. etwa bei der für deinen Wohnsitz zuständigen Landesdatenschutzbehörde.
</p> </p>
<h2 className="settings-title">// 11. Änderungen</h2> <h2 className="settings-title">11. Änderungen</h2>
<p> <p>
Wir passen diese Datenschutzerklärung an, wenn sich die angebotenen Funktionen Wir passen diese Datenschutzerklärung an, wenn sich die angebotenen Funktionen
ändern. Es gilt jeweils die hier veröffentlichte Fassung. ändern. Es gilt jeweils die hier veröffentlichte Fassung.
-2
View File
@@ -41,8 +41,6 @@ export default function DevlogDetail() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('devlog.tag')}</div> <div className="page-tag">{t('devlog.tag')}</div>
<h1 className="page-title">{item ? datum(item.posted_at, DATUM) : t('nav.devlog')}</h1> <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 ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('devlogs.tag')}</div> <div className="page-tag">{t('devlogs.tag')}</div>
<h1 className="page-title">{t('nav.devlog')}</h1> <h1 className="page-title">{t('nav.devlog')}</h1>
-2
View File
@@ -23,8 +23,6 @@ export default function Events() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('events.tag')}</div> <div className="page-tag">{t('events.tag')}</div>
<h1 className="page-title">{t('nav.events')}</h1> <h1 className="page-title">{t('nav.events')}</h1>
-2
View File
@@ -30,8 +30,6 @@ export default function Galerie() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('gallery.tag')}</div> <div className="page-tag">{t('gallery.tag')}</div>
<h1 className="page-title">{t('nav.gallery')}</h1> <h1 className="page-title">{t('nav.gallery')}</h1>
+38 -33
View File
@@ -51,56 +51,61 @@ export default function Home() {
return ( return (
<> <>
<section className="page-header home-hero"> <section className="hero hero-hub">
<div className="page-bg-text">D4RKST3R</div> <div className="hero-text">
<div className="page-header-grid" aria-hidden="true" /> <div className="page-tag dot">{t('home.tag')}</div>
<div className="page-header-content"> <h1 className="hero-title">
<div className="page-tag">{t('home.tag')}</div> {t('home.welcome')} {t('home.at')}<br />
<h1 className="page-title"> <span className="grad-text">D4RKST3R</span>
{t('home.welcome')}<br />{t('home.at')} D4RKST<span className="home-accent">3</span>R
</h1> </h1>
<p className="page-subtitle">{t('home.subtitle')}</p> <p className="hero-sub">{t('home.subtitle')}</p>
<div className="home-cta"> <div className="home-cta">
{invite && ( {invite && (
<a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer"> <a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer">
<IconMessage /> {t('home.discord')} <IconMessage /> {t('home.discord')}
</a> </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')} <IconDevlog /> {t('home.readDevlogs')}
</Link> </Link>
</div> </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> </div>
</section> </section>
<section className="content"> <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"> <div className="home-tiles">
{TILES.map(([to, Icon, titleKey, descKey]) => ( {TILES.map(([to, Icon, titleKey, descKey]) => (
<Link className="card home-tile" to={to} key={to}> <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-title">{t(titleKey)}</span>
<span className="home-tile-desc">{t(descKey)}</span> <span className="home-tile-desc">{t(descKey)}</span>
</Link> </Link>
+8 -10
View File
@@ -17,10 +17,8 @@ export default function Impressum() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">// Pflichtangaben</div> <div className="page-tag">Pflichtangaben</div>
<h1 className="page-title">Impressum</h1> <h1 className="page-title">Impressum</h1>
<p className="page-subtitle">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p> <p className="page-subtitle">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p>
</div> </div>
@@ -34,7 +32,7 @@ export default function Impressum() {
</p> </p>
)} )}
<h2 className="settings-title">// Diensteanbieter</h2> <h2 className="settings-title">Diensteanbieter</h2>
<p> <p>
{legal.name || '—'}<br /> {legal.name || '—'}<br />
{(legal.address || '').split('\n').map((line, i) => ( {(legal.address || '').split('\n').map((line, i) => (
@@ -42,25 +40,25 @@ export default function Impressum() {
))} ))}
</p> </p>
<h2 className="settings-title">// Kontakt</h2> <h2 className="settings-title">Kontakt</h2>
<p> <p>
E-Mail: {legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : '—'} E-Mail: {legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : '—'}
</p> </p>
{legal.extra && ( {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> <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> <p>
Verantwortlich für journalistisch-redaktionelle Inhalte nach § 18 Abs. 2 MStV: Verantwortlich für journalistisch-redaktionelle Inhalte nach § 18 Abs. 2 MStV:
{' '}{legal.name || '—'}{legal.address ? `, ${legal.address.replace(/\n/g, ', ')}` : ''} {' '}{legal.name || '—'}{legal.address ? `, ${legal.address.replace(/\n/g, ', ')}` : ''}
</p> </p>
<h2 className="settings-title">// Haftung für Inhalte</h2> <h2 className="settings-title">Haftung für Inhalte</h2>
<p> <p>
Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet, allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet,
@@ -73,7 +71,7 @@ export default function Impressum() {
entfernen wir diese Inhalte umgehend. entfernen wir diese Inhalte umgehend.
</p> </p>
<h2 className="settings-title">// Haftung für Links</h2> <h2 className="settings-title">Haftung für Links</h2>
<p> <p>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte 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 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. von Rechtsverletzungen entfernen wir derartige Links umgehend.
</p> </p>
<h2 className="settings-title">// Urheberrecht</h2> <h2 className="settings-title">Urheberrecht</h2>
<p> <p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
unterliegen dem deutschen Urheberrecht. Beiträge Dritter insbesondere unterliegen dem deutschen Urheberrecht. Beiträge Dritter insbesondere
-2
View File
@@ -60,8 +60,6 @@ export default function Level() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('level.tag')}</div> <div className="page-tag">{t('level.tag')}</div>
<h1 className="page-title">{t('nav.level')}</h1> <h1 className="page-title">{t('nav.level')}</h1>
-2
View File
@@ -49,8 +49,6 @@ export default function Mods() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('mods.tag')}</div> <div className="page-tag">{t('mods.tag')}</div>
<h1 className="page-title">{daten?.server?.name ?? t('mods.title')}</h1> <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 ( return (
<section className="page-header"> <section className="page-header">
<div className="page-header-content"> <div className="page-header-content">
<div className="page-tag">// Profil</div> <div className="page-tag">Profil</div>
<h1 className="page-title">Nicht eingeloggt</h1> <h1 className="page-title">Nicht eingeloggt</h1>
<p className="page-subtitle"> <p className="page-subtitle">
<a href="/auth/login" style={{ textDecoration: 'underline' }}>Mit Discord einloggen</a>, um dein Profil zu sehen. <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 ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">// Member</div> <div className="page-tag">Member</div>
<h1 className="page-title">Profil</h1> <h1 className="page-title">Profil</h1>
<p className="page-subtitle">Dein Stand auf dem Server Rang, Rollen und Keys.</p> <p className="page-subtitle">Dein Stand auf dem Server Rang, Rollen und Keys.</p>
</div> </div>
@@ -145,7 +143,7 @@ export default function Profil({ me }) {
{/* Alpha-Key */} {/* Alpha-Key */}
{profile.alphaKey && ( {profile.alphaKey && (
<div className="roadmap-group"> <div className="roadmap-group">
<h2 className="settings-title">// Dein Alpha-Key</h2> <h2 className="settings-title">Dein Alpha-Key</h2>
<div className="field-row"> <div className="field-row">
<code className="alpha-key">{profile.alphaKey}</code> <code className="alpha-key">{profile.alphaKey}</code>
<button className="btn btn-mini" onClick={copyKey}> <button className="btn btn-mini" onClick={copyKey}>
@@ -158,7 +156,7 @@ export default function Profil({ me }) {
{/* Rollen-Selfservice */} {/* Rollen-Selfservice */}
{menus.length > 0 && ( {menus.length > 0 && (
<div className="roadmap-group"> <div className="roadmap-group">
<h2 className="settings-title">// Rollen wählen</h2> <h2 className="settings-title">Rollen wählen</h2>
{menus.map((m) => ( {menus.map((m) => (
<div className="rolemenu-box" key={m.id}> <div className="rolemenu-box" key={m.id}>
<h3 className="rolemenu-title">{m.title}</h3> <h3 className="rolemenu-title">{m.title}</h3>
@@ -183,7 +181,7 @@ export default function Profil({ me }) {
{/* DSGVO Art. 17: eigene Daten löschen */} {/* DSGVO Art. 17: eigene Daten löschen */}
<div className="roadmap-group danger-zone"> <div className="roadmap-group danger-zone">
<h2 className="settings-title">// Meine Daten</h2> <h2 className="settings-title">Meine Daten</h2>
<p className="field-hint"> <p className="field-hint">
Du kannst jederzeit alle Daten löschen, die hier zu dir gespeichert Du kannst jederzeit alle Daten löschen, die hier zu dir gespeichert
sind: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen, sind: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen,
-2
View File
@@ -191,8 +191,6 @@ export default function Roadmap() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('roadmap.tag')}</div> <div className="page-tag">{t('roadmap.tag')}</div>
<h1 className="page-title">{t('nav.roadmap')}</h1> <h1 className="page-title">{t('nav.roadmap')}</h1>
+2 -4
View File
@@ -21,7 +21,7 @@ export default function Seite() {
return ( return (
<section className="page-header"> <section className="page-header">
<div className="page-header-content"> <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> <h1 className="page-title">Seite nicht gefunden</h1>
<p className="page-subtitle"> <p className="page-subtitle">
<Link to="/" style={{ textDecoration: 'underline' }}>Zurück zur Startseite</Link> <Link to="/" style={{ textDecoration: 'underline' }}>Zurück zur Startseite</Link>
@@ -34,10 +34,8 @@ export default function Seite() {
return ( return (
<> <>
<section className="page-header"> <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-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> <h1 className="page-title">{page?.title ?? ' '}</h1>
</div> </div>
</section> </section>
-2
View File
@@ -251,8 +251,6 @@ export default function Server() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('server.tag')}</div> <div className="page-tag">{t('server.tag')}</div>
<h1 className="page-title">{t('nav.server')}</h1> <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)), 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 = ( const header = (
<section className="page-header"> <section className="page-header wide">
<div className="page-bg-text">CONFIG</div> <div className="page-header-content dash-title-row">
<div className="page-header-grid" aria-hidden="true" /> <h1 className="page-title dash-title">Dash<span className="grad-text">board</span></h1>
<div className="page-header-content"> <span className="dash-meta">Alles an einem Ort Änderungen greifen sofort</span>
<div className="page-tag">// Admin</div> {data?.status && (
<h1 className="page-title">Config</h1> <span className="status-pill">
<p className="page-subtitle">Alles an einem Ort Änderungen greifen sofort.</p> Bot online{data.status.ping != null && ` · ${data.status.ping} ms`}
</span>
)}
</div> </div>
</section> </section>
); );
@@ -1337,11 +1342,11 @@ export default function Settings({ me }) {
const tabStatus = ( const tabStatus = (
<> <>
<div className="settings-section"> <div className="settings-section dash-panel">
<div className="dash-head"> <div className="dash-head">
{data.status.botAvatar {data.status.botAvatar
? <img className="dash-avatar" src={data.status.botAvatar} alt="" /> ? <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"> <div className="dash-ident">
<span className="dash-name"> <span className="dash-name">
{data.status.botName ?? 'Bot'} {data.status.botName ?? 'Bot'}
@@ -1402,55 +1407,65 @@ export default function Settings({ me }) {
</div> </div>
</div> </div>
{aktivitaet && aktivitaet.gesamt > 0 && ( {/* Verlauf und Modul-Verteilung stehen nebeneinander beides sind
<div className="settings-section"> Übersichten, keine Formulare */}
<h2 className="settings-title"> <div className="dash-cols">
// Aktivität {aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten in 30 Tagen {aktivitaet && aktivitaet.gesamt > 0 && (
</h2> <div className="settings-section dash-panel">
<div className="dash-spark"> <h2 className="settings-title">
{aktivitaet.tage.map((t) => ( Aktivität
<span <span className="section-meta">
key={t.key} {aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten / 30 Tage
className="dash-bar" </span>
style={{ height: `${Math.max(3, (t.n / aktivitaet.max) * 100)}%` }} </h2>
title={`${t.key}: ${t.n} Nachrichten`} <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>
</div> )}
)}
{modules.length > 0 && moduleGroups.length > 0 && ( {modules.length > 0 && moduleGroups.length > 0 && (
<div className="settings-section"> <div className="settings-section dash-panel">
<h2 className="settings-title">// Module nach Bereich</h2> <h2 className="settings-title">
<div className="dash-groups"> Module
{moduleGroups.map((g) => { <span className="section-meta">{activeModules.length}/{modules.length} an</span>
const drin = modules.filter((m) => m.group === g.id); </h2>
if (drin.length === 0) return null; <div className="dash-groups">
const an = drin.filter((m) => m.enabled).length; {moduleGroups.map((g) => {
const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length; const drin = modules.filter((m) => m.group === g.id);
return ( if (drin.length === 0) return null;
<button const an = drin.filter((m) => m.enabled).length;
className="dash-group" const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length;
key={g.id} return (
onClick={() => setTab('module')} <button
title={`${g.label} im Modul-Überblick öffnen`} className="dash-group"
> key={g.id}
<span className="dash-group-name">{g.label}</span> onClick={() => setTab('module')}
<span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}> title={`${g.label} im Modul-Überblick öffnen`}
<i style={{ width: `${(an / drin.length) * 100}%` }} /> >
</span> <span className="dash-group-name">{g.label}</span>
<span className="dash-group-n">{an}/{drin.length}</span> <span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}>
</button> <i style={{ width: `${(an / drin.length) * 100}%` }} />
); </span>
})} <span className="dash-group-n">{an}/{drin.length}</span>
</button>
);
})}
</div>
</div> </div>
</div> )}
)} </div>
{modules.length > 0 && ( {modules.length > 0 && (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Noch einzurichten</h2> <h2 className="settings-title">Noch einzurichten</h2>
{unfinished.length === 0 ? ( {unfinished.length === 0 ? (
<p className="section-intro todo-done with-icon"> <p className="section-intro todo-done with-icon">
<IconCheck size={15} /> Alles eingerichtet jedes eingeschaltete Modul hat, was es braucht. <IconCheck size={15} /> Alles eingerichtet jedes eingeschaltete Modul hat, was es braucht.
@@ -1567,7 +1582,7 @@ export default function Settings({ me }) {
const willkommensKarte = ( const willkommensKarte = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Willkommen</h2> <h2 className="settings-title">Willkommen</h2>
<div className="field" data-setting="welcome_channel_id"> <div className="field" data-setting="welcome_channel_id">
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label> <label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label>
<PickSelect <PickSelect
@@ -1580,7 +1595,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Willkommens-Karte</h2> <h2 className="settings-title">Willkommens-Karte</h2>
<p className="section-intro"> <p className="section-intro">
Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau
zeigt sofort, wie es aussieht gespeichert wird erst mit dem Knopf unten. zeigt sofort, wie es aussieht gespeichert wird erst mit dem Knopf unten.
@@ -1714,7 +1729,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Embeds</h2> <h2 className="settings-title">Embeds</h2>
<div className="field" data-setting="brand_name"> <div className="field" data-setting="brand_name">
<label>Brand-Name <span className="field-hint">erscheint in allen Embed-Footern (NAME // DEVLOG")</span></label> <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 })} /> <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>
<div className="settings-section"> <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"> <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> <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 })} /> <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 = ( const tabFeeds = (
<> <>
<div className="settings-section"> <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"> <div className="field" data-setting="devlog_channel_id">
<label>Devlog-Kanal <span className="field-hint">öffentlich hier postet der Bot die Devlogs</span></label> <label>Devlog-Kanal <span className="field-hint">öffentlich hier postet der Bot die Devlogs</span></label>
<div className="field-row"> <div className="field-row">
@@ -1765,7 +1780,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="social_announce_channel_id">
<label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + neue YouTube-Videos</span></label> <label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + neue YouTube-Videos</span></label>
<PickSelect <PickSelect
@@ -1817,7 +1832,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <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> <label>Ping-Rolle <span className="field-hint">wird bei jedem Devlog erwähnt; 🔔-Button am Post leer = kein Ping</span></label>
<PickSelect <PickSelect
@@ -1847,7 +1862,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="commit_feed_enabled">
<label className="toggle"> <label className="toggle">
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} /> <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 = ( const tabCommunity = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Wünsche</h2> <h2 className="settings-title">Wünsche</h2>
<p className="section-intro"> <p className="section-intro">
Ein Wunsch ohne sichtbaren Ausgang ist ein Wunsch, den beim nächsten Mal 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; 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"> <div className="settings-section">
<h2 className="settings-title">// Wunsch-Bereiche</h2> <h2 className="settings-title">Wunsch-Bereiche</h2>
<p className="section-intro"> <p className="section-intro">
Schubladen für Ideen Fahrzeuge", „Wirtschaft", Technik". Ohne Bereiche Schubladen für Ideen Fahrzeuge", „Wirtschaft", Technik". Ohne Bereiche
bleibt die Liste eine Liste; mit ihnen kann man auf der Roadmap filtern, und 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>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Geburtstage</h2> <h2 className="settings-title">Geburtstage</h2>
<div className="field" data-setting="birthday_channel_id"> <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> <label>Geburtstags-Kanal <span className="field-hint">Member tragen sich mit /geburtstag ein; Gratulation morgens ab 09:00 leer = aus</span></label>
<PickSelect <PickSelect
@@ -2056,7 +2071,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <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> <label>Event-Ankündigungs-Kanal <span className="field-hint">neue Discord-Events werden hier angekündigt + Events-Seite</span></label>
<PickSelect <PickSelect
@@ -2069,7 +2084,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="starboard_channel_id">
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug landen hier</span></label> <label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug landen hier</span></label>
<div className="field-row"> <div className="field-row">
@@ -2103,7 +2118,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="levels_enabled">
<label className="toggle"> <label className="toggle">
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} /> <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>
<div className="settings-section"> <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: {/* Der Schalter steht hier und nicht nur im Modul-Reiter:
wer die Sender pflegt, sucht ihn genau an dieser Stelle. */} wer die Sender pflegt, sucht ihn genau an dieser Stelle. */}
<div className="field" data-setting="radio_enabled"> <div className="field" data-setting="radio_enabled">
@@ -2224,7 +2239,7 @@ export default function Settings({ me }) {
const tabRollen = ( const tabRollen = (
<> <>
<div className="settings-section"> <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"> <div className="field" data-setting="autorole_id">
<label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch leer = aus</span></label> <label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch leer = aus</span></label>
<PickSelect <PickSelect
@@ -2247,7 +2262,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Rollen-Menüs</h2> <h2 className="settings-title">Rollen-Menüs</h2>
<p className="section-intro"> <p className="section-intro">
Selbstbedienungs-Rollen wie bei Carl-bot nur ohne Paywall. Der Bot postet Selbstbedienungs-Rollen wie bei Carl-bot nur ohne Paywall. Der Bot postet
ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind
@@ -2468,7 +2483,7 @@ export default function Settings({ me }) {
const tabSupport = ( const tabSupport = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Ticket-Anliegen</h2> <h2 className="settings-title">Ticket-Anliegen</h2>
<p className="section-intro"> <p className="section-intro">
Ohne Anliegen hat der Ticket-Post einen Knopf und jedes Ticket sieht gleich aus. 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, Mit Anliegen wird daraus ein Auswahlmenü: der Titel steht im Thread-Namen,
@@ -2563,7 +2578,7 @@ export default function Settings({ me }) {
</div> </div>
{automod && ( {automod && (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// AutoMod</h2> <h2 className="settings-title">AutoMod</h2>
<p className="section-intro"> <p className="section-intro">
Discords eigener Filter er läuft serverseitig und ohne Umweg über den Bot. Discords eigener Filter er läuft serverseitig und ohne Umweg über den Bot.
Hier siehst du, was eingerichtet ist, und kannst Regeln schalten. Hier siehst du, was eingerichtet ist, und kannst Regeln schalten.
@@ -2618,7 +2633,7 @@ export default function Settings({ me }) {
</div> </div>
)} )}
<div className="settings-section"> <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"> <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> <label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal DMs an den Bot landen hier als Threads</span></label>
<PickSelect <PickSelect
@@ -2653,7 +2668,7 @@ export default function Settings({ me }) {
const tabPlaytester = ( const tabPlaytester = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Playtester</h2> <h2 className="settings-title">Playtester</h2>
<p className="section-intro"> <p className="section-intro">
Beworben wird sich über ein Formular, angenommen vom Team wer angenommen Beworben wird sich über ein Formular, angenommen vom Team wer angenommen
wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt
@@ -2729,7 +2744,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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' }}> <div className="field-row" style={{ marginBottom: '1rem' }}>
<button className="btn" onClick={() => setKeyDialog(true)}> <button className="btn" onClick={() => setKeyDialog(true)}>
<IconKey size={14} /> Schlüssel nachlegen <IconKey size={14} /> Schlüssel nachlegen
@@ -2835,7 +2850,7 @@ export default function Settings({ me }) {
const tabBewerbungen = ( const tabBewerbungen = (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Bewerbungs-Formulare</h2> <h2 className="settings-title">Bewerbungs-Formulare</h2>
<p className="section-intro"> <p className="section-intro">
Button-Post Discord-Formular (bis 5 Fragen) Review mit / im Staff-Kanal. 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. 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} /> <EmbedComposer channelOptions={channelOptions} flash={flash} />
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Geplante Posts</h2> <h2 className="settings-title">Geplante Posts</h2>
{scheduled.length > 0 && ( {scheduled.length > 0 && (
<div className="key-list" style={{ marginBottom: '1.2rem' }}> <div className="key-list" style={{ marginBottom: '1.2rem' }}>
{scheduled.map((p) => ( {scheduled.map((p) => (
@@ -2996,7 +3011,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Triggers</h2> <h2 className="settings-title">Triggers</h2>
<p className="section-intro"> <p className="section-intro">
Auto-Antworten: Enthält eine Nachricht das Keyword, antwortet der Bot (30s-Cooldown pro Kanal). Auto-Antworten: Enthält eine Nachricht das Keyword, antwortet der Bot (30s-Cooldown pro Kanal).
</p> </p>
@@ -3022,7 +3037,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Tags (/tag)</h2> <h2 className="settings-title">Tags (/tag)</h2>
<p className="section-intro"> <p className="section-intro">
Text-Bausteine für wiederkehrende Antworten jeder Member kann sie mit <code>/tag name</code> abrufen. Text-Bausteine für wiederkehrende Antworten jeder Member kann sie mit <code>/tag name</code> abrufen.
</p> </p>
@@ -3052,7 +3067,7 @@ export default function Settings({ me }) {
const tabSystem = ( const tabSystem = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Allgemein</h2> <h2 className="settings-title">Allgemein</h2>
<div className="field" data-setting="public_url"> <div className="field" data-setting="public_url">
<label>Öffentliche URL <span className="field-hint">Basis für Buttons, RSS, OAuth-Redirect</span></label> <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 })} /> <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>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Zwei Seiten</h2> <h2 className="settings-title">Zwei Seiten</h2>
<p className="section-intro"> <p className="section-intro">
Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange
beide Felder leer sind, läuft alles unverändert auf der öffentlichen URL. 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"> <div className="settings-section">
<h2 className="settings-title">// Dienste (Portal)</h2> <h2 className="settings-title">Dienste (Portal)</h2>
<p className="section-intro"> <p className="section-intro">
Erscheinen als Kacheln auf der Startseite z. B. Gitea, Kanban oder die Cloud. Erscheinen als Kacheln auf der Startseite z. B. Gitea, Kanban oder die Cloud.
</p> </p>
@@ -3135,7 +3150,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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' }}> <p className="field-hint" style={{ marginBottom: '.9rem' }}>
Erscheint auf <code>/impressum</code> und <code>/datenschutz</code>. Für § 5 DDG 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 ist eine ladungsfähige Anschrift nötig (kein Postfach) nur der Owner sieht
@@ -3160,7 +3175,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="member_gate_enabled">
<label className="toggle"> <label className="toggle">
<input type="checkbox" checked={form.member_gate_enabled} onChange={(e) => setForm({ ...form, member_gate_enabled: e.target.checked })} /> <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>
<div className="settings-section"> <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"> <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> <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 <PickSelect
@@ -3188,7 +3203,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <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"> <div className="field" data-setting="backup_enabled">
<label className="toggle"> <label className="toggle">
<input type="checkbox" checked={form.backup_enabled} onChange={(e) => setForm({ ...form, backup_enabled: e.target.checked })} /> <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 && ( {watchdog?.adressen?.length > 0 && (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Erreichbarkeit</h2> <h2 className="settings-title">Erreichbarkeit</h2>
<p className="section-intro"> <p className="section-intro">
Ein Strich je Stunde über die letzten 7 Tage grün erreichbar, rot Ein Strich je Stunde über die letzten 7 Tage grün erreichbar, rot
Ausfall, grau keine Messung. Geprüft wird alle {watchdog.intervall} Minuten. Ausfall, grau keine Messung. Geprüft wird alle {watchdog.intervall} Minuten.
@@ -3294,7 +3309,7 @@ export default function Settings({ me }) {
{watchdog?.stoerungen?.length > 0 && ( {watchdog?.stoerungen?.length > 0 && (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Störungen</h2> <h2 className="settings-title">Störungen</h2>
<div className="st-incidents"> <div className="st-incidents">
{watchdog.stoerungen.slice(0, 20).map((v) => ( {watchdog.stoerungen.slice(0, 20).map((v) => (
<div className={`st-incident${v.beendet ? '' : ' offen'}`} key={v.id}> <div className={`st-incident${v.beendet ? '' : ' offen'}`} key={v.id}>
@@ -3314,7 +3329,7 @@ export default function Settings({ me }) {
const tabApi = ( const tabApi = (
<> <>
<div className="settings-section"> <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 && ( {keys.length > 0 && (
<div className="key-list"> <div className="key-list">
{keys.map((k) => ( {keys.map((k) => (
@@ -3363,7 +3378,7 @@ export default function Settings({ me }) {
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Single Sign-On</h2> <h2 className="settings-title">Single Sign-On</h2>
<p className="section-intro"> <p className="section-intro">
Andere Dienste (Kanban, Platform ) können den Discord-Login mitnutzen: Sie Andere Dienste (Kanban, Platform ) können den Discord-Login mitnutzen: Sie
leiten auf <code>/sso/authorize</code> weiter und tauschen den Token per leiten auf <code>/sso/authorize</code> weiter und tauschen den Token per
@@ -3412,7 +3427,7 @@ export default function Settings({ me }) {
const tabServer = ( const tabServer = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Game-Server</h2> <h2 className="settings-title">Game-Server</h2>
<p className="section-intro"> <p className="section-intro">
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴, Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
Spieler-Balken, Map, Adresse) aktualisiert alle 2 Minuten, DiscordGSM-Style. Spieler-Balken, Map, Adresse) aktualisiert alle 2 Minuten, DiscordGSM-Style.
@@ -3684,7 +3699,7 @@ export default function Settings({ me }) {
/> />
) : ( ) : (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Module</h2> <h2 className="settings-title">Module</h2>
<p className="section-intro"> <p className="section-intro">
Jede Funktion des Bots lässt sich hier ein- und ausschalten. Ausgeschaltete Jede Funktion des Bots lässt sich hier ein- und ausschalten. Ausgeschaltete
Module reagieren auf nichts mehr keine Posts, keine Befehle, keine Module reagieren auf nichts mehr keine Posts, keine Befehle, keine
@@ -3767,7 +3782,7 @@ export default function Settings({ me }) {
const tabTexte = ( const tabTexte = (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Texte</h2> <h2 className="settings-title">Texte</h2>
<p className="section-intro"> <p className="section-intro">
Alle Texte am Stück wer nur die einer Funktion sucht, findet sie Alle Texte am Stück wer nur die einer Funktion sucht, findet sie
auch auf deren Modul-Seite. Zeile anklicken zum Bearbeiten auch auf deren Modul-Seite. Zeile anklicken zum Bearbeiten
@@ -3875,7 +3890,7 @@ export default function Settings({ me }) {
const tabWerte = ( const tabWerte = (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Werte</h2> <h2 className="settings-title">Werte</h2>
<p className="section-intro"> <p className="section-intro">
Alle Werte am Stück; jeder steht zusätzlich auf der Seite seiner 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, Funktion. Zahlen, die das Verhalten steuern wie viel XP eine Nachricht bringt,
@@ -3954,7 +3969,7 @@ export default function Settings({ me }) {
const tabSeiten = ( const tabSeiten = (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Eigene Seiten</h2> <h2 className="settings-title">Eigene Seiten</h2>
<p className="section-intro"> <p className="section-intro">
Inhalte pflegen, ohne dass jemand Code anfassen muss Regeln, Über uns, Inhalte pflegen, ohne dass jemand Code anfassen muss Regeln, Über uns,
Mitmachen, FAQ. Geschrieben wird in Markdown (wie im Devlog), rechts siehst Mitmachen, FAQ. Geschrieben wird in Markdown (wie im Devlog), rechts siehst
@@ -4130,7 +4145,7 @@ export default function Settings({ me }) {
const tabTeam = ( const tabTeam = (
<> <>
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Team</h2> <h2 className="settings-title">Team</h2>
<p className="section-intro"> <p className="section-intro">
Team-Mitglieder loggen sich ganz normal mit Discord ein und sehen nur ihre Team-Mitglieder loggen sich ganz normal mit Discord ein und sehen nur ihre
Bereiche. Brand, System, API-Keys und diese Verwaltung bleiben immer dir Bereiche. Brand, System, API-Keys und diese Verwaltung bleiben immer dir
@@ -4230,7 +4245,7 @@ export default function Settings({ me }) {
{audit.length > 0 && ( {audit.length > 0 && (
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Protokoll</h2> <h2 className="settings-title">Protokoll</h2>
<p className="section-intro"> <p className="section-intro">
Wer hat was im Webinterface geändert. Wer hat was im Webinterface geändert.
</p> </p>
-2
View File
@@ -49,8 +49,6 @@ export default function Status() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('status.tag')}</div> <div className="page-tag">{t('status.tag')}</div>
<h1 className="page-title">{t('nav.status')}</h1> <h1 className="page-title">{t('nav.status')}</h1>
-2
View File
@@ -55,8 +55,6 @@ export default function WunschDetail() {
return ( return (
<> <>
<section className="page-header"> <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-header-content">
<div className="page-tag">{t('wunsch.tag')}</div> <div className="page-tag">{t('wunsch.tag')}</div>
<h1 className="page-title">{w?.idea ?? '…'}</h1> <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) { export function registerBrandRoutes(app) {
app.get('/brand.css', async (request, reply) => { app.get('/brand.css', async (request, reply) => {
const neon = getSetting('brand_color') || '#f5c518'; const neon = getSetting('brand_color') || '#ffb340';
const neon2 = getSetting('brand_color2') || '#ff4d00'; const neon2 = getSetting('brand_color2') || '#f07800';
return reply return reply
.type('text/css; charset=utf-8') .type('text/css; charset=utf-8')
@@ -27,88 +27,106 @@ export function registerBrandRoutes(app) {
.header('access-control-allow-origin', '*') .header('access-control-allow-origin', '*')
.send(`/* D4RKST3R Design-Tokens — erzeugt von ${publicUrl()} */ .send(`/* D4RKST3R Design-Tokens — erzeugt von ${publicUrl()} */
:root { :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}; --neon: ${neon};
--neon2: ${neon2}; --neon2: ${neon2};
--bg: #0a0a0a; --bg: #131316;
--bg2: #111; --bg2: #1b1b20;
--bg3: #161616; --bg3: #1f1f25;
--text: #e8e0d0; --text: #ececee;
--muted: #8a8378; --text2: #c9c9cf;
--muted2: #666; --muted: #9a9aa3;
--border: color-mix(in srgb, ${neon} 15%, transparent); --muted2: #66666e;
--success: #00ff88; --border: rgba(255,255,255,.08);
--success: #3ba55d;
--danger: #f23f43; --danger: #f23f43;
/* Schriften per @fontsource in der App installieren: /* Schriften per @fontsource in der App installieren:
npm i @fontsource/bebas-neue @fontsource/barlow-condensed @fontsource/share-tech-mono */ npm i @fontsource/chakra-petch @fontsource/ibm-plex-mono */
--display: 'Bebas Neue', Impact, sans-serif; --display: 'Chakra Petch', system-ui, sans-serif;
--body: 'Barlow Condensed', system-ui, sans-serif; --body: 'Chakra Petch', system-ui, sans-serif;
--mono: 'Share Tech Mono', ui-monospace, monospace; --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 { .d4rk-body {
background: var(--bg); color: var(--text); 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 { .d4rk-title {
font-family: var(--display); letter-spacing: .03em; font-family: var(--display); font-weight: 700;
color: var(--text); line-height: .95; letter-spacing: .02em; text-transform: uppercase;
color: var(--text); line-height: 1.02;
} }
.d4rk-tag { .d4rk-tag {
font-family: var(--mono); font-size: .7rem; letter-spacing: .3em; font-family: var(--mono); font-size: .68rem; letter-spacing: .2em;
text-transform: uppercase; color: var(--neon2); text-transform: uppercase; color: var(--accent-hi);
} }
.d4rk-card { .d4rk-card {
background: var(--bg2); border: 1px solid rgba(255,255,255,.06); background: var(--bg2); border: 1px solid var(--border);
border-radius: 12px; padding: 1.3rem 1.6rem; padding: 1.3rem 1.6rem;
transition: border-color .25s, transform .25s, box-shadow .25s; transition: border-color .2s, background .2s, transform .2s;
} }
.d4rk-card:hover { .d4rk-card:hover {
border-color: var(--border); transform: translateY(-2px); border-color: color-mix(in srgb, ${neon} 50%, transparent);
box-shadow: 0 10px 30px rgba(0,0,0,.45); background: var(--bg3); transform: translateY(-3px);
} }
.d4rk-btn { .d4rk-btn {
--bv: 10px;
display: inline-flex; align-items: center; gap: .5rem; display: inline-flex; align-items: center; gap: .5rem;
font-family: var(--mono); font-size: .7rem; font-family: var(--mono); font-size: .72rem; font-weight: 500;
letter-spacing: .2em; text-transform: uppercase; letter-spacing: .14em; text-transform: uppercase;
color: var(--neon); background: none; color: var(--bg); background: var(--grad);
border: 1px solid var(--border); border-radius: 999px; border: 0; padding: .55rem 1.3rem; cursor: pointer;
padding: .5rem 1.2rem; cursor: pointer; transition: filter .2s;
transition: background .2s, border-color .2s, box-shadow .2s;
} }
.d4rk-btn:hover { .d4rk-btn:hover { filter: brightness(1.1); }
border-color: var(--neon); background: color-mix(in srgb, var(--neon) 8%, transparent); /* Zweite Stufe: nur Kontur, für alles neben der Hauptaktion */
box-shadow: 0 0 16px color-mix(in srgb, var(--neon) 15%, transparent); .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 */ /* Navbar, die brand-nav.js einfügt */
.d4rk-nav { .d4rk-nav {
position: sticky; top: 0; z-index: 200; position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; gap: 1.6rem; flex-wrap: wrap; display: flex; align-items: center; gap: 1.8rem; flex-wrap: wrap;
padding: 1rem 2rem; padding: .85rem 2rem;
background: rgba(10,10,10,.9); background: rgba(19,19,22,.88);
backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(14px) saturate(1.2); -webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.d4rk-nav-logo { .d4rk-nav-logo {
font-family: var(--display); font-size: 1.5rem; letter-spacing: .15em; font-family: var(--display); font-weight: 700;
color: var(--neon); text-decoration: none; line-height: 1; 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) { .d4rk-nav a:not(.d4rk-nav-logo) {
position: relative; font-family: var(--mono); font-size: .72rem;
font-family: var(--mono); font-size: .7rem; letter-spacing: .14em; text-transform: uppercase;
letter-spacing: .25em; text-transform: uppercase; color: var(--muted); text-decoration: none;
color: var(--muted); text-decoration: none; padding: .3rem 0; border-bottom: 2px solid transparent; padding-bottom: .2rem;
transition: color .2s; transition: color .2s, border-color .2s;
} }
.d4rk-nav a:not(.d4rk-nav-logo)::after { .d4rk-nav a:not(.d4rk-nav-logo):hover {
content: ''; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px; color: var(--accent-hi); border-bottom-color: var(--accent);
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(--text); }
.d4rk-nav a:not(.d4rk-nav-logo):hover::after { transform: scaleX(1); }
.d4rk-nav-spacer { flex: 1; } .d4rk-nav-spacer { flex: 1; }
@media (max-width: 640px) { @media (max-width: 640px) {
.d4rk-nav { padding: .8rem 1rem; gap: 1rem; } .d4rk-nav { padding: .8rem 1rem; gap: 1rem; }
@@ -149,7 +167,7 @@ export function registerBrandRoutes(app) {
a.href = link.url; a.href = link.url;
a.textContent = link.name; a.textContent = link.name;
// aktuelle Seite hervorheben // 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); 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:description" content="${esc(description)}">`,
`<meta property="og:url" content="${esc(base + path)}">`, `<meta property="og:url" content="${esc(base + path)}">`,
`<meta property="og:type" content="website">`, `<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 name="description" content="${esc(description)}">`,
`<meta property="og:image" content="${esc(image)}">`, `<meta property="og:image" content="${esc(image)}">`,
`<meta name="twitter:card" content="summary_large_image">`, `<meta name="twitter:card" content="summary_large_image">`,