diff --git a/public/styles/dashboard.css b/public/styles/dashboard.css new file mode 100644 index 0000000..07ea185 --- /dev/null +++ b/public/styles/dashboard.css @@ -0,0 +1,336 @@ +:root { + --bg: #080c15; + --card: rgba(16, 19, 28, 0.65); + --surface: rgba(12, 15, 22, 0.75); + --text: #f7fafc; + --accent: #f97316; + --accent-strong: #ff9b3d; + --muted: #a8b2c5; + --border: rgba(255, 255, 255, 0.08); + --danger: #ef4444; +} + +* { + box-sizing: border-box; +} + +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; +} + +#app { + display: flex; + min-height: 100vh; + width: 100%; +} + +.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; + 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; + background: linear-gradient(145deg, rgba(255, 153, 73, 0.05) 0%, rgba(255, 255, 255, 0) 32%); +} + +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +h1 { + margin: 0; + font-size: 26px; + letter-spacing: 0.6px; + font-weight: 800; +} + +.muted { + color: var(--muted); + font-size: 13px; +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.select, +select { + min-height: 44px; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: var(--text); +} + +.btn { + padding: 12px 14px; + border-radius: 14px; + border: 1px solid rgba(249, 115, 22, 0.4); + background: linear-gradient(130deg, #ff9b3d, #f97316); + color: white; + font-weight: 800; + cursor: pointer; + box-shadow: 0 14px 30px rgba(249, 115, 22, 0.35); + transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease; +} + +.btn:hover { + transform: translateY(-1px); + filter: brightness(1.05); + box-shadow: 0 18px 40px rgba(249, 115, 22, 0.4); +} + +.btn.secondary { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.14); + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.24); +} + +.btn.danger { + background: linear-gradient(135deg, #ef4444, #b91c1c); + box-shadow: 0 12px 28px rgba(239, 68, 68, 0.32); + border-color: rgba(239, 68, 68, 0.5); +} + +main .section { + margin-top: 22px; + display: none; +} + +main .section.active { + display: block; +} + +.grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.cards-grid { + margin-top: 16px; +} + +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px 20px; + box-shadow: 0 18px 45px rgba(0, 0, 0, 0.35); + backdrop-filter: blur(14px); +} + +.card.clickable { + cursor: pointer; + transition: transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; +} + +.card.clickable:hover { + transform: translateY(-2px); + border-color: rgba(249, 115, 22, 0.35); + box-shadow: 0 22px 40px rgba(0, 0, 0, 0.4); + background: rgba(255, 255, 255, 0.05); +} + +.row { + display: flex; + gap: 12px; + align-items: center; +} + +.stat { + font-size: 34px; + font-weight: 800; + margin: 0; +} + +.label { + color: var(--muted); + margin: 4px 0 0 0; + letter-spacing: 0.2px; +} + +.pill { + display: inline-block; + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + background: rgba(249, 115, 22, 0.16); + color: var(--text); + border: 1px solid rgba(249, 115, 22, 0.3); + box-shadow: 0 8px 18px rgba(249, 115, 22, 0.18); +} + +.toast { + position: fixed; + top: 20px; + right: 24px; + padding: 12px 14px; + border-radius: 12px; + background: rgba(249, 115, 22, 0.18); + border: 1px solid rgba(249, 115, 22, 0.45); + color: #ffe6d0; + font-weight: 700; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.34); + opacity: 0; + transform: translateY(-10px); + transition: opacity 150ms ease, transform 150ms ease; + z-index: 1100; + backdrop-filter: blur(10px); +} + +.toast.error { + background: rgba(239, 68, 68, 0.18); + border-color: rgba(239, 68, 68, 0.4); + color: #ffe4e6; +} + +.toast.show { + opacity: 1; + transform: translateY(0); +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: none; + align-items: center; + justify-content: center; + z-index: 1200; + backdrop-filter: blur(4px); +} + +.modal-backdrop.show { + display: flex; +} + +.modal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 16px; + padding: 18px; + width: min(640px, 94vw); + box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45); +} + +.form-field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 10px; +} + +.form-label { + font-size: 13px; + color: var(--muted); + letter-spacing: 0.2px; + font-weight: 600; +} + +input, +textarea { + padding: 12px 12px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: var(--text); + backdrop-filter: blur(8px); +} + +input::placeholder, +textarea::placeholder { + color: rgba(229, 231, 235, 0.65); +} + +.section-title { + margin: 0 0 10px 0; + font-size: 20px; + font-weight: 800; +} + +.empty-state { + padding: 20px; + text-align: center; + color: var(--muted); + border: 1px dashed rgba(255, 255, 255, 0.16); + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); +} + +.hidden { + display: none !important; +} diff --git a/public/styles/dashboard.sections.css b/public/styles/dashboard.sections.css new file mode 100644 index 0000000..4059d62 --- /dev/null +++ b/public/styles/dashboard.sections.css @@ -0,0 +1,106 @@ +.tickets-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 18px; +} + +.ticket-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.ticket-item { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + border-radius: 14px; + padding: 13px 15px; + display: flex; + flex-direction: column; + gap: 6px; + box-shadow: 0 14px 32px rgba(0, 0, 0, 0.3); +} + +.ticket-status { + padding: 5px 11px; + border-radius: 999px; + font-weight: 700; + font-size: 12px; + text-transform: capitalize; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: var(--text); + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.24); +} + +.status-open { + background: rgba(249, 115, 22, 0.18); + color: var(--accent-strong); + border-color: rgba(249, 115, 22, 0.45); +} + +.status-closed { + background: rgba(239, 68, 68, 0.16); + color: #f87171; + border-color: rgba(239, 68, 68, 0.42); +} + +.module-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.module-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.07); + background: rgba(255, 255, 255, 0.03); + box-shadow: 0 12px 26px rgba(0, 0, 0, 0.25); +} + +.module-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.switch { + position: relative; + width: 52px; + height: 28px; + border-radius: 28px; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.1); + cursor: pointer; + transition: background 140ms ease, border 140ms ease, box-shadow 140ms ease; + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.switch::after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 22px; + height: 22px; + border-radius: 50%; + background: #9ca3af; + transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.switch.on { + background: rgba(249, 115, 22, 0.3); + border-color: rgba(249, 115, 22, 0.6); + box-shadow: 0 8px 20px rgba(249, 115, 22, 0.24); +} + +.switch.on::after { + transform: translateX(22px); + background: #ffd9b3; + box-shadow: 0 6px 14px rgba(249, 115, 22, 0.35); +} diff --git a/public/ts-build/app.js b/public/ts-build/app.js new file mode 100644 index 0000000..2eca8c0 --- /dev/null +++ b/public/ts-build/app.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +require("./core/app.js"); diff --git a/public/ts-build/components/admin/index.js b/public/ts-build/components/admin/index.js new file mode 100644 index 0000000..d9d1b48 --- /dev/null +++ b/public/ts-build/components/admin/index.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initAdminSection = initAdminSection; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function initAdminSection(guildId) { + const section = document.getElementById('section-admin'); + if (!section) + return; + section.innerHTML = '

Lade Admin-Daten...

'; + try { + const data = await api_js_1.api.settings(guildId); + section.innerHTML = ` +

Admin

+
+

Rohdaten (nur Admin):

+
${JSON.stringify(data, null, 2)}
+
+ `; + } + catch (err) { + console.error(err); + section.innerHTML = '
Admin-Daten konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Admin-Daten', true); + } +} diff --git a/public/ts-build/components/dashboard.js b/public/ts-build/components/dashboard.js new file mode 100644 index 0000000..6b36a84 --- /dev/null +++ b/public/ts-build/components/dashboard.js @@ -0,0 +1,93 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initDashboardView = initDashboardView; +const api_js_1 = require("../services/api.js"); +const store_js_1 = require("../state/store.js"); +const toast_js_1 = require("../ui/toast.js"); +const overview_js_1 = require("./overview.js"); +const index_js_1 = require("./tickets/index.js"); +const index_js_2 = require("./modules/index.js"); +const index_js_3 = require("./events/index.js"); +const index_js_4 = require("./admin/index.js"); +const settings_js_1 = require("./settings.js"); +let overviewInterval = null; +let ticketsInterval = null; +async function populateGuildSelect() { + const select = document.getElementById('guildSelect'); + const cfg = (0, store_js_1.getConfig)(); + if (!select || !cfg) + return; + select.innerHTML = ``; + try { + const data = await api_js_1.api.guilds(); + select.innerHTML = ''; + data.guilds.forEach((g) => { + const opt = document.createElement('option'); + opt.value = g.id; + opt.textContent = g.name; + if (g.id === cfg.initialGuildId) + opt.selected = true; + select.appendChild(opt); + }); + const current = select.value || cfg.initialGuildId || data.guilds[0]?.id; + (0, store_js_1.setState)({ guildId: current || undefined }); + select.value = current || ''; + } + catch (err) { + console.error(err); + (0, toast_js_1.showToast)('Guilds konnten nicht geladen werden', true); + } +} +function registerGuildChange() { + const select = document.getElementById('guildSelect'); + if (!select) + return; + select.addEventListener('change', () => { + const guildId = select.value; + (0, store_js_1.setState)({ guildId }); + refreshSections(); + }); +} +async function refreshSections() { + const { guildId } = (0, store_js_1.getState)(); + if (!guildId) + return; + await (0, overview_js_1.renderOverview)(guildId); + await (0, index_js_1.initTicketsSection)(guildId); + await (0, index_js_2.initModulesSection)(guildId); + await (0, settings_js_1.renderSettingsSection)(guildId); + await (0, index_js_3.initEventsSection)(guildId); + const cfg = (0, store_js_1.getConfig)(); + if (cfg?.isAdmin) { + await (0, index_js_4.initAdminSection)(guildId); + } +} +function setupPolling() { + const { guildId } = (0, store_js_1.getState)(); + if (overviewInterval) + window.clearInterval(overviewInterval); + if (ticketsInterval) + window.clearInterval(ticketsInterval); + overviewInterval = window.setInterval(() => { + const current = (0, store_js_1.getState)().guildId; + if (current) + (0, overview_js_1.renderOverview)(current); + }, 10000); + ticketsInterval = window.setInterval(() => { + const current = (0, store_js_1.getState)().guildId; + if (current) + (0, index_js_1.initTicketsSection)(current); + }, 12000); +} +function initDashboardView() { + const cfg = (0, store_js_1.getConfig)(); + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn && cfg) { + logoutBtn.addEventListener('click', () => (window.location.href = `${cfg.baseAuth}/logout`)); + } + populateGuildSelect().then(() => { + registerGuildChange(); + refreshSections(); + setupPolling(); + }); +} diff --git a/public/ts-build/components/events/index.js b/public/ts-build/components/events/index.js new file mode 100644 index 0000000..0ecc8bf --- /dev/null +++ b/public/ts-build/components/events/index.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initEventsSection = initEventsSection; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function initEventsSection(guildId) { + const section = document.getElementById('section-events'); + if (!section) + return; + section.innerHTML = '

Lade Events...

'; + try { + const data = await api_js_1.api.events(guildId); + const events = data?.events || data || []; + section.innerHTML = '

Events

'; + if (!events.length) { + section.innerHTML += '
Keine Events geplant.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + events.forEach((ev) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${ev.title || 'Event'}
+
${ev.date || ''}
+
+ ${ev.status || 'open'} +
+
${ev.description || ''}
+ `; + list.appendChild(item); + }); + section.appendChild(list); + } + catch (err) { + console.error(err); + section.innerHTML = '
Events konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Events', true); + } +} diff --git a/public/ts-build/components/guildSelect.js b/public/ts-build/components/guildSelect.js new file mode 100644 index 0000000..a8566fa --- /dev/null +++ b/public/ts-build/components/guildSelect.js @@ -0,0 +1,59 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initSelectionView = initSelectionView; +const api_js_1 = require("../services/api.js"); +const store_js_1 = require("../state/store.js"); +const toast_js_1 = require("../ui/toast.js"); +async function initSelectionView() { + const cfg = (0, store_js_1.getConfig)(); + const grid = document.getElementById('guildGrid'); + const logoutBtn = document.getElementById('logoutBtn'); + const userInfo = document.getElementById('userInfo'); + if (logoutBtn && cfg) { + logoutBtn.addEventListener('click', () => { + window.location.href = `${cfg.baseAuth}/logout`; + }); + } + try { + const me = await api_js_1.api.me(); + if (userInfo && me?.user) + userInfo.textContent = `${me.user.username}#${me.user.discriminator}`; + } + catch { + // ignore + } + if (!grid || !cfg) + return; + grid.innerHTML = '
Lade Guilds...
'; + try { + const data = await api_js_1.api.guilds(); + grid.innerHTML = ''; + (data.guilds || []).forEach((g) => { + const card = document.createElement('div'); + card.className = 'card clickable'; + card.innerHTML = ` +
+ icon +
+
${g.name}
+
ID: ${g.id}
+
+
+
Zum Dashboard
+ `; + card.addEventListener('click', () => { + const qs = g.id ? `?guildId=${encodeURIComponent(g.id)}` : ''; + window.location.href = `${cfg.baseDashboard}${qs}`; + }); + grid.appendChild(card); + }); + if (!data.guilds?.length) { + grid.innerHTML = '
Bot ist in keiner Guild. Bitte Bot einladen.
'; + } + } + catch (err) { + console.error(err); + grid.innerHTML = '
Fehler beim Laden der Guilds
'; + (0, toast_js_1.showToast)('Guilds konnten nicht geladen werden', true); + } +} diff --git a/public/ts-build/components/modules/dynamicVoice.js b/public/ts-build/components/modules/dynamicVoice.js new file mode 100644 index 0000000..d658ab5 --- /dev/null +++ b/public/ts-build/components/modules/dynamicVoice.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderDynamicVoiceModule = renderDynamicVoiceModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderDynamicVoiceModule(guildId) { + const container = document.getElementById('module-dynamicvoice'); + if (!container) + return; + container.innerHTML = '

Lade Dynamic Voice...

'; + try { + const data = await api_js_1.api.dynamicVoice(guildId); + const cfg = data?.config || data?.dynamicVoiceConfig || {}; + container.innerHTML = ` +

Dynamic Voice

+

Lobby: ${cfg.lobbyChannelId || '-'}

+

Template: ${cfg.template || '-'}

+

User-Limit: ${cfg.userLimit ?? '-'}

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
Dynamic Voice konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden von Dynamic Voice', true); + } +} diff --git a/public/ts-build/components/modules/index.js b/public/ts-build/components/modules/index.js new file mode 100644 index 0000000..f164ab0 --- /dev/null +++ b/public/ts-build/components/modules/index.js @@ -0,0 +1,104 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initModulesSection = initModulesSection; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +const switch_js_1 = require("../../ui/switch.js"); +const welcome_js_1 = require("./welcome.js"); +const logging_js_1 = require("./logging.js"); +const reactionRoles_js_1 = require("./reactionRoles.js"); +const dynamicVoice_js_1 = require("./dynamicVoice.js"); +const statuspage_js_1 = require("./statuspage.js"); +const serverstats_js_1 = require("./serverstats.js"); +async function initModulesSection(guildId) { + const section = document.getElementById('section-modules'); + if (!section) + return; + section.innerHTML = ` +

Module

+
+
+
+
+
+
+
+
+
+
+
+ `; + await Promise.all([ + renderModuleToggles(guildId), + (0, welcome_js_1.renderWelcomeModule)(guildId), + (0, logging_js_1.renderLoggingModule)(guildId), + (0, reactionRoles_js_1.renderReactionRolesModule)(guildId), + (0, dynamicVoice_js_1.renderDynamicVoiceModule)(guildId), + (0, statuspage_js_1.renderStatuspageModule)(guildId), + (0, serverstats_js_1.renderServerStatsModule)(guildId) + ]); +} +async function renderModuleToggles(guildId) { + const container = document.getElementById('module-toggles'); + if (!container) + return; + container.innerHTML = '

Lade Module...

'; + try { + const data = await api_js_1.api.modules(guildId); + const modules = data?.modules || data || {}; + container.innerHTML = ''; + const entries = [ + { key: 'ticketsEnabled', label: 'Tickets', desc: 'Ticket-System aktivieren' }, + { key: 'automodEnabled', label: 'Automod', desc: 'Moderations-Filter' }, + { key: 'welcomeEnabled', label: 'Welcome', desc: 'Begrueßungsnachrichten' }, + { key: 'musicEnabled', label: 'Musik', desc: 'Musiksteuerung' }, + { key: 'levelingEnabled', label: 'Leveling', desc: 'XP/Level System' }, + { key: 'statuspageEnabled', label: 'Statuspage', desc: 'Statusberichte' }, + { key: 'serverStatsEnabled', label: 'Server Stats', desc: 'Stat-Channel' }, + { key: 'birthdayEnabled', label: 'Birthday', desc: 'Geburtstagsmodul' }, + { key: 'reactionRolesEnabled', label: 'Reaction Roles', desc: 'Selbstzuweisbare Rollen' }, + { key: 'eventsEnabled', label: 'Events', desc: 'Event-Planung' }, + { key: 'dynamicVoiceEnabled', label: 'Dynamic Voice', desc: 'Dynamische Voice Channels' } + ]; + entries.forEach((entry) => { + const row = document.createElement('div'); + row.className = 'module-item'; + row.innerHTML = ` +
+
${entry.label}
+
${entry.desc}
+
+
+ `; + const toggle = row.querySelector('.switch'); + toggle.addEventListener('click', async () => { + (0, switch_js_1.setSwitch)(toggle, !(0, switch_js_1.getSwitch)(toggle)); + await saveModules(guildId); + }); + container.appendChild(row); + }); + } + catch (err) { + console.error(err); + container.innerHTML = '
Module konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Module', true); + } +} +async function saveModules(guildId) { + const toggles = Array.from(document.querySelectorAll('#module-toggles .switch')); + const payload = { guildId }; + toggles.forEach((t) => { + const key = t.dataset.key; + if (!key) + return; + payload[key] = t.classList.contains('on'); + }); + try { + await api_js_1.api.saveSettings(payload); + (0, toast_js_1.showToast)('Module gespeichert'); + } + catch (err) { + console.error(err); + (0, toast_js_1.showToast)('Module speichern fehlgeschlagen', true); + } +} diff --git a/public/ts-build/components/modules/logging.js b/public/ts-build/components/modules/logging.js new file mode 100644 index 0000000..7faf5f1 --- /dev/null +++ b/public/ts-build/components/modules/logging.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderLoggingModule = renderLoggingModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderLoggingModule(guildId) { + const container = document.getElementById('module-logging'); + if (!container) + return; + container.innerHTML = '

Lade Logging...

'; + try { + const data = await api_js_1.api.settings(guildId); + const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {}; + container.innerHTML = ` +

Logging

+

Channel: ${cfg.logChannelId || '-'}

+

Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}

