import { useEffect, useRef, useState } from 'react'; import { apiGet, apiPut, apiPost, apiDelete } from '../api.js'; import EmbedComposer from '../components/EmbedComposer.jsx'; const API_SCOPES = [ { id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' }, { id: 'dm', label: 'dm', hint: 'Direktnachrichten senden' }, { id: 'roles', label: 'roles', hint: 'Rollen geben/nehmen' }, { id: 'read', label: 'read', hint: 'Member-Infos & Statistiken lesen' }, ]; 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' }, ]; // Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner) const TAB_SCOPES = { status: 'any', brand: null, feeds: 'settings', community: 'community', rollen: 'rollen', support: 'settings', bewerbungen: 'bewerbungen', composer: 'content', server: 'server', system: null, api: null, team: null, }; const TEAM_SCOPE_LABELS = { content: 'Composer, Tags, Triggers & geplante Posts', community: 'Playtester & Alpha-Keys', rollen: 'Rollen-Menüs', bewerbungen: 'Bewerbungen', server: 'Game-Server', settings: 'Kanäle & Feed-Einstellungen', devlogs: 'Devlogs löschen', }; /** Sekunden → "2d 4h 13m" */ /** Combobox für die Spiel-Auswahl — datalist ist nicht stylebar, das hier schon. * Filtert die Presets beim Tippen, freie gamedig-IDs bleiben erlaubt. */ function GameSelect({ presets, value, onChange }) { const [open, setOpen] = useState(false); const wrapRef = useRef(null); useEffect(() => { function onDocClick(e) { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); } document.addEventListener('mousedown', onDocClick); return () => document.removeEventListener('mousedown', onDocClick); }, []); const q = value.trim().toLowerCase(); const filtered = presets.filter(([id, label]) => !q || id.includes(q) || label.toLowerCase().includes(q) ); return (
setOpen(true)} onChange={(e) => { onChange(e.target.value); setOpen(true); }} onKeyDown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') setOpen(false); }} /> { e.preventDefault(); setOpen((o) => !o); }}>▾ {open && filtered.length > 0 && (
{filtered.map(([id, label]) => ( ))}
)}
); } function fmtUptime(s) { const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); return [d && `${d}d`, (d || h) && `${h}h`, `${m}m`].filter(Boolean).join(' '); } function fmtBytes(b) { if (b > 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`; return `${(b / 1024).toFixed(0)} KB`; } export default function Settings({ me }) { const [tab, setTab] = useState('status'); const [data, setData] = useState(null); const [form, setForm] = useState(null); const [error, setError] = useState(null); const [feedback, setFeedback] = useState(null); // Community const [playtesters, setPlaytesters] = useState([]); // Rollen-Menüs const emptyMenu = { id: null, title: '', description: '', channel_id: '', exclusive: false, entries: [{ emoji: '', label: '', role_id: '', style: 'secondary' }], }; const [menus, setMenus] = useState([]); const [menuDraft, setMenuDraft] = useState(emptyMenu); // Branding const [giteaToken, setGiteaToken] = useState(''); const [botName, setBotName] = useState(''); const [twitchCreds, setTwitchCreds] = useState({ id: '', secret: '' }); // Alpha-Keys const [alphaKeys, setAlphaKeys] = useState(null); const [keyUpload, setKeyUpload] = useState(''); // Bewerbungs-Formulare const emptyForm = { id: null, title: '', description: '', review_channel_id: '', approve_role_id: '', post_channel_id: '', questions: [''], }; const [appForms, setAppForms] = useState([]); const [formDraft, setFormDraft] = useState(emptyForm); // Triggers const [triggers, setTriggers] = useState([]); const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' }); // Game-Server const GAME_PRESETS = [ ['fivem', '🚗 FiveM'], ['http', '🌐 HTTP-Check'], ['minecraft', '⛏️ Minecraft (Java)'], ['minecraftbe', '⛏️ Minecraft (Bedrock)'], ['rust', '🪓 Rust'], ['valheim', '🛡️ Valheim'], ['palworld', '🐑 Palworld'], ['arkse', '🦖 ARK: Survival Evolved'], ['dayz', '🧟 DayZ'], ['projectzomboid', '🧟 Project Zomboid'], ['sevendaystodie', '🧟 7 Days to Die'], ['teamfortress2', '🎩 Team Fortress 2'], ['counterstrike2', '🔫 Counter-Strike 2'], ['garrysmod', '🔧 Garry’s Mod'], ['satisfactory', '🏭 Satisfactory'], ['terraria', '🌳 Terraria'], ['arma3', '🪖 Arma 3'], ['squad', '🪖 Squad'], ['unturned', '🏝️ Unturned'], ]; const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '' }; const [servers, setServers] = useState([]); const [serverDraft, setServerDraft] = useState(emptyServer); // Bot-Beschreibung + Team const [botDesc, setBotDesc] = useState(''); const [team, setTeam] = useState([]); const [teamDraft, setTeamDraft] = useState({ user_id: '', scopes: [] }); const canTab = (id) => { if (me.admin) return true; const need = TAB_SCOPES[id]; if (need === 'any') return (me.scopes?.length ?? 0) > 0; return need ? me.scopes?.includes(need) : false; }; const visibleTabs = TABS.filter((t) => canTab(t.id)); // Tags + geplante Posts const [tags, setTags] = useState([]); const [tagDraft, setTagDraft] = useState({ name: '', content: '' }); const [scheduled, setScheduled] = useState([]); const [schedDraft, setSchedDraft] = useState({ channel_id: '', embed_title: '', content: '', type: 'once', date: '', time: '18:00', weekday: '5', }); // API-Keys const [keys, setKeys] = useState([]); const [newKeyName, setNewKeyName] = useState(''); const [newKeyScopes, setNewKeyScopes] = useState(['message']); const [createdKey, setCreatedKey] = useState(null); useEffect(() => { if (!me.admin && !(me.scopes?.length > 0)) return; apiGet('/api/settings') .then((d) => { setData(d); setForm(d.settings); }) .catch((e) => setError(e.status)); apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {}); apiGet('/api/playtesters').then((d) => setPlaytesters(d.playtesters)).catch(() => {}); apiGet('/api/rolemenus').then((d) => setMenus(d.menus)).catch(() => {}); apiGet('/api/tags').then((d) => setTags(d.tags)).catch(() => {}); apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {}); apiGet('/api/alphakeys').then(setAlphaKeys).catch(() => {}); apiGet('/api/appforms').then((d) => setAppForms(d.forms)).catch(() => {}); apiGet('/api/triggers').then((d) => setTriggers(d.triggers)).catch(() => {}); apiGet('/api/gameservers').then((d) => setServers(d.servers)).catch(() => {}); apiGet('/api/webadmins').then((d) => setTeam(d.admins)).catch(() => {}); }, [me.admin, me.scopes?.length]); useEffect(() => { if (data?.status?.botName) setBotName(data.status.botName); if (data?.status?.botDescription != null) setBotDesc(data.status.botDescription); }, [data]); // Falls der aktive Tab nicht (mehr) erlaubt ist → auf den ersten erlaubten wechseln useEffect(() => { if (!canTab(tab) && visibleTabs.length > 0) setTab(visibleTabs[0].id); }, [me.admin, me.scopes]); async function saveTeam() { if (!teamDraft.user_id.trim() || teamDraft.scopes.length === 0) { flash('✗ User-ID und mindestens ein Bereich nötig'); return; } try { const res = await apiPut('/api/webadmins', teamDraft); setTeam(res.admins); setTeamDraft({ user_id: '', scopes: [] }); flash('✓ Team-Mitglied gespeichert'); } catch { flash('✗ Fehlgeschlagen — User-ID prüfen'); } } async function removeTeam(userId) { if (!window.confirm('Team-Mitglied entfernen? Der Web-Zugriff erlischt sofort.')) return; const res = await apiDelete(`/api/webadmins/${userId}`).catch(() => null); if (res) setTeam(res.admins); } const serverNeedsUrl = ['fivem', 'http'].includes(serverDraft.type); async function saveServer() { if (!serverDraft.name.trim() || (serverNeedsUrl ? !serverDraft.query_url.trim() : !serverDraft.host.trim())) { flash(serverNeedsUrl ? '✗ Name und Query-URL nötig' : '✗ Name und Host nötig'); return; } try { if (serverDraft.id) await apiPut(`/api/gameservers/${serverDraft.id}`, serverDraft); else await apiPost('/api/gameservers', serverDraft); setServers((await apiGet('/api/gameservers')).servers); setServerDraft(emptyServer); flash('✓ Server gespeichert — Embed erscheint beim nächsten Tick'); } catch { flash('✗ Speichern fehlgeschlagen (URL mit http/https?)'); } } async function removeServer(id) { if (!window.confirm('Server entfernen? Das Status-Embed wird mit gelöscht.')) return; await apiDelete(`/api/gameservers/${id}`).catch(() => {}); setServers((await apiGet('/api/gameservers')).servers); if (serverDraft.id === id) setServerDraft(emptyServer); } async function uploadAlphaKeys() { if (!keyUpload.trim()) return; try { const res = await apiPost('/api/alphakeys', { keys: keyUpload }); setAlphaKeys(res); setKeyUpload(''); flash(`✓ ${res.added} Keys hinzugefügt`); } catch { flash('✗ Upload fehlgeschlagen'); } } async function distributeKeys() { if (!window.confirm(`Alpha-Keys jetzt per DM an ${alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen?`)) return; try { const res = await apiPost('/api/alphakeys/distribute'); setAlphaKeys(res); flash(`✓ ${res.sent} Keys verschickt${res.failed.length ? ` — ${res.failed.length}× DM blockiert` : ''}`); } catch { flash('✗ Verteilung fehlgeschlagen'); } } async function saveForm(publish = false) { const questions = formDraft.questions.map((q) => q.trim()).filter(Boolean); if (!formDraft.title.trim() || questions.length === 0) { flash('✗ Titel und mindestens eine Frage nötig'); return; } try { const body = { ...formDraft, questions }; const res = formDraft.id ? await apiPut(`/api/appforms/${formDraft.id}`, body) : await apiPost('/api/appforms', body); if (publish) await apiPost(`/api/appforms/${res.form.id}/publish`); setAppForms((await apiGet('/api/appforms')).forms); setFormDraft(emptyForm); flash(publish ? '✓ Formular gepostet' : '✓ Gespeichert'); } catch { flash('✗ Fehlgeschlagen — Kanäle/Fragen prüfen'); } } async function removeForm(id) { if (!window.confirm('Formular löschen? Der Discord-Post wird mit entfernt.')) return; await apiDelete(`/api/appforms/${id}`).catch(() => {}); setAppForms((await apiGet('/api/appforms')).forms); if (formDraft.id === id) setFormDraft(emptyForm); } async function saveTriggerDraft() { if (triggerDraft.keyword.trim().length < 3 || !triggerDraft.reply.trim()) { flash('✗ Keyword (min 3 Zeichen) und Antwort nötig'); return; } try { await apiPut('/api/triggers', triggerDraft); setTriggers((await apiGet('/api/triggers')).triggers); setTriggerDraft({ keyword: '', reply: '' }); flash('✓ Trigger gespeichert'); } catch { flash('✗ Trigger fehlgeschlagen'); } } async function removeTrigger(keyword) { await apiDelete(`/api/triggers/${encodeURIComponent(keyword)}`).catch(() => {}); setTriggers((await apiGet('/api/triggers')).triggers); } 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, bot_name: botName, bot_description: botDesc }; if (giteaToken.trim()) body.gitea_api_token = giteaToken.trim(); if (twitchCreds.id.trim()) body.twitch_client_id = twitchCreds.id.trim(); if (twitchCreds.secret.trim()) body.twitch_client_secret = twitchCreds.secret.trim(); const res = await apiPut('/api/settings', body); setForm(res.settings); setGiteaToken(''); setTwitchCreds({ id: '', secret: '' }); flash('✓ Gespeichert & angewendet'); } catch { flash('✗ Speichern fehlgeschlagen (Farben/Name prüfen)'); } } async function saveTagDraft() { if (!tagDraft.name.trim() || !tagDraft.content.trim()) { flash('✗ Name und Inhalt nötig'); return; } try { await apiPut(`/api/tags/${encodeURIComponent(tagDraft.name.trim().toLowerCase())}`, { content: tagDraft.content }); setTags((await apiGet('/api/tags')).tags); setTagDraft({ name: '', content: '' }); flash('✓ Tag gespeichert'); } catch { flash('✗ Tag fehlgeschlagen (Name: a-z, 0-9, -, _)'); } } async function removeTag(name) { if (!window.confirm(`Tag „${name}" löschen?`)) return; await apiDelete(`/api/tags/${encodeURIComponent(name)}`).catch(() => {}); setTags((await apiGet('/api/tags')).tags); } async function createSchedule() { if (!schedDraft.channel_id || !schedDraft.content.trim()) { flash('✗ Kanal und Text nötig'); return; } try { await apiPost('/api/scheduled', schedDraft); setScheduled((await apiGet('/api/scheduled')).posts); setSchedDraft({ ...schedDraft, content: '', embed_title: '' }); flash('✓ Post geplant'); } catch { flash('✗ Planung fehlgeschlagen — Datum/Zeit prüfen'); } } async function removeSchedule(id) { await apiDelete(`/api/scheduled/${id}`).catch(() => {}); setScheduled((await apiGet('/api/scheduled')).posts); } function flash(msg) { setFeedback(msg); setTimeout(() => setFeedback(null), 3000); } async function save() { try { const body = { ...form }; if (twitchCreds.id.trim()) body.twitch_client_id = twitchCreds.id.trim(); if (twitchCreds.secret.trim()) body.twitch_client_secret = twitchCreds.secret.trim(); const res = await apiPut('/api/settings', body); setForm(res.settings); setTwitchCreds({ id: '', secret: '' }); flash('✓ Gespeichert'); } catch { flash('✗ Speichern fehlgeschlagen'); } } async function sendTest(target) { try { await apiPost(`/api/settings/test/${target}`); flash('✓ Test-Nachricht gesendet'); } catch { flash('✗ Test fehlgeschlagen — Kanal/Rechte prüfen'); } } async function createKey() { if (!newKeyName.trim() || newKeyScopes.length === 0) return; try { const res = await apiPost('/api/apikeys', { name: newKeyName.trim(), scopes: newKeyScopes }); setCreatedKey(res); setNewKeyName(''); setKeys((await apiGet('/api/apikeys')).keys); } catch { flash('✗ Key-Erstellung fehlgeschlagen'); } } async function revokeKey(id) { if (!window.confirm('Diesen API-Key widerrufen? Skripte mit diesem Key verlieren sofort den Zugriff.')) return; await apiDelete(`/api/apikeys/${id}`).catch(() => {}); setKeys((await apiGet('/api/apikeys')).keys); if (createdKey?.id === id) setCreatedKey(null); } const reloadMenus = async () => setMenus((await apiGet('/api/rolemenus')).menus); async function saveMenu(publish = false) { const entries = menuDraft.entries.filter((e) => e.label.trim() && e.role_id); if (!menuDraft.title.trim() || entries.length === 0) { flash('✗ Titel und mindestens eine Rolle nötig'); return; } if (publish && !menuDraft.channel_id) { flash('✗ Zum Posten einen Kanal wählen'); return; } try { const body = { ...menuDraft, entries }; const res = menuDraft.id ? await apiPut(`/api/rolemenus/${menuDraft.id}`, body) : await apiPost('/api/rolemenus', body); let menu = res.menu; if (publish) { await apiPost(`/api/rolemenus/${menu.id}/publish`); flash('✓ Menü in Discord gepostet'); } else { flash(res.published ? '✓ Gespeichert + Discord-Post aktualisiert' : '✓ Gespeichert'); } await reloadMenus(); setMenuDraft(emptyMenu); } catch { flash('✗ Speichern fehlgeschlagen — Einträge prüfen'); } } async function removeMenu(id) { if (!window.confirm('Menü löschen? Der Discord-Post wird mit entfernt.')) return; await apiDelete(`/api/rolemenus/${id}`).catch(() => {}); if (menuDraft.id === id) setMenuDraft(emptyMenu); await reloadMenus(); } const editMenu = (m) => setMenuDraft({ id: m.id, title: m.title, description: m.description, channel_id: m.channel_id ?? '', exclusive: m.exclusive, entries: m.entries.length ? m.entries.map((e) => ({ style: 'secondary', ...e })) : [{ emoji: '', label: '', role_id: '', style: 'secondary' }], }); const setMenuEntry = (i, patch) => setMenuDraft({ ...menuDraft, entries: menuDraft.entries.map((e, j) => (j === i ? { ...e, ...patch } : e)), }); const header = (
SETUP
); if (me.loading) return <>{header}

Lade …

; if (!me.user || (!me.admin && !(me.scopes?.length > 0))) { return ( <> {header}

🔒 Nur für den Admin.

{!me.user && Login mit Discord}
); } if (error) return <>{header}

Fehler beim Laden ({error}).

; if (!data || !form) return <>{header}

Lade …

; const channelOptions = data.channels.map((c) => ( )); const roleOptions = (data.roles ?? []).map((r) => ( )); const channelName = (id) => { const c = data.channels.find((x) => x.id === id); return c ? `#${c.name}` : id; }; /* ── Tab-Inhalte ───────────────────────────────── */ const tabStatus = (

// Status

{data.status.botTag ?? '—'}Bot
{fmtUptime(data.status.uptimeSeconds)}Uptime
{data.status.devlogs}Devlogs
{data.status.commits}Commits
{fmtBytes(data.status.dbSizeBytes)}Datenbank
{data.status.lastBackup ? data.status.lastBackup.slice(0, 10) : '—'} Letztes Backup
); const statusDot = { online: '#23a55a', idle: '#f0b232', dnd: '#f23f43' }[form.bot_presence_status] ?? '#23a55a'; const tabBrand = ( <>
{data.status.botAvatar ? Bot-Avatar :
🤖
}
setBotName(e.target.value)} />