Webinterface: Emojis durch Lucide-Icons ersetzt
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:37:25 +02:00
co-authored by Claude Fable 5
parent 6daa1f7d43
commit 1afbe9d238
20 changed files with 295 additions and 79 deletions
+4 -3
View File
@@ -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() {
</div>
{r.url && (
<div className="card-footer">
<a href={r.url} target="_blank" rel="noreferrer">Release in Gitea ansehen </a>
<a href={r.url} target="_blank" rel="noreferrer">Release in Gitea ansehen <IconChevronRight size={13} /></a>
</div>
)}
</article>
@@ -61,13 +62,13 @@ export default function Changelog() {
{pages > 1 && (
<div className="pager">
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Neuere
<IconChevronLeft size={14} /> Neuere
</button>
<span className="pager-info">
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
</span>
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
Ältere
Ältere <IconChevronRight size={14} />
</button>
</div>
)}
+3 -2
View File
@@ -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 && (
<div className="pager">
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Neuere
<IconChevronLeft size={14} /> Neuere
</button>
<span className="pager-info">
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
</span>
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
Ältere
Ältere <IconChevronRight size={14} />
</button>
</div>
)}
+3 -2
View File
@@ -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() {
</article>
<div className="field-row" style={{ marginTop: '1.2rem' }}>
<Link className="btn" to="/devlogs"> Alle Devlogs</Link>
<button className="btn" onClick={copyLink}>{copied ? '✓ Link kopiert' : '🔗 Link kopieren'}</button>
<Link className="btn with-icon" to="/devlogs"><IconChevronLeft size={14} /> Alle Devlogs</Link>
<button className="btn" onClick={copyLink}>{copied ? <span className="with-icon"><IconCheck size={14} /> Link kopiert</span> : <span className="with-icon"><IconLink size={14} /> Link kopieren</span>}</button>
</div>
{lightbox !== null && (
+6 -5
View File
@@ -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 }) {
<section className="content">
<div className="search">
<span className="search-icon"></span>
<span className="search-icon"><IconSearch size={16} /></span>
<input
type="search"
placeholder="Devlogs durchsuchen …"
@@ -135,14 +136,14 @@ export default function Devlogs({ me }) {
</span>
{project && <span className="card-project">{project}</span>}
<span className="card-rel">{relative(d.posted_at)}</span>
<a className="card-permalink" href={`/devlogs/${d.message_id}`} title="Direktlink zu diesem Devlog">🔗</a>
<a className="card-permalink" href={`/devlogs/${d.message_id}`} title="Direktlink zu diesem Devlog"><IconLink size={14} /></a>
{me?.admin && (
<button
className="card-delete"
title="Aus dem Archiv löschen"
onClick={() => remove(d.message_id)}
>
<IconX size={15} />
</button>
)}
</div>
@@ -187,13 +188,13 @@ export default function Devlogs({ me }) {
{pages > 1 && (
<div className="pager">
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Neuere
<IconChevronLeft size={14} /> Neuere
</button>
<span className="pager-info">
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
</span>
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
Ältere
Ältere <IconChevronRight size={14} />
</button>
</div>
)}
+3 -2
View File
@@ -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 && <a href={e.url} target="_blank" rel="noreferrer"><img className="event-cover" src={e.cover} alt="" /></a>}
<div className="card-meta">
<span className="card-date">{e.start ? dateFmt.format(new Date(e.start)) : 'Termin folgt'}</span>
{e.interested != null && <span className="card-rel"> {e.interested} interessiert</span>}
{e.interested != null && <span className="card-rel with-icon"><IconStar size={13} /> {e.interested} interessiert</span>}
</div>
<h3 className="release-name">{e.name}</h3>
{e.description && (
@@ -46,7 +47,7 @@ export default function Events() {
</div>
)}
<div className="card-footer">
<a href={e.url} target="_blank" rel="noreferrer">Im Discord ansehen </a>
<a href={e.url} target="_blank" rel="noreferrer">Im Discord ansehen <IconChevronRight size={13} /></a>
</div>
</article>
))}
+3 -2
View File
@@ -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 && (
<div className="pager">
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Neuere
<IconChevronLeft size={14} /> Neuere
</button>
<span className="pager-info">
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
</span>
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
Ältere
Ältere <IconChevronRight size={14} />
</button>
</div>
)}
+26 -13
View File
@@ -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.
</p>
<div className="home-cta">
{invite && <a className="btn btn-save" href={invite} target="_blank" rel="noreferrer">💬 Zum Discord</a>}
<Link className="btn btn-ghost home-cta-alt" to="/devlogs">📔 Devlogs lesen</Link>
{invite && (
<a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer">
<IconMessage /> Zum Discord
</a>
)}
<Link className="btn btn-ghost home-cta-alt with-icon" to="/devlogs">
<IconDevlog /> Devlogs lesen
</Link>
</div>
</div>
</section>
@@ -88,9 +98,9 @@ export default function Home() {
)}
<div className="home-tiles">
{TILES.map(([to, icon, title, desc]) => (
{TILES.map(([to, Icon, title, desc]) => (
<Link className="card home-tile" to={to} key={to}>
<span className="home-tile-icon">{icon}</span>
<span className="home-tile-icon"><Icon size={26} /></span>
<span className="home-tile-title">{title}</span>
<span className="home-tile-desc">{desc}</span>
</Link>
@@ -101,10 +111,13 @@ export default function Home() {
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">// Dienste</h2>
<div className="home-tiles">
{/* Dienst-Icons pflegt der Owner selbst — daher bleiben es Emojis */}
{services.map((s) => (
<a className="card home-tile" href={s.url} target="_blank" rel="noreferrer" key={s.id}>
<span className="home-tile-icon">{s.icon}</span>
<span className="home-tile-title">{s.name} <span className="home-tile-ext"></span></span>
<span className="home-tile-icon home-tile-emoji">{s.icon}</span>
<span className="home-tile-title with-icon">
{s.name} <IconLink size={14} className="home-tile-ext" />
</span>
{s.description && <span className="home-tile-desc">{s.description}</span>}
</a>
))}
@@ -119,7 +132,7 @@ export default function Home() {
<div className="card-body">
<Markdown text={teaser(latest.content)} />
</div>
<span className="home-devlog-more">Weiterlesen </span>
<span className="home-devlog-more with-icon">Weiterlesen <IconArrowRight size={14} /></span>
</Link>
</div>
)}
+1 -1
View File
@@ -29,7 +29,7 @@ export default function Impressum() {
<section className="content legal-text">
{incomplete && (
<p className="notice">
Die Impressums-Angaben sind noch nicht vollständig hinterlegt
Die Impressums-Angaben sind noch nicht vollständig hinterlegt
(Setup System Impressum).
</p>
)}
+9 -3
View File
@@ -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 13 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() {
<section className="content">
{error && <p className="notice">Bestenliste konnte nicht geladen werden.</p>}
{!error && !rows && <SkeletonRows n={10} />}
{rows?.length === 0 && <p className="notice">Noch keine XP vergeben — schreibt was im Discord! ✍️</p>}
{rows?.length === 0 && <p className="notice">Noch keine XP vergeben — schreibt was im Discord!</p>}
{rows?.length > 0 && (
<div className="lvl-list">
@@ -76,7 +78,11 @@ export default function Level() {
const pct = Math.floor(((r.xp - base) / (next - base)) * 100);
return (
<div className="lvl-row" key={r.user_id}>
<span className="lvl-rank">{MEDALS[i] ?? `#${i + 1}`}</span>
<span className="lvl-rank">
{i < 3
? <IconTrophy size={18} style={{ color: MEDAL_COLORS[i] }} />
: `#${i + 1}`}
</span>
<span className="lvl-name">{r.username ?? r.user_id}</span>
<div className="lvl-progress">
<div className="progress"><div className="progress-bar" style={{ width: `${pct}%` }} /></div>
+10 -5
View File
@@ -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 }) {
</section>
<section className="content">
{error && <p className="notice"> {error}</p>}
{error && <p className="notice"> {error}</p>}
{!profile && !error && <p className="notice">Lade </p>}
{profile && (
@@ -108,7 +109,9 @@ export default function Profil({ me }) {
<h2 className="profile-name">{profile.user.displayName}</h2>
<p className="profile-meta">
Dabei seit {fmtDate(profile.user.joinedAt)}
{profile.playtester && <span className="profile-badge">🧪 Playtester</span>}
{profile.playtester && (
<span className="profile-badge with-icon"><IconMagic size={14} /> Playtester</span>
)}
</p>
{profile.roles.length > 0 && (
<div className="profile-roles">
@@ -145,7 +148,9 @@ export default function Profil({ me }) {
<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}>{copied ? '✓ Kopiert' : 'Kopieren'}</button>
<button className="btn btn-mini" onClick={copyKey}>
{copied ? <span className="with-icon"><IconCheck size={13} /> Kopiert</span> : 'Kopieren'}
</button>
</div>
</div>
)}
+8 -7
View File
@@ -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() {
<div className="wish-row" key={w.message_id ?? `${w.created_at}-${i}`}>
{canVote && w.message_id ? (
<button
className={`wish-vote${w.voted ? ' voted' : ''}`}
className={`wish-vote with-icon${w.voted ? ' voted' : ''}`}
title={w.voted ? 'Vote zurücknehmen' : 'Dafür stimmen'}
onClick={() => vote(w.message_id)}
>
👍 {w.score}
<IconThumbsUp size={14} /> {w.score}
</button>
) : (
<span className="wish-score">👍 {w.score}</span>
<span className="wish-score with-icon"><IconThumbsUp size={14} /> {w.score}</span>
)}
<span className="wish-idea">{w.idea}</span>
<span className="wish-author">{w.author}</span>
+13 -4
View File
@@ -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 }) {
<div className="srv-title">
<span className="srv-name">{s.name}</span>
<span className={`srv-status ${s.online ? 'on' : s.online === false ? 'off' : ''}`}>
{s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'}
{s.online === null
? <><IconClock size={13} /> Warte auf Check</>
: <><span className="srv-dot" /> {s.online ? 'Online' : 'Offline'}</>}
{s.ping != null && s.online && <span className="srv-ping"> · {s.ping}ms</span>}
{s.map && s.online && <span className="srv-map"> · 🗺 {s.map}</span>}
{s.map && s.online && (
<span className="srv-map"> · <IconMapPin size={13} /> {s.map}</span>
)}
</span>
</div>
{s.online && s.players != null && (
@@ -102,10 +107,14 @@ function ServerCard({ s }) {
{(s.connect_url || s.address) && (
<div className="srv-connect">
{s.connect_url && <a className="btn btn-save" href={s.connect_url}>🎮 Connect</a>}
{s.connect_url && (
<a className="btn btn-save with-icon" href={s.connect_url}>
<IconGame size={15} /> Connect
</a>
)}
{s.address && (
<button className="alpha-key srv-addr" onClick={copyAddress} title="Adresse kopieren">
{copied ? '✓ Kopiert!' : s.address}
{copied ? <span className="with-icon"><IconCheck size={14} /> Kopiert!</span> : s.address}
</button>
)}
</div>
+24 -19
View File
@@ -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}
<section className="content">
<div className="notice center">
<p>🔒 Nur für den Admin.</p>
<p className="with-icon"><IconLock /> Nur für den Admin.</p>
{!me.user && <a className="btn" href="/auth/login">Login mit Discord</a>}
</div>
</section>
@@ -622,15 +627,15 @@ export default function Settings({ me }) {
<div className="bot-avatar-wrap">
{data.status.botAvatar
? <img className="bot-avatar" src={data.status.botAvatar} alt="Bot-Avatar" />
: <div className="bot-avatar bot-avatar-empty">🤖</div>}
: <div className="bot-avatar bot-avatar-empty"><IconBot size={30} /></div>}
<span className="bot-dot" style={{ background: statusDot }} />
</div>
<label className="btn btn-upload">
Bild ändern
<IconUpload size={14} /> Bild ändern
<input type="file" accept="image/*" hidden onChange={(e) => uploadImage('avatar', e.target.files?.[0])} />
</label>
<label className="btn btn-upload btn-ghost">
🖼 Banner
<IconUpload size={14} /> Banner
<input type="file" accept="image/*" hidden onChange={(e) => uploadImage('banner', e.target.files?.[0])} />
</label>
</div>
@@ -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
<IconKey size={14} /> An {alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen
</button>
</div>
{alphaKeys?.assigned?.length > 0 && (
@@ -937,7 +942,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Rollen-Menüs</h2>
<p className="section-intro">
Selbstbedienungs-Rollen wie bei Carl-bot nur ohne Paywall. 😄 Der Bot postet
Selbstbedienungs-Rollen wie bei Carl-bot nur ohne Paywall. Der Bot postet
ein Embed mit Buttons; Klick = Rolle nehmen, nochmal = abgeben. Menüs sind
jederzeit editierbar, der Discord-Post zieht automatisch mit.
</p>
@@ -1712,7 +1717,7 @@ export default function Settings({ me }) {
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
<span className="settings-nav-icon">{t.icon}</span>
<span className="settings-nav-icon"><t.Icon /></span>
{t.label}
</button>
))}