+

System: ${cfg.categories?.system ? 'an' : 'aus'}

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
Logging konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden von Logging', true); + } +} diff --git a/public/ts-build/components/modules/reactionRoles.js b/public/ts-build/components/modules/reactionRoles.js new file mode 100644 index 0000000..160df16 --- /dev/null +++ b/public/ts-build/components/modules/reactionRoles.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderReactionRolesModule = renderReactionRolesModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderReactionRolesModule(guildId) { + const container = document.getElementById('module-reactionroles'); + if (!container) + return; + container.innerHTML = '

Lade Reaction Roles...

'; + try { + const data = await api_js_1.api.reactionRoles(guildId); + const entries = data?.entries || data?.reactionRoles || []; + container.innerHTML = '

Reaction Roles

'; + if (!entries.length) { + container.innerHTML += '
Keine Reaction Roles.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + entries.slice(0, 3).forEach((e) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
${e.title || e.messageId || 'Eintrag'}
+
${e.channelId || ''}
+ `; + list.appendChild(item); + }); + container.appendChild(list); + } + catch (err) { + console.error(err); + container.innerHTML = '
Reaction Roles konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Reaction Roles', true); + } +} diff --git a/public/ts-build/components/modules/serverstats.js b/public/ts-build/components/modules/serverstats.js new file mode 100644 index 0000000..b41a47b --- /dev/null +++ b/public/ts-build/components/modules/serverstats.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderServerStatsModule = renderServerStatsModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderServerStatsModule(guildId) { + const container = document.getElementById('module-serverstats'); + if (!container) + return; + container.innerHTML = '

Lade Server Stats...

'; + try { + const data = await api_js_1.api.serverStats(guildId); + const cfg = data?.config || data || {}; + const items = cfg.items || []; + container.innerHTML = ` +

Server Stats

+

Kategorie: ${cfg.categoryId || '-'}

+

Refresh: ${cfg.refresh || '-'}m

+

Items: ${items.length}

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
Server Stats konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Server Stats', true); + } +} diff --git a/public/ts-build/components/modules/statuspage.js b/public/ts-build/components/modules/statuspage.js new file mode 100644 index 0000000..e620860 --- /dev/null +++ b/public/ts-build/components/modules/statuspage.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderStatuspageModule = renderStatuspageModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderStatuspageModule(guildId) { + const container = document.getElementById('module-statuspage'); + if (!container) + return; + container.innerHTML = '

Lade Statuspage...

'; + try { + const data = await api_js_1.api.statuspage(guildId); + const cfg = data?.config || data || {}; + const services = cfg.services || []; + container.innerHTML = ` +

Statuspage

+

Channel: ${cfg.channelId || '-'}

+

Intervall: ${cfg.interval || '-'}m

+

Services: ${services.length}

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
Statuspage konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Statuspage', true); + } +} diff --git a/public/ts-build/components/modules/welcome.js b/public/ts-build/components/modules/welcome.js new file mode 100644 index 0000000..e5aa2ed --- /dev/null +++ b/public/ts-build/components/modules/welcome.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderWelcomeModule = renderWelcomeModule; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderWelcomeModule(guildId) { + const container = document.getElementById('module-welcome'); + if (!container) + return; + container.innerHTML = '

Lade Welcome...

'; + try { + const data = await api_js_1.api.settings(guildId); + const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {}; + container.innerHTML = ` +

Welcome

+

Channel: ${cfg.channelId || '-'}

+

Embed Titel: ${cfg.embedTitle || '-'}

+

Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
Welcome konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden von Welcome', true); + } +} diff --git a/public/ts-build/components/overview.js b/public/ts-build/components/overview.js new file mode 100644 index 0000000..685229d --- /dev/null +++ b/public/ts-build/components/overview.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderOverview = renderOverview; +const api_js_1 = require("../services/api.js"); +const toast_js_1 = require("../ui/toast.js"); +async function renderOverview(guildId) { + const section = document.getElementById('section-overview'); + if (!section) + return; + section.innerHTML = '

Lade Uebersicht...

'; + try { + const data = await api_js_1.api.overview(guildId); + const stats = data?.stats || {}; + section.innerHTML = ` +

Uebersicht

+
+
+

Tickets offen

+

${stats.openTickets ?? '-'}

+
+
+

Module aktiv

+

${stats.activeModules ?? '-'}

+
+
+

Events geplant

+

${stats.events ?? '-'}

+
+
+ `; + } + catch (err) { + console.error(err); + section.innerHTML = '
Uebersicht konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Uebersicht', true); + } +} diff --git a/public/ts-build/components/settings.js b/public/ts-build/components/settings.js new file mode 100644 index 0000000..fac4e9e --- /dev/null +++ b/public/ts-build/components/settings.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderSettingsSection = renderSettingsSection; +const api_js_1 = require("../services/api.js"); +const toast_js_1 = require("../ui/toast.js"); +async function renderSettingsSection(guildId) { + const section = document.getElementById('section-settings'); + if (!section) + return; + section.innerHTML = '

Lade Einstellungen...

'; + try { + const data = await api_js_1.api.settings(guildId); + const settings = data?.settings || {}; + section.innerHTML = ` +

Einstellungen

+
+
${JSON.stringify(settings, null, 2)}
+
+ `; + } + catch (err) { + console.error(err); + section.innerHTML = '
Einstellungen konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Einstellungen', true); + } +} diff --git a/public/ts-build/components/tickets/automations.js b/public/ts-build/components/tickets/automations.js new file mode 100644 index 0000000..0726330 --- /dev/null +++ b/public/ts-build/components/tickets/automations.js @@ -0,0 +1,42 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderAutomations = renderAutomations; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderAutomations(guildId) { + const container = document.getElementById('tickets-automations'); + if (!container) + return; + container.innerHTML = '

Lade Automationen...

'; + try { + const data = await api_js_1.api.automations(guildId); + const rules = data?.rules || data || []; + if (!rules.length) { + container.innerHTML = '
Keine Regeln angelegt.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + rules.forEach((r) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${r.name || 'Regel'}
+
${r.condition?.type || r.condition?.status || ''}
+
+ ${r.active ? 'aktiv' : 'inaktiv'} +
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Automationen

'; + container.appendChild(list); + } + catch (err) { + console.error(err); + container.innerHTML = '
Automationen konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Automationen', true); + } +} diff --git a/public/ts-build/components/tickets/index.js b/public/ts-build/components/tickets/index.js new file mode 100644 index 0000000..f38c463 --- /dev/null +++ b/public/ts-build/components/tickets/index.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initTicketsSection = initTicketsSection; +const list_js_1 = require("./list.js"); +const pipeline_js_1 = require("./pipeline.js"); +const sla_js_1 = require("./sla.js"); +const automations_js_1 = require("./automations.js"); +const kb_js_1 = require("./kb.js"); +async function initTicketsSection(guildId) { + const section = document.getElementById('section-tickets'); + if (!section) + return; + section.innerHTML = ` +

Tickets

+
+
+
+
+
+
+
+
+
+ `; + await Promise.all([ + (0, list_js_1.renderTicketList)(guildId), + (0, pipeline_js_1.renderPipeline)(guildId), + (0, sla_js_1.renderSla)(guildId), + (0, automations_js_1.renderAutomations)(guildId), + (0, kb_js_1.renderKb)(guildId) + ]); +} diff --git a/public/ts-build/components/tickets/kb.js b/public/ts-build/components/tickets/kb.js new file mode 100644 index 0000000..8369db2 --- /dev/null +++ b/public/ts-build/components/tickets/kb.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderKb = renderKb; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderKb(guildId) { + const container = document.getElementById('tickets-kb'); + if (!container) + return; + container.innerHTML = '

Lade Knowledge Base...

'; + try { + const data = await api_js_1.api.kb(guildId); + const entries = data?.articles || data?.kb || []; + if (!entries.length) { + container.innerHTML = '
Keine KB-Eintraege.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + entries.slice(0, 4).forEach((k) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
${k.title || 'Artikel'}
+
${k.keywords || ''}
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Knowledge Base

'; + container.appendChild(list); + } + catch (err) { + console.error(err); + container.innerHTML = '
KB konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der KB', true); + } +} diff --git a/public/ts-build/components/tickets/list.js b/public/ts-build/components/tickets/list.js new file mode 100644 index 0000000..7a72c58 --- /dev/null +++ b/public/ts-build/components/tickets/list.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderTicketList = renderTicketList; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderTicketList(guildId) { + const container = document.getElementById('tickets-list'); + if (!container) + return; + container.innerHTML = '

Lade Tickets...

'; + try { + const data = await api_js_1.api.tickets(guildId); + const tickets = data?.tickets || []; + if (!tickets.length) { + container.innerHTML = '
Keine Tickets
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + tickets.slice(0, 5).forEach((t) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${t.title || t.id}
+
${t.user || ''}
+
+
${t.status || 'open'}
+
+
${t.description || ''}
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Aktuelle Tickets

'; + container.appendChild(list); + } + catch (err) { + console.error(err); + container.innerHTML = '
Tickets konnten nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Tickets', true); + } +} diff --git a/public/ts-build/components/tickets/pipeline.js b/public/ts-build/components/tickets/pipeline.js new file mode 100644 index 0000000..5cdfcbf --- /dev/null +++ b/public/ts-build/components/tickets/pipeline.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderPipeline = renderPipeline; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderPipeline(guildId) { + const container = document.getElementById('tickets-pipeline'); + if (!container) + return; + container.innerHTML = '

Lade Pipeline...

'; + try { + const data = await api_js_1.api.pipeline(guildId); + const lanes = data?.lanes || []; + container.innerHTML = '

Pipeline

'; + if (!lanes.length) { + container.innerHTML += '
Keine Pipeline-Daten
'; + return; + } + const grid = document.createElement('div'); + grid.className = 'grid'; + lanes.forEach((lane) => { + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +

${lane.name || 'Lane'}

+

${lane.count ?? 0}

+ `; + grid.appendChild(card); + }); + container.appendChild(grid); + } + catch (err) { + console.error(err); + container.innerHTML = '
Pipeline konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der Pipeline', true); + } +} diff --git a/public/ts-build/components/tickets/sla.js b/public/ts-build/components/tickets/sla.js new file mode 100644 index 0000000..ee42a37 --- /dev/null +++ b/public/ts-build/components/tickets/sla.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderSla = renderSla; +const api_js_1 = require("../../services/api.js"); +const toast_js_1 = require("../../ui/toast.js"); +async function renderSla(guildId) { + const container = document.getElementById('tickets-sla'); + if (!container) + return; + container.innerHTML = '

Lade SLA...

'; + try { + const data = await api_js_1.api.sla(guildId); + const stats = data?.stats || {}; + container.innerHTML = ` +

SLA

+

${stats.averageResponse ?? '-'}m

+

Durchschnittliche Antwortzeit

+ `; + } + catch (err) { + console.error(err); + container.innerHTML = '
SLA konnte nicht geladen werden.
'; + (0, toast_js_1.showToast)('Fehler beim Laden der SLA', true); + } +} diff --git a/public/ts-build/core/app.js b/public/ts-build/core/app.js new file mode 100644 index 0000000..a89217e --- /dev/null +++ b/public/ts-build/core/app.js @@ -0,0 +1,69 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const api_js_1 = require("../services/api.js"); +const store_js_1 = require("../state/store.js"); +const navigation_js_1 = require("../ui/navigation.js"); +const toast_js_1 = require("../ui/toast.js"); +const guildSelect_js_1 = require("../components/guildSelect.js"); +const dashboard_js_1 = require("../components/dashboard.js"); +function readConfig() { + const root = document.getElementById('app'); + if (!root) + throw new Error('App-Container fehlt'); + const view = root.dataset.view || 'selection'; + const baseRoot = root.dataset.baseRoot || '/ucp'; + const baseDashboard = root.dataset.baseDashboard || `${baseRoot}/dashboard`; + const baseAuth = root.dataset.baseAuth || `${baseRoot}/auth`; + const baseApi = root.dataset.baseApi || `${baseRoot}/api`; + const initialGuildId = root.dataset.guildId || undefined; + const isAdmin = root.dataset.userAdmin === 'true'; + const userLabel = root.dataset.userName + ? `${root.dataset.userName}${root.dataset.userDisc ? '#' + root.dataset.userDisc : ''}` + : undefined; + (0, store_js_1.initConfig)({ baseRoot, baseDashboard, baseAuth, baseApi, view, initialGuildId, isAdmin, userLabel }); +} +async function ensureAuth() { + try { + const me = await api_js_1.api.me(); + if (!me?.user) { + const cfg = (0, store_js_1.getConfig)(); + window.location.href = (cfg?.baseAuth || '/auth') + '/discord'; + return null; + } + const userInfo = document.getElementById('userInfo'); + if (userInfo && me.user) + userInfo.textContent = `${me.user.username}#${me.user.discriminator}`; + (0, store_js_1.setState)({ isAdmin: !!me.user?.isAdmin, userLabel: me.user ? `${me.user.username}#${me.user.discriminator}` : undefined }); + return me; + } + catch (err) { + console.error(err); + (0, toast_js_1.showToast)('Authentifizierung fehlgeschlagen', true); + return null; + } +} +async function bootstrap() { + readConfig(); + const cfg = (0, store_js_1.getConfig)(); + if (!cfg) + return; + const sidebarRoot = document.getElementById('sidebar-root'); + if (sidebarRoot) + (0, navigation_js_1.renderSidebar)(sidebarRoot, !!cfg.isAdmin); + if (cfg.view === 'selection') { + (0, guildSelect_js_1.initSelectionView)(); + } + else { + await ensureAuth(); + (0, dashboard_js_1.initDashboardView)(); + (0, navigation_js_1.initNavigation)((section) => { + // Sections werden innerhalb der jeweiligen Komponenten bedient + if (section === 'admin' && !cfg.isAdmin) + (0, toast_js_1.showToast)('Kein Admin-Recht', true); + }); + } +} +bootstrap().catch((err) => { + console.error(err); + (0, toast_js_1.showToast)('Fehler beim Laden', true); +}); diff --git a/public/ts-build/services/api.js b/public/ts-build/services/api.js new file mode 100644 index 0000000..b52bb74 --- /dev/null +++ b/public/ts-build/services/api.js @@ -0,0 +1,74 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.api = void 0; +const store_js_1 = require("../state/store.js"); +function buildUrl(path, query) { + const cfg = (0, store_js_1.getConfig)(); + const base = cfg?.baseApi || ''; + const url = new URL(path.startsWith('http') ? path : `${base}${path}`, window.location.origin); + if (query) { + Object.entries(query).forEach(([k, v]) => { + if (v === undefined || v === null) + return; + url.searchParams.set(k, String(v)); + }); + } + return url.toString(); +} +async function request(path, options = {}) { + const { query, headers, ...rest } = options; + const url = buildUrl(path, query); + const res = await fetch(url, { + ...rest, + headers: { + 'Content-Type': 'application/json', + ...(headers || {}) + } + }); + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new Error(`Request failed: ${res.status} ${text}`); + } + const contentType = res.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return (await res.json()); + } + return (await res.text()); +} +exports.api = { + me: () => request('/me'), + guilds: () => request('/guilds'), + overview: (guildId) => request(`/overview`, { query: { guildId } }), + settings: (guildId) => request(`/settings`, { query: { guildId } }), + saveSettings: (payload) => request('/settings', { method: 'POST', body: JSON.stringify(payload) }), + modules: (guildId) => request(`/modules`, { query: { guildId } }), + tickets: (guildId) => request(`/tickets`, { query: { guildId } }), + pipeline: (guildId, filter) => request(`/tickets/pipeline`, { query: { guildId, filter } }), + sla: (guildId, range) => request(`/tickets/sla`, { query: { guildId, range } }), + automations: (guildId) => request(`/automations`, { query: { guildId } }), + saveAutomation: (payload) => request(payload['id'] ? `/automations/${payload['id']}` : '/automations', { + method: payload['id'] ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + kb: (guildId) => request(`/kb`, { query: { guildId } }), + saveKb: (payload) => request(payload['id'] ? `/kb/${payload['id']}` : '/kb', { + method: payload['id'] ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + reactionRoles: (guildId) => request(`/reactionroles`, { query: { guildId } }), + saveReactionRole: (payload) => request(payload.id ? `/reactionroles/${payload.id}` : '/reactionroles', { + method: payload.id ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + events: (guildId) => request(`/events`, { query: { guildId } }), + saveEvent: (payload) => request(payload.id ? `/events/${payload.id}` : '/events', { + method: payload.id ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + statuspage: (guildId) => request(`/statuspage`, { query: { guildId } }), + saveStatuspage: (payload) => request('/statuspage', { method: 'POST', body: JSON.stringify(payload) }), + serverStats: (guildId) => request(`/serverstats`, { query: { guildId } }), + saveServerStats: (payload) => request('/serverstats', { method: 'POST', body: JSON.stringify(payload) }), + dynamicVoice: (guildId) => request(`/dynamicvoice`, { query: { guildId } }), + saveDynamicVoice: (payload) => request('/dynamicvoice', { method: 'POST', body: JSON.stringify(payload) }) +}; diff --git a/public/ts-build/state/store.js b/public/ts-build/state/store.js new file mode 100644 index 0000000..a1c6943 --- /dev/null +++ b/public/ts-build/state/store.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initConfig = initConfig; +exports.getConfig = getConfig; +exports.getState = getState; +exports.setState = setState; +exports.subscribe = subscribe; +let config = null; +let state = {}; +const listeners = new Set(); +function initConfig(next) { + config = next; + state = { + guildId: next.initialGuildId, + isAdmin: next.isAdmin, + userLabel: next.userLabel + }; +} +function getConfig() { + return config; +} +function getState() { + return state; +} +function setState(partial) { + state = { ...state, ...partial }; + listeners.forEach((l) => l(state)); +} +function subscribe(listener) { + listeners.add(listener); + return () => listeners.delete(listener); +} diff --git a/public/ts-build/ui/modal.js b/public/ts-build/ui/modal.js new file mode 100644 index 0000000..c7dc843 --- /dev/null +++ b/public/ts-build/ui/modal.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.showModal = showModal; +exports.hideModal = hideModal; +let activeModal = null; +let backdrop = null; +function ensureBackdrop() { + if (backdrop) + return backdrop; + backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + backdrop.addEventListener('click', hideModal); + document.body.appendChild(backdrop); + return backdrop; +} +function showModal(content) { + const bd = ensureBackdrop(); + if (!content.parentElement) + bd.appendChild(content); + activeModal = content; + bd.classList.add('show'); +} +function hideModal() { + if (backdrop) + backdrop.classList.remove('show'); + activeModal = null; +} diff --git a/public/ts-build/ui/navigation.js b/public/ts-build/ui/navigation.js new file mode 100644 index 0000000..2369c27 --- /dev/null +++ b/public/ts-build/ui/navigation.js @@ -0,0 +1,57 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renderSidebar = renderSidebar; +exports.initNavigation = initNavigation; +const store_js_1 = require("../state/store.js"); +const defaultNav = [ + { id: 'overview', label: 'Uebersicht', icon: '[*]' }, + { id: 'tickets', label: 'Ticketsystem', icon: '[*]' }, + { id: 'modules', label: 'Module', icon: '[*]' }, + { id: 'settings', label: 'Einstellungen', icon: '[*]' }, + { id: 'events', label: 'Events', icon: '[*]' }, + { id: 'admin', label: 'Admin', icon: '[*]', requiresAdmin: true } +]; +function renderSidebar(container, isAdmin) { + container.innerHTML = ''; + const brand = document.createElement('div'); + brand.className = 'brand'; + brand.textContent = 'Papo Control'; + const nav = document.createElement('div'); + nav.className = 'nav'; + defaultNav.forEach((item) => { + if (item.requiresAdmin && !isAdmin) + return; + const a = document.createElement('a'); + a.href = `#${item.id}`; + a.dataset.target = item.id; + a.innerHTML = `${item.icon || ''}${item.label}`; + nav.appendChild(a); + }); + container.appendChild(brand); + container.appendChild(nav); +} +function initNavigation(onChange) { + const navLinks = Array.from(document.querySelectorAll('.nav a')); + const activate = (section) => { + navLinks.forEach((link) => link.classList.toggle('active', link.dataset.target === section)); + document.querySelectorAll('.section').forEach((sec) => { + sec.classList.toggle('active', sec.id === `section-${section}`); + }); + (0, store_js_1.setState)({}); // trigger listeners for potential observers + onChange(section); + }; + navLinks.forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const target = link.dataset.target || 'overview'; + history.replaceState(null, '', `#${target}`); + activate(target); + }); + }); + const initial = (location.hash || '#overview').replace('#', ''); + activate(initial); + window.addEventListener('hashchange', () => { + const section = (location.hash || '#overview').replace('#', ''); + activate(section); + }); +} diff --git a/public/ts-build/ui/switch.js b/public/ts-build/ui/switch.js new file mode 100644 index 0000000..46b01ba --- /dev/null +++ b/public/ts-build/ui/switch.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.toggleSwitch = toggleSwitch; +exports.getSwitch = getSwitch; +exports.setSwitch = setSwitch; +function toggleSwitch(el, force) { + if (!el) + return; + const next = force === undefined ? !el.classList.contains('on') : force; + el.classList.toggle('on', next); +} +function getSwitch(el) { + return el?.classList.contains('on') ?? false; +} +function setSwitch(el, value) { + if (!el) + return; + el.classList.toggle('on', value); +} diff --git a/public/ts-build/ui/toast.js b/public/ts-build/ui/toast.js new file mode 100644 index 0000000..dbdf5b8 --- /dev/null +++ b/public/ts-build/ui/toast.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.showToast = showToast; +exports.hideToast = hideToast; +let currentTimeout = null; +function showToast(message, isError = false, duration = 2500) { + let toast = document.getElementById('toast-root'); + if (!toast) { + toast = document.createElement('div'); + toast.id = 'toast-root'; + document.body.appendChild(toast); + } + toast.className = `toast ${isError ? 'error' : ''}`; + toast.textContent = message; + requestAnimationFrame(() => { + toast?.classList.add('show'); + }); + if (currentTimeout) + window.clearTimeout(currentTimeout); + currentTimeout = window.setTimeout(() => hideToast(), duration); +} +function hideToast() { + const toast = document.getElementById('toast-root'); + if (!toast) + return; + toast.classList.remove('show'); +} diff --git a/public/ts/app.ts b/public/ts/app.ts new file mode 100644 index 0000000..c2c03c1 --- /dev/null +++ b/public/ts/app.ts @@ -0,0 +1 @@ +import './core/app.js'; diff --git a/public/ts/components/admin/index.ts b/public/ts/components/admin/index.ts new file mode 100644 index 0000000..3874230 --- /dev/null +++ b/public/ts/components/admin/index.ts @@ -0,0 +1,22 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function initAdminSection(guildId: string) { + const section = document.getElementById('section-admin'); + if (!section) return; + section.innerHTML = '

Lade Admin-Daten...

'; + try { + const data: any = await api.settings(guildId); + section.innerHTML = ` +

Admin

+
+

Rohdaten (nur Admin):

+
${JSON.stringify(data, null, 2)}
+
+ `; + } catch (err) { + console.error(err); + section.innerHTML = '
Admin-Daten konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Admin-Daten', true); + } +} diff --git a/public/ts/components/dashboard.ts b/public/ts/components/dashboard.ts new file mode 100644 index 0000000..e9c9bc4 --- /dev/null +++ b/public/ts/components/dashboard.ts @@ -0,0 +1,88 @@ +import { api } from '../services/api.js'; +import { getConfig, getState, setState } from '../state/store.js'; +import { showToast } from '../ui/toast.js'; +import { renderOverview } from './overview.js'; +import { initTicketsSection } from './tickets/index.js'; +import { initModulesSection } from './modules/index.js'; +import { initEventsSection } from './events/index.js'; +import { initAdminSection } from './admin/index.js'; +import { renderSettingsSection } from './settings.js'; + +let overviewInterval: number | null = null; +let ticketsInterval: number | null = null; + +async function populateGuildSelect() { + const select = document.getElementById('guildSelect') as HTMLSelectElement | null; + const cfg = getConfig(); + if (!select || !cfg) return; + select.innerHTML = ``; + try { + const data = await api.guilds(); + select.innerHTML = ''; + data.guilds.forEach((g) => { + const opt = document.createElement('option'); + opt.value = g.id; + opt.textContent = g.name; + if (g.id === cfg.initialGuildId) opt.selected = true; + select.appendChild(opt); + }); + const current = select.value || cfg.initialGuildId || data.guilds[0]?.id; + setState({ guildId: current || undefined }); + select.value = current || ''; + } catch (err) { + console.error(err); + showToast('Guilds konnten nicht geladen werden', true); + } +} + +function registerGuildChange() { + const select = document.getElementById('guildSelect') as HTMLSelectElement | null; + if (!select) return; + select.addEventListener('change', () => { + const guildId = select.value; + setState({ guildId }); + refreshSections(); + }); +} + +async function refreshSections() { + const { guildId } = getState(); + if (!guildId) return; + await renderOverview(guildId); + await initTicketsSection(guildId); + await initModulesSection(guildId); + await renderSettingsSection(guildId); + await initEventsSection(guildId); + const cfg = getConfig(); + if (cfg?.isAdmin) { + await initAdminSection(guildId); + } +} + +function setupPolling() { + const { guildId } = getState(); + if (overviewInterval) window.clearInterval(overviewInterval); + if (ticketsInterval) window.clearInterval(ticketsInterval); + overviewInterval = window.setInterval(() => { + const current = getState().guildId; + if (current) renderOverview(current); + }, 10000); + ticketsInterval = window.setInterval(() => { + const current = getState().guildId; + if (current) initTicketsSection(current); + }, 12000); +} + +export function initDashboardView() { + const cfg = getConfig(); + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn && cfg) { + logoutBtn.addEventListener('click', () => (window.location.href = `${cfg.baseAuth}/logout`)); + } + + populateGuildSelect().then(() => { + registerGuildChange(); + refreshSections(); + setupPolling(); + }); +} diff --git a/public/ts/components/events/index.ts b/public/ts/components/events/index.ts new file mode 100644 index 0000000..42470a8 --- /dev/null +++ b/public/ts/components/events/index.ts @@ -0,0 +1,39 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function initEventsSection(guildId: string) { + const section = document.getElementById('section-events'); + if (!section) return; + section.innerHTML = '

Lade Events...

'; + try { + const data: any = await api.events(guildId); + const events = data?.events || data || []; + section.innerHTML = '

Events

'; + if (!events.length) { + section.innerHTML += '
Keine Events geplant.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + events.forEach((ev: any) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${ev.title || 'Event'}
+
${ev.date || ''}
+
+ ${ev.status || 'open'} +
+
${ev.description || ''}
+ `; + list.appendChild(item); + }); + section.appendChild(list); + } catch (err) { + console.error(err); + section.innerHTML = '
Events konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Events', true); + } +} diff --git a/public/ts/components/guildSelect.ts b/public/ts/components/guildSelect.ts new file mode 100644 index 0000000..6286152 --- /dev/null +++ b/public/ts/components/guildSelect.ts @@ -0,0 +1,56 @@ +import { api } from '../services/api.js'; +import { getConfig } from '../state/store.js'; +import { showToast } from '../ui/toast.js'; + +export async function initSelectionView() { + const cfg = getConfig(); + const grid = document.getElementById('guildGrid'); + const logoutBtn = document.getElementById('logoutBtn'); + const userInfo = document.getElementById('userInfo'); + + if (logoutBtn && cfg) { + logoutBtn.addEventListener('click', () => { + window.location.href = `${cfg.baseAuth}/logout`; + }); + } + + try { + const me = await api.me(); + if (userInfo && me?.user) userInfo.textContent = `${me.user.username}#${me.user.discriminator}`; + } catch { + // ignore + } + + if (!grid || !cfg) return; + grid.innerHTML = '
Lade Guilds...
'; + try { + const data = await api.guilds(); + grid.innerHTML = ''; + (data.guilds || []).forEach((g) => { + const card = document.createElement('div'); + card.className = 'card clickable'; + card.innerHTML = ` +
+ icon +
+
${g.name}
+
ID: ${g.id}
+
+
+
Zum Dashboard
+ `; + card.addEventListener('click', () => { + const qs = g.id ? `?guildId=${encodeURIComponent(g.id)}` : ''; + window.location.href = `${cfg.baseDashboard}${qs}`; + }); + grid.appendChild(card); + }); + if (!data.guilds?.length) { + grid.innerHTML = '
Bot ist in keiner Guild. Bitte Bot einladen.
'; + } + } catch (err) { + console.error(err); + grid.innerHTML = '
Fehler beim Laden der Guilds
'; + showToast('Guilds konnten nicht geladen werden', true); + } +} diff --git a/public/ts/components/modules/dynamicVoice.ts b/public/ts/components/modules/dynamicVoice.ts new file mode 100644 index 0000000..ac84ce8 --- /dev/null +++ b/public/ts/components/modules/dynamicVoice.ts @@ -0,0 +1,22 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderDynamicVoiceModule(guildId: string) { + const container = document.getElementById('module-dynamicvoice'); + if (!container) return; + container.innerHTML = '

Lade Dynamic Voice...

'; + try { + const data: any = await api.dynamicVoice(guildId); + const cfg = data?.config || data?.dynamicVoiceConfig || {}; + container.innerHTML = ` +

Dynamic Voice

+

Lobby: ${cfg.lobbyChannelId || '-'}

+

Template: ${cfg.template || '-'}

+

User-Limit: ${cfg.userLimit ?? '-'}

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
Dynamic Voice konnte nicht geladen werden.
'; + showToast('Fehler beim Laden von Dynamic Voice', true); + } +} diff --git a/public/ts/components/modules/index.ts b/public/ts/components/modules/index.ts new file mode 100644 index 0000000..cd7efec --- /dev/null +++ b/public/ts/components/modules/index.ts @@ -0,0 +1,99 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; +import { setSwitch, getSwitch } from '../../ui/switch.js'; +import { renderWelcomeModule } from './welcome.js'; +import { renderLoggingModule } from './logging.js'; +import { renderReactionRolesModule } from './reactionRoles.js'; +import { renderDynamicVoiceModule } from './dynamicVoice.js'; +import { renderStatuspageModule } from './statuspage.js'; +import { renderServerStatsModule } from './serverstats.js'; + +export async function initModulesSection(guildId: string) { + const section = document.getElementById('section-modules'); + if (!section) return; + section.innerHTML = ` +

Module

+
+
+
+
+
+
+
+
+
+
+
+ `; + await Promise.all([ + renderModuleToggles(guildId), + renderWelcomeModule(guildId), + renderLoggingModule(guildId), + renderReactionRolesModule(guildId), + renderDynamicVoiceModule(guildId), + renderStatuspageModule(guildId), + renderServerStatsModule(guildId) + ]); +} + +async function renderModuleToggles(guildId: string) { + const container = document.getElementById('module-toggles'); + if (!container) return; + container.innerHTML = '

Lade Module...

'; + try { + const data: any = await api.modules(guildId); + const modules = data?.modules || data || {}; + container.innerHTML = ''; + const entries: Array<{ key: string; label: string; desc: string }> = [ + { key: 'ticketsEnabled', label: 'Tickets', desc: 'Ticket-System aktivieren' }, + { key: 'automodEnabled', label: 'Automod', desc: 'Moderations-Filter' }, + { key: 'welcomeEnabled', label: 'Welcome', desc: 'Begrueßungsnachrichten' }, + { key: 'musicEnabled', label: 'Musik', desc: 'Musiksteuerung' }, + { key: 'levelingEnabled', label: 'Leveling', desc: 'XP/Level System' }, + { key: 'statuspageEnabled', label: 'Statuspage', desc: 'Statusberichte' }, + { key: 'serverStatsEnabled', label: 'Server Stats', desc: 'Stat-Channel' }, + { key: 'birthdayEnabled', label: 'Birthday', desc: 'Geburtstagsmodul' }, + { key: 'reactionRolesEnabled', label: 'Reaction Roles', desc: 'Selbstzuweisbare Rollen' }, + { key: 'eventsEnabled', label: 'Events', desc: 'Event-Planung' }, + { key: 'dynamicVoiceEnabled', label: 'Dynamic Voice', desc: 'Dynamische Voice Channels' } + ]; + entries.forEach((entry) => { + const row = document.createElement('div'); + row.className = 'module-item'; + row.innerHTML = ` +
+
${entry.label}
+
${entry.desc}
+
+
+ `; + const toggle = row.querySelector('.switch') as HTMLElement; + toggle.addEventListener('click', async () => { + setSwitch(toggle, !getSwitch(toggle)); + await saveModules(guildId); + }); + container.appendChild(row); + }); + } catch (err) { + console.error(err); + container.innerHTML = '
Module konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Module', true); + } +} + +async function saveModules(guildId: string) { + const toggles = Array.from(document.querySelectorAll('#module-toggles .switch')); + const payload: Record = { guildId }; + toggles.forEach((t) => { + const key = t.dataset.key; + if (!key) return; + payload[key] = t.classList.contains('on'); + }); + try { + await api.saveSettings(payload); + showToast('Module gespeichert'); + } catch (err) { + console.error(err); + showToast('Module speichern fehlgeschlagen', true); + } +} diff --git a/public/ts/components/modules/logging.ts b/public/ts/components/modules/logging.ts new file mode 100644 index 0000000..eeff4a0 --- /dev/null +++ b/public/ts/components/modules/logging.ts @@ -0,0 +1,22 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderLoggingModule(guildId: string) { + const container = document.getElementById('module-logging'); + if (!container) return; + container.innerHTML = '

Lade Logging...

'; + try { + const data: any = await api.settings(guildId); + const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {}; + container.innerHTML = ` +

Logging

+

Channel: ${cfg.logChannelId || '-'}

+

Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}

