Webseiten-Redesign: Graphit + Orange, abgeschraegte Ecken
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:
+15
-44
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
||||
import { apiGet } from './api.js';
|
||||
import Devlogs from './pages/Devlogs.jsx';
|
||||
@@ -62,35 +62,9 @@ function DiscordMark() {
|
||||
);
|
||||
}
|
||||
|
||||
/* Aufsteigende Neon-Partikel wie auf der Platform-Seite */
|
||||
function Particles() {
|
||||
const particles = useMemo(
|
||||
() =>
|
||||
Array.from({ length: 24 }, (_, i) => ({
|
||||
id: i,
|
||||
left: `${Math.random() * 100}%`,
|
||||
dur: `${6 + Math.random() * 8}s`,
|
||||
delay: `${Math.random() * 8}s`,
|
||||
})),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div className="particles-wrap" aria-hidden="true">
|
||||
{particles.map((p) => (
|
||||
<span
|
||||
key={p.id}
|
||||
className="particle"
|
||||
style={{ left: p.left, '--dur': p.dur, '--delay': p.delay, bottom: 0 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// Login-Status einmal beim Laden holen
|
||||
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
const location = useLocation();
|
||||
const { t } = useT();
|
||||
@@ -116,18 +90,12 @@ export default function App() {
|
||||
if (gated) apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {});
|
||||
}, [gated]);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Particles />
|
||||
|
||||
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
|
||||
{/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
|
||||
<header className="nav">
|
||||
<NavLink to="/" className="nav-logo">
|
||||
<img className="nav-mark" src="/brand/mark.png" alt="" />
|
||||
D4RKST<span>3R</span>
|
||||
</NavLink>
|
||||
|
||||
@@ -238,18 +206,21 @@ export default function App() {
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<span className="footer-logo">D4RKST<span>3R</span></span>
|
||||
<span className="footer-sep">//</span>
|
||||
<span className="footer-logo">
|
||||
<img className="footer-mark" src="/brand/mark.png" alt="" />
|
||||
D4RKST<span>3R</span>
|
||||
</span>
|
||||
<a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
|
||||
git.d4rkst3r.de
|
||||
</a>
|
||||
<span className="footer-sep">//</span>
|
||||
<NavLink to="/status">{t('footer.status')}</NavLink>
|
||||
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
|
||||
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
|
||||
{pages.map((p) => (
|
||||
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
|
||||
))}
|
||||
<span className="footer-right">
|
||||
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
|
||||
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
|
||||
{pages.map((p) => (
|
||||
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
|
||||
))}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
+17
-19
@@ -1,4 +1,4 @@
|
||||
// Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe.
|
||||
// Rahmen der Bot-Produktseite — eigene Navigation, gleiche Marke wie der Hub.
|
||||
// Das Dashboard ist dieselbe Config-Seite wie bisher, nur unter /dashboard.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
||||
@@ -14,7 +14,6 @@ import { useT, LangSwitch } from './i18n.jsx';
|
||||
|
||||
export default function BotApp() {
|
||||
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
const [hub, setHub] = useState(null);
|
||||
// Seiten wie die Nutzungsbedingungen liegen auf dem Hub — verlinkt
|
||||
@@ -33,19 +32,15 @@ export default function BotApp() {
|
||||
apiGet('/api/pages').then((d) => setPages(d.pages)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const canDashboard = me.admin || me.scopes?.length > 0;
|
||||
|
||||
return (
|
||||
<div className="app site-bot">
|
||||
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
|
||||
<div className="app">
|
||||
{/* Die Leiste ist immer kompakt — kein Zustandswechsel beim Scrollen */}
|
||||
<header className="nav">
|
||||
<NavLink to="/" className="nav-logo">
|
||||
D4RKBOT<span>.</span>
|
||||
<img className="nav-mark" src="/brand/mark.png" alt="" />
|
||||
D4RKBOT<span>_</span>
|
||||
</NavLink>
|
||||
|
||||
<nav className="nav-links">
|
||||
@@ -105,18 +100,21 @@ export default function BotApp() {
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<span className="footer-logo">D4RKBOT<span>.</span></span>
|
||||
<span className="footer-sep">//</span>
|
||||
<span className="footer-logo">
|
||||
<img className="footer-mark" src="/brand/mark.png" alt="" />
|
||||
D4RKBOT<span>_</span>
|
||||
</span>
|
||||
<a href="https://git.d4rkst3r.de/D4rkst3r/d4rkbot" target="_blank" rel="noreferrer">
|
||||
{t('footer.source')}
|
||||
</a>
|
||||
<span className="footer-sep">//</span>
|
||||
{hub && <a href={`${hub}/status`}>{t('footer.status')}</a>}
|
||||
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
|
||||
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
|
||||
{hub && pages.map((p) => (
|
||||
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
|
||||
))}
|
||||
<span className="footer-right">
|
||||
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
|
||||
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
|
||||
{hub && pages.map((p) => (
|
||||
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
|
||||
))}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export default function EmbedComposer({ channelOptions, flash }) {
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Post verfassen</h2>
|
||||
<h2 className="settings-title">Post verfassen</h2>
|
||||
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
|
||||
+40
-36
@@ -44,7 +44,7 @@ export default {
|
||||
'common.monthsAgo': 'vor %s Monaten',
|
||||
|
||||
// ── Startseite (Hub) ──────────────────────────────
|
||||
'home.tag': '// Community-Hub',
|
||||
'home.tag': 'Community-Hub',
|
||||
'home.welcome': 'Willkommen',
|
||||
'home.at': 'bei',
|
||||
'home.subtitle': 'Devlogs, Roadmap, Server-Status und alles aus der Community — an einem Ort, direkt aus dem Discord.',
|
||||
@@ -53,8 +53,8 @@ export default {
|
||||
'home.members': 'Member im Discord',
|
||||
'home.serversOnline': 'Server online',
|
||||
'home.playersNow': 'Spieler gerade drauf',
|
||||
'home.services': '// Dienste',
|
||||
'home.latestDevlog': '// Neuestes Devlog',
|
||||
'home.services': 'Dienste',
|
||||
'home.latestDevlog': 'Neuestes Devlog',
|
||||
'home.more': 'Weiterlesen',
|
||||
'home.tile.devlog': 'Entwicklungs-Updates, frisch aus dem Editor',
|
||||
'home.tile.roadmap': 'Meilensteine + Community-Wünsche zum Abstimmen',
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
'home.tile.events': 'Playtests, Streams und was sonst ansteht',
|
||||
|
||||
'nav.status': 'Status',
|
||||
'status.tag': '// Betriebslage',
|
||||
'status.tag': 'Betriebslage',
|
||||
'status.subtitle': 'Erreichbarkeit der Dienste und Game-Server — alle 60 Sekunden aktualisiert.',
|
||||
'status.error': 'Der Status konnte nicht geladen werden.',
|
||||
'status.allOk': 'Alle Systeme betriebsbereit',
|
||||
@@ -72,9 +72,9 @@ export default {
|
||||
'status.majorOutage': 'Größere Störung',
|
||||
'status.waiting': 'Wird geprüft',
|
||||
'status.nothing': 'Nichts eingerichtet',
|
||||
'status.services': '// Dienste',
|
||||
'status.gameservers': '// Game-Server',
|
||||
'status.incidents': '// Störungen der letzten 90 Tage',
|
||||
'status.services': 'Dienste',
|
||||
'status.gameservers': 'Game-Server',
|
||||
'status.incidents': 'Störungen der letzten 90 Tage',
|
||||
'status.noIncidents': 'Keine Störungen aufgezeichnet.',
|
||||
'status.ok': 'erreichbar',
|
||||
'status.down': 'nicht erreichbar',
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
'status.minutes': '%s min',
|
||||
'status.days90': 'vor 90 Tagen',
|
||||
// ── Devlogs ───────────────────────────────────────
|
||||
'devlogs.tag': '// Archiv',
|
||||
'devlogs.tag': 'Archiv',
|
||||
'devlogs.subtitle': 'Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.',
|
||||
'devlogs.all': 'Alle',
|
||||
'devlogs.search': 'Devlogs durchsuchen …',
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
'devlogs.screenshot': 'Devlog-Screenshot',
|
||||
|
||||
// ── Devlog-Permalink ──────────────────────────────
|
||||
'devlog.tag': '// Devlog',
|
||||
'devlog.tag': 'Devlog',
|
||||
'devlog.project': 'Projekt: %s',
|
||||
'devlog.notFound': 'Devlog nicht gefunden —',
|
||||
'devlog.backToArchive': 'zurück zum Archiv',
|
||||
@@ -107,17 +107,17 @@ export default {
|
||||
'devlog.linkCopied': 'Link kopiert',
|
||||
|
||||
// ── Roadmap ───────────────────────────────────────
|
||||
'roadmap.tag': '// Wohin die Reise geht',
|
||||
'roadmap.tag': 'Wohin die Reise geht',
|
||||
'roadmap.subtitle': 'Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.',
|
||||
'roadmap.error': 'Roadmap konnte nicht geladen werden.',
|
||||
'roadmap.noMilestones': 'Noch keine Meilensteine angelegt.',
|
||||
'roadmap.active': '// In Arbeit',
|
||||
'roadmap.planned': '// Geplant',
|
||||
'roadmap.done': '// Fertig',
|
||||
'roadmap.active': 'In Arbeit',
|
||||
'roadmap.planned': 'Geplant',
|
||||
'roadmap.done': 'Fertig',
|
||||
'roadmap.nothingActive': 'Gerade nichts in Arbeit.',
|
||||
'roadmap.due': 'Ziel: %s',
|
||||
'roadmap.tasks': '%s / %s Aufgaben',
|
||||
'roadmap.activity': '// Aktivität — %s Commits in 12 Monaten',
|
||||
'roadmap.activity': 'Aktivität — %s Commits in 12 Monaten',
|
||||
'roadmap.commitsOn': '%s: %s Commits',
|
||||
'roadmap.commitOn': '%s: 1 Commit',
|
||||
'roadmap.sort': 'Sortierung',
|
||||
@@ -127,13 +127,13 @@ export default {
|
||||
'roadmap.status': 'Stand',
|
||||
'roadmap.area': 'Bereich',
|
||||
'roadmap.areaPick': '— Bereich —',
|
||||
'wunsch.tag': '// Idee',
|
||||
'wunsch.tag': 'Idee',
|
||||
'wunsch.back': 'Zurück zur Roadmap',
|
||||
'wunsch.notFound': 'Diese Idee gibt es nicht (mehr).',
|
||||
'wunsch.by': 'von %s',
|
||||
'wunsch.teamSays': 'Vom Team',
|
||||
'wunsch.comments': '// %s Beiträge',
|
||||
'wunsch.oneComment': '// 1 Beitrag',
|
||||
'wunsch.comments': '%s Beiträge',
|
||||
'wunsch.oneComment': '1 Beitrag',
|
||||
'wunsch.noComments': 'Noch nichts dazu geschrieben — mach den Anfang im Thread.',
|
||||
'wunsch.joinThread': 'Im Discord mitreden',
|
||||
'wish.alle': 'Alle',
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
'wish.abgelehnt': 'Nicht geplant',
|
||||
'roadmap.noneInFilter': 'Hier steht gerade nichts.',
|
||||
'roadmap.alreadyThere': 'Gibt es schon — stimm lieber dafür:',
|
||||
'roadmap.wishes': '// Community-Wünsche',
|
||||
'roadmap.wishes': 'Community-Wünsche',
|
||||
'roadmap.unvote': 'Vote zurücknehmen',
|
||||
'roadmap.vote': 'Dafür stimmen',
|
||||
'roadmap.ideaPlaceholder': 'Deine Idee für den Server …',
|
||||
@@ -157,7 +157,7 @@ export default {
|
||||
'roadmap.submitFailed': '✗ Konnte den Wunsch nicht einreichen.',
|
||||
|
||||
// ── Server ────────────────────────────────────────
|
||||
'server.tag': '// Live-Status',
|
||||
'server.tag': 'Live-Status',
|
||||
'server.subtitle': 'Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.',
|
||||
'server.error': 'Server-Status konnte nicht geladen werden.',
|
||||
'server.none': 'Noch keine Server eingetragen.',
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
'server.waiting': 'Warte auf Check',
|
||||
'server.players24h': 'Spielerzahl · letzte 24h',
|
||||
// ── Mods eines LS-Servers ─────────────────────────
|
||||
'mods.tag': '// Modliste',
|
||||
'mods.tag': 'Modliste',
|
||||
'mods.title': 'Mods',
|
||||
'mods.subtitle': 'Alle Mods dieses Servers einzeln zum Laden — statt eines Pakets über alles.',
|
||||
'mods.error': 'Modliste konnte nicht geladen werden.',
|
||||
@@ -207,7 +207,7 @@ export default {
|
||||
'server.modsDownload': 'Mods laden',
|
||||
|
||||
// ── Galerie ───────────────────────────────────────
|
||||
'gallery.tag': '// Community',
|
||||
'gallery.tag': 'Community',
|
||||
'gallery.subtitle': 'Screenshots aus der Community — direkt aus dem Discord.',
|
||||
'gallery.error': 'Galerie konnte nicht geladen werden.',
|
||||
'gallery.emptyTitle': 'Noch keine Screenshots',
|
||||
@@ -215,17 +215,17 @@ export default {
|
||||
'gallery.shotBy': 'Screenshot von %s',
|
||||
|
||||
// ── Level ─────────────────────────────────────────
|
||||
'level.tag': '// Bestenliste',
|
||||
'level.tag': 'Bestenliste',
|
||||
'level.subtitle': 'XP gibt’s fürs Mitreden im Discord — hier stehen die Aktivsten. %s zeigt deinen Platz.',
|
||||
'level.error': 'Bestenliste konnte nicht geladen werden.',
|
||||
'level.emptyTitle': 'Noch keine XP vergeben',
|
||||
'level.emptyText': 'XP gibt es fürs Mitreden im Discord. Sobald geschrieben wird, füllt sich die Bestenliste von selbst.',
|
||||
'level.lv': 'Lv. %s',
|
||||
'level.activity': '// Server-Aktivität — %s Nachrichten in 30 Tagen · %s Member',
|
||||
'level.activity': 'Server-Aktivität — %s Nachrichten in 30 Tagen · %s Member',
|
||||
'level.barTitle': '%s: %s Nachrichten · +%s/−%s Member',
|
||||
|
||||
// ── Events ────────────────────────────────────────
|
||||
'events.tag': '// Termine',
|
||||
'events.tag': 'Termine',
|
||||
'events.subtitle': 'Playtests, Streams & Community-Abende — direkt aus dem Discord.',
|
||||
'events.error': 'Events konnten nicht geladen werden.',
|
||||
'events.emptyTitle': 'Noch keine Events geplant',
|
||||
@@ -235,7 +235,7 @@ export default {
|
||||
'events.viewInDiscord': 'Im Discord ansehen',
|
||||
|
||||
// ── Changelog ─────────────────────────────────────
|
||||
'changelog.tag': '// Releases',
|
||||
'changelog.tag': 'Releases',
|
||||
'changelog.subtitle': 'Alle Versionen und Release-Notes.',
|
||||
'changelog.error': 'Changelog konnte nicht geladen werden.',
|
||||
'changelog.emptyTitle': 'Noch keine Releases',
|
||||
@@ -245,18 +245,21 @@ export default {
|
||||
'changelog.viewInGitea': 'Release in Gitea ansehen',
|
||||
|
||||
// ── Bot-Produktseite: Start ───────────────────────
|
||||
'bot.tag': '// Selbst gebaut, keine Paywall',
|
||||
'bot.tag': 'Selbst gebaut · keine Paywall',
|
||||
'bot.title1': 'Ein Discord-Bot,',
|
||||
'bot.title2': 'der alles macht',
|
||||
'bot.title2': 'der alles [macht]',
|
||||
'bot.subtitle': 'Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets — was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem, mit eigenem Webinterface.',
|
||||
'bot.allFeatures': 'Alle Funktionen',
|
||||
'bot.viewCommands': 'Befehle ansehen',
|
||||
'bot.statFeatures': 'Funktionen, einzeln schaltbar',
|
||||
'bot.statFeatures': 'Funktionen',
|
||||
'bot.statCommands': 'Slash-Befehle',
|
||||
'bot.statGames': 'Spiele im Monitor',
|
||||
'bot.statCost': 'Kosten, keine Premium-Stufen',
|
||||
'bot.whatItDoes': '// Was er kann',
|
||||
'bot.forDevs': '// Für Entwickler',
|
||||
'bot.statCost': 'Kosten',
|
||||
'bot.online': 'Online',
|
||||
'bot.hosting': 'Hosting',
|
||||
'bot.modulesMeta': '%s Module · einzeln schaltbar',
|
||||
'bot.whatItDoes': 'Was er kann',
|
||||
'bot.forDevs': 'Für Entwickler',
|
||||
'bot.runsFor': 'Der Bot läuft für die D4RKST3R-Community',
|
||||
'bot.runsForWith': 'Der Bot läuft für die D4RKST3R-Community mit %s Mitgliedern',
|
||||
'bot.toHub': 'zum Community-Hub',
|
||||
@@ -285,11 +288,12 @@ export default {
|
||||
'bot.dev.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.',
|
||||
|
||||
// ── Bot-Produktseite: Funktionen ──────────────────
|
||||
'features.tag': '// Alles drin, nichts hinter einer Paywall',
|
||||
'features.title': 'Funktionen',
|
||||
'features.tag': 'Alles drin, nichts hinter einer Paywall',
|
||||
'features.title': 'Funk[tionen]',
|
||||
'features.subtitleN': '%s Funktionen — jede einzeln ein- und ausschaltbar, jede im Browser einstellbar.',
|
||||
'features.subtitle': 'Jede Funktion einzeln ein- und ausschaltbar, jede im Browser einstellbar.',
|
||||
'features.extras': '// Drumherum',
|
||||
'features.count': '%s Module',
|
||||
'features.extras': 'Drumherum',
|
||||
'features.outro': 'Gebaut, weil drei Bots mit je eigenem Abo für eine Community weder übersichtlich noch günstig sind. Der Quelltext liegt offen, der Betrieb kostet einen Container.',
|
||||
'features.overview': 'Zur Übersicht',
|
||||
'features.x.brand': 'Webinterface im eigenen Look',
|
||||
@@ -306,8 +310,8 @@ export default {
|
||||
'features.x.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.',
|
||||
|
||||
// ── Bot-Produktseite: Befehle ─────────────────────
|
||||
'commands.tag': '// Referenz',
|
||||
'commands.title': 'Befehle',
|
||||
'commands.tag': 'Referenz',
|
||||
'commands.title': 'Be[fehle]',
|
||||
'commands.subtitle': 'Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe direkt in den Nachrichten.',
|
||||
'commands.note': 'Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden Discord-Rechten vorbehalten.',
|
||||
'commands.g.everyone': 'Für alle',
|
||||
|
||||
+40
-36
@@ -43,7 +43,7 @@ export default {
|
||||
'common.monthsAgo': '%s months ago',
|
||||
|
||||
// ── Home (hub) ────────────────────────────────────
|
||||
'home.tag': '// Community hub',
|
||||
'home.tag': 'Community hub',
|
||||
'home.welcome': 'Welcome',
|
||||
'home.at': 'to',
|
||||
'home.subtitle': 'Devlogs, roadmap, server status and everything from the community — in one place, straight out of Discord.',
|
||||
@@ -52,8 +52,8 @@ export default {
|
||||
'home.members': 'members on Discord',
|
||||
'home.serversOnline': 'servers online',
|
||||
'home.playersNow': 'players right now',
|
||||
'home.services': '// Services',
|
||||
'home.latestDevlog': '// Latest devlog',
|
||||
'home.services': 'Services',
|
||||
'home.latestDevlog': 'Latest devlog',
|
||||
'home.more': 'Read more',
|
||||
'home.tile.devlog': 'Development updates, fresh from the editor',
|
||||
'home.tile.roadmap': 'Milestones plus community wishes to vote on',
|
||||
@@ -63,7 +63,7 @@ export default {
|
||||
'home.tile.events': 'Playtests, streams and whatever else is coming up',
|
||||
|
||||
'nav.status': 'Status',
|
||||
'status.tag': '// Operational status',
|
||||
'status.tag': 'Operational status',
|
||||
'status.subtitle': 'Reachability of services and game servers — refreshed every 60 seconds.',
|
||||
'status.error': 'The status could not be loaded.',
|
||||
'status.allOk': 'All systems operational',
|
||||
@@ -71,9 +71,9 @@ export default {
|
||||
'status.majorOutage': 'Major outage',
|
||||
'status.waiting': 'Checking',
|
||||
'status.nothing': 'Nothing set up',
|
||||
'status.services': '// Services',
|
||||
'status.gameservers': '// Game servers',
|
||||
'status.incidents': '// Incidents in the last 90 days',
|
||||
'status.services': 'Services',
|
||||
'status.gameservers': 'Game servers',
|
||||
'status.incidents': 'Incidents in the last 90 days',
|
||||
'status.noIncidents': 'No incidents recorded.',
|
||||
'status.ok': 'operational',
|
||||
'status.down': 'down',
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
'status.minutes': '%s min',
|
||||
'status.days90': '90 days ago',
|
||||
// ── Devlogs ───────────────────────────────────────
|
||||
'devlogs.tag': '// Archive',
|
||||
'devlogs.tag': 'Archive',
|
||||
'devlogs.subtitle': 'Development updates, fresh from the editor — archived automatically.',
|
||||
'devlogs.all': 'All',
|
||||
'devlogs.search': 'Search devlogs …',
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
'devlogs.screenshot': 'Devlog screenshot',
|
||||
|
||||
// ── Devlog permalink ──────────────────────────────
|
||||
'devlog.tag': '// Devlog',
|
||||
'devlog.tag': 'Devlog',
|
||||
'devlog.project': 'Project: %s',
|
||||
'devlog.notFound': 'Devlog not found —',
|
||||
'devlog.backToArchive': 'back to the archive',
|
||||
@@ -106,17 +106,17 @@ export default {
|
||||
'devlog.linkCopied': 'Link copied',
|
||||
|
||||
// ── Roadmap ───────────────────────────────────────
|
||||
'roadmap.tag': '// Where this is heading',
|
||||
'roadmap.tag': 'Where this is heading',
|
||||
'roadmap.subtitle': 'Milestones straight from development — live from the issue tracker.',
|
||||
'roadmap.error': 'The roadmap could not be loaded.',
|
||||
'roadmap.noMilestones': 'No milestones yet.',
|
||||
'roadmap.active': '// In progress',
|
||||
'roadmap.planned': '// Planned',
|
||||
'roadmap.done': '// Done',
|
||||
'roadmap.active': 'In progress',
|
||||
'roadmap.planned': 'Planned',
|
||||
'roadmap.done': 'Done',
|
||||
'roadmap.nothingActive': 'Nothing in progress right now.',
|
||||
'roadmap.due': 'Due: %s',
|
||||
'roadmap.tasks': '%s / %s tasks',
|
||||
'roadmap.activity': '// Activity — %s commits in 12 months',
|
||||
'roadmap.activity': 'Activity — %s commits in 12 months',
|
||||
'roadmap.commitsOn': '%s: %s commits',
|
||||
'roadmap.commitOn': '%s: 1 commit',
|
||||
'roadmap.sort': 'Sort',
|
||||
@@ -126,13 +126,13 @@ export default {
|
||||
'roadmap.status': 'Status',
|
||||
'roadmap.area': 'Area',
|
||||
'roadmap.areaPick': '— Area —',
|
||||
'wunsch.tag': '// Idea',
|
||||
'wunsch.tag': 'Idea',
|
||||
'wunsch.back': 'Back to the roadmap',
|
||||
'wunsch.notFound': 'This idea does not exist (any more).',
|
||||
'wunsch.by': 'by %s',
|
||||
'wunsch.teamSays': 'From the team',
|
||||
'wunsch.comments': '// %s comments',
|
||||
'wunsch.oneComment': '// 1 comment',
|
||||
'wunsch.comments': '%s comments',
|
||||
'wunsch.oneComment': '1 comment',
|
||||
'wunsch.noComments': 'Nothing said about this yet — start it off in the thread.',
|
||||
'wunsch.joinThread': 'Join the thread on Discord',
|
||||
'wish.alle': 'All',
|
||||
@@ -143,7 +143,7 @@ export default {
|
||||
'wish.abgelehnt': 'Not planned',
|
||||
'roadmap.noneInFilter': 'Nothing here right now.',
|
||||
'roadmap.alreadyThere': 'Already suggested — vote for it instead:',
|
||||
'roadmap.wishes': '// Community wishes',
|
||||
'roadmap.wishes': 'Community wishes',
|
||||
'roadmap.unvote': 'Take back your vote',
|
||||
'roadmap.vote': 'Vote for this',
|
||||
'roadmap.ideaPlaceholder': 'Your idea for the server …',
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
'roadmap.submitFailed': '✗ Could not submit the wish.',
|
||||
|
||||
// ── Servers ───────────────────────────────────────
|
||||
'server.tag': '// Live status',
|
||||
'server.tag': 'Live status',
|
||||
'server.subtitle': 'Every game server at a glance — refreshed every 2 minutes.',
|
||||
'server.error': 'The server status could not be loaded.',
|
||||
'server.none': 'No servers added yet.',
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
'server.waiting': 'Waiting for check',
|
||||
'server.players24h': 'Player count · last 24h',
|
||||
// ── Mods of an FS server ──────────────────────────
|
||||
'mods.tag': '// Mod list',
|
||||
'mods.tag': 'Mod list',
|
||||
'mods.title': 'Mods',
|
||||
'mods.subtitle': 'Every mod on this server, one by one — instead of one big bundle.',
|
||||
'mods.error': 'Could not load the mod list.',
|
||||
@@ -206,7 +206,7 @@ export default {
|
||||
'server.modsDownload': 'Get mods',
|
||||
|
||||
// ── Gallery ───────────────────────────────────────
|
||||
'gallery.tag': '// Community',
|
||||
'gallery.tag': 'Community',
|
||||
'gallery.subtitle': 'Screenshots from the community — straight out of Discord.',
|
||||
'gallery.error': 'The gallery could not be loaded.',
|
||||
'gallery.emptyTitle': 'No screenshots yet',
|
||||
@@ -214,17 +214,17 @@ export default {
|
||||
'gallery.shotBy': 'Screenshot by %s',
|
||||
|
||||
// ── Levels ────────────────────────────────────────
|
||||
'level.tag': '// Leaderboard',
|
||||
'level.tag': 'Leaderboard',
|
||||
'level.subtitle': 'XP comes from joining the conversation on Discord — here are the most active. %s shows your rank.',
|
||||
'level.error': 'The leaderboard could not be loaded.',
|
||||
'level.emptyTitle': 'No XP awarded yet',
|
||||
'level.emptyText': 'XP comes from joining the conversation on Discord. As soon as people start writing, the leaderboard fills itself.',
|
||||
'level.lv': 'Lv. %s',
|
||||
'level.activity': '// Server activity — %s messages in 30 days · %s members',
|
||||
'level.activity': 'Server activity — %s messages in 30 days · %s members',
|
||||
'level.barTitle': '%s: %s messages · +%s/−%s members',
|
||||
|
||||
// ── Events ────────────────────────────────────────
|
||||
'events.tag': '// Dates',
|
||||
'events.tag': 'Dates',
|
||||
'events.subtitle': 'Playtests, streams & community nights — straight out of Discord.',
|
||||
'events.error': 'Events could not be loaded.',
|
||||
'events.emptyTitle': 'No events planned yet',
|
||||
@@ -234,7 +234,7 @@ export default {
|
||||
'events.viewInDiscord': 'View on Discord',
|
||||
|
||||
// ── Changelog ─────────────────────────────────────
|
||||
'changelog.tag': '// Releases',
|
||||
'changelog.tag': 'Releases',
|
||||
'changelog.subtitle': 'Every version and its release notes.',
|
||||
'changelog.error': 'The changelog could not be loaded.',
|
||||
'changelog.emptyTitle': 'No releases yet',
|
||||
@@ -244,18 +244,21 @@ export default {
|
||||
'changelog.viewInGitea': 'View release on Gitea',
|
||||
|
||||
// ── Bot product page: landing ─────────────────────
|
||||
'bot.tag': '// Self-built, no paywall',
|
||||
'bot.tag': 'Self-built · no paywall',
|
||||
'bot.title1': 'A Discord bot',
|
||||
'bot.title2': 'that does it all',
|
||||
'bot.title2': 'that does it [all]',
|
||||
'bot.subtitle': 'Devlogs, levels, moderation, server monitoring, giveaways, tickets — what usually takes three subscription bots runs here in one, with its own web interface.',
|
||||
'bot.allFeatures': 'All features',
|
||||
'bot.viewCommands': 'View commands',
|
||||
'bot.statFeatures': 'features, each one switchable',
|
||||
'bot.statFeatures': 'features',
|
||||
'bot.statCommands': 'slash commands',
|
||||
'bot.statGames': 'games in the monitor',
|
||||
'bot.statCost': 'cost, no premium tiers',
|
||||
'bot.whatItDoes': '// What it does',
|
||||
'bot.forDevs': '// For developers',
|
||||
'bot.statCost': 'cost',
|
||||
'bot.online': 'Online',
|
||||
'bot.hosting': 'Hosting',
|
||||
'bot.modulesMeta': '%s modules · individually switchable',
|
||||
'bot.whatItDoes': 'What it does',
|
||||
'bot.forDevs': 'For developers',
|
||||
'bot.runsFor': 'The bot runs for the D4RKST3R community',
|
||||
'bot.runsForWith': 'The bot runs for the D4RKST3R community with %s members',
|
||||
'bot.toHub': 'to the community hub',
|
||||
@@ -284,11 +287,12 @@ export default {
|
||||
'bot.dev.host.text': 'Node and SQLite in one container, one volume for the data. Nightly backups included — of the database and every repository.',
|
||||
|
||||
// ── Bot product page: features ────────────────────
|
||||
'features.tag': '// Everything included, nothing behind a paywall',
|
||||
'features.title': 'Features',
|
||||
'features.tag': 'Everything included, nothing behind a paywall',
|
||||
'features.title': 'Fea[tures]',
|
||||
'features.subtitleN': '%s features — each one switchable, each one configurable in the browser.',
|
||||
'features.subtitle': 'Every feature switchable on its own, every one configurable in the browser.',
|
||||
'features.extras': '// Around it',
|
||||
'features.count': '%s modules',
|
||||
'features.extras': 'Around it',
|
||||
'features.outro': 'Built because three bots with three subscriptions for one community is neither tidy nor cheap. The source is open, running it costs one container.',
|
||||
'features.overview': 'Back to the overview',
|
||||
'features.x.brand': 'Web interface in your own look',
|
||||
@@ -395,8 +399,8 @@ export default {
|
||||
'feat.member_gate.desc': 'Only people on the Discord server can sign in on the web.',
|
||||
|
||||
// ── Bot product page: commands ────────────────────
|
||||
'commands.tag': '// Reference',
|
||||
'commands.title': 'Commands',
|
||||
'commands.tag': 'Reference',
|
||||
'commands.title': 'Com[mands]',
|
||||
'commands.subtitle': 'Every slash command at a glance. A lot also runs through buttons right inside the messages.',
|
||||
'commands.note': 'Moderation and setup commands are reserved for members with the matching Discord permissions.',
|
||||
'commands.g.everyone': 'For everyone',
|
||||
|
||||
@@ -5,11 +5,12 @@ import App from './App.jsx';
|
||||
import BotApp from './BotApp.jsx';
|
||||
import { IS_BOT } from './site.js';
|
||||
import { LangProvider } from './i18n.jsx';
|
||||
import '@fontsource/bebas-neue';
|
||||
import '@fontsource/barlow-condensed/300.css';
|
||||
import '@fontsource/barlow-condensed/400.css';
|
||||
import '@fontsource/barlow-condensed/600.css';
|
||||
import '@fontsource/share-tech-mono';
|
||||
import '@fontsource/chakra-petch/400.css';
|
||||
import '@fontsource/chakra-petch/500.css';
|
||||
import '@fontsource/chakra-petch/600.css';
|
||||
import '@fontsource/chakra-petch/700.css';
|
||||
import '@fontsource/ibm-plex-mono/400.css';
|
||||
import '@fontsource/ibm-plex-mono/500.css';
|
||||
import './style.css';
|
||||
|
||||
// Zwei Gesichter, eine Anwendung: Der Server sagt über data-site am <body>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Übersicht aller Slash-Befehle des Bots.
|
||||
import { useEffect } from 'react';
|
||||
import { IconLock } from '../icons.jsx';
|
||||
import { Accent } from '../components/Accent.jsx';
|
||||
import { useT } from '../i18n.jsx';
|
||||
|
||||
// Befehlsname bleibt wie er ist — nur die Erklärung wird übersetzt.
|
||||
@@ -40,11 +41,9 @@ export default function BotCommands() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">BEFEHLE</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('commands.tag')}</div>
|
||||
<h1 className="page-title">{t('commands.title')}</h1>
|
||||
<h1 className="page-title"><Accent text={t('commands.title')} /></h1>
|
||||
<p className="page-subtitle">{t('commands.subtitle')}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -53,8 +52,12 @@ export default function BotCommands() {
|
||||
{GROUPS.map(([groupKey, restricted, commands]) => (
|
||||
<div className="roadmap-group" key={groupKey}>
|
||||
<h2 className="settings-title">
|
||||
// {t(groupKey)}
|
||||
{restricted && <IconLock size={13} />}
|
||||
{/* Schloss gehört zum Titel, nicht als eigene
|
||||
Spalte daneben — deshalb in einer Hülle */}
|
||||
<span className="settings-title-text">
|
||||
{t(groupKey)}
|
||||
{restricted && <IconLock size={15} className="cmd-lock" />}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="cmd-list">
|
||||
{commands.map(([name, descKey]) => (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
|
||||
IconBot, IconArrowRight,
|
||||
} from '../icons.jsx';
|
||||
import { Accent } from '../components/Accent.jsx';
|
||||
import { moduleIcon } from '../module-icons.jsx';
|
||||
|
||||
// Was kein Discord-Modul ist, sondern zum Drumherum gehört
|
||||
@@ -38,11 +39,9 @@ export default function BotFeatures() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">FEATURES</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('features.tag')}</div>
|
||||
<h1 className="page-title">{t('features.title')}</h1>
|
||||
<h1 className="page-title"><Accent text={t('features.title')} /></h1>
|
||||
<p className="page-subtitle">
|
||||
{total ? t('features.subtitleN', total) : t('features.subtitle')}
|
||||
</p>
|
||||
@@ -57,17 +56,20 @@ export default function BotFeatures() {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div className="feature-group" key={group.id}>
|
||||
<h2 className="settings-title">// {tOr(`fgroup.${group.id}`, group.label)}</h2>
|
||||
<h2 className="settings-title">
|
||||
{tOr(`fgroup.${group.id}`, group.label)}
|
||||
<span className="section-meta">{t('features.count', items.length)}</span>
|
||||
</h2>
|
||||
<div className="feature-grid">
|
||||
{items.map((f) => {
|
||||
const Icon = moduleIcon(f.id);
|
||||
return (
|
||||
<div className="card feature-card" key={f.id}>
|
||||
<span className="feature-icon"><Icon size={20} /></span>
|
||||
<div>
|
||||
<div className="feature-head">
|
||||
<span className="feature-icon"><Icon size={16} /></span>
|
||||
<span className="feature-name">{tOr(`feat.${f.id}`, f.name)}</span>
|
||||
<p className="feature-desc">{tOr(`feat.${f.id}.desc`, f.desc)}</p>
|
||||
</div>
|
||||
<p className="feature-desc">{tOr(`feat.${f.id}.desc`, f.desc)}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -78,15 +80,18 @@ export default function BotFeatures() {
|
||||
|
||||
{data && (
|
||||
<div className="feature-group">
|
||||
<h2 className="settings-title">{t('features.extras')}</h2>
|
||||
<h2 className="settings-title">
|
||||
{t('features.extras')}
|
||||
<span className="section-meta">{t('features.count', EXTRAS.length)}</span>
|
||||
</h2>
|
||||
<div className="feature-grid">
|
||||
{EXTRAS.map(([Icon, key]) => (
|
||||
<div className="card feature-card" key={key}>
|
||||
<span className="feature-icon"><Icon size={20} /></span>
|
||||
<div>
|
||||
<div className="feature-head">
|
||||
<span className="feature-icon"><Icon size={16} /></span>
|
||||
<span className="feature-name">{t(key)}</span>
|
||||
<p className="feature-desc">{t(`${key}.text`)}</p>
|
||||
</div>
|
||||
<p className="feature-desc">{t(`${key}.text`)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { apiGet } from '../api.js';
|
||||
import { useT } from '../i18n.jsx';
|
||||
import { Accent } from '../components/Accent.jsx';
|
||||
import {
|
||||
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
|
||||
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot,
|
||||
IconBirthday, IconShield, IconMagic, IconArrowRight, IconBot,
|
||||
} from '../icons.jsx';
|
||||
|
||||
const HIGHLIGHTS = [
|
||||
@@ -38,15 +39,14 @@ export default function BotLanding() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header bot-hero">
|
||||
<div className="page-bg-text">BOT</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('bot.tag')}</div>
|
||||
<h1 className="page-title">
|
||||
{t('bot.title1')}<br />{t('bot.title2')}
|
||||
<section className="hero hero-bot">
|
||||
<div className="hero-text">
|
||||
<div className="page-tag dot">{t('bot.tag')}</div>
|
||||
<h1 className="hero-title">
|
||||
{t('bot.title1')}<br />
|
||||
<Accent text={t('bot.title2')} outline />
|
||||
</h1>
|
||||
<p className="page-subtitle">{t('bot.subtitle')}</p>
|
||||
<p className="hero-sub">{t('bot.subtitle')}</p>
|
||||
<div className="home-cta">
|
||||
<Link className="btn btn-save with-icon" to="/features">
|
||||
{t('bot.allFeatures')} <IconArrowRight size={15} />
|
||||
@@ -56,63 +56,79 @@ export default function BotLanding() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Karte mit Logo, Zustand und den vier Kennzahlen. Der versetzte
|
||||
Rahmen liegt als eigenes Element dahinter — als Pseudo-Element
|
||||
am Panel würde ihn dessen Abschrägung wegschneiden. */}
|
||||
<div className="hero-card">
|
||||
<span className="hero-frame" aria-hidden="true" />
|
||||
<div className="hero-panel">
|
||||
<div className="hero-lockup">
|
||||
<img src="/brand/lockup.png" alt="D4RKST3R" />
|
||||
</div>
|
||||
{/* Diese Seite liefert der Bot selbst aus — wenn sie
|
||||
ankommt, läuft er. Antwortzeit und Erreichbarkeit
|
||||
stehen bisher nur im Panel, nicht öffentlich. */}
|
||||
<div className="hero-state">
|
||||
<span className="hero-dot" />
|
||||
{t('bot.online')}
|
||||
</div>
|
||||
<div className="hero-stats">
|
||||
<div className="hero-stat">
|
||||
<span className="hero-stat-n">{featureCount ?? '—'}</span>
|
||||
<span className="hero-stat-label">{t('bot.statFeatures')}</span>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span className="hero-stat-n">18</span>
|
||||
<span className="hero-stat-label">{t('bot.statCommands')}</span>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span className="hero-stat-n">300+</span>
|
||||
<span className="hero-stat-label">{t('bot.statGames')}</span>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span className="hero-stat-n">0 €</span>
|
||||
<span className="hero-stat-label">{t('bot.statCost')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<div className="srv-summary">
|
||||
{featureCount && (
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">{featureCount}</span>
|
||||
<span className="srv-sum-label">{t('bot.statFeatures')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">18</span>
|
||||
<span className="srv-sum-label">{t('bot.statCommands')}</span>
|
||||
</div>
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">300<span className="srv-sum-of">+</span></span>
|
||||
<span className="srv-sum-label">{t('bot.statGames')}</span>
|
||||
</div>
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">0<span className="srv-sum-of"> €</span></span>
|
||||
<span className="srv-sum-label">{t('bot.statCost')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="settings-title" style={{ marginTop: '2.5rem' }}>{t('bot.whatItDoes')}</h2>
|
||||
<h2 className="settings-title">
|
||||
{t('bot.whatItDoes')}
|
||||
{featureCount && <span className="section-meta">{t('bot.modulesMeta', featureCount)}</span>}
|
||||
</h2>
|
||||
<div className="home-tiles">
|
||||
{HIGHLIGHTS.map(([Icon, key]) => (
|
||||
<div className="card home-tile" key={key}>
|
||||
<span className="home-tile-icon"><Icon size={24} /></span>
|
||||
<span className="home-tile-icon"><Icon size={18} /></span>
|
||||
<span className="home-tile-title">{t(key)}</span>
|
||||
<span className="home-tile-desc">{t(`${key}.text`)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
|
||||
<h2 className="settings-title">{t('bot.forDevs')}</h2>
|
||||
<div className="home-tiles">
|
||||
<div className="card home-tile">
|
||||
<span className="home-tile-icon"><IconKey size={24} /></span>
|
||||
<span className="home-tile-title">{t('bot.dev.api')}</span>
|
||||
<span className="home-tile-desc">{t('bot.dev.api.text')}</span>
|
||||
</div>
|
||||
<div className="card home-tile">
|
||||
<span className="home-tile-icon"><IconServer size={24} /></span>
|
||||
<span className="home-tile-title">{t('bot.dev.host')}</span>
|
||||
<span className="home-tile-desc">{t('bot.dev.host.text')}</span>
|
||||
</div>
|
||||
<div className="info-panels">
|
||||
<div className="info-panel">
|
||||
<span className="info-panel-tag">{t('bot.forDevs')}</span>
|
||||
<h3 className="info-panel-title">{t('bot.dev.api')}</h3>
|
||||
<p>{t('bot.dev.api.text')}</p>
|
||||
</div>
|
||||
<div className="info-panel warm">
|
||||
<span className="info-panel-tag">{t('bot.hosting')}</span>
|
||||
<h3 className="info-panel-title">{t('bot.dev.host')}</h3>
|
||||
<p>{t('bot.dev.host.text')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hub && (
|
||||
<p className="notice" style={{ marginTop: '2rem' }}>
|
||||
<p className="footnote">
|
||||
{status?.members
|
||||
? t('bot.runsForWith', status.members)
|
||||
: t('bot.runsFor')} —
|
||||
{' '}<a href={hub} style={{ textDecoration: 'underline' }}>{t('bot.toHub')}</a>.
|
||||
{' '}<a href={hub}>{t('bot.toHub')}</a>.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -27,8 +27,6 @@ export default function Changelog() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">CHANGELOG</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('changelog.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.changelog')}</h1>
|
||||
|
||||
@@ -20,10 +20,8 @@ export default function Commits({ me }) {
|
||||
|
||||
const header = (
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">COMMITS</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Privat</div>
|
||||
<div className="page-tag">Privat</div>
|
||||
<h1 className="page-title">Commits</h1>
|
||||
{data && <p className="page-subtitle">{data.total} archivierte Commits über alle Repos.</p>}
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,8 @@ export default function Datenschutz() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">LEGAL</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// DSGVO</div>
|
||||
<div className="page-tag">DSGVO</div>
|
||||
<h1 className="page-title">Datenschutz</h1>
|
||||
<p className="page-subtitle">
|
||||
Welche Daten hier verarbeitet werden, warum — und wie du sie wieder loswirst.
|
||||
@@ -29,7 +27,7 @@ export default function Datenschutz() {
|
||||
</section>
|
||||
|
||||
<section className="content legal-text">
|
||||
<h2 className="settings-title">// 1. Verantwortlicher</h2>
|
||||
<h2 className="settings-title">1. Verantwortlicher</h2>
|
||||
<p>
|
||||
Verantwortlich für die Datenverarbeitung auf dieser Webseite im Sinne der
|
||||
Datenschutz-Grundverordnung (DSGVO) ist:<br /><br />
|
||||
@@ -40,7 +38,7 @@ export default function Datenschutz() {
|
||||
{legal.email && <>E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a></>}
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 2. Aufruf der Webseite (Server-Logs)</h2>
|
||||
<h2 className="settings-title">2. Aufruf der Webseite (Server-Logs)</h2>
|
||||
<p>
|
||||
Beim Aufruf dieser Seite verarbeitet der Server technische Zugriffsdaten:
|
||||
IP-Adresse, Zeitpunkt der Anfrage, aufgerufene Adresse, HTTP-Statuscode und
|
||||
@@ -51,7 +49,7 @@ export default function Datenschutz() {
|
||||
zusammengeführt und nach kurzer Zeit im Rahmen der üblichen Rotation gelöscht.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 3. Cookies</h2>
|
||||
<h2 className="settings-title">3. Cookies</h2>
|
||||
<p>
|
||||
Diese Seite verwendet ausschließlich technisch notwendige Cookies. Konkret
|
||||
wird nach einem Login ein signiertes Sitzungs-Cookie gesetzt, damit du
|
||||
@@ -64,7 +62,7 @@ export default function Datenschutz() {
|
||||
Verbindung zu Dritten.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 4. Login mit Discord</h2>
|
||||
<h2 className="settings-title">4. Login mit Discord</h2>
|
||||
<p>
|
||||
Für den Mitglieder-Bereich kannst du dich freiwillig mit deinem
|
||||
Discord-Konto anmelden (OAuth2, Berechtigung <code>identify</code>). Dabei
|
||||
@@ -85,7 +83,7 @@ export default function Datenschutz() {
|
||||
Datenschutzerklärung von Discord.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 5. Community-Funktionen</h2>
|
||||
<h2 className="settings-title">5. Community-Funktionen</h2>
|
||||
<p>
|
||||
{brand} betreibt einen eigenen Discord-Bot, der zur Verwaltung der Community
|
||||
Daten speichert. Verarbeitet werden — je nachdem, welche Funktionen du nutzt:
|
||||
@@ -133,7 +131,7 @@ export default function Datenschutz() {
|
||||
am Betrieb einer funktionierenden Community) für den technischen Betrieb.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 6. Moderation und Protokolle</h2>
|
||||
<h2 className="settings-title">6. Moderation und Protokolle</h2>
|
||||
<p>
|
||||
Zur Durchsetzung der Serverregeln werden Verwarnungen mit Benutzer-ID, Grund
|
||||
und Zeitpunkt gespeichert. Änderungen im Administrationsbereich der Webseite
|
||||
@@ -142,7 +140,7 @@ export default function Datenschutz() {
|
||||
Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 7. Speicherdauer</h2>
|
||||
<h2 className="settings-title">7. Speicherdauer</h2>
|
||||
<p>
|
||||
Wir speichern personenbezogene Daten so lange, wie du Mitglied der Community
|
||||
bist oder die jeweilige Funktion nutzt. Verlässt du den Discord-Server, kannst
|
||||
@@ -152,14 +150,14 @@ export default function Datenschutz() {
|
||||
werden.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 8. Weitergabe an Dritte</h2>
|
||||
<h2 className="settings-title">8. Weitergabe an Dritte</h2>
|
||||
<p>
|
||||
Eine Weitergabe deiner Daten zu Werbezwecken findet nicht statt. Ein Verkauf
|
||||
von Daten erfolgt nicht. Übermittlungen erfolgen nur an Discord, soweit dies
|
||||
für die Nutzung des Discord-Servers und des Logins technisch notwendig ist.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 9. Deine Rechte</h2>
|
||||
<h2 className="settings-title">9. Deine Rechte</h2>
|
||||
<p>
|
||||
Du hast jederzeit das Recht auf Auskunft über die zu deiner Person
|
||||
gespeicherten Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), auf Löschung
|
||||
@@ -179,14 +177,14 @@ export default function Datenschutz() {
|
||||
{' '}{legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : 'die im Impressum genannte Adresse'}.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 10. Beschwerderecht</h2>
|
||||
<h2 className="settings-title">10. Beschwerderecht</h2>
|
||||
<p>
|
||||
Wenn du der Ansicht bist, dass die Verarbeitung deiner Daten gegen die DSGVO
|
||||
verstößt, kannst du dich bei einer Datenschutz-Aufsichtsbehörde beschweren —
|
||||
etwa bei der für deinen Wohnsitz zuständigen Landesdatenschutzbehörde.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// 11. Änderungen</h2>
|
||||
<h2 className="settings-title">11. Änderungen</h2>
|
||||
<p>
|
||||
Wir passen diese Datenschutzerklärung an, wenn sich die angebotenen Funktionen
|
||||
ändern. Es gilt jeweils die hier veröffentlichte Fassung.
|
||||
|
||||
@@ -41,8 +41,6 @@ export default function DevlogDetail() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">DEVLOG</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('devlog.tag')}</div>
|
||||
<h1 className="page-title">{item ? datum(item.posted_at, DATUM) : t('nav.devlog')}</h1>
|
||||
|
||||
@@ -101,8 +101,6 @@ export default function Devlogs({ me }) {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">DEVLOG</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('devlogs.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.devlog')}</h1>
|
||||
|
||||
@@ -23,8 +23,6 @@ export default function Events() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">EVENTS</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('events.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.events')}</h1>
|
||||
|
||||
@@ -30,8 +30,6 @@ export default function Galerie() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">GALERIE</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('gallery.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.gallery')}</h1>
|
||||
|
||||
+38
-33
@@ -51,56 +51,61 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header home-hero">
|
||||
<div className="page-bg-text">D4RKST3R</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('home.tag')}</div>
|
||||
<h1 className="page-title">
|
||||
{t('home.welcome')}<br />{t('home.at')} D4RKST<span className="home-accent">3</span>R
|
||||
<section className="hero hero-hub">
|
||||
<div className="hero-text">
|
||||
<div className="page-tag dot">{t('home.tag')}</div>
|
||||
<h1 className="hero-title">
|
||||
{t('home.welcome')} {t('home.at')}<br />
|
||||
<span className="grad-text">D4RKST3R</span>
|
||||
</h1>
|
||||
<p className="page-subtitle">{t('home.subtitle')}</p>
|
||||
<p className="hero-sub">{t('home.subtitle')}</p>
|
||||
<div className="home-cta">
|
||||
{invite && (
|
||||
<a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer">
|
||||
<IconMessage /> {t('home.discord')}
|
||||
</a>
|
||||
)}
|
||||
<Link className="btn btn-ghost home-cta-alt with-icon" to="/devlogs">
|
||||
<Link className="btn btn-ghost with-icon" to="/devlogs">
|
||||
<IconDevlog /> {t('home.readDevlogs')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{(stats || servers) && (
|
||||
<div className="hero-figures">
|
||||
{stats?.members > 0 && (
|
||||
<div>
|
||||
<span className="hero-figure-n">{zahl(stats.members)}</span>
|
||||
<span className="hero-figure-label">{t('home.members')}</span>
|
||||
</div>
|
||||
)}
|
||||
{servers?.length > 0 && (
|
||||
<div>
|
||||
<span className="hero-figure-n">
|
||||
{online.length}<span className="hero-figure-of"> / {servers.length}</span>
|
||||
</span>
|
||||
<span className="hero-figure-label">{t('home.serversOnline')}</span>
|
||||
</div>
|
||||
)}
|
||||
{online.length > 0 && (
|
||||
<div>
|
||||
<span className="hero-figure-n">{players}</span>
|
||||
<span className="hero-figure-label">{t('home.playersNow')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hero-logo">
|
||||
<img src="/brand/lockup.png" alt="D4RKST3R" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{(stats || servers) && (
|
||||
<div className="srv-summary home-stats">
|
||||
{stats?.members > 0 && (
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">{zahl(stats.members)}</span>
|
||||
<span className="srv-sum-label">{t('home.members')}</span>
|
||||
</div>
|
||||
)}
|
||||
{servers?.length > 0 && (
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">{online.length}<span className="srv-sum-of"> / {servers.length}</span></span>
|
||||
<span className="srv-sum-label">{t('home.serversOnline')}</span>
|
||||
</div>
|
||||
)}
|
||||
{online.length > 0 && (
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">{players}</span>
|
||||
<span className="srv-sum-label">{t('home.playersNow')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="home-tiles">
|
||||
{TILES.map(([to, Icon, titleKey, descKey]) => (
|
||||
<Link className="card home-tile" to={to} key={to}>
|
||||
<span className="home-tile-icon"><Icon size={26} /></span>
|
||||
<span className="home-tile-icon"><Icon size={18} /></span>
|
||||
<span className="home-tile-title">{t(titleKey)}</span>
|
||||
<span className="home-tile-desc">{t(descKey)}</span>
|
||||
</Link>
|
||||
|
||||
@@ -17,10 +17,8 @@ export default function Impressum() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">LEGAL</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Pflichtangaben</div>
|
||||
<div className="page-tag">Pflichtangaben</div>
|
||||
<h1 className="page-title">Impressum</h1>
|
||||
<p className="page-subtitle">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p>
|
||||
</div>
|
||||
@@ -34,7 +32,7 @@ export default function Impressum() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h2 className="settings-title">// Diensteanbieter</h2>
|
||||
<h2 className="settings-title">Diensteanbieter</h2>
|
||||
<p>
|
||||
{legal.name || '—'}<br />
|
||||
{(legal.address || '').split('\n').map((line, i) => (
|
||||
@@ -42,25 +40,25 @@ export default function Impressum() {
|
||||
))}
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// Kontakt</h2>
|
||||
<h2 className="settings-title">Kontakt</h2>
|
||||
<p>
|
||||
E-Mail: {legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : '—'}
|
||||
</p>
|
||||
|
||||
{legal.extra && (
|
||||
<>
|
||||
<h2 className="settings-title">// Weitere Angaben</h2>
|
||||
<h2 className="settings-title">Weitere Angaben</h2>
|
||||
<p>{legal.extra.split('\n').map((line, i) => <span key={i}>{line}<br /></span>)}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="settings-title">// Verantwortlich für den Inhalt</h2>
|
||||
<h2 className="settings-title">Verantwortlich für den Inhalt</h2>
|
||||
<p>
|
||||
Verantwortlich für journalistisch-redaktionelle Inhalte nach § 18 Abs. 2 MStV:
|
||||
{' '}{legal.name || '—'}{legal.address ? `, ${legal.address.replace(/\n/g, ', ')}` : ''}
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// Haftung für Inhalte</h2>
|
||||
<h2 className="settings-title">Haftung für Inhalte</h2>
|
||||
<p>
|
||||
Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den
|
||||
allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet,
|
||||
@@ -73,7 +71,7 @@ export default function Impressum() {
|
||||
entfernen wir diese Inhalte umgehend.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// Haftung für Links</h2>
|
||||
<h2 className="settings-title">Haftung für Links</h2>
|
||||
<p>
|
||||
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte
|
||||
wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch
|
||||
@@ -82,7 +80,7 @@ export default function Impressum() {
|
||||
von Rechtsverletzungen entfernen wir derartige Links umgehend.
|
||||
</p>
|
||||
|
||||
<h2 className="settings-title">// Urheberrecht</h2>
|
||||
<h2 className="settings-title">Urheberrecht</h2>
|
||||
<p>
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
|
||||
unterliegen dem deutschen Urheberrecht. Beiträge Dritter — insbesondere
|
||||
|
||||
@@ -60,8 +60,6 @@ export default function Level() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">LEVEL</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('level.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.level')}</h1>
|
||||
|
||||
@@ -49,8 +49,6 @@ export default function Mods() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">MODS</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('mods.tag')}</div>
|
||||
<h1 className="page-title">{daten?.server?.name ?? t('mods.title')}</h1>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Profil({ me }) {
|
||||
return (
|
||||
<section className="page-header">
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Profil</div>
|
||||
<div className="page-tag">Profil</div>
|
||||
<h1 className="page-title">Nicht eingeloggt</h1>
|
||||
<p className="page-subtitle">
|
||||
<a href="/auth/login" style={{ textDecoration: 'underline' }}>Mit Discord einloggen</a>, um dein Profil zu sehen.
|
||||
@@ -87,10 +87,8 @@ export default function Profil({ me }) {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">PROFIL</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Member</div>
|
||||
<div className="page-tag">Member</div>
|
||||
<h1 className="page-title">Profil</h1>
|
||||
<p className="page-subtitle">Dein Stand auf dem Server — Rang, Rollen und Keys.</p>
|
||||
</div>
|
||||
@@ -145,7 +143,7 @@ export default function Profil({ me }) {
|
||||
{/* Alpha-Key */}
|
||||
{profile.alphaKey && (
|
||||
<div className="roadmap-group">
|
||||
<h2 className="settings-title">// Dein Alpha-Key</h2>
|
||||
<h2 className="settings-title">Dein Alpha-Key</h2>
|
||||
<div className="field-row">
|
||||
<code className="alpha-key">{profile.alphaKey}</code>
|
||||
<button className="btn btn-mini" onClick={copyKey}>
|
||||
@@ -158,7 +156,7 @@ export default function Profil({ me }) {
|
||||
{/* Rollen-Selfservice */}
|
||||
{menus.length > 0 && (
|
||||
<div className="roadmap-group">
|
||||
<h2 className="settings-title">// Rollen wählen</h2>
|
||||
<h2 className="settings-title">Rollen wählen</h2>
|
||||
{menus.map((m) => (
|
||||
<div className="rolemenu-box" key={m.id}>
|
||||
<h3 className="rolemenu-title">{m.title}</h3>
|
||||
@@ -183,7 +181,7 @@ export default function Profil({ me }) {
|
||||
|
||||
{/* DSGVO Art. 17: eigene Daten löschen */}
|
||||
<div className="roadmap-group danger-zone">
|
||||
<h2 className="settings-title">// Meine Daten</h2>
|
||||
<h2 className="settings-title">Meine Daten</h2>
|
||||
<p className="field-hint">
|
||||
Du kannst jederzeit alle Daten löschen, die hier zu dir gespeichert
|
||||
sind: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen,
|
||||
|
||||
@@ -191,8 +191,6 @@ export default function Roadmap() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">ROADMAP</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('roadmap.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.roadmap')}</h1>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Seite() {
|
||||
return (
|
||||
<section className="page-header">
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// 404</div>
|
||||
<div className="page-tag">404</div>
|
||||
<h1 className="page-title">Seite nicht gefunden</h1>
|
||||
<p className="page-subtitle">
|
||||
<Link to="/" style={{ textDecoration: 'underline' }}>Zurück zur Startseite</Link>
|
||||
@@ -34,10 +34,8 @@ export default function Seite() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">{(page?.title ?? '').slice(0, 12).toUpperCase()}</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// {page?.published === false ? 'Entwurf' : 'Info'}</div>
|
||||
<div className="page-tag">{page?.published === false ? 'Entwurf' : 'Info'}</div>
|
||||
<h1 className="page-title">{page?.title ?? ' '}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -251,8 +251,6 @@ export default function Server() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">SERVER</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('server.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.server')}</h1>
|
||||
|
||||
+111
-96
@@ -1248,14 +1248,19 @@ export default function Settings({ me }) {
|
||||
entries: menuDraft.entries.map((e, j) => (j === i ? { ...e, ...patch } : e)),
|
||||
});
|
||||
|
||||
// Kopf des Panels: eine Zeile statt eines eigenen Seitenkopfs — darunter
|
||||
// beginnt sofort die Arbeitsfläche. Die Antwortzeit steht nur hier, weil
|
||||
// sie aus der Panel-API kommt und nicht öffentlich ist.
|
||||
const header = (
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">CONFIG</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Admin</div>
|
||||
<h1 className="page-title">Config</h1>
|
||||
<p className="page-subtitle">Alles an einem Ort — Änderungen greifen sofort.</p>
|
||||
<section className="page-header wide">
|
||||
<div className="page-header-content dash-title-row">
|
||||
<h1 className="page-title dash-title">Dash<span className="grad-text">board</span></h1>
|
||||
<span className="dash-meta">Alles an einem Ort — Änderungen greifen sofort</span>
|
||||
{data?.status && (
|
||||
<span className="status-pill">
|
||||
Bot online{data.status.ping != null && ` · ${data.status.ping} ms`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -1337,11 +1342,11 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabStatus = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<div className="settings-section dash-panel">
|
||||
<div className="dash-head">
|
||||
{data.status.botAvatar
|
||||
? <img className="dash-avatar" src={data.status.botAvatar} alt="" />
|
||||
: <span className="dash-avatar leer">?</span>}
|
||||
: <img className="dash-avatar mark" src="/brand/mark.png" alt="" />}
|
||||
<div className="dash-ident">
|
||||
<span className="dash-name">
|
||||
{data.status.botName ?? 'Bot'}
|
||||
@@ -1402,55 +1407,65 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aktivitaet && aktivitaet.gesamt > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">
|
||||
// Aktivität — {aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten in 30 Tagen
|
||||
</h2>
|
||||
<div className="dash-spark">
|
||||
{aktivitaet.tage.map((t) => (
|
||||
<span
|
||||
key={t.key}
|
||||
className="dash-bar"
|
||||
style={{ height: `${Math.max(3, (t.n / aktivitaet.max) * 100)}%` }}
|
||||
title={`${t.key}: ${t.n} Nachrichten`}
|
||||
/>
|
||||
))}
|
||||
{/* Verlauf und Modul-Verteilung stehen nebeneinander — beides sind
|
||||
Übersichten, keine Formulare */}
|
||||
<div className="dash-cols">
|
||||
{aktivitaet && aktivitaet.gesamt > 0 && (
|
||||
<div className="settings-section dash-panel">
|
||||
<h2 className="settings-title">
|
||||
Aktivität
|
||||
<span className="section-meta">
|
||||
{aktivitaet.gesamt.toLocaleString('de-DE')} Nachrichten / 30 Tage
|
||||
</span>
|
||||
</h2>
|
||||
<div className="dash-spark">
|
||||
{aktivitaet.tage.map((t) => (
|
||||
<span
|
||||
key={t.key}
|
||||
className="dash-bar"
|
||||
style={{ height: `${Math.max(3, (t.n / aktivitaet.max) * 100)}%` }}
|
||||
title={`${t.key}: ${t.n} Nachrichten`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{modules.length > 0 && moduleGroups.length > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Module nach Bereich</h2>
|
||||
<div className="dash-groups">
|
||||
{moduleGroups.map((g) => {
|
||||
const drin = modules.filter((m) => m.group === g.id);
|
||||
if (drin.length === 0) return null;
|
||||
const an = drin.filter((m) => m.enabled).length;
|
||||
const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length;
|
||||
return (
|
||||
<button
|
||||
className="dash-group"
|
||||
key={g.id}
|
||||
onClick={() => setTab('module')}
|
||||
title={`${g.label} im Modul-Überblick öffnen`}
|
||||
>
|
||||
<span className="dash-group-name">{g.label}</span>
|
||||
<span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}>
|
||||
<i style={{ width: `${(an / drin.length) * 100}%` }} />
|
||||
</span>
|
||||
<span className="dash-group-n">{an}/{drin.length}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{modules.length > 0 && moduleGroups.length > 0 && (
|
||||
<div className="settings-section dash-panel">
|
||||
<h2 className="settings-title">
|
||||
Module
|
||||
<span className="section-meta">{activeModules.length}/{modules.length} an</span>
|
||||
</h2>
|
||||
<div className="dash-groups">
|
||||
{moduleGroups.map((g) => {
|
||||
const drin = modules.filter((m) => m.group === g.id);
|
||||
if (drin.length === 0) return null;
|
||||
const an = drin.filter((m) => m.enabled).length;
|
||||
const offen = drin.filter((m) => m.enabled && m.missing.length > 0).length;
|
||||
return (
|
||||
<button
|
||||
className="dash-group"
|
||||
key={g.id}
|
||||
onClick={() => setTab('module')}
|
||||
title={`${g.label} im Modul-Überblick öffnen`}
|
||||
>
|
||||
<span className="dash-group-name">{g.label}</span>
|
||||
<span className={`dash-group-bar${offen > 0 ? ' warn' : ''}`}>
|
||||
<i style={{ width: `${(an / drin.length) * 100}%` }} />
|
||||
</span>
|
||||
<span className="dash-group-n">{an}/{drin.length}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{modules.length > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Noch einzurichten</h2>
|
||||
<h2 className="settings-title">Noch einzurichten</h2>
|
||||
{unfinished.length === 0 ? (
|
||||
<p className="section-intro todo-done with-icon">
|
||||
<IconCheck size={15} /> Alles eingerichtet — jedes eingeschaltete Modul hat, was es braucht.
|
||||
@@ -1567,7 +1582,7 @@ export default function Settings({ me }) {
|
||||
const willkommensKarte = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Willkommen</h2>
|
||||
<h2 className="settings-title">Willkommen</h2>
|
||||
<div className="field" data-setting="welcome_channel_id">
|
||||
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label>
|
||||
<PickSelect
|
||||
@@ -1580,7 +1595,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Willkommens-Karte</h2>
|
||||
<h2 className="settings-title">Willkommens-Karte</h2>
|
||||
<p className="section-intro">
|
||||
Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau
|
||||
zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten.
|
||||
@@ -1714,7 +1729,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Embeds</h2>
|
||||
<h2 className="settings-title">Embeds</h2>
|
||||
<div className="field" data-setting="brand_name">
|
||||
<label>Brand-Name <span className="field-hint">erscheint in allen Embed-Footern („NAME // DEVLOG")</span></label>
|
||||
<input type="text" placeholder="D4RKST3R" value={form.brand_name} onChange={(e) => setForm({ ...form, brand_name: e.target.value })} />
|
||||
@@ -1730,7 +1745,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Aus der Env verlagert</h2>
|
||||
<h2 className="settings-title">Aus der Env verlagert</h2>
|
||||
<div className="field" data-setting="discord_guild_id">
|
||||
<label>Guild-ID <span className="field-hint">für sofortige Slash-Commands; greift beim nächsten Neustart</span></label>
|
||||
<input type="text" placeholder="Server-ID" value={form.discord_guild_id} onChange={(e) => setForm({ ...form, discord_guild_id: e.target.value })} />
|
||||
@@ -1751,7 +1766,7 @@ export default function Settings({ me }) {
|
||||
const tabFeeds = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Kanäle</h2>
|
||||
<h2 className="settings-title">Kanäle</h2>
|
||||
<div className="field" data-setting="devlog_channel_id">
|
||||
<label>Devlog-Kanal <span className="field-hint">öffentlich — hier postet der Bot die Devlogs</span></label>
|
||||
<div className="field-row">
|
||||
@@ -1765,7 +1780,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Twitch & YouTube</h2>
|
||||
<h2 className="settings-title">Twitch & YouTube</h2>
|
||||
<div className="field" data-setting="social_announce_channel_id">
|
||||
<label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + ▶️ neue YouTube-Videos</span></label>
|
||||
<PickSelect
|
||||
@@ -1817,7 +1832,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Devlog</h2>
|
||||
<h2 className="settings-title">Devlog</h2>
|
||||
<div className="field" data-setting="devlog_ping_role_id">
|
||||
<label>Ping-Rolle <span className="field-hint">wird bei jedem Devlog erwähnt; 🔔-Button am Post — leer = kein Ping</span></label>
|
||||
<PickSelect
|
||||
@@ -1847,7 +1862,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Commit-Feed</h2>
|
||||
<h2 className="settings-title">Commit-Feed</h2>
|
||||
<div className="field" data-setting="commit_feed_enabled">
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} />
|
||||
@@ -1870,7 +1885,7 @@ export default function Settings({ me }) {
|
||||
const tabCommunity = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Wünsche</h2>
|
||||
<h2 className="settings-title">Wünsche</h2>
|
||||
<p className="section-intro">
|
||||
Ein Wunsch ohne sichtbaren Ausgang ist ein Wunsch, den beim nächsten Mal
|
||||
niemand mehr einreicht. Der Stand steht auf der Roadmap und im Discord-Post;
|
||||
@@ -1966,7 +1981,7 @@ export default function Settings({ me }) {
|
||||
)}
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Wunsch-Bereiche</h2>
|
||||
<h2 className="settings-title">Wunsch-Bereiche</h2>
|
||||
<p className="section-intro">
|
||||
Schubladen für Ideen — „Fahrzeuge", „Wirtschaft", „Technik". Ohne Bereiche
|
||||
bleibt die Liste eine Liste; mit ihnen kann man auf der Roadmap filtern, und
|
||||
@@ -2034,7 +2049,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Geburtstage</h2>
|
||||
<h2 className="settings-title">Geburtstage</h2>
|
||||
<div className="field" data-setting="birthday_channel_id">
|
||||
<label>Geburtstags-Kanal <span className="field-hint">Member tragen sich mit /geburtstag ein; Gratulation morgens ab 09:00 — leer = aus</span></label>
|
||||
<PickSelect
|
||||
@@ -2056,7 +2071,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Events</h2>
|
||||
<h2 className="settings-title">Events</h2>
|
||||
<div className="field" data-setting="events_announce_channel_id">
|
||||
<label>Event-Ankündigungs-Kanal <span className="field-hint">neue Discord-Events werden hier angekündigt + Events-Seite</span></label>
|
||||
<PickSelect
|
||||
@@ -2069,7 +2084,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Starboard & Galerie</h2>
|
||||
<h2 className="settings-title">Starboard & Galerie</h2>
|
||||
<div className="field" data-setting="starboard_channel_id">
|
||||
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug ⭐ landen hier</span></label>
|
||||
<div className="field-row">
|
||||
@@ -2103,7 +2118,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Level-System</h2>
|
||||
<h2 className="settings-title">Level-System</h2>
|
||||
<div className="field" data-setting="levels_enabled">
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} />
|
||||
@@ -2125,7 +2140,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Radio</h2>
|
||||
<h2 className="settings-title">Radio</h2>
|
||||
{/* Der Schalter steht hier und nicht nur im Modul-Reiter:
|
||||
wer die Sender pflegt, sucht ihn genau an dieser Stelle. */}
|
||||
<div className="field" data-setting="radio_enabled">
|
||||
@@ -2224,7 +2239,7 @@ export default function Settings({ me }) {
|
||||
const tabRollen = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Rollen bei Join</h2>
|
||||
<h2 className="settings-title">Rollen bei Join</h2>
|
||||
<div className="field" data-setting="autorole_id">
|
||||
<label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch — leer = aus</span></label>
|
||||
<PickSelect
|
||||
@@ -2247,7 +2262,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Rollen-Menüs</h2>
|
||||
<h2 className="settings-title">Rollen-Menüs</h2>
|
||||
<p className="section-intro">
|
||||
Selbstbedienungs-Rollen wie bei Carl-bot — nur ohne Paywall. Der Bot postet
|
||||
ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind
|
||||
@@ -2468,7 +2483,7 @@ export default function Settings({ me }) {
|
||||
const tabSupport = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Ticket-Anliegen</h2>
|
||||
<h2 className="settings-title">Ticket-Anliegen</h2>
|
||||
<p className="section-intro">
|
||||
Ohne Anliegen hat der Ticket-Post einen Knopf und jedes Ticket sieht gleich aus.
|
||||
Mit Anliegen wird daraus ein Auswahlmenü: der Titel steht im Thread-Namen,
|
||||
@@ -2563,7 +2578,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
{automod && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// AutoMod</h2>
|
||||
<h2 className="settings-title">AutoMod</h2>
|
||||
<p className="section-intro">
|
||||
Discords eigener Filter — er läuft serverseitig und ohne Umweg über den Bot.
|
||||
Hier siehst du, was eingerichtet ist, und kannst Regeln schalten.
|
||||
@@ -2618,7 +2633,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Moderation & Kontakt</h2>
|
||||
<h2 className="settings-title">Moderation & Kontakt</h2>
|
||||
<div className="field" data-setting="modmail_channel_id">
|
||||
<label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal — DMs an den Bot landen hier als Threads</span></label>
|
||||
<PickSelect
|
||||
@@ -2653,7 +2668,7 @@ export default function Settings({ me }) {
|
||||
const tabPlaytester = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Playtester</h2>
|
||||
<h2 className="settings-title">Playtester</h2>
|
||||
<p className="section-intro">
|
||||
Beworben wird sich über ein Formular, angenommen vom Team — wer angenommen
|
||||
wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt
|
||||
@@ -2729,7 +2744,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Schlüssel-Vorrat</h2>
|
||||
<h2 className="settings-title">Schlüssel-Vorrat</h2>
|
||||
<div className="field-row" style={{ marginBottom: '1rem' }}>
|
||||
<button className="btn" onClick={() => setKeyDialog(true)}>
|
||||
<IconKey size={14} /> Schlüssel nachlegen
|
||||
@@ -2835,7 +2850,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabBewerbungen = (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Bewerbungs-Formulare</h2>
|
||||
<h2 className="settings-title">Bewerbungs-Formulare</h2>
|
||||
<p className="section-intro">
|
||||
Button-Post → Discord-Formular (bis 5 Fragen) → Review mit ✅/❌ im Staff-Kanal.
|
||||
Annahme vergibt optional eine Rolle — z. B. für FiveM-Whitelist oder Team-Bewerbungen.
|
||||
@@ -2937,7 +2952,7 @@ export default function Settings({ me }) {
|
||||
<EmbedComposer channelOptions={channelOptions} flash={flash} />
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Geplante Posts</h2>
|
||||
<h2 className="settings-title">Geplante Posts</h2>
|
||||
{scheduled.length > 0 && (
|
||||
<div className="key-list" style={{ marginBottom: '1.2rem' }}>
|
||||
{scheduled.map((p) => (
|
||||
@@ -2996,7 +3011,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Triggers</h2>
|
||||
<h2 className="settings-title">Triggers</h2>
|
||||
<p className="section-intro">
|
||||
Auto-Antworten: Enthält eine Nachricht das Keyword, antwortet der Bot (30s-Cooldown pro Kanal).
|
||||
</p>
|
||||
@@ -3022,7 +3037,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Tags (/tag)</h2>
|
||||
<h2 className="settings-title">Tags (/tag)</h2>
|
||||
<p className="section-intro">
|
||||
Text-Bausteine für wiederkehrende Antworten — jeder Member kann sie mit <code>/tag name</code> abrufen.
|
||||
</p>
|
||||
@@ -3052,7 +3067,7 @@ export default function Settings({ me }) {
|
||||
const tabSystem = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Allgemein</h2>
|
||||
<h2 className="settings-title">Allgemein</h2>
|
||||
<div className="field" data-setting="public_url">
|
||||
<label>Öffentliche URL <span className="field-hint">Basis für Buttons, RSS, OAuth-Redirect</span></label>
|
||||
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.public_url} onChange={(e) => setForm({ ...form, public_url: e.target.value })} />
|
||||
@@ -3064,7 +3079,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Zwei Seiten</h2>
|
||||
<h2 className="settings-title">Zwei Seiten</h2>
|
||||
<p className="section-intro">
|
||||
Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange
|
||||
beide Felder leer sind, läuft alles unverändert auf der öffentlichen URL.
|
||||
@@ -3095,7 +3110,7 @@ export default function Settings({ me }) {
|
||||
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Dienste (Portal)</h2>
|
||||
<h2 className="settings-title">Dienste (Portal)</h2>
|
||||
<p className="section-intro">
|
||||
Erscheinen als Kacheln auf der Startseite — z. B. Gitea, Kanban oder die Cloud.
|
||||
</p>
|
||||
@@ -3135,7 +3150,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Impressum & Datenschutz</h2>
|
||||
<h2 className="settings-title">Impressum & Datenschutz</h2>
|
||||
<p className="field-hint" style={{ marginBottom: '.9rem' }}>
|
||||
Erscheint auf <code>/impressum</code> und <code>/datenschutz</code>. Für § 5 DDG
|
||||
ist eine ladungsfähige Anschrift nötig (kein Postfach) — nur der Owner sieht
|
||||
@@ -3160,7 +3175,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Web-Login (Member)</h2>
|
||||
<h2 className="settings-title">Web-Login (Member)</h2>
|
||||
<div className="field" data-setting="member_gate_enabled">
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={form.member_gate_enabled} onChange={(e) => setForm({ ...form, member_gate_enabled: e.target.checked })} />
|
||||
@@ -3175,7 +3190,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Sprachkanäle</h2>
|
||||
<h2 className="settings-title">Sprachkanäle</h2>
|
||||
<div className="field" data-setting="tempvoice_channel_id">
|
||||
<label>Temp-Voice-Hub <span className="field-hint">„Join to Create": Beitritt erzeugt eigenen Voice-Kanal (Bot braucht Kanäle verwalten + Member verschieben)</span></label>
|
||||
<PickSelect
|
||||
@@ -3188,7 +3203,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Backups & Watchdog</h2>
|
||||
<h2 className="settings-title">Backups & Watchdog</h2>
|
||||
<div className="field" data-setting="backup_enabled">
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={form.backup_enabled} onChange={(e) => setForm({ ...form, backup_enabled: e.target.checked })} />
|
||||
@@ -3226,7 +3241,7 @@ export default function Settings({ me }) {
|
||||
|
||||
{watchdog?.adressen?.length > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Erreichbarkeit</h2>
|
||||
<h2 className="settings-title">Erreichbarkeit</h2>
|
||||
<p className="section-intro">
|
||||
Ein Strich je Stunde über die letzten 7 Tage — grün erreichbar, rot
|
||||
Ausfall, grau keine Messung. Geprüft wird alle {watchdog.intervall} Minuten.
|
||||
@@ -3294,7 +3309,7 @@ export default function Settings({ me }) {
|
||||
|
||||
{watchdog?.stoerungen?.length > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Störungen</h2>
|
||||
<h2 className="settings-title">Störungen</h2>
|
||||
<div className="st-incidents">
|
||||
{watchdog.stoerungen.slice(0, 20).map((v) => (
|
||||
<div className={`st-incident${v.beendet ? '' : ' offen'}`} key={v.id}>
|
||||
@@ -3314,7 +3329,7 @@ export default function Settings({ me }) {
|
||||
const tabApi = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// API-Keys (/api/v1)</h2>
|
||||
<h2 className="settings-title">API-Keys (/api/v1)</h2>
|
||||
{keys.length > 0 && (
|
||||
<div className="key-list">
|
||||
{keys.map((k) => (
|
||||
@@ -3363,7 +3378,7 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Single Sign-On</h2>
|
||||
<h2 className="settings-title">Single Sign-On</h2>
|
||||
<p className="section-intro">
|
||||
Andere Dienste (Kanban, Platform …) können den Discord-Login mitnutzen: Sie
|
||||
leiten auf <code>/sso/authorize</code> weiter und tauschen den Token per
|
||||
@@ -3412,7 +3427,7 @@ export default function Settings({ me }) {
|
||||
const tabServer = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Game-Server</h2>
|
||||
<h2 className="settings-title">Game-Server</h2>
|
||||
<p className="section-intro">
|
||||
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
|
||||
Spieler-Balken, Map, Adresse) — aktualisiert alle 2 Minuten, DiscordGSM-Style.
|
||||
@@ -3684,7 +3699,7 @@ export default function Settings({ me }) {
|
||||
/>
|
||||
) : (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Module</h2>
|
||||
<h2 className="settings-title">Module</h2>
|
||||
<p className="section-intro">
|
||||
Jede Funktion des Bots lässt sich hier ein- und ausschalten. Ausgeschaltete
|
||||
Module reagieren auf nichts mehr — keine Posts, keine Befehle, keine
|
||||
@@ -3767,7 +3782,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabTexte = (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Texte</h2>
|
||||
<h2 className="settings-title">Texte</h2>
|
||||
<p className="section-intro">
|
||||
Alle Texte am Stück — wer nur die einer Funktion sucht, findet sie
|
||||
auch auf deren Modul-Seite. Zeile anklicken zum Bearbeiten —
|
||||
@@ -3875,7 +3890,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabWerte = (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Werte</h2>
|
||||
<h2 className="settings-title">Werte</h2>
|
||||
<p className="section-intro">
|
||||
Alle Werte am Stück; jeder steht zusätzlich auf der Seite seiner
|
||||
Funktion. Zahlen, die das Verhalten steuern — wie viel XP eine Nachricht bringt,
|
||||
@@ -3954,7 +3969,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabSeiten = (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Eigene Seiten</h2>
|
||||
<h2 className="settings-title">Eigene Seiten</h2>
|
||||
<p className="section-intro">
|
||||
Inhalte pflegen, ohne dass jemand Code anfassen muss — Regeln, Über uns,
|
||||
Mitmachen, FAQ. Geschrieben wird in Markdown (wie im Devlog), rechts siehst
|
||||
@@ -4130,7 +4145,7 @@ export default function Settings({ me }) {
|
||||
const tabTeam = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Team</h2>
|
||||
<h2 className="settings-title">Team</h2>
|
||||
<p className="section-intro">
|
||||
Team-Mitglieder loggen sich ganz normal mit Discord ein und sehen nur ihre
|
||||
Bereiche. Brand, System, API-Keys und diese Verwaltung bleiben immer dir
|
||||
@@ -4230,7 +4245,7 @@ export default function Settings({ me }) {
|
||||
|
||||
{audit.length > 0 && (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Protokoll</h2>
|
||||
<h2 className="settings-title">Protokoll</h2>
|
||||
<p className="section-intro">
|
||||
Wer hat was im Webinterface geändert.
|
||||
</p>
|
||||
|
||||
@@ -49,8 +49,6 @@ export default function Status() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">STATUS</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('status.tag')}</div>
|
||||
<h1 className="page-title">{t('nav.status')}</h1>
|
||||
|
||||
@@ -55,8 +55,6 @@ export default function WunschDetail() {
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">IDEE</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">{t('wunsch.tag')}</div>
|
||||
<h1 className="page-title">{w?.idea ?? '…'}</h1>
|
||||
|
||||
+851
-599
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user