Branding-Tab: Name/Farben/Bot-Status/Avatar/Banner + Button-Farben für Rollen-Menüs
- Alle Embeds nutzen jetzt brandName/brandColor/brandColor2/brandFooter aus den Settings (Codemod über 20 Module) — Farbe & Name serverweit per Klick änderbar - Bot-Status (Presence): Typ (Spielt/Schaut/Hört/Status) + Text, sofort angewendet; Server-Monitor nutzt die Presence nur noch, wenn kein eigener Status gesetzt ist - Avatar-/Banner-Upload direkt aufs Bot-Profil (Base64, 8-MB-Limit, Discord-Rate-Limit sauber gemeldet) - Env-Verlagerung: GITEA_API_TOKEN (write-only Setting) und DISCORD_GUILD_ID jetzt auch über den Brand-Tab pflegbar — weniger Redeploys - Rollen-Menüs: Button-Farbe pro Eintrag wählbar (Grau/Blau/Grün/Rot) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ 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' },
|
||||
@@ -43,12 +44,14 @@ export default function Settings({ me }) {
|
||||
// Rollen-Menüs
|
||||
const emptyMenu = {
|
||||
id: null, title: '', description: '', channel_id: '', exclusive: false,
|
||||
entries: [{ emoji: '', label: '', role_id: '' }],
|
||||
entries: [{ emoji: '', label: '', role_id: '', style: 'secondary' }],
|
||||
};
|
||||
const [menus, setMenus] = useState([]);
|
||||
const [menuDraft, setMenuDraft] = useState(emptyMenu);
|
||||
// Composer
|
||||
const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' });
|
||||
// Branding
|
||||
const [giteaToken, setGiteaToken] = useState('');
|
||||
// Tags + geplante Posts
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
|
||||
@@ -78,6 +81,33 @@ export default function Settings({ me }) {
|
||||
apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {});
|
||||
}, [me.admin]);
|
||||
|
||||
async function uploadImage(target, file) {
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
try {
|
||||
await apiPost(`/api/branding/${target}`, { data: reader.result });
|
||||
flash(`✓ ${target === 'avatar' ? 'Avatar' : 'Banner'} gesetzt`);
|
||||
} catch {
|
||||
flash('✗ Discord lehnt ab (Rate-Limit? ~2 Änderungen / 10 min)');
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function saveBrand() {
|
||||
try {
|
||||
const body = { ...form };
|
||||
if (giteaToken.trim()) body.gitea_api_token = giteaToken.trim();
|
||||
const res = await apiPut('/api/settings', body);
|
||||
setForm(res.settings);
|
||||
setGiteaToken('');
|
||||
flash('✓ Gespeichert & angewendet');
|
||||
} catch {
|
||||
flash('✗ Speichern fehlgeschlagen (Farben als #rrggbb?)');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTagDraft() {
|
||||
if (!tagDraft.name.trim() || !tagDraft.content.trim()) {
|
||||
flash('✗ Name und Inhalt nötig');
|
||||
@@ -223,7 +253,9 @@ export default function Settings({ me }) {
|
||||
setMenuDraft({
|
||||
id: m.id, title: m.title, description: m.description,
|
||||
channel_id: m.channel_id ?? '', exclusive: m.exclusive,
|
||||
entries: m.entries.length ? m.entries : [{ emoji: '', label: '', role_id: '' }],
|
||||
entries: m.entries.length
|
||||
? m.entries.map((e) => ({ style: 'secondary', ...e }))
|
||||
: [{ emoji: '', label: '', role_id: '', style: 'secondary' }],
|
||||
});
|
||||
|
||||
const setMenuEntry = (i, patch) =>
|
||||
@@ -291,6 +323,73 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabBrand = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Identität</h2>
|
||||
<div className="field">
|
||||
<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 })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Embed-Farben <span className="field-hint">primär (Embeds) · sekundär (Releases, Mod-Aktionen)</span></label>
|
||||
<div className="field-row">
|
||||
<input type="color" className="color-pick" value={form.brand_color} onChange={(e) => setForm({ ...form, brand_color: e.target.value })} />
|
||||
<input type="color" className="color-pick" value={form.brand_color2} onChange={(e) => setForm({ ...form, brand_color2: e.target.value })} />
|
||||
<span className="field-hint" style={{ alignSelf: 'center' }}>{form.brand_color} · {form.brand_color2}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Bot-Status</h2>
|
||||
<div className="field">
|
||||
<label>Presence <span className="field-hint">leer = Server-Monitor darf die Spielerzahl anzeigen</span></label>
|
||||
<div className="field-row">
|
||||
<select style={{ flex: 'none', width: '9rem' }} value={form.bot_status_type} onChange={(e) => setForm({ ...form, bot_status_type: e.target.value })}>
|
||||
<option value="custom">Status</option>
|
||||
<option value="playing">Spielt</option>
|
||||
<option value="watching">Schaut</option>
|
||||
<option value="listening">Hört</option>
|
||||
</select>
|
||||
<input type="text" placeholder="z. B. ⛏️ baut an EcoGame" value={form.bot_status_text} onChange={(e) => setForm({ ...form, bot_status_text: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Avatar & Banner</h2>
|
||||
<p className="section-intro">
|
||||
Wird direkt beim Bot-Account gesetzt. Discord erlaubt nur ~2 Änderungen pro 10 Minuten.
|
||||
</p>
|
||||
<div className="field">
|
||||
<label>Bot-Avatar <span className="field-hint">PNG/JPG/GIF, quadratisch empfohlen</span></label>
|
||||
<input type="file" accept="image/*" onChange={(e) => uploadImage('avatar', e.target.files?.[0])} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Bot-Banner <span className="field-hint">breites Bild fürs Bot-Profil</span></label>
|
||||
<input type="file" accept="image/*" onChange={(e) => uploadImage('banner', e.target.files?.[0])} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Aus der Env verlagert</h2>
|
||||
<div className="field">
|
||||
<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 })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Gitea-API-Token <span className="field-hint">{form.gitea_api_token_set ? '✓ gesetzt — neuen Wert eingeben zum Ersetzen' : 'für /bug; wird nie wieder angezeigt'}</span></label>
|
||||
<input type="password" placeholder="Token (write-only)" value={giteaToken} onChange={(e) => setGiteaToken(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-actions">
|
||||
<button className="btn btn-save" onClick={saveBrand}>Speichern & anwenden</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const tabFeeds = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
@@ -533,6 +632,17 @@ export default function Settings({ me }) {
|
||||
<option value="" disabled>— Rolle —</option>
|
||||
{roleOptions}
|
||||
</select>
|
||||
<select
|
||||
className={`btn-style-pick style-${entry.style ?? 'secondary'}`}
|
||||
title="Button-Farbe"
|
||||
value={entry.style ?? 'secondary'}
|
||||
onChange={(e) => setMenuEntry(i, { style: e.target.value })}
|
||||
>
|
||||
<option value="secondary">⬜ Grau</option>
|
||||
<option value="primary">🟦 Blau</option>
|
||||
<option value="success">🟩 Grün</option>
|
||||
<option value="danger">🟥 Rot</option>
|
||||
</select>
|
||||
<button
|
||||
className="card-delete"
|
||||
title="Zeile entfernen"
|
||||
@@ -822,6 +932,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const content = {
|
||||
status: tabStatus,
|
||||
brand: tabBrand,
|
||||
feeds: tabFeeds,
|
||||
community: tabCommunity,
|
||||
rollen: tabRollen,
|
||||
|
||||
Reference in New Issue
Block a user