Dashboard UX cleanup: fix German typos, add loading spinner, fix popstate, create static dir
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled

This commit is contained in:
Pepe44DEV
2026-07-01 02:42:57 +02:00
parent 691447d473
commit 85fd5ec915
3 changed files with 126 additions and 21 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.env
node_modules
debug.log
dist/

View File

@@ -98,9 +98,11 @@ router.get('/', (req, res) => {
<div class="layout">
${sidebar}
<div class="content">
<h1>Waehle eine Guild aus</h1>
<div class="muted">Nur Guilds, auf denen der Bot ist.</div>
<main id="guildGrid"></main>
<h1>Wähle einen Server aus</h1>
<div class="muted">Nur Server, auf denen der Bot ist.</div>
<main id="guildGrid">
<div class="loading-wrap"><div class="spinner"></div><span>Lade Server...</span></div>
</main>
</div>
</div>
<script>
@@ -273,6 +275,9 @@ router.get('/', (req, res) => {
@media (max-width: 1100px) {
.tickets-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}
.spinner { width:28px; height:28px; border:3px solid rgba(255,255,255,0.12); border-top-color:var(--accent); border-radius:50%; animation:spin .6s linear infinite; margin:12px auto; }
@keyframes spin { to { transform:rotate(360deg); } }
.loading-wrap { display:flex; flex-direction:column; align-items:center; gap:6px; padding:24px; color:var(--muted); font-size:13px; }
</style>
</head>
<body>
@@ -331,10 +336,10 @@ router.get('/', (req, res) => {
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
<div>
<p class="section-title">Tickets</p>
<p class="section-sub">bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
<p class="section-sub">Übersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
</div>
<div class="row" style="gap:8px; flex-wrap:wrap;">
<button class="secondary-btn ticket-tab-btn active" data-tab="overview">bersicht</button>
<button class="secondary-btn ticket-tab-btn active" data-tab="overview">Übersicht</button>
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
@@ -347,7 +352,7 @@ router.get('/', (req, res) => {
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<div>
<p class="section-title">Ticketliste</p>
<p class="section-sub">Links auswhlen, Details im Modal. Plus ffnet Panel-Erstellung.</p>
<p class="section-sub">Links auswählen, Details im Modal. Plus öffnet Panel-Erstellung.</p>
</div>
<div class="row" style="gap:10px;">
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
@@ -382,7 +387,7 @@ router.get('/', (req, res) => {
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
<div>
<p class="section-title">Status-Pipeline</p>
<p class="section-sub">Tickets nach Phase. Status per Dropdown ndern.</p>
<p class="section-sub">Tickets nach Phase. Status per Dropdown ändern.</p>
</div>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
@@ -542,7 +547,7 @@ router.get('/', (req, res) => {
<div class="form-field">
<label class="form-label">Custom Badwords (kommagetrennt oder zeilenweise)</label>
<textarea id="automodBadwords" rows="3" placeholder="badword1, badword2"></textarea>
<p class="muted">Diese Woerter werden zusaetzlich zum Standard-Filter geblockt.</p>
<p class="muted">Diese Wörter werden zusätzlich zum Standard-Filter geblockt.</p>
</div>
<div class="form-field">
<label class="form-label">Whitelist-Rollen (IDs, Kommagetrennt)</label>
@@ -627,7 +632,7 @@ router.get('/', (req, res) => {
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
<div>
<p class="section-title">Dynamic Voice</p>
<p class="section-sub">Lobby waehlen, Channel-Namen & Limits setzen.</p>
<p class="section-sub">Lobby wählen, Channel-Namen & Limits setzen.</p>
</div>
<div class="inline">
<span class="form-label">Aktivieren</span>
@@ -676,7 +681,7 @@ router.get('/', (req, res) => {
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<p class="section-title">Birthday</p>
<p class="section-sub">Automatische Glueckwuensche je Guild.</p>
<p class="section-sub">Automatische Glückwünsche je Server.</p>
</div>
<div class="row" style="align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end;">
<label class="form-label">Sendezeit (Stunde)</label>
@@ -700,7 +705,7 @@ router.get('/', (req, res) => {
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Gespeicherte Geburtstage</p>
<p class="section-sub">Eintraege werden per /birthday angelegt.</p>
<p class="section-sub">Einträge werden per /birthday angelegt.</p>
</div>
</div>
<div id="birthdayList" class="module-list"></div>
@@ -736,7 +741,7 @@ router.get('/', (req, res) => {
<textarea id="reactionRoleDescription" rows="2" placeholder="Kurze Beschreibung fuer das Embed"></textarea>
</div>
<div class="form-field">
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
<label class="form-label">Einträge (Emoji | Role ID | Label | Beschreibung)</label>
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
</div>
@@ -1446,7 +1451,7 @@ router.get('/', (req, res) => {
edit.addEventListener('click', () => editServerStat(item));
const del = document.createElement('button');
del.className = 'danger-btn';
del.textContent = 'Loeschen';
del.textContent = 'Löschen';
del.addEventListener('click', () => {
serverStatsCache.items = (serverStatsCache.items || []).filter((x) => x !== item);
renderServerStats();
@@ -1999,14 +2004,14 @@ router.get('/', (req, res) => {
edit.addEventListener('click', () => fillAutomationForm(r));
const del = document.createElement('button');
del.className = 'danger-btn';
del.textContent = 'Lschen';
del.textContent = 'Löschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/automations/' + r.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
showToast(res.ok ? 'Regel gelscht' : 'Lschen fehlgeschlagen', !res.ok);
showToast(res.ok ? 'Regel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
if (res.ok) loadAutomations();
});
actions.appendChild(edit);
@@ -2066,14 +2071,14 @@ router.get('/', (req, res) => {
edit.addEventListener('click', () => fillKbForm(a));
const del = document.createElement('button');
del.className = 'danger-btn';
del.textContent = 'Lschen';
del.textContent = 'Löschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/kb/' + a.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
showToast(res.ok ? 'Artikel gelscht' : 'Lschen fehlgeschlagen', !res.ok);
showToast(res.ok ? 'Artikel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
if (res.ok) loadKb();
});
actions.appendChild(edit);
@@ -2319,7 +2324,7 @@ router.get('/', (req, res) => {
editBtn.addEventListener('click', () => fillEventForm(ev));
const delBtn = document.createElement('button');
delBtn.className = 'danger-btn';
delBtn.textContent = 'Loeschen';
delBtn.textContent = 'Löschen';
delBtn.style.padding = '8px 10px';
delBtn.addEventListener('click', () => deleteEvent(ev.id));
actions.appendChild(editBtn);
@@ -2385,7 +2390,7 @@ router.get('/', (req, res) => {
async function deleteEvent(id) {
if (!currentGuild) return;
const res = await fetch('/api/events/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
showToast(res.ok ? 'Event geloescht' : 'Event loeschen fehlgeschlagen', !res.ok);
showToast(res.ok ? 'Event gelöscht' : 'Event löschen fehlgeschlagen', !res.ok);
if (res.ok) loadEvents();
}
@@ -2539,7 +2544,7 @@ router.get('/', (req, res) => {
delBtn.className = 'danger-btn';
delBtn.style.padding = '8px 10px';
delBtn.style.fontSize = '12px';
delBtn.textContent = 'Loeschen';
delBtn.textContent = 'Löschen';
delBtn.addEventListener('click', () => deleteReactionRole(set.id));
actions.appendChild(editBtn);
actions.appendChild(syncBtn);
@@ -2569,7 +2574,7 @@ router.get('/', (req, res) => {
async function deleteReactionRole(id) {
if (!currentGuild) return;
const res = await fetch('/api/reactionroles/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
showToast(res.ok ? 'Geloescht' : 'Fehler beim Loeschen', !res.ok);
showToast(res.ok ? 'Gelöscht' : 'Fehler beim Löschen', !res.ok);
if (res.ok) {
if (editingReactionRole === id) resetReactionRoleForm();
loadReactionRoles();
@@ -2999,6 +3004,12 @@ router.get('/', (req, res) => {
});
}
window.addEventListener('popstate', () => {
const section = location.hash.replace('#', '') || 'overview';
activateSection(section);
if (section === 'admin' && isAdmin) loadAdminAll();
});
const initialSection = location.hash.replace('#', '') || 'overview';
activateSection(initialSection);

92
static/dashboard.css Normal file
View File

@@ -0,0 +1,92 @@
:root {
--bg: #080c15;
--card: rgba(16,19,28,0.65);
--text: #f7fafc;
--accent: #f97316;
--accent-strong: #ff9b3d;
--muted: #a8b2c5;
--border: rgba(255,255,255,0.08);
--surface: rgba(12,15,22,0.75);
}
body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at 12% 18%, rgba(249,115,22,0.12), transparent 32%), radial-gradient(circle at 78% -6%, rgba(255,153,73,0.12), transparent 32%), linear-gradient(135deg, #070a11 0%, #0b0f18 50%, #080c15 100%);
color: var(--text);
min-height: 100vh;
}
.layout {
display: flex;
width: 100%;
min-height: 100vh;
background: radial-gradient(circle at 50% 20%, rgba(255,153,73,0.08), transparent 30%);
}
.sidebar {
width: 240px;
background: linear-gradient(180deg, rgba(12,14,22,0.85), rgba(10,12,18,0.78));
border-right: 1px solid rgba(255,255,255,0.06);
padding: 24px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 18px;
backdrop-filter: blur(14px);
box-shadow: 8px 0 32px rgba(0,0,0,0.45);
}
.brand {
font-size: 20px;
font-weight: 800;
letter-spacing: 0.6px;
color: var(--text);
}
.nav { display: flex; flex-direction: column; gap: 8px; }
.nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 13px;
border-radius: 14px;
color: var(--muted);
text-decoration: none;
font-weight: 700;
transition: background 140ms ease, color 140ms ease, box-shadow 140ms ease, border 140ms ease;
border: 1px solid transparent;
background: rgba(255,255,255,0.02);
}
.nav a .icon { opacity: 0.85; width: 18px; display: inline-flex; justify-content: center; }
.nav a.active {
background: rgba(249,115,22,0.18);
color: var(--accent-strong);
border-color: rgba(249,115,22,0.45);
box-shadow: 0 10px 30px rgba(249,115,22,0.28);
}
.nav a:hover {
background: rgba(255,255,255,0.06);
color: var(--text);
border-color: rgba(255,255,255,0.08);
box-shadow: 0 8px 22px rgba(0,0,0,0.28);
}
.nav a.hidden { display: none; }
.content {
flex: 1;
padding: 28px 34px 56px;
box-sizing: border-box;
background: linear-gradient(145deg, rgba(255,153,73,0.05) 0%, rgba(255,255,255,0) 32%);
}
h1 { margin: 0; font-size: 26px; letter-spacing: 0.6px; font-weight: 800; }
.muted { color: var(--muted); font-size: 13px; }
main { padding-top: 18px; display: flex; flex-direction: column; gap: 18px; }
.section { display: none; }
.section.active { display: block; }
.section.hidden { display: none !important; }
.logout {
margin-top: auto;
border: none;
color: white;
padding: 10px 12px;
border-radius: 12px;
cursor: pointer;
background: linear-gradient(135deg, #ef4444, #b91c1c);
width: 100%;
box-shadow: 0 12px 28px rgba(239,68,68,0.32);
}