+

System: ${cfg.categories?.system ? 'an' : 'aus'}

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
Logging konnte nicht geladen werden.
'; + showToast('Fehler beim Laden von Logging', true); + } +} diff --git a/public/ts/components/modules/reactionRoles.ts b/public/ts/components/modules/reactionRoles.ts new file mode 100644 index 0000000..b5c89dc --- /dev/null +++ b/public/ts/components/modules/reactionRoles.ts @@ -0,0 +1,33 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderReactionRolesModule(guildId: string) { + const container = document.getElementById('module-reactionroles'); + if (!container) return; + container.innerHTML = '

Lade Reaction Roles...

'; + try { + const data: any = await api.reactionRoles(guildId); + const entries = data?.entries || data?.reactionRoles || []; + container.innerHTML = '

Reaction Roles

'; + if (!entries.length) { + container.innerHTML += '
Keine Reaction Roles.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + entries.slice(0, 3).forEach((e: any) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
${e.title || e.messageId || 'Eintrag'}
+
${e.channelId || ''}
+ `; + list.appendChild(item); + }); + container.appendChild(list); + } catch (err) { + console.error(err); + container.innerHTML = '
Reaction Roles konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Reaction Roles', true); + } +} diff --git a/public/ts/components/modules/serverstats.ts b/public/ts/components/modules/serverstats.ts new file mode 100644 index 0000000..78191df --- /dev/null +++ b/public/ts/components/modules/serverstats.ts @@ -0,0 +1,23 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderServerStatsModule(guildId: string) { + const container = document.getElementById('module-serverstats'); + if (!container) return; + container.innerHTML = '

Lade Server Stats...

'; + try { + const data: any = await api.serverStats(guildId); + const cfg = data?.config || data || {}; + const items = cfg.items || []; + container.innerHTML = ` +

Server Stats

+

Kategorie: ${cfg.categoryId || '-'}

+

Refresh: ${cfg.refresh || '-'}m

+

Items: ${items.length}

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
Server Stats konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Server Stats', true); + } +} diff --git a/public/ts/components/modules/statuspage.ts b/public/ts/components/modules/statuspage.ts new file mode 100644 index 0000000..ec804f0 --- /dev/null +++ b/public/ts/components/modules/statuspage.ts @@ -0,0 +1,23 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderStatuspageModule(guildId: string) { + const container = document.getElementById('module-statuspage'); + if (!container) return; + container.innerHTML = '

Lade Statuspage...

'; + try { + const data: any = await api.statuspage(guildId); + const cfg = data?.config || data || {}; + const services = cfg.services || []; + container.innerHTML = ` +

Statuspage

+

Channel: ${cfg.channelId || '-'}

+

Intervall: ${cfg.interval || '-'}m

+

Services: ${services.length}

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
Statuspage konnte nicht geladen werden.
'; + showToast('Fehler beim Laden der Statuspage', true); + } +} diff --git a/public/ts/components/modules/welcome.ts b/public/ts/components/modules/welcome.ts new file mode 100644 index 0000000..435c9e3 --- /dev/null +++ b/public/ts/components/modules/welcome.ts @@ -0,0 +1,22 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderWelcomeModule(guildId: string) { + const container = document.getElementById('module-welcome'); + if (!container) return; + container.innerHTML = '

Lade Welcome...

'; + try { + const data: any = await api.settings(guildId); + const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {}; + container.innerHTML = ` +

Welcome

+

Channel: ${cfg.channelId || '-'}

+

Embed Titel: ${cfg.embedTitle || '-'}

+

Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
Welcome konnte nicht geladen werden.
'; + showToast('Fehler beim Laden von Welcome', true); + } +} diff --git a/public/ts/components/overview.ts b/public/ts/components/overview.ts new file mode 100644 index 0000000..a25f04a --- /dev/null +++ b/public/ts/components/overview.ts @@ -0,0 +1,33 @@ +import { api } from '../services/api.js'; +import { showToast } from '../ui/toast.js'; + +export async function renderOverview(guildId: string) { + const section = document.getElementById('section-overview'); + if (!section) return; + section.innerHTML = '

Lade Uebersicht...

'; + try { + const data: any = await api.overview(guildId); + const stats = data?.stats || {}; + section.innerHTML = ` +

Uebersicht

+
+
+

Tickets offen

+

${stats.openTickets ?? '-'}

+
+
+

Module aktiv

+

${stats.activeModules ?? '-'}

+
+
+

Events geplant

+

${stats.events ?? '-'}

+
+
+ `; + } catch (err) { + console.error(err); + section.innerHTML = '
Uebersicht konnte nicht geladen werden.
'; + showToast('Fehler beim Laden der Uebersicht', true); + } +} diff --git a/public/ts/components/settings.ts b/public/ts/components/settings.ts new file mode 100644 index 0000000..8d0eefe --- /dev/null +++ b/public/ts/components/settings.ts @@ -0,0 +1,22 @@ +import { api } from '../services/api.js'; +import { showToast } from '../ui/toast.js'; + +export async function renderSettingsSection(guildId: string) { + const section = document.getElementById('section-settings'); + if (!section) return; + section.innerHTML = '

Lade Einstellungen...

'; + try { + const data: any = await api.settings(guildId); + const settings = data?.settings || {}; + section.innerHTML = ` +

Einstellungen

+
+
${JSON.stringify(settings, null, 2)}
+
+ `; + } catch (err) { + console.error(err); + section.innerHTML = '
Einstellungen konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Einstellungen', true); + } +} diff --git a/public/ts/components/tickets/automations.ts b/public/ts/components/tickets/automations.ts new file mode 100644 index 0000000..404dab3 --- /dev/null +++ b/public/ts/components/tickets/automations.ts @@ -0,0 +1,38 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderAutomations(guildId: string) { + const container = document.getElementById('tickets-automations'); + if (!container) return; + container.innerHTML = '

Lade Automationen...

'; + try { + const data: any = await api.automations(guildId); + const rules = data?.rules || data || []; + if (!rules.length) { + container.innerHTML = '
Keine Regeln angelegt.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + rules.forEach((r: any) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${r.name || 'Regel'}
+
${r.condition?.type || r.condition?.status || ''}
+
+ ${r.active ? 'aktiv' : 'inaktiv'} +
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Automationen

'; + container.appendChild(list); + } catch (err) { + console.error(err); + container.innerHTML = '
Automationen konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Automationen', true); + } +} diff --git a/public/ts/components/tickets/index.ts b/public/ts/components/tickets/index.ts new file mode 100644 index 0000000..cbd0b4d --- /dev/null +++ b/public/ts/components/tickets/index.ts @@ -0,0 +1,29 @@ +import { renderTicketList } from './list.js'; +import { renderPipeline } from './pipeline.js'; +import { renderSla } from './sla.js'; +import { renderAutomations } from './automations.js'; +import { renderKb } from './kb.js'; + +export async function initTicketsSection(guildId: string) { + const section = document.getElementById('section-tickets'); + if (!section) return; + section.innerHTML = ` +

Tickets

+
+
+
+
+
+
+
+
+
+ `; + await Promise.all([ + renderTicketList(guildId), + renderPipeline(guildId), + renderSla(guildId), + renderAutomations(guildId), + renderKb(guildId) + ]); +} diff --git a/public/ts/components/tickets/kb.ts b/public/ts/components/tickets/kb.ts new file mode 100644 index 0000000..ba0dc44 --- /dev/null +++ b/public/ts/components/tickets/kb.ts @@ -0,0 +1,33 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderKb(guildId: string) { + const container = document.getElementById('tickets-kb'); + if (!container) return; + container.innerHTML = '

Lade Knowledge Base...

'; + try { + const data: any = await api.kb(guildId); + const entries = data?.articles || data?.kb || []; + if (!entries.length) { + container.innerHTML = '
Keine KB-Eintraege.
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + entries.slice(0, 4).forEach((k: any) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
${k.title || 'Artikel'}
+
${k.keywords || ''}
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Knowledge Base

'; + container.appendChild(list); + } catch (err) { + console.error(err); + container.innerHTML = '
KB konnte nicht geladen werden.
'; + showToast('Fehler beim Laden der KB', true); + } +} diff --git a/public/ts/components/tickets/list.ts b/public/ts/components/tickets/list.ts new file mode 100644 index 0000000..70a7589 --- /dev/null +++ b/public/ts/components/tickets/list.ts @@ -0,0 +1,39 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderTicketList(guildId: string) { + const container = document.getElementById('tickets-list'); + if (!container) return; + container.innerHTML = '

Lade Tickets...

'; + try { + const data: any = await api.tickets(guildId); + const tickets = data?.tickets || []; + if (!tickets.length) { + container.innerHTML = '
Keine Tickets
'; + return; + } + const list = document.createElement('div'); + list.className = 'ticket-list'; + tickets.slice(0, 5).forEach((t: any) => { + const item = document.createElement('div'); + item.className = 'ticket-item'; + item.innerHTML = ` +
+
+
${t.title || t.id}
+
${t.user || ''}
+
+
${t.status || 'open'}
+
+
${t.description || ''}
+ `; + list.appendChild(item); + }); + container.innerHTML = '

Aktuelle Tickets

'; + container.appendChild(list); + } catch (err) { + console.error(err); + container.innerHTML = '
Tickets konnten nicht geladen werden.
'; + showToast('Fehler beim Laden der Tickets', true); + } +} diff --git a/public/ts/components/tickets/pipeline.ts b/public/ts/components/tickets/pipeline.ts new file mode 100644 index 0000000..9c4bc6e --- /dev/null +++ b/public/ts/components/tickets/pipeline.ts @@ -0,0 +1,33 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderPipeline(guildId: string) { + const container = document.getElementById('tickets-pipeline'); + if (!container) return; + container.innerHTML = '

Lade Pipeline...

'; + try { + const data: any = await api.pipeline(guildId); + const lanes = data?.lanes || []; + container.innerHTML = '

Pipeline

'; + if (!lanes.length) { + container.innerHTML += '
Keine Pipeline-Daten
'; + return; + } + const grid = document.createElement('div'); + grid.className = 'grid'; + lanes.forEach((lane: any) => { + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +

${lane.name || 'Lane'}

+

${lane.count ?? 0}

+ `; + grid.appendChild(card); + }); + container.appendChild(grid); + } catch (err) { + console.error(err); + container.innerHTML = '
Pipeline konnte nicht geladen werden.
'; + showToast('Fehler beim Laden der Pipeline', true); + } +} diff --git a/public/ts/components/tickets/sla.ts b/public/ts/components/tickets/sla.ts new file mode 100644 index 0000000..6d1050e --- /dev/null +++ b/public/ts/components/tickets/sla.ts @@ -0,0 +1,21 @@ +import { api } from '../../services/api.js'; +import { showToast } from '../../ui/toast.js'; + +export async function renderSla(guildId: string) { + const container = document.getElementById('tickets-sla'); + if (!container) return; + container.innerHTML = '

Lade SLA...

'; + try { + const data: any = await api.sla(guildId); + const stats = data?.stats || {}; + container.innerHTML = ` +

SLA

+

${stats.averageResponse ?? '-'}m

+

Durchschnittliche Antwortzeit

+ `; + } catch (err) { + console.error(err); + container.innerHTML = '
SLA konnte nicht geladen werden.
'; + showToast('Fehler beim Laden der SLA', true); + } +} diff --git a/public/ts/core/app.ts b/public/ts/core/app.ts new file mode 100644 index 0000000..22dfb3f --- /dev/null +++ b/public/ts/core/app.ts @@ -0,0 +1,67 @@ +import { api } from '../services/api.js'; +import { initConfig, setState, getConfig } from '../state/store.js'; +import { renderSidebar, initNavigation } from '../ui/navigation.js'; +import { showToast } from '../ui/toast.js'; +import { initSelectionView } from '../components/guildSelect.js'; +import { initDashboardView } from '../components/dashboard.js'; + +function readConfig(): void { + const root = document.getElementById('app'); + if (!root) throw new Error('App-Container fehlt'); + const view = (root.dataset.view as 'selection' | 'dashboard') || 'selection'; + const baseRoot = root.dataset.baseRoot || '/ucp'; + const baseDashboard = root.dataset.baseDashboard || `${baseRoot}/dashboard`; + const baseAuth = root.dataset.baseAuth || `${baseRoot}/auth`; + const baseApi = root.dataset.baseApi || `${baseRoot}/api`; + const initialGuildId = root.dataset.guildId || undefined; + const isAdmin = root.dataset.userAdmin === 'true'; + const userLabel = root.dataset.userName + ? `${root.dataset.userName}${root.dataset.userDisc ? '#' + root.dataset.userDisc : ''}` + : undefined; + + initConfig({ baseRoot, baseDashboard, baseAuth, baseApi, view, initialGuildId, isAdmin, userLabel }); +} + +async function ensureAuth() { + try { + const me = await api.me(); + if (!me?.user) { + const cfg = getConfig(); + window.location.href = (cfg?.baseAuth || '/auth') + '/discord'; + return null; + } + const userInfo = document.getElementById('userInfo'); + if (userInfo && me.user) userInfo.textContent = `${me.user.username}#${me.user.discriminator}`; + setState({ isAdmin: !!me.user?.isAdmin, userLabel: me.user ? `${me.user.username}#${me.user.discriminator}` : undefined }); + return me; + } catch (err) { + console.error(err); + showToast('Authentifizierung fehlgeschlagen', true); + return null; + } +} + +async function bootstrap() { + readConfig(); + const cfg = getConfig(); + if (!cfg) return; + + const sidebarRoot = document.getElementById('sidebar-root'); + if (sidebarRoot) renderSidebar(sidebarRoot, !!cfg.isAdmin); + + if (cfg.view === 'selection') { + initSelectionView(); + } else { + await ensureAuth(); + initDashboardView(); + initNavigation((section) => { + // Sections werden innerhalb der jeweiligen Komponenten bedient + if (section === 'admin' && !cfg.isAdmin) showToast('Kein Admin-Recht', true); + }); + } +} + +bootstrap().catch((err) => { + console.error(err); + showToast('Fehler beim Laden', true); +}); diff --git a/public/ts/services/api.ts b/public/ts/services/api.ts new file mode 100644 index 0000000..7a672cb --- /dev/null +++ b/public/ts/services/api.ts @@ -0,0 +1,79 @@ +import { getConfig } from '../state/store.js'; + +type FetchOptions = RequestInit & { query?: Record }; + +function buildUrl(path: string, query?: Record) { + const cfg = getConfig(); + const base = cfg?.baseApi || ''; + const url = new URL(path.startsWith('http') ? path : `${base}${path}`, window.location.origin); + if (query) { + Object.entries(query).forEach(([k, v]) => { + if (v === undefined || v === null) return; + url.searchParams.set(k, String(v)); + }); + } + return url.toString(); +} + +async function request(path: string, options: FetchOptions = {}): Promise { + const { query, headers, ...rest } = options; + const url = buildUrl(path, query); + const res = await fetch(url, { + ...rest, + headers: { + 'Content-Type': 'application/json', + ...(headers || {}) + } + }); + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new Error(`Request failed: ${res.status} ${text}`); + } + const contentType = res.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return (await res.json()) as T; + } + return (await res.text()) as unknown as T; +} + +export const api = { + me: () => request<{ user?: { username: string; discriminator: string; isAdmin?: boolean } }>('/me'), + guilds: () => request<{ guilds: Array<{ id: string; name: string; icon?: string }> }>('/guilds'), + overview: (guildId: string) => request(`/overview`, { query: { guildId } }), + settings: (guildId: string) => request(`/settings`, { query: { guildId } }), + saveSettings: (payload: Record) => request('/settings', { method: 'POST', body: JSON.stringify(payload) }), + modules: (guildId: string) => request(`/modules`, { query: { guildId } }), + tickets: (guildId: string) => request(`/tickets`, { query: { guildId } }), + pipeline: (guildId: string, filter?: string) => request(`/tickets/pipeline`, { query: { guildId, filter } }), + sla: (guildId: string, range?: number) => request(`/tickets/sla`, { query: { guildId, range } }), + automations: (guildId: string) => request(`/automations`, { query: { guildId } }), + saveAutomation: (payload: Record) => + request(payload['id'] ? `/automations/${payload['id']}` : '/automations', { + method: payload['id'] ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + kb: (guildId: string) => request(`/kb`, { query: { guildId } }), + saveKb: (payload: Record) => + request(payload['id'] ? `/kb/${payload['id']}` : '/kb', { + method: payload['id'] ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + reactionRoles: (guildId: string) => request(`/reactionroles`, { query: { guildId } }), + saveReactionRole: (payload: Record & { id?: string }) => + request(payload.id ? `/reactionroles/${payload.id}` : '/reactionroles', { + method: payload.id ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + events: (guildId: string) => request(`/events`, { query: { guildId } }), + saveEvent: (payload: Record & { id?: string }) => + request(payload.id ? `/events/${payload.id}` : '/events', { + method: payload.id ? 'PUT' : 'POST', + body: JSON.stringify(payload) + }), + statuspage: (guildId: string) => request(`/statuspage`, { query: { guildId } }), + saveStatuspage: (payload: Record) => request('/statuspage', { method: 'POST', body: JSON.stringify(payload) }), + serverStats: (guildId: string) => request(`/serverstats`, { query: { guildId } }), + saveServerStats: (payload: Record) => request('/serverstats', { method: 'POST', body: JSON.stringify(payload) }), + dynamicVoice: (guildId: string) => request(`/dynamicvoice`, { query: { guildId } }), + saveDynamicVoice: (payload: Record) => request('/dynamicvoice', { method: 'POST', body: JSON.stringify(payload) }) +}; diff --git a/public/ts/state/store.ts b/public/ts/state/store.ts new file mode 100644 index 0000000..4c809db --- /dev/null +++ b/public/ts/state/store.ts @@ -0,0 +1,49 @@ +export interface AppConfig { + baseRoot: string; + baseDashboard: string; + baseAuth: string; + baseApi: string; + view: 'selection' | 'dashboard'; + initialGuildId?: string; + isAdmin?: boolean; + userLabel?: string; +} + +export interface AppState { + guildId?: string; + isAdmin?: boolean; + userLabel?: string; +} + +type Listener = (state: AppState) => void; + +let config: AppConfig | null = null; +let state: AppState = {}; +const listeners = new Set(); + +export function initConfig(next: AppConfig) { + config = next; + state = { + guildId: next.initialGuildId, + isAdmin: next.isAdmin, + userLabel: next.userLabel + }; +} + +export function getConfig() { + return config; +} + +export function getState() { + return state; +} + +export function setState(partial: Partial) { + state = { ...state, ...partial }; + listeners.forEach((l) => l(state)); +} + +export function subscribe(listener: Listener) { + listeners.add(listener); + return () => listeners.delete(listener); +} diff --git a/public/ts/ui/modal.ts b/public/ts/ui/modal.ts new file mode 100644 index 0000000..e6292a5 --- /dev/null +++ b/public/ts/ui/modal.ts @@ -0,0 +1,23 @@ +let activeModal: HTMLElement | null = null; +let backdrop: HTMLElement | null = null; + +function ensureBackdrop() { + if (backdrop) return backdrop; + backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + backdrop.addEventListener('click', hideModal); + document.body.appendChild(backdrop); + return backdrop; +} + +export function showModal(content: HTMLElement) { + const bd = ensureBackdrop(); + if (!content.parentElement) bd.appendChild(content); + activeModal = content; + bd.classList.add('show'); +} + +export function hideModal() { + if (backdrop) backdrop.classList.remove('show'); + activeModal = null; +} diff --git a/public/ts/ui/navigation.ts b/public/ts/ui/navigation.ts new file mode 100644 index 0000000..06a9594 --- /dev/null +++ b/public/ts/ui/navigation.ts @@ -0,0 +1,67 @@ +import { getState, setState } from '../state/store.js'; + +export interface NavItem { + id: string; + label: string; + icon?: string; + requiresAdmin?: boolean; +} + +const defaultNav: NavItem[] = [ + { id: 'overview', label: 'Uebersicht', icon: '[*]' }, + { id: 'tickets', label: 'Ticketsystem', icon: '[*]' }, + { id: 'modules', label: 'Module', icon: '[*]' }, + { id: 'settings', label: 'Einstellungen', icon: '[*]' }, + { id: 'events', label: 'Events', icon: '[*]' }, + { id: 'admin', label: 'Admin', icon: '[*]', requiresAdmin: true } +]; + +export function renderSidebar(container: HTMLElement, isAdmin: boolean) { + container.innerHTML = ''; + const brand = document.createElement('div'); + brand.className = 'brand'; + brand.textContent = 'Papo Control'; + const nav = document.createElement('div'); + nav.className = 'nav'; + + defaultNav.forEach((item) => { + if (item.requiresAdmin && !isAdmin) return; + const a = document.createElement('a'); + a.href = `#${item.id}`; + a.dataset.target = item.id; + a.innerHTML = `${item.icon || ''}${item.label}`; + nav.appendChild(a); + }); + + container.appendChild(brand); + container.appendChild(nav); +} + +export function initNavigation(onChange: (section: string) => void) { + const navLinks = Array.from(document.querySelectorAll('.nav a')); + const activate = (section: string) => { + navLinks.forEach((link) => link.classList.toggle('active', link.dataset.target === section)); + document.querySelectorAll('.section').forEach((sec) => { + sec.classList.toggle('active', sec.id === `section-${section}`); + }); + setState({}); // trigger listeners for potential observers + onChange(section); + }; + + navLinks.forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const target = link.dataset.target || 'overview'; + history.replaceState(null, '', `#${target}`); + activate(target); + }); + }); + + const initial = (location.hash || '#overview').replace('#', ''); + activate(initial); + + window.addEventListener('hashchange', () => { + const section = (location.hash || '#overview').replace('#', ''); + activate(section); + }); +} diff --git a/public/ts/ui/switch.ts b/public/ts/ui/switch.ts new file mode 100644 index 0000000..d3430a9 --- /dev/null +++ b/public/ts/ui/switch.ts @@ -0,0 +1,14 @@ +export function toggleSwitch(el: HTMLElement | null, force?: boolean) { + if (!el) return; + const next = force === undefined ? !el.classList.contains('on') : force; + el.classList.toggle('on', next); +} + +export function getSwitch(el: HTMLElement | null) { + return el?.classList.contains('on') ?? false; +} + +export function setSwitch(el: HTMLElement | null, value: boolean) { + if (!el) return; + el.classList.toggle('on', value); +} diff --git a/public/ts/ui/toast.ts b/public/ts/ui/toast.ts new file mode 100644 index 0000000..68dcf39 --- /dev/null +++ b/public/ts/ui/toast.ts @@ -0,0 +1,23 @@ +let currentTimeout: number | null = null; + +export function showToast(message: string, isError = false, duration = 2500) { + let toast = document.getElementById('toast-root') as HTMLElement | null; + if (!toast) { + toast = document.createElement('div'); + toast.id = 'toast-root'; + document.body.appendChild(toast); + } + toast.className = `toast ${isError ? 'error' : ''}`; + toast.textContent = message; + requestAnimationFrame(() => { + toast?.classList.add('show'); + }); + if (currentTimeout) window.clearTimeout(currentTimeout); + currentTimeout = window.setTimeout(() => hideToast(), duration); +} + +export function hideToast() { + const toast = document.getElementById('toast-root'); + if (!toast) return; + toast.classList.remove('show'); +} diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts index 7fbfc1b..fdbedf8 100644 --- a/src/web/routes/dashboard.ts +++ b/src/web/routes/dashboard.ts @@ -8,3011 +8,29 @@ router.get('/', (req, res) => { return res.redirect(`${baseRoot}/auth/discord`); } - const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : ''; const baseRoot = process.env.WEB_BASE_PATH || '/ucp'; const baseDashboard = `${baseRoot}/dashboard`; const baseAuth = `${baseRoot}/auth`; const baseApi = `${baseRoot}/api`; - const baseScript = ` - const BASE_ROOT = '${baseRoot}'; - const BASE_DASH = '${baseDashboard}'; - const BASE_API = '${baseApi}'; - const BASE_AUTH = '${baseAuth}'; - const prependBase = (url) => { - if (typeof url !== 'string') return url; - if (BASE_ROOT && url.startsWith(BASE_ROOT + '/')) return url; - if (url.startsWith('/')) return (BASE_ROOT || '') + url; - return url; - }; - const _fetch = window.fetch.bind(window); - window.fetch = (u, o) => _fetch(prependBase(u), o); - const gotoDashboard = (guildId) => { - const qs = guildId ? ('?guildId=' + encodeURIComponent(guildId)) : ''; - window.location.href = (BASE_DASH || '/dashboard') + qs; - }; - `; - - // TODO: TICKETS: Dashboard-Layout neu aufsetzen. - // - Filter/Status/Live-Ansicht statt statischem Inline-HTML. - // - Ticketdaten per API/WebSocket laden und live aktualisieren. - // TODO: MODULE: Musik-Modul als toggelbares Modul mit Status- und Queue-Ansicht einbinden. - // - Play/Pause/Skip/Loop als UI-Controls anbieten und Bot-Status spiegeln. - // TODO: AUTOMOD: Konfiguration (Schwellenwerte, Filter, Logging) im Dashboard editierbar machen. - // - Grenzwerte, Whitelist/Badwords und Log-Ziel in ein Formular ueberfuehren. - const sidebar = ` - - `; + const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : ''; if (!guildId) { - res.send(` - - - - - - Papo Dashboard - Auswahl - - - -
- ${sidebar} -
-

Waehle eine Guild aus

-
Nur Guilds, auf denen der Bot ist.
-
-
-
- - - - `); - return; + return res.render('dashboardSelection', { + baseRoot, + baseDashboard, + baseAuth, + baseApi + }); } - res.send(` - - - - - - Papo Dashboard - - - -
- ${sidebar} -
-
-
-

Guild Dashboard

-
Dashboard & Guild-Config
-
-
-

Guild

- -
-
-
-
-
-
- icon -
-

Guild

-

ID: -

-
-
-
Bot aktiv
-
-
-
-

Guild Infos

-
-
Owner
-
-
Erstellt
-
-
Member
-
-
Channels
-
-
Tickets offen
-
-
Tickets IP / Closed
- / -
-
-
-
-

Activity

-
-
Messages (24h)
-
-
Commands (24h)
-
-
Automod (24h)
-
-
-
-
-

Guild Logs

-
    -
    -
    -
    -
    -
    -
    -

    Tickets

    -

    bersicht, Pipeline, SLA, Automationen, Knowledge-Base.

    -
    -
    - - - - - -
    -
    - -
    -
    -
    -
    -

    Ticketliste

    -

    Links auswhlen, Details im Modal. Plus ffnet Panel-Erstellung.

    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -

    Support-Login Status

    -

    Aktive Supporter und letzte Sessions.

    -
    - -
    -
    -
    -

    Aktiv

    -
    -
    -
    -

    Letzte Sessions

    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -

    Status-Pipeline

    -

    Tickets nach Phase. Status per Dropdown ndern.

    -
    -
    -
    -

    Neu

    -

    In Bearbeitung

    -

    Warten auf User

    -

    Erledigt

    -
    -
    -
    - -
    -
    -
    -
    -

    SLA / Response-Zeiten

    -

    Average Time to Claim / First Response.

    -
    - -
    -
    -
    -

    SLA pro Supporter

    - - - -
    SupporterTicketsTTCTTFR
    -
    -
    -

    SLA pro Tag

    - - - -
    DatumTicketsTTCTTFR
    -
    -
    -
    -
    - -
    -
    -
    -
    -

    Automationen

    -

    Regeln fr Ticket-Aktionen.

    -
    - -
    -
    -
    -

    Regel bearbeiten

    -
    - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -

    Knowledge-Base

    -

    Artikel fr Self-Service.

    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Automod Einstellungen

    -

    Automod pro Guild aktivieren und Regeln konfigurieren.

    -
    -
    - Automod aktivieren -
    -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -

    Diese Woerter werden zusaetzlich zum Standard-Filter geblockt.

    -
    -
    - - -

    Nachrichten von diesen Rollen werden nicht gefiltert.

    -
    -
    -
    - -
    -

    -
    -
    -
    -
    -
    -
    -
    -

    Willkommensnachrichten

    -

    Embed konfigurieren und Feature aktivieren.

    -
    -
    - Aktivieren -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    Willkommen!
    -
    Schön, dass du da bist.
    - - -
    -
    -
    - -
    -

    -
    -
    -
    -
    -
    -
    -
    -

    Dynamic Voice

    -

    Lobby waehlen, Channel-Namen & Limits setzen.

    -
    -
    - Aktivieren -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -

    {user} wird durch den Mitgliedsnamen ersetzt.

    -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -

    -
    -
    -
    -
    -
    -

    Module

    -
    -
    -
    -
    -
    -
    -
    -

    Birthday

    -

    Automatische Glueckwuensche je Guild.

    -
    -
    - - - - -
    -
    -
    -
    - - -

    Nutze {user} als Platzhalter.

    -
    -
    - -
    -

    -
    -
    -
    -
    -

    Gespeicherte Geburtstage

    -

    Eintraege werden per /birthday angelegt.

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Reaction Roles

    -

    Reaktionen verteilen oder entfernen Rollen.

    -
    -
    -
    -
    - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - - -

    Eine Zeile pro Zuordnung. Label/Beschreibung optional.

    -
    -
    - - -
    -
    -
    -
    -
    -
    -

    Reaction Role Nachrichten

    -

    Bestehende Setups bearbeiten oder syncen.

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Statuspage

    -

    Services verwalten und Checks steuern.

    -
    -
    - - - - -
    -
    -
    -
    -
    -
    -
    -

    Services

    -

    Status, Uptime, letzter Check

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -

    Server Stats

    -

    Kategorie und Counter verwalten.

    -
    -
    - - - - -
    -
    -
    -
    -
    -
    -
    -

    Statistiken

    -

    Counter und Format anpassen.

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -

    Events

    -

    Termine planen und Erinnerungen senden.

    -
    - -
    -
    -
    -
    -
    -
    -
    -

    Guilds

    -

    -

    -

    aktiv (24h): -

    -
    -
    -

    Uptime

    -

    -

    -

    Start: -

    -
    -
    -
    -
    -
    -

    Aktivität (letzte 24h)

    -

    Events/Commands pro Stunde

    -
    -
    -
    -
    -
    -
    -
    -

    Logs

    -

    Neueste Einträge

    -
    -
    -
      -
      -
      -
      -
      -

      Einstellungen speichern

      -
      - - - - -

      -
      -
      -
      -

      Logging

      -
      -
      - - -

      Falls leer, wird der allgemeine Log Channel genutzt.

      -
      -
      -
      -
      👋 User Join / Leave
      -
      ✏️ Message Edit
      -
      🗑️ Message Delete
      -
      🛡️ Automod Actions
      -
      🎫 Ticket Actions
      -
      🎵 Musik-Events
      -
      ⚙️ System / Channels
      -
      -
      - -
      -

      -
      -
      -
      -
      -
      - - - - - - - - - - - -
      - - - - `); + return res.render('dashboard', { + baseRoot, + baseDashboard, + baseAuth, + baseApi, + guildId, + user: req.session.user + }); }); router.get('/settings', (_req, res) => { @@ -3020,5 +38,3 @@ router.get('/settings', (_req, res) => { }); export default router; - - diff --git a/tsconfig.frontend.json b/tsconfig.frontend.json new file mode 100644 index 0000000..3e29cb2 --- /dev/null +++ b/tsconfig.frontend.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "public/ts-build", + "rootDir": "public/ts", + "skipLibCheck": true, + "noEmitOnError": false + }, + "include": ["public/ts/**/*"] +} diff --git a/views/dashboard.ejs b/views/dashboard.ejs new file mode 100644 index 0000000..2924c03 --- /dev/null +++ b/views/dashboard.ejs @@ -0,0 +1,46 @@ + + + + + + Papo Dashboard + + + + +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + diff --git a/views/dashboardSelection.ejs b/views/dashboardSelection.ejs new file mode 100644 index 0000000..106cc29 --- /dev/null +++ b/views/dashboardSelection.ejs @@ -0,0 +1,36 @@ + + + + + + Papo Dashboard - Auswahl + + + + +
      + +
      + +
      +
      +
      +
      +
      + + +