From 1afbe9d238b705561fe8f6c5ca24b6f9905ce88b Mon Sep 17 00:00:00 2001
From: D4rkst3r
Date: Fri, 31 Jul 2026 14:37:25 +0200
Subject: [PATCH] Webinterface: Emojis durch Lucide-Icons ersetzt
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Emojis werden vom Betriebssystem gerendert — auf Windows, Mac und Android
sehen sie unterschiedlich aus, und sie sind bunt. Im streng zweifarbigen
D4RKST3R-Look stachen sie entsprechend heraus.
Jetzt kommen die Symbole aus einem einzigen Set (Lucide, dünne Outlines),
zentral in src/icons.jsx gebündelt: eine Größe, eine Strichstärke, überall
gleich — und sie erben die Textfarbe, sind also Teil der Marke statt
Fremdkörper. Betroffen sind Navigation, Startseite, Server, Roadmap,
Profil, Level, Galerie, Devlogs, Lightbox, Composer und alle Setup-Tabs.
Bewusst Emojis geblieben sind:
- die Spiel-Presets der Server (landen als Icon im Discord-Embed)
- Rollen-Menü-Emojis und Platzhalter für Discord-Inhalte
- beschreibende Hinweise wie „🔔-Button am Post" oder „🟢/🔴 im Embed",
die zeigen, wie etwas in Discord aussehen wird
- die selbst gepflegten Dienst-Icons im Portal
Dazu Detailarbeit: Server-Status als farbiger Punkt statt Emoji, Platz 1–3
der Bestenliste als Pokal in Gold/Silber/Bronze, Pagination mit Chevrons.
Bundle wächst um 19 kB (Tree-Shaking greift, nur die ~70 genutzten Icons).
Co-Authored-By: Claude Fable 5
---
frontend/package-lock.json | 10 +++
frontend/package.json | 1 +
frontend/src/App.jsx | 12 +--
frontend/src/components/EmbedComposer.jsx | 7 +-
frontend/src/components/Lightbox.jsx | 7 +-
frontend/src/icons.jsx | 105 ++++++++++++++++++++++
frontend/src/pages/Changelog.jsx | 7 +-
frontend/src/pages/Commits.jsx | 5 +-
frontend/src/pages/DevlogDetail.jsx | 5 +-
frontend/src/pages/Devlogs.jsx | 11 +--
frontend/src/pages/Events.jsx | 5 +-
frontend/src/pages/Galerie.jsx | 5 +-
frontend/src/pages/Home.jsx | 39 +++++---
frontend/src/pages/Impressum.jsx | 2 +-
frontend/src/pages/Level.jsx | 12 ++-
frontend/src/pages/Profil.jsx | 15 ++--
frontend/src/pages/Roadmap.jsx | 15 ++--
frontend/src/pages/Server.jsx | 17 +++-
frontend/src/pages/Settings.jsx | 43 +++++----
frontend/src/style.css | 51 +++++++++++
20 files changed, 295 insertions(+), 79 deletions(-)
create mode 100644 frontend/src/icons.jsx
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 0dd6ad8..b43ee5d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -11,6 +11,7 @@
"@fontsource/barlow-condensed": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
+ "lucide-react": "^1.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0"
@@ -1541,6 +1542,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz",
+ "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 494ec7a..5aef18c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"@fontsource/barlow-condensed": "^5.3.0",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/share-tech-mono": "^5.3.0",
+ "lucide-react": "^1.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0"
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index b893aac..5fab1ab 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -15,6 +15,7 @@ import Home from './pages/Home.jsx';
import DevlogDetail from './pages/DevlogDetail.jsx';
import Impressum from './pages/Impressum.jsx';
import Datenschutz from './pages/Datenschutz.jsx';
+import { IconChevronDown, IconMenu, IconX, IconLock } from './icons.jsx';
const COMMUNITY_LINKS = [
['/galerie', 'Galerie'],
@@ -35,7 +36,7 @@ function NavDropdown({ label, active, children }) {
return (
setOpen((o) => !o)}>
- {label} ▾
+ {label}
{open &&
setOpen(false)}>{children}
}
@@ -151,7 +152,7 @@ export default function App() {
setDrawer((d) => !d)} aria-label="Menü">
- {drawer ? '✕' : '☰'}
+ {drawer ? : }
@@ -175,8 +176,9 @@ export default function App() {
{gated && (
-
- 🚪 Der Login ist Mitgliedern des Discord-Servers vorbehalten —
+
+
+ Der Login ist Mitgliedern des Discord-Servers vorbehalten —
tritt erst dem Server bei und logg dich dann nochmal ein.
{invite && (
@@ -184,7 +186,7 @@ export default function App() {
Zum Discord
)}
- setGated(false)} title="Ausblenden">✕
+ setGated(false)} title="Ausblenden">
)}
diff --git a/frontend/src/components/EmbedComposer.jsx b/frontend/src/components/EmbedComposer.jsx
index f1696dd..1aa2ab1 100644
--- a/frontend/src/components/EmbedComposer.jsx
+++ b/frontend/src/components/EmbedComposer.jsx
@@ -2,6 +2,7 @@
// mit Live-Vorschau im Discord-Look + speicherbaren Vorlagen.
import { useEffect, useState } from 'react';
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
+import { IconBot, IconSave, IconX } from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
const EMPTY_EMBED = {
@@ -54,7 +55,7 @@ function DiscordPreview({ content, embed, useEmbed }) {
return (
-
🤖
+
d4rkbot APP heute um {new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr
{content.trim() &&
}
@@ -179,8 +180,8 @@ export default function EmbedComposer({ channelOptions, flash }) {
{templates.map((t) =>
{t.name} )}
setTemplateName(e.target.value)} />
-
💾
-
✕
+
+
diff --git a/frontend/src/components/Lightbox.jsx b/frontend/src/components/Lightbox.jsx
index 3947112..fc13ae7 100644
--- a/frontend/src/components/Lightbox.jsx
+++ b/frontend/src/components/Lightbox.jsx
@@ -3,6 +3,7 @@
// die Pfeile stehen NEBEN dem Bild, die Info-Zeile darunter.
import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
+import { IconX, IconChevronLeft, IconChevronRight } from '../icons.jsx';
export default function Lightbox({ images, start = 0, onClose }) {
const [index, setIndex] = useState(start);
@@ -42,10 +43,10 @@ export default function Lightbox({ images, start = 0, onClose }) {
}}
role="dialog" aria-modal="true"
>
-
✕
+
{images.length > 1 && (
-
{ e.stopPropagation(); prev(); }} title="Zurück (←)">‹
+
{ e.stopPropagation(); prev(); }} title="Zurück">
)}
e.stopPropagation()}>
@@ -55,7 +56,7 @@ export default function Lightbox({ images, start = 0, onClose }) {
{images.length > 1 && (
-
{ e.stopPropagation(); next(); }} title="Weiter (→)">›
+
{ e.stopPropagation(); next(); }} title="Weiter">
)}
,
diff --git a/frontend/src/icons.jsx b/frontend/src/icons.jsx
new file mode 100644
index 0000000..21460d7
--- /dev/null
+++ b/frontend/src/icons.jsx
@@ -0,0 +1,105 @@
+// Zentrale Icon-Sammlung (Lucide) — ein Set, eine Strichstärke, überall gleich.
+//
+// Bewusst NICHT ersetzt werden Emojis, die im Discord landen oder von Nutzern
+// kommen: Spiel-Icons der Server-Presets, Rollen-Menü-Emojis, Embed-Inhalte.
+// Discord kennt keine SVGs — dort sind Emojis die richtige Wahl.
+import {
+ Activity, AlertTriangle, Archive, ArrowLeft, ArrowRight, Award, Bell, Bot,
+ Cake, Calendar, Camera, Check, ChevronDown, ChevronLeft, ChevronRight,
+ Clock, Copy, Database, Download, Ticket, FileText, Gamepad2, Gauge, Gift,
+ Globe, Hash, Heart, Image, Key, Layers, Link2, List, Lock, LogOut, Mail,
+ MapPin, Megaphone, MessageSquare, Mic, Monitor, Moon, Package, Palette, UserCog,
+ PartyPopper, Pencil, Play, Plus, RefreshCw, Rocket, Save, Search, Send,
+ Server, Settings, Shield, Sparkles, Star, Tag, Terminal, ThumbsUp, Trash2,
+ TrendingUp, Trophy, Upload, User, Users, Wand2, Wrench, X, Zap,
+} from 'lucide-react';
+
+/** Einheitliche Darstellung: 18px, dünne Linien, am Text ausgerichtet */
+const defaults = { size: 18, strokeWidth: 1.75, 'aria-hidden': true };
+
+const make = (Cmp) => function Icon(props) {
+ return ;
+};
+
+/* ── Bereiche & Navigation ──────────────────────────── */
+export const IconDevlog = make(FileText);
+export const IconRoadmap = make(MapPin);
+export const IconMapPin = make(MapPin);
+export const IconServer = make(Server);
+export const IconGallery = make(Image);
+export const IconLevel = make(TrendingUp);
+export const IconEvents = make(Calendar);
+export const IconChangelog = make(Rocket);
+export const IconCommits = make(Terminal);
+export const IconSetup = make(Settings);
+export const IconProfile = make(User);
+export const IconHome = make(Layers);
+export const IconLogout = make(LogOut);
+export const IconMenu = make(List);
+
+/* ── Setup-Tabs ─────────────────────────────────────── */
+export const IconStatus = make(Gauge);
+export const IconBrand = make(Palette);
+export const IconFeeds = make(Megaphone);
+export const IconCommunity = make(Users);
+export const IconRoles = make(Shield);
+export const IconSupport = make(Ticket);
+export const IconApplications = make(FileText);
+export const IconComposer = make(Pencil);
+export const IconSystem = make(Wrench);
+export const IconApi = make(Key);
+export const IconTeam = make(UserCog);
+
+/* ── Aktionen & Zustände ────────────────────────────── */
+export const IconCheck = make(Check);
+export const IconX = make(X);
+export const IconPlus = make(Plus);
+export const IconTrash = make(Trash2);
+export const IconEdit = make(Pencil);
+export const IconSave = make(Save);
+export const IconSend = make(Send);
+export const IconCopy = make(Copy);
+export const IconSearch = make(Search);
+export const IconRefresh = make(RefreshCw);
+export const IconUpload = make(Upload);
+export const IconDownload = make(Download);
+export const IconLink = make(Link2);
+export const IconPlay = make(Play);
+export const IconWarning = make(AlertTriangle);
+export const IconLock = make(Lock);
+export const IconArrowLeft = make(ArrowLeft);
+export const IconArrowRight = make(ArrowRight);
+export const IconChevronDown = make(ChevronDown);
+export const IconChevronLeft = make(ChevronLeft);
+export const IconChevronRight = make(ChevronRight);
+
+/* ── Inhalte & Features ─────────────────────────────── */
+export const IconBot = make(Bot);
+export const IconBell = make(Bell);
+export const IconBirthday = make(Cake);
+export const IconGiveaway = make(Gift);
+export const IconStar = make(Star);
+export const IconTag = make(Tag);
+export const IconKey = make(Key);
+export const IconTrophy = make(Trophy);
+export const IconAward = make(Award);
+export const IconThumbsUp = make(ThumbsUp);
+export const IconIdea = make(Sparkles);
+export const IconMessage = make(MessageSquare);
+export const IconMail = make(Mail);
+export const IconVoice = make(Mic);
+export const IconGame = make(Gamepad2);
+export const IconMonitor = make(Monitor);
+export const IconDatabase = make(Database);
+export const IconArchive = make(Archive);
+export const IconPackage = make(Package);
+export const IconGlobe = make(Globe);
+export const IconChannel = make(Hash);
+export const IconClock = make(Clock);
+export const IconActivity = make(Activity);
+export const IconCamera = make(Camera);
+export const IconHeart = make(Heart);
+export const IconParty = make(PartyPopper);
+export const IconMagic = make(Wand2);
+export const IconZap = make(Zap);
+export const IconMoon = make(Moon);
diff --git a/frontend/src/pages/Changelog.jsx b/frontend/src/pages/Changelog.jsx
index a6fed42..c629382 100644
--- a/frontend/src/pages/Changelog.jsx
+++ b/frontend/src/pages/Changelog.jsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
+import { IconChevronLeft, IconChevronRight } from '../icons.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
import { Markdown } from '../markdown.jsx';
@@ -52,7 +53,7 @@ export default function Changelog() {
{r.url && (
)}
@@ -61,13 +62,13 @@ export default function Changelog() {
{pages > 1 && (
setPage(page - 1)}>
- ← Neuere
+ Neuere
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
= pages} onClick={() => setPage(page + 1)}>
- Ältere →
+ Ältere
)}
diff --git a/frontend/src/pages/Commits.jsx b/frontend/src/pages/Commits.jsx
index 6462450..c2530a3 100644
--- a/frontend/src/pages/Commits.jsx
+++ b/frontend/src/pages/Commits.jsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
+import { IconChevronLeft, IconChevronRight } from '../icons.jsx';
const dateFmt = new Intl.DateTimeFormat('de-DE', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
@@ -90,13 +91,13 @@ export default function Commits({ me }) {
{pages > 1 && (
setPage(page - 1)}>
- ← Neuere
+ Neuere
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
= pages} onClick={() => setPage(page + 1)}>
- Ältere →
+ Ältere
)}
diff --git a/frontend/src/pages/DevlogDetail.jsx b/frontend/src/pages/DevlogDetail.jsx
index cda6dfd..e46c188 100644
--- a/frontend/src/pages/DevlogDetail.jsx
+++ b/frontend/src/pages/DevlogDetail.jsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { apiGet } from '../api.js';
+import { IconChevronLeft, IconCheck, IconLink } from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
@@ -79,8 +80,8 @@ export default function DevlogDetail() {
- ← Alle Devlogs
- {copied ? '✓ Link kopiert' : '🔗 Link kopieren'}
+ Alle Devlogs
+ {copied ? Link kopiert : Link kopieren }
{lightbox !== null && (
diff --git a/frontend/src/pages/Devlogs.jsx b/frontend/src/pages/Devlogs.jsx
index cff47ee..8272ecb 100644
--- a/frontend/src/pages/Devlogs.jsx
+++ b/frontend/src/pages/Devlogs.jsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { apiGet, apiDelete } from '../api.js';
+import { IconChevronLeft, IconChevronRight, IconSearch, IconLink, IconX } from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
@@ -102,7 +103,7 @@ export default function Devlogs({ me }) {
-
⌕
+
{project &&
{project} }
{relative(d.posted_at)}
-
🔗
+
{me?.admin && (
remove(d.message_id)}
>
- ✕
+
)}
@@ -187,13 +188,13 @@ export default function Devlogs({ me }) {
{pages > 1 && (
setPage(page - 1)}>
- ← Neuere
+ Neuere
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
= pages} onClick={() => setPage(page + 1)}>
- Ältere →
+ Ältere
)}
diff --git a/frontend/src/pages/Events.jsx b/frontend/src/pages/Events.jsx
index 4448d49..58526f2 100644
--- a/frontend/src/pages/Events.jsx
+++ b/frontend/src/pages/Events.jsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
+import { IconStar, IconChevronRight } from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
const dateFmt = new Intl.DateTimeFormat('de-DE', {
@@ -37,7 +38,7 @@ export default function Events() {
{e.cover && }
{e.start ? dateFmt.format(new Date(e.start)) : 'Termin folgt'}
- {e.interested != null && ⭐ {e.interested} interessiert }
+ {e.interested != null && {e.interested} interessiert }
{e.name}
{e.description && (
@@ -46,7 +47,7 @@ export default function Events() {
)}
))}
diff --git a/frontend/src/pages/Galerie.jsx b/frontend/src/pages/Galerie.jsx
index 3f89126..068f6c4 100644
--- a/frontend/src/pages/Galerie.jsx
+++ b/frontend/src/pages/Galerie.jsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
+import { IconChevronLeft, IconChevronRight } from '../icons.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonGrid } from '../components/Skeleton.jsx';
@@ -68,13 +69,13 @@ export default function Galerie() {
{pages > 1 && (
setPage(page - 1)}>
- ← Neuere
+ Neuere
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
= pages} onClick={() => setPage(page + 1)}>
- Ältere →
+ Ältere
)}
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index b1e842a..73138d8 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -4,14 +4,18 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../api.js';
import { Markdown } from '../markdown.jsx';
+import {
+ IconDevlog, IconRoadmap, IconServer, IconGallery, IconLevel, IconEvents,
+ IconMessage, IconArrowRight, IconLink,
+} from '../icons.jsx';
const TILES = [
- ['/devlogs', '📔', 'Devlog', 'Entwicklungs-Updates, frisch aus dem Editor'],
- ['/roadmap', '🗺️', 'Roadmap', 'Meilensteine + Community-Wünsche zum Abstimmen'],
- ['/server', '🎮', 'Server', 'Live-Status aller Game-Server mit Verlauf'],
- ['/galerie', '🖼️', 'Galerie', 'Screenshots aus der Community'],
- ['/level', '📈', 'Level', 'XP-Bestenliste — wer redet am meisten mit?'],
- ['/events', '📅', 'Events', 'Playtests, Streams und was sonst ansteht'],
+ ['/devlogs', IconDevlog, 'Devlog', 'Entwicklungs-Updates, frisch aus dem Editor'],
+ ['/roadmap', IconRoadmap, 'Roadmap', 'Meilensteine + Community-Wünsche zum Abstimmen'],
+ ['/server', IconServer, 'Server', 'Live-Status aller Game-Server mit Verlauf'],
+ ['/galerie', IconGallery, 'Galerie', 'Screenshots aus der Community'],
+ ['/level', IconLevel, 'Level', 'XP-Bestenliste — wer redet am meisten mit?'],
+ ['/events', IconEvents, 'Events', 'Playtests, Streams und was sonst ansteht'],
];
/** Devlog-Text für den Teaser kürzen (erste Sätze, ohne Titel-Zeile) */
@@ -57,8 +61,14 @@ export default function Home() {
an einem Ort, direkt aus dem Discord.
@@ -88,9 +98,9 @@ export default function Home() {
)}
- {TILES.map(([to, icon, title, desc]) => (
+ {TILES.map(([to, Icon, title, desc]) => (
-
{icon}
+
{title}
{desc}
@@ -101,10 +111,13 @@ export default function Home() {
// Dienste
)}
diff --git a/frontend/src/pages/Impressum.jsx b/frontend/src/pages/Impressum.jsx
index a8a4955..e8a7529 100644
--- a/frontend/src/pages/Impressum.jsx
+++ b/frontend/src/pages/Impressum.jsx
@@ -29,7 +29,7 @@ export default function Impressum() {
{incomplete && (
- ⚠️ Die Impressums-Angaben sind noch nicht vollständig hinterlegt
+ ✗ Die Impressums-Angaben sind noch nicht vollständig hinterlegt
(Setup → System → Impressum).
)}
diff --git a/frontend/src/pages/Level.jsx b/frontend/src/pages/Level.jsx
index c41d541..0f77911 100644
--- a/frontend/src/pages/Level.jsx
+++ b/frontend/src/pages/Level.jsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
import { SkeletonRows } from '../components/Skeleton.jsx';
+import { IconTrophy } from '../icons.jsx';
/** Muss der Formel in src/bot/levels.js entsprechen */
function xpForLevel(level) {
@@ -9,7 +10,8 @@ function xpForLevel(level) {
return total;
}
-const MEDALS = ['🥇', '🥈', '🥉'];
+// Platz 1–3 bekommen einen Pokal in Gold/Silber/Bronze, danach schlicht die Nummer
+const MEDAL_COLORS = ['#f5c518', '#c0c4cc', '#c07a3e'];
/** Nachrichten/Tag der letzten 30 Tage als Balken */
function ActivityChart({ stats }) {
@@ -66,7 +68,7 @@ export default function Level() {
{error && Bestenliste konnte nicht geladen werden.
}
{!error && !rows && }
- {rows?.length === 0 && Noch keine XP vergeben — schreibt was im Discord! ✍️
}
+ {rows?.length === 0 && Noch keine XP vergeben — schreibt was im Discord!
}
{rows?.length > 0 && (
@@ -76,7 +78,11 @@ export default function Level() {
const pct = Math.floor(((r.xp - base) / (next - base)) * 100);
return (
-
{MEDALS[i] ?? `#${i + 1}`}
+
+ {i < 3
+ ?
+ : `#${i + 1}`}
+
{r.username ?? r.user_id}
diff --git a/frontend/src/pages/Profil.jsx b/frontend/src/pages/Profil.jsx
index 20d3d02..769be2d 100644
--- a/frontend/src/pages/Profil.jsx
+++ b/frontend/src/pages/Profil.jsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet, apiPost, apiDelete } from '../api.js';
+import { IconCheck, IconMagic } from '../icons.jsx';
function fmtDate(iso) {
if (!iso) return '—';
@@ -52,10 +53,10 @@ export default function Profil({ me }) {
try {
const res = await apiDelete('/api/profile');
const total = Object.values(res.deleted).reduce((a, b) => a + b, 0);
- setWipeMsg(`✅ ${total} Einträge gelöscht.`);
+ setWipeMsg(`✓ ${total} Einträge gelöscht.`);
apiGet('/api/profile').then(setProfile).catch(() => {});
} catch {
- setWipeMsg('⚠️ Löschen fehlgeschlagen — melde dich bitte beim Team.');
+ setWipeMsg('✗ Löschen fehlgeschlagen — melde dich bitte beim Team.');
}
}
@@ -96,7 +97,7 @@ export default function Profil({ me }) {
- {error && ⚠️ {error}
}
+ {error && ✗ {error}
}
{!profile && !error && Lade …
}
{profile && (
@@ -108,7 +109,9 @@ export default function Profil({ me }) {
{profile.user.displayName}
Dabei seit {fmtDate(profile.user.joinedAt)}
- {profile.playtester && 🧪 Playtester }
+ {profile.playtester && (
+ Playtester
+ )}
{profile.roles.length > 0 && (
@@ -145,7 +148,9 @@ export default function Profil({ me }) {
// Dein Alpha-Key
{profile.alphaKey}
- {copied ? '✓ Kopiert' : 'Kopieren'}
+
+ {copied ? Kopiert : 'Kopieren'}
+
)}
diff --git a/frontend/src/pages/Roadmap.jsx b/frontend/src/pages/Roadmap.jsx
index 1cbca4b..21e6638 100644
--- a/frontend/src/pages/Roadmap.jsx
+++ b/frontend/src/pages/Roadmap.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { apiGet, apiPost } from '../api.js';
import { Markdown } from '../markdown.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
+import { IconThumbsUp } from '../icons.jsx';
const dueFmt = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
@@ -104,20 +105,20 @@ export default function Roadmap() {
? w
: { ...w, voted: res.voted, score: w.score + (res.voted ? 1 : -1) }));
} catch (e) {
- if (e.status === 403) setSubmitMsg('⚠️ Voten dürfen nur Discord-Member.');
+ if (e.status === 403) setSubmitMsg('✗ Voten dürfen nur Discord-Member.');
}
}
async function submitWish() {
const text = idea.trim();
- if (text.length < 5) { setSubmitMsg('⚠️ Etwas mehr Text bitte.'); return; }
+ if (text.length < 5) { setSubmitMsg('✗ Etwas mehr Text bitte.'); return; }
try {
await apiPost('/api/wishes', { idea: text });
setIdea('');
- setSubmitMsg('✅ Wunsch eingereicht — er steht jetzt auch im Discord zur Abstimmung!');
+ setSubmitMsg('✓ Wunsch eingereicht — er steht jetzt auch im Discord zur Abstimmung!');
apiGet('/api/wishes').then((d) => setWishes(d.wishes)).catch(() => {});
} catch (e) {
- setSubmitMsg(e.status === 403 ? '⚠️ Einreichen dürfen nur Discord-Member.' : '⚠️ Konnte den Wunsch nicht einreichen.');
+ setSubmitMsg(e.status === 403 ? '✗ Einreichen dürfen nur Discord-Member.' : '✗ Konnte den Wunsch nicht einreichen.');
}
}
@@ -151,14 +152,14 @@ export default function Roadmap() {
{canVote && w.message_id ? (
vote(w.message_id)}
>
- 👍 {w.score}
+ {w.score}
) : (
-
👍 {w.score}
+
{w.score}
)}
{w.idea}
{w.author}
diff --git a/frontend/src/pages/Server.jsx b/frontend/src/pages/Server.jsx
index d62f402..2ccdcbd 100644
--- a/frontend/src/pages/Server.jsx
+++ b/frontend/src/pages/Server.jsx
@@ -4,6 +4,7 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
import { SkeletonCards } from '../components/Skeleton.jsx';
+import { IconGame, IconMapPin, IconClock, IconCheck } from '../icons.jsx';
const timeFmt = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit' });
@@ -68,9 +69,13 @@ function ServerCard({ s }) {
{s.name}
- {s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'}
+ {s.online === null
+ ? <> Warte auf Check>
+ : <> {s.online ? 'Online' : 'Offline'}>}
{s.ping != null && s.online && · {s.ping}ms }
- {s.map && s.online && · 🗺️ {s.map} }
+ {s.map && s.online && (
+ · {s.map}
+ )}
{s.online && s.players != null && (
@@ -102,10 +107,14 @@ function ServerCard({ s }) {
{(s.connect_url || s.address) && (
- {s.connect_url &&
🎮 Connect }
+ {s.connect_url && (
+
+ Connect
+
+ )}
{s.address && (
- {copied ? '✓ Kopiert!' : s.address}
+ {copied ? Kopiert! : s.address}
)}
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
index 52872b6..86ceb8e 100644
--- a/frontend/src/pages/Settings.jsx
+++ b/frontend/src/pages/Settings.jsx
@@ -1,6 +1,11 @@
import { useEffect, useRef, useState } from 'react';
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
import EmbedComposer from '../components/EmbedComposer.jsx';
+import {
+ IconStatus, IconBrand, IconFeeds, IconCommunity, IconRoles, IconSupport,
+ IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
+ IconLock, IconBot, IconUpload, IconChevronDown, IconKey,
+} from '../icons.jsx';
const API_SCOPES = [
{ id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' },
@@ -10,18 +15,18 @@ const API_SCOPES = [
];
const TABS = [
- { id: 'status', icon: '📊', label: 'Status' },
- { id: 'brand', icon: '🎨', label: 'Brand' },
- { id: 'feeds', icon: '📔', label: 'Feeds' },
- { id: 'community', icon: '🙌', label: 'Community' },
- { id: 'rollen', icon: '🎭', label: 'Rollen' },
- { id: 'support', icon: '📬', label: 'Support' },
- { id: 'bewerbungen', icon: '📋', label: 'Bewerbungen' },
- { id: 'composer', icon: '📝', label: 'Composer' },
- { id: 'server', icon: '🎮', label: 'Server' },
- { id: 'system', icon: '⚙️', label: 'System' },
- { id: 'api', icon: '🔑', label: 'API' },
- { id: 'team', icon: '👥', label: 'Team' },
+ { id: 'status', Icon: IconStatus, label: 'Status' },
+ { id: 'brand', Icon: IconBrand, label: 'Brand' },
+ { id: 'feeds', Icon: IconFeeds, label: 'Feeds' },
+ { id: 'community', Icon: IconCommunity, label: 'Community' },
+ { id: 'rollen', Icon: IconRoles, label: 'Rollen' },
+ { id: 'support', Icon: IconSupport, label: 'Support' },
+ { id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen' },
+ { id: 'composer', Icon: IconComposer, label: 'Composer' },
+ { id: 'server', Icon: IconServer, label: 'Server' },
+ { id: 'system', Icon: IconSystem, label: 'System' },
+ { id: 'api', Icon: IconApi, label: 'API' },
+ { id: 'team', Icon: IconTeam, label: 'Team' },
];
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
@@ -574,7 +579,7 @@ export default function Settings({ me }) {
{header}
@@ -622,15 +627,15 @@ export default function Settings({ me }) {
{data.status.botAvatar
?
- :
🤖
}
+ :
}
- ⬆ Bild ändern
+ Bild ändern
uploadImage('avatar', e.target.files?.[0])} />
- 🖼 Banner
+ Banner
uploadImage('banner', e.target.files?.[0])} />
@@ -844,7 +849,7 @@ export default function Settings({ me }) {
onClick={distributeKeys}
disabled={!alphaKeys || alphaKeys.free === 0 || alphaKeys.playtestersWithout.length === 0}
>
- 🎟️ An {alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen
+ An {alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen
{alphaKeys?.assigned?.length > 0 && (
@@ -937,7 +942,7 @@ export default function Settings({ me }) {
// Rollen-Menüs
- Selbstbedienungs-Rollen wie bei Carl-bot — nur ohne Paywall. 😄 Der Bot postet
+ Selbstbedienungs-Rollen wie bei Carl-bot — nur ohne Paywall. Der Bot postet
ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind
jederzeit editierbar, der Discord-Post zieht automatisch mit.
@@ -1712,7 +1717,7 @@ export default function Settings({ me }) {
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
-
{t.icon}
+
{t.label}
))}
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 2640251..4429f21 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -1512,3 +1512,54 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
.btn-danger { border-color: rgba(242, 63, 67, .5); color: #f23f43; }
.btn-danger:hover { border-color: #f23f43; background: rgba(242, 63, 67, .1); }
.home-tile-ext { font-size: .8rem; color: var(--muted2); }
+
+/* ── ICONS (Lucide) ────────────────────────────────── */
+/* Einheitliche Ausrichtung: Icons sitzen auf der Textlinie, erben die Farbe */
+.ic {
+ display: inline-block;
+ vertical-align: -0.18em;
+ flex-shrink: 0;
+}
+/* Text + Icon nebeneinander, ohne Zeilenumbruch dazwischen */
+.with-icon {
+ display: inline-flex;
+ align-items: center;
+ gap: .45rem;
+}
+button.with-icon, a.with-icon { justify-content: center; }
+
+/* Nav-Pfeil des Dropdowns */
+.nav-caret { transition: transform .2s; }
+.nav-drop.open .nav-caret { transform: rotate(180deg); }
+
+/* Setup-Sidebar: Icons in Markenfarbe, aktiver Tab hebt sich ab */
+.settings-nav-icon { display: inline-flex; color: var(--muted); transition: color .2s; }
+.settings-nav-item:hover .settings-nav-icon { color: var(--text); }
+.settings-nav-item.active .settings-nav-icon { color: var(--neon); }
+
+/* Kachel-Icons auf der Startseite */
+.home-tile-icon { color: var(--neon); display: inline-flex; }
+.home-tile:hover .home-tile-icon { color: var(--neon2); }
+.home-tile-emoji { font-size: 1.6rem; line-height: 1; }
+.home-tile-ext { color: var(--muted2); }
+
+/* Server-Status: Punkt statt Emoji */
+.srv-status { flex-wrap: wrap; row-gap: .1rem; }
+.srv-status > span { white-space: nowrap; }
+.srv-dot {
+ width: 8px; height: 8px; border-radius: 50%;
+ background: var(--muted2); display: inline-block; flex-shrink: 0;
+}
+.srv-status.on .srv-dot { background: var(--success); box-shadow: 0 0 8px #00ff8866; }
+.srv-status.off .srv-dot { background: #f23f43; box-shadow: 0 0 8px #f23f4366; }
+.srv-status { display: inline-flex; align-items: center; gap: .4rem; }
+.srv-map, .srv-ping { display: inline-flex; align-items: center; gap: .3rem; }
+
+/* Bestenliste: Pokal statt Medaillen-Emoji */
+.lvl-rank { display: inline-flex; align-items: center; justify-content: center; }
+
+/* Bot-Avatar-Platzhalter */
+.bot-avatar-empty { display: flex; align-items: center; justify-content: center; color: var(--muted); }
+
+/* Discord-Vorschau im Composer */
+.dpv-avatar { display: flex; align-items: center; justify-content: center; color: var(--muted); }