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
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
|
debug.log
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -98,9 +98,11 @@ router.get('/', (req, res) => {
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
${sidebar}
|
${sidebar}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>Waehle eine Guild aus</h1>
|
<h1>Wähle einen Server aus</h1>
|
||||||
<div class="muted">Nur Guilds, auf denen der Bot ist.</div>
|
<div class="muted">Nur Server, auf denen der Bot ist.</div>
|
||||||
<main id="guildGrid"></main>
|
<main id="guildGrid">
|
||||||
|
<div class="loading-wrap"><div class="spinner"></div><span>Lade Server...</span></div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -273,6 +275,9 @@ router.get('/', (req, res) => {
|
|||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.tickets-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Tickets</p>
|
<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>
|
||||||
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
<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="pipeline">Pipeline</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</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>
|
<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 style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Ticketliste</p>
|
<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>
|
||||||
<div class="row" style="gap:10px;">
|
<div class="row" style="gap:10px;">
|
||||||
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
<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 style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Status-Pipeline</p>
|
<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>
|
</div>
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
<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">
|
<div class="form-field">
|
||||||
<label class="form-label">Custom Badwords (kommagetrennt oder zeilenweise)</label>
|
<label class="form-label">Custom Badwords (kommagetrennt oder zeilenweise)</label>
|
||||||
<textarea id="automodBadwords" rows="3" placeholder="badword1, badword2"></textarea>
|
<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>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Whitelist-Rollen (IDs, Kommagetrennt)</label>
|
<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 style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Dynamic Voice</p>
|
<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>
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<span class="form-label">Aktivieren</span>
|
<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 class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Birthday</p>
|
<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>
|
||||||
<div class="row" style="align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end;">
|
<div class="row" style="align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end;">
|
||||||
<label class="form-label">Sendezeit (Stunde)</label>
|
<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 class="row" style="justify-content:space-between; align-items:center;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Gespeicherte Geburtstage</p>
|
<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>
|
</div>
|
||||||
<div id="birthdayList" class="module-list"></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>
|
<textarea id="reactionRoleDescription" rows="2" placeholder="Kurze Beschreibung fuer das Embed"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<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>
|
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
||||||
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1446,7 +1451,7 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => editServerStat(item));
|
edit.addEventListener('click', () => editServerStat(item));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'Loeschen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', () => {
|
del.addEventListener('click', () => {
|
||||||
serverStatsCache.items = (serverStatsCache.items || []).filter((x) => x !== item);
|
serverStatsCache.items = (serverStatsCache.items || []).filter((x) => x !== item);
|
||||||
renderServerStats();
|
renderServerStats();
|
||||||
@@ -1999,14 +2004,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillAutomationForm(r));
|
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'Lschen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/automations/' + r.id, {
|
const res = await fetch('/api/automations/' + r.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
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();
|
if (res.ok) loadAutomations();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -2066,14 +2071,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillKbForm(a));
|
edit.addEventListener('click', () => fillKbForm(a));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'Lschen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/kb/' + a.id, {
|
const res = await fetch('/api/kb/' + a.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
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();
|
if (res.ok) loadKb();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -2319,7 +2324,7 @@ router.get('/', (req, res) => {
|
|||||||
editBtn.addEventListener('click', () => fillEventForm(ev));
|
editBtn.addEventListener('click', () => fillEventForm(ev));
|
||||||
const delBtn = document.createElement('button');
|
const delBtn = document.createElement('button');
|
||||||
delBtn.className = 'danger-btn';
|
delBtn.className = 'danger-btn';
|
||||||
delBtn.textContent = 'Loeschen';
|
delBtn.textContent = 'Löschen';
|
||||||
delBtn.style.padding = '8px 10px';
|
delBtn.style.padding = '8px 10px';
|
||||||
delBtn.addEventListener('click', () => deleteEvent(ev.id));
|
delBtn.addEventListener('click', () => deleteEvent(ev.id));
|
||||||
actions.appendChild(editBtn);
|
actions.appendChild(editBtn);
|
||||||
@@ -2385,7 +2390,7 @@ router.get('/', (req, res) => {
|
|||||||
async function deleteEvent(id) {
|
async function deleteEvent(id) {
|
||||||
if (!currentGuild) return;
|
if (!currentGuild) return;
|
||||||
const res = await fetch('/api/events/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
|
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();
|
if (res.ok) loadEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2539,7 +2544,7 @@ router.get('/', (req, res) => {
|
|||||||
delBtn.className = 'danger-btn';
|
delBtn.className = 'danger-btn';
|
||||||
delBtn.style.padding = '8px 10px';
|
delBtn.style.padding = '8px 10px';
|
||||||
delBtn.style.fontSize = '12px';
|
delBtn.style.fontSize = '12px';
|
||||||
delBtn.textContent = 'Loeschen';
|
delBtn.textContent = 'Löschen';
|
||||||
delBtn.addEventListener('click', () => deleteReactionRole(set.id));
|
delBtn.addEventListener('click', () => deleteReactionRole(set.id));
|
||||||
actions.appendChild(editBtn);
|
actions.appendChild(editBtn);
|
||||||
actions.appendChild(syncBtn);
|
actions.appendChild(syncBtn);
|
||||||
@@ -2569,7 +2574,7 @@ router.get('/', (req, res) => {
|
|||||||
async function deleteReactionRole(id) {
|
async function deleteReactionRole(id) {
|
||||||
if (!currentGuild) return;
|
if (!currentGuild) return;
|
||||||
const res = await fetch('/api/reactionroles/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
|
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 (res.ok) {
|
||||||
if (editingReactionRole === id) resetReactionRoleForm();
|
if (editingReactionRole === id) resetReactionRoleForm();
|
||||||
loadReactionRoles();
|
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';
|
const initialSection = location.hash.replace('#', '') || 'overview';
|
||||||
activateSection(initialSection);
|
activateSection(initialSection);
|
||||||
|
|
||||||
|
|||||||
92
static/dashboard.css
Normal file
92
static/dashboard.css
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user