This commit is contained in:
336
public/styles/dashboard.css
Normal file
336
public/styles/dashboard.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
106
public/styles/dashboard.sections.css
Normal file
106
public/styles/dashboard.sections.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
3
public/ts-build/app.js
Normal file
3
public/ts-build/app.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
require("./core/app.js");
|
||||||
26
public/ts-build/components/admin/index.js
Normal file
26
public/ts-build/components/admin/index.js
Normal file
@@ -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 = '<p class="muted">Lade Admin-Daten...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.settings(guildId);
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Admin</h2>
|
||||||
|
<div class="card">
|
||||||
|
<p class="muted">Rohdaten (nur Admin):</p>
|
||||||
|
<pre style="white-space:pre-wrap;max-height:320px;overflow:auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Admin-Daten konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Admin-Daten', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
public/ts-build/components/dashboard.js
Normal file
93
public/ts-build/components/dashboard.js
Normal file
@@ -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 = `<option>Loading...</option>`;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
43
public/ts-build/components/events/index.js
Normal file
43
public/ts-build/components/events/index.js
Normal file
@@ -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 = '<p class="muted">Lade Events...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.events(guildId);
|
||||||
|
const events = data?.events || data || [];
|
||||||
|
section.innerHTML = '<h2 class="section-title">Events</h2>';
|
||||||
|
if (!events.length) {
|
||||||
|
section.innerHTML += '<div class="empty-state">Keine Events geplant.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'ticket-list';
|
||||||
|
events.forEach((ev) => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'ticket-item';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;">${ev.title || 'Event'}</div>
|
||||||
|
<div class="muted">${ev.date || ''}</div>
|
||||||
|
</div>
|
||||||
|
<span class="pill">${ev.status || 'open'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="muted">${ev.description || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
section.appendChild(list);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Events konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Events', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
public/ts-build/components/guildSelect.js
Normal file
59
public/ts-build/components/guildSelect.js
Normal file
@@ -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 = '<div class="muted">Lade Guilds...</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="row">
|
||||||
|
<img src="${g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : 'https://cdn.discordapp.com/embed/avatars/0.png'}" alt="icon" style="width:42px;height:42px;border-radius:12px;object-fit:cover;"/>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:700;">${g.name}</div>
|
||||||
|
<div class="muted">ID: ${g.id}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px;" class="pill">Zum Dashboard</div>
|
||||||
|
`;
|
||||||
|
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 = '<div class="empty-state">Bot ist in keiner Guild. Bitte Bot einladen.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
grid.innerHTML = '<div class="empty-state">Fehler beim Laden der Guilds</div>';
|
||||||
|
(0, toast_js_1.showToast)('Guilds konnten nicht geladen werden', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/ts-build/components/modules/dynamicVoice.js
Normal file
26
public/ts-build/components/modules/dynamicVoice.js
Normal file
@@ -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 = '<p class="muted">Lade Dynamic Voice...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.dynamicVoice(guildId);
|
||||||
|
const cfg = data?.config || data?.dynamicVoiceConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Dynamic Voice</h3>
|
||||||
|
<p class="muted">Lobby: ${cfg.lobbyChannelId || '-'}</p>
|
||||||
|
<p class="muted">Template: ${cfg.template || '-'}</p>
|
||||||
|
<p class="muted">User-Limit: ${cfg.userLimit ?? '-'}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Dynamic Voice konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden von Dynamic Voice', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
public/ts-build/components/modules/index.js
Normal file
104
public/ts-build/components/modules/index.js
Normal file
@@ -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 = `
|
||||||
|
<h2 class="section-title">Module</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="module-list" id="module-toggles"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="margin-top:16px;">
|
||||||
|
<div class="card" id="module-welcome"></div>
|
||||||
|
<div class="card" id="module-logging"></div>
|
||||||
|
<div class="card" id="module-reactionroles"></div>
|
||||||
|
<div class="card" id="module-dynamicvoice"></div>
|
||||||
|
<div class="card" id="module-statuspage"></div>
|
||||||
|
<div class="card" id="module-serverstats"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = '<p class="muted">Lade Module...</p>';
|
||||||
|
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 = `
|
||||||
|
<div class="module-meta">
|
||||||
|
<div class="module-title">${entry.label}</div>
|
||||||
|
<div class="module-desc">${entry.desc}</div>
|
||||||
|
</div>
|
||||||
|
<div class="switch ${modules[entry.key] ? 'on' : ''}" data-key="${entry.key}"></div>
|
||||||
|
`;
|
||||||
|
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 = '<div class="empty-state">Module konnten nicht geladen werden.</div>';
|
||||||
|
(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/ts-build/components/modules/logging.js
Normal file
26
public/ts-build/components/modules/logging.js
Normal file
@@ -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 = '<p class="muted">Lade Logging...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.settings(guildId);
|
||||||
|
const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Logging</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.logChannelId || '-'}</p>
|
||||||
|
<p class="muted">Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}</p>
|
||||||
|
<p class="muted">System: ${cfg.categories?.system ? 'an' : 'aus'}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Logging konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden von Logging', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
public/ts-build/components/modules/reactionRoles.js
Normal file
37
public/ts-build/components/modules/reactionRoles.js
Normal file
@@ -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 = '<p class="muted">Lade Reaction Roles...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.reactionRoles(guildId);
|
||||||
|
const entries = data?.entries || data?.reactionRoles || [];
|
||||||
|
container.innerHTML = '<h3 class="label">Reaction Roles</h3>';
|
||||||
|
if (!entries.length) {
|
||||||
|
container.innerHTML += '<div class="empty-state">Keine Reaction Roles.</div>';
|
||||||
|
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 = `
|
||||||
|
<div style="font-weight:750;">${e.title || e.messageId || 'Eintrag'}</div>
|
||||||
|
<div class="muted">${e.channelId || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.appendChild(list);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Reaction Roles konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Reaction Roles', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
public/ts-build/components/modules/serverstats.js
Normal file
27
public/ts-build/components/modules/serverstats.js
Normal file
@@ -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 = '<p class="muted">Lade Server Stats...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.serverStats(guildId);
|
||||||
|
const cfg = data?.config || data || {};
|
||||||
|
const items = cfg.items || [];
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Server Stats</h3>
|
||||||
|
<p class="muted">Kategorie: ${cfg.categoryId || '-'}</p>
|
||||||
|
<p class="muted">Refresh: ${cfg.refresh || '-'}m</p>
|
||||||
|
<p class="muted">Items: ${items.length}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Server Stats konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Server Stats', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
public/ts-build/components/modules/statuspage.js
Normal file
27
public/ts-build/components/modules/statuspage.js
Normal file
@@ -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 = '<p class="muted">Lade Statuspage...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.statuspage(guildId);
|
||||||
|
const cfg = data?.config || data || {};
|
||||||
|
const services = cfg.services || [];
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Statuspage</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.channelId || '-'}</p>
|
||||||
|
<p class="muted">Intervall: ${cfg.interval || '-'}m</p>
|
||||||
|
<p class="muted">Services: ${services.length}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Statuspage konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Statuspage', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/ts-build/components/modules/welcome.js
Normal file
26
public/ts-build/components/modules/welcome.js
Normal file
@@ -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 = '<p class="muted">Lade Welcome...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.settings(guildId);
|
||||||
|
const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Welcome</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.channelId || '-'}</p>
|
||||||
|
<p class="muted">Embed Titel: ${cfg.embedTitle || '-'}</p>
|
||||||
|
<p class="muted">Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Welcome konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden von Welcome', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
public/ts-build/components/overview.js
Normal file
37
public/ts-build/components/overview.js
Normal file
@@ -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 = '<p class="muted">Lade Uebersicht...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.overview(guildId);
|
||||||
|
const stats = data?.stats || {};
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Uebersicht</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Tickets offen</p>
|
||||||
|
<p class="stat">${stats.openTickets ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Module aktiv</p>
|
||||||
|
<p class="stat">${stats.activeModules ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Events geplant</p>
|
||||||
|
<p class="stat">${stats.events ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Uebersicht konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Uebersicht', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/ts-build/components/settings.js
Normal file
26
public/ts-build/components/settings.js
Normal file
@@ -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 = '<p class="muted">Lade Einstellungen...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.settings(guildId);
|
||||||
|
const settings = data?.settings || {};
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Einstellungen</h2>
|
||||||
|
<div class="card">
|
||||||
|
<pre style="white-space:pre-wrap;">${JSON.stringify(settings, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Einstellungen konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Einstellungen', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
public/ts-build/components/tickets/automations.js
Normal file
42
public/ts-build/components/tickets/automations.js
Normal file
@@ -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 = '<p class="muted">Lade Automationen...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.automations(guildId);
|
||||||
|
const rules = data?.rules || data || [];
|
||||||
|
if (!rules.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine Regeln angelegt.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'ticket-list';
|
||||||
|
rules.forEach((r) => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'ticket-item';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;">${r.name || 'Regel'}</div>
|
||||||
|
<div class="muted">${r.condition?.type || r.condition?.status || ''}</div>
|
||||||
|
</div>
|
||||||
|
<span class="pill">${r.active ? 'aktiv' : 'inaktiv'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Automationen</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Automationen konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Automationen', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
public/ts-build/components/tickets/index.js
Normal file
32
public/ts-build/components/tickets/index.js
Normal file
@@ -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 = `
|
||||||
|
<h2 class="section-title">Tickets</h2>
|
||||||
|
<div class="tickets-grid">
|
||||||
|
<div class="card" id="tickets-list"></div>
|
||||||
|
<div class="card" id="tickets-pipeline"></div>
|
||||||
|
<div class="card" id="tickets-sla"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="margin-top:16px;">
|
||||||
|
<div class="card" id="tickets-automations"></div>
|
||||||
|
<div class="card" id="tickets-kb"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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)
|
||||||
|
]);
|
||||||
|
}
|
||||||
37
public/ts-build/components/tickets/kb.js
Normal file
37
public/ts-build/components/tickets/kb.js
Normal file
@@ -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 = '<p class="muted">Lade Knowledge Base...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.kb(guildId);
|
||||||
|
const entries = data?.articles || data?.kb || [];
|
||||||
|
if (!entries.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine KB-Eintraege.</div>';
|
||||||
|
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 = `
|
||||||
|
<div style="font-weight:750;">${k.title || 'Artikel'}</div>
|
||||||
|
<div class="muted">${k.keywords || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Knowledge Base</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">KB konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der KB', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
public/ts-build/components/tickets/list.js
Normal file
43
public/ts-build/components/tickets/list.js
Normal file
@@ -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 = '<p class="muted">Lade Tickets...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.tickets(guildId);
|
||||||
|
const tickets = data?.tickets || [];
|
||||||
|
if (!tickets.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine Tickets</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;font-size:15px;">${t.title || t.id}</div>
|
||||||
|
<div class="muted">${t.user || ''}</div>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-status status-${t.status || 'open'}">${t.status || 'open'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="muted">${t.description || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Aktuelle Tickets</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Tickets konnten nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Tickets', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
public/ts-build/components/tickets/pipeline.js
Normal file
37
public/ts-build/components/tickets/pipeline.js
Normal file
@@ -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 = '<p class="muted">Lade Pipeline...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.pipeline(guildId);
|
||||||
|
const lanes = data?.lanes || [];
|
||||||
|
container.innerHTML = '<h3 class="label">Pipeline</h3>';
|
||||||
|
if (!lanes.length) {
|
||||||
|
container.innerHTML += '<div class="empty-state">Keine Pipeline-Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'grid';
|
||||||
|
lanes.forEach((lane) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<p class="label">${lane.name || 'Lane'}</p>
|
||||||
|
<p class="stat">${lane.count ?? 0}</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
container.appendChild(grid);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Pipeline konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der Pipeline', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
public/ts-build/components/tickets/sla.js
Normal file
25
public/ts-build/components/tickets/sla.js
Normal file
@@ -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 = '<p class="muted">Lade SLA...</p>';
|
||||||
|
try {
|
||||||
|
const data = await api_js_1.api.sla(guildId);
|
||||||
|
const stats = data?.stats || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">SLA</h3>
|
||||||
|
<p class="stat">${stats.averageResponse ?? '-'}m</p>
|
||||||
|
<p class="muted">Durchschnittliche Antwortzeit</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">SLA konnte nicht geladen werden.</div>';
|
||||||
|
(0, toast_js_1.showToast)('Fehler beim Laden der SLA', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
public/ts-build/core/app.js
Normal file
69
public/ts-build/core/app.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
74
public/ts-build/services/api.js
Normal file
74
public/ts-build/services/api.js
Normal file
@@ -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) })
|
||||||
|
};
|
||||||
32
public/ts-build/state/store.js
Normal file
32
public/ts-build/state/store.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
27
public/ts-build/ui/modal.js
Normal file
27
public/ts-build/ui/modal.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
57
public/ts-build/ui/navigation.js
Normal file
57
public/ts-build/ui/navigation.js
Normal file
@@ -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 = `<span class="icon">${item.icon || ''}</span>${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);
|
||||||
|
});
|
||||||
|
}
|
||||||
19
public/ts-build/ui/switch.js
Normal file
19
public/ts-build/ui/switch.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
27
public/ts-build/ui/toast.js
Normal file
27
public/ts-build/ui/toast.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
1
public/ts/app.ts
Normal file
1
public/ts/app.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './core/app.js';
|
||||||
22
public/ts/components/admin/index.ts
Normal file
22
public/ts/components/admin/index.ts
Normal file
@@ -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 = '<p class="muted">Lade Admin-Daten...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.settings(guildId);
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Admin</h2>
|
||||||
|
<div class="card">
|
||||||
|
<p class="muted">Rohdaten (nur Admin):</p>
|
||||||
|
<pre style="white-space:pre-wrap;max-height:320px;overflow:auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Admin-Daten konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Admin-Daten', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
public/ts/components/dashboard.ts
Normal file
88
public/ts/components/dashboard.ts
Normal file
@@ -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 = `<option>Loading...</option>`;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
39
public/ts/components/events/index.ts
Normal file
39
public/ts/components/events/index.ts
Normal file
@@ -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 = '<p class="muted">Lade Events...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.events(guildId);
|
||||||
|
const events = data?.events || data || [];
|
||||||
|
section.innerHTML = '<h2 class="section-title">Events</h2>';
|
||||||
|
if (!events.length) {
|
||||||
|
section.innerHTML += '<div class="empty-state">Keine Events geplant.</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;">${ev.title || 'Event'}</div>
|
||||||
|
<div class="muted">${ev.date || ''}</div>
|
||||||
|
</div>
|
||||||
|
<span class="pill">${ev.status || 'open'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="muted">${ev.description || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
section.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Events konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Events', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
public/ts/components/guildSelect.ts
Normal file
56
public/ts/components/guildSelect.ts
Normal file
@@ -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 = '<div class="muted">Lade Guilds...</div>';
|
||||||
|
try {
|
||||||
|
const data = await api.guilds();
|
||||||
|
grid.innerHTML = '';
|
||||||
|
(data.guilds || []).forEach((g) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card clickable';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="row">
|
||||||
|
<img src="${g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : 'https://cdn.discordapp.com/embed/avatars/0.png'}" alt="icon" style="width:42px;height:42px;border-radius:12px;object-fit:cover;"/>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:700;">${g.name}</div>
|
||||||
|
<div class="muted">ID: ${g.id}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px;" class="pill">Zum Dashboard</div>
|
||||||
|
`;
|
||||||
|
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 = '<div class="empty-state">Bot ist in keiner Guild. Bitte Bot einladen.</div>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
grid.innerHTML = '<div class="empty-state">Fehler beim Laden der Guilds</div>';
|
||||||
|
showToast('Guilds konnten nicht geladen werden', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
public/ts/components/modules/dynamicVoice.ts
Normal file
22
public/ts/components/modules/dynamicVoice.ts
Normal file
@@ -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 = '<p class="muted">Lade Dynamic Voice...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.dynamicVoice(guildId);
|
||||||
|
const cfg = data?.config || data?.dynamicVoiceConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Dynamic Voice</h3>
|
||||||
|
<p class="muted">Lobby: ${cfg.lobbyChannelId || '-'}</p>
|
||||||
|
<p class="muted">Template: ${cfg.template || '-'}</p>
|
||||||
|
<p class="muted">User-Limit: ${cfg.userLimit ?? '-'}</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Dynamic Voice konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden von Dynamic Voice', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
public/ts/components/modules/index.ts
Normal file
99
public/ts/components/modules/index.ts
Normal file
@@ -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 = `
|
||||||
|
<h2 class="section-title">Module</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="module-list" id="module-toggles"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="margin-top:16px;">
|
||||||
|
<div class="card" id="module-welcome"></div>
|
||||||
|
<div class="card" id="module-logging"></div>
|
||||||
|
<div class="card" id="module-reactionroles"></div>
|
||||||
|
<div class="card" id="module-dynamicvoice"></div>
|
||||||
|
<div class="card" id="module-statuspage"></div>
|
||||||
|
<div class="card" id="module-serverstats"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = '<p class="muted">Lade Module...</p>';
|
||||||
|
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 = `
|
||||||
|
<div class="module-meta">
|
||||||
|
<div class="module-title">${entry.label}</div>
|
||||||
|
<div class="module-desc">${entry.desc}</div>
|
||||||
|
</div>
|
||||||
|
<div class="switch ${modules[entry.key] ? 'on' : ''}" data-key="${entry.key}"></div>
|
||||||
|
`;
|
||||||
|
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 = '<div class="empty-state">Module konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Module', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveModules(guildId: string) {
|
||||||
|
const toggles = Array.from(document.querySelectorAll<HTMLElement>('#module-toggles .switch'));
|
||||||
|
const payload: Record<string, unknown> = { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
public/ts/components/modules/logging.ts
Normal file
22
public/ts/components/modules/logging.ts
Normal file
@@ -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 = '<p class="muted">Lade Logging...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.settings(guildId);
|
||||||
|
const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Logging</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.logChannelId || '-'}</p>
|
||||||
|
<p class="muted">Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}</p>
|
||||||
|
<p class="muted">System: ${cfg.categories?.system ? 'an' : 'aus'}</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Logging konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden von Logging', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/ts/components/modules/reactionRoles.ts
Normal file
33
public/ts/components/modules/reactionRoles.ts
Normal file
@@ -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 = '<p class="muted">Lade Reaction Roles...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.reactionRoles(guildId);
|
||||||
|
const entries = data?.entries || data?.reactionRoles || [];
|
||||||
|
container.innerHTML = '<h3 class="label">Reaction Roles</h3>';
|
||||||
|
if (!entries.length) {
|
||||||
|
container.innerHTML += '<div class="empty-state">Keine Reaction Roles.</div>';
|
||||||
|
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 = `
|
||||||
|
<div style="font-weight:750;">${e.title || e.messageId || 'Eintrag'}</div>
|
||||||
|
<div class="muted">${e.channelId || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Reaction Roles konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Reaction Roles', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
public/ts/components/modules/serverstats.ts
Normal file
23
public/ts/components/modules/serverstats.ts
Normal file
@@ -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 = '<p class="muted">Lade Server Stats...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.serverStats(guildId);
|
||||||
|
const cfg = data?.config || data || {};
|
||||||
|
const items = cfg.items || [];
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Server Stats</h3>
|
||||||
|
<p class="muted">Kategorie: ${cfg.categoryId || '-'}</p>
|
||||||
|
<p class="muted">Refresh: ${cfg.refresh || '-'}m</p>
|
||||||
|
<p class="muted">Items: ${items.length}</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Server Stats konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Server Stats', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
public/ts/components/modules/statuspage.ts
Normal file
23
public/ts/components/modules/statuspage.ts
Normal file
@@ -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 = '<p class="muted">Lade Statuspage...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.statuspage(guildId);
|
||||||
|
const cfg = data?.config || data || {};
|
||||||
|
const services = cfg.services || [];
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Statuspage</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.channelId || '-'}</p>
|
||||||
|
<p class="muted">Intervall: ${cfg.interval || '-'}m</p>
|
||||||
|
<p class="muted">Services: ${services.length}</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Statuspage konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Statuspage', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
public/ts/components/modules/welcome.ts
Normal file
22
public/ts/components/modules/welcome.ts
Normal file
@@ -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 = '<p class="muted">Lade Welcome...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.settings(guildId);
|
||||||
|
const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">Welcome</h3>
|
||||||
|
<p class="muted">Channel: ${cfg.channelId || '-'}</p>
|
||||||
|
<p class="muted">Embed Titel: ${cfg.embedTitle || '-'}</p>
|
||||||
|
<p class="muted">Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Welcome konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden von Welcome', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/ts/components/overview.ts
Normal file
33
public/ts/components/overview.ts
Normal file
@@ -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 = '<p class="muted">Lade Uebersicht...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.overview(guildId);
|
||||||
|
const stats = data?.stats || {};
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Uebersicht</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Tickets offen</p>
|
||||||
|
<p class="stat">${stats.openTickets ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Module aktiv</p>
|
||||||
|
<p class="stat">${stats.activeModules ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="label">Events geplant</p>
|
||||||
|
<p class="stat">${stats.events ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Uebersicht konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Uebersicht', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
public/ts/components/settings.ts
Normal file
22
public/ts/components/settings.ts
Normal file
@@ -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 = '<p class="muted">Lade Einstellungen...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.settings(guildId);
|
||||||
|
const settings = data?.settings || {};
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">Einstellungen</h2>
|
||||||
|
<div class="card">
|
||||||
|
<pre style="white-space:pre-wrap;">${JSON.stringify(settings, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
section.innerHTML = '<div class="empty-state">Einstellungen konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Einstellungen', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
public/ts/components/tickets/automations.ts
Normal file
38
public/ts/components/tickets/automations.ts
Normal file
@@ -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 = '<p class="muted">Lade Automationen...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.automations(guildId);
|
||||||
|
const rules = data?.rules || data || [];
|
||||||
|
if (!rules.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine Regeln angelegt.</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;">${r.name || 'Regel'}</div>
|
||||||
|
<div class="muted">${r.condition?.type || r.condition?.status || ''}</div>
|
||||||
|
</div>
|
||||||
|
<span class="pill">${r.active ? 'aktiv' : 'inaktiv'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Automationen</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Automationen konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Automationen', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
public/ts/components/tickets/index.ts
Normal file
29
public/ts/components/tickets/index.ts
Normal file
@@ -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 = `
|
||||||
|
<h2 class="section-title">Tickets</h2>
|
||||||
|
<div class="tickets-grid">
|
||||||
|
<div class="card" id="tickets-list"></div>
|
||||||
|
<div class="card" id="tickets-pipeline"></div>
|
||||||
|
<div class="card" id="tickets-sla"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="margin-top:16px;">
|
||||||
|
<div class="card" id="tickets-automations"></div>
|
||||||
|
<div class="card" id="tickets-kb"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
await Promise.all([
|
||||||
|
renderTicketList(guildId),
|
||||||
|
renderPipeline(guildId),
|
||||||
|
renderSla(guildId),
|
||||||
|
renderAutomations(guildId),
|
||||||
|
renderKb(guildId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
33
public/ts/components/tickets/kb.ts
Normal file
33
public/ts/components/tickets/kb.ts
Normal file
@@ -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 = '<p class="muted">Lade Knowledge Base...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.kb(guildId);
|
||||||
|
const entries = data?.articles || data?.kb || [];
|
||||||
|
if (!entries.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine KB-Eintraege.</div>';
|
||||||
|
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 = `
|
||||||
|
<div style="font-weight:750;">${k.title || 'Artikel'}</div>
|
||||||
|
<div class="muted">${k.keywords || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Knowledge Base</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">KB konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der KB', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
public/ts/components/tickets/list.ts
Normal file
39
public/ts/components/tickets/list.ts
Normal file
@@ -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 = '<p class="muted">Lade Tickets...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.tickets(guildId);
|
||||||
|
const tickets = data?.tickets || [];
|
||||||
|
if (!tickets.length) {
|
||||||
|
container.innerHTML = '<div class="empty-state">Keine Tickets</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="row" style="justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:750;font-size:15px;">${t.title || t.id}</div>
|
||||||
|
<div class="muted">${t.user || ''}</div>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-status status-${t.status || 'open'}">${t.status || 'open'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="muted">${t.description || ''}</div>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.innerHTML = '<h3 class="label">Aktuelle Tickets</h3>';
|
||||||
|
container.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Tickets konnten nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Tickets', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/ts/components/tickets/pipeline.ts
Normal file
33
public/ts/components/tickets/pipeline.ts
Normal file
@@ -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 = '<p class="muted">Lade Pipeline...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.pipeline(guildId);
|
||||||
|
const lanes = data?.lanes || [];
|
||||||
|
container.innerHTML = '<h3 class="label">Pipeline</h3>';
|
||||||
|
if (!lanes.length) {
|
||||||
|
container.innerHTML += '<div class="empty-state">Keine Pipeline-Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'grid';
|
||||||
|
lanes.forEach((lane: any) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<p class="label">${lane.name || 'Lane'}</p>
|
||||||
|
<p class="stat">${lane.count ?? 0}</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
container.appendChild(grid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">Pipeline konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der Pipeline', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
public/ts/components/tickets/sla.ts
Normal file
21
public/ts/components/tickets/sla.ts
Normal file
@@ -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 = '<p class="muted">Lade SLA...</p>';
|
||||||
|
try {
|
||||||
|
const data: any = await api.sla(guildId);
|
||||||
|
const stats = data?.stats || {};
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="label">SLA</h3>
|
||||||
|
<p class="stat">${stats.averageResponse ?? '-'}m</p>
|
||||||
|
<p class="muted">Durchschnittliche Antwortzeit</p>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
container.innerHTML = '<div class="empty-state">SLA konnte nicht geladen werden.</div>';
|
||||||
|
showToast('Fehler beim Laden der SLA', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
public/ts/core/app.ts
Normal file
67
public/ts/core/app.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
79
public/ts/services/api.ts
Normal file
79
public/ts/services/api.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { getConfig } from '../state/store.js';
|
||||||
|
|
||||||
|
type FetchOptions = RequestInit & { query?: Record<string, string | number | boolean | undefined> };
|
||||||
|
|
||||||
|
function buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||||||
|
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<T = unknown>(path: string, options: FetchOptions = {}): Promise<T> {
|
||||||
|
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<string, unknown>) => 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<string, unknown>) =>
|
||||||
|
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<string, unknown>) =>
|
||||||
|
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<string, unknown> & { 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<string, unknown> & { 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<string, unknown>) => request('/statuspage', { method: 'POST', body: JSON.stringify(payload) }),
|
||||||
|
serverStats: (guildId: string) => request(`/serverstats`, { query: { guildId } }),
|
||||||
|
saveServerStats: (payload: Record<string, unknown>) => request('/serverstats', { method: 'POST', body: JSON.stringify(payload) }),
|
||||||
|
dynamicVoice: (guildId: string) => request(`/dynamicvoice`, { query: { guildId } }),
|
||||||
|
saveDynamicVoice: (payload: Record<string, unknown>) => request('/dynamicvoice', { method: 'POST', body: JSON.stringify(payload) })
|
||||||
|
};
|
||||||
49
public/ts/state/store.ts
Normal file
49
public/ts/state/store.ts
Normal file
@@ -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<Listener>();
|
||||||
|
|
||||||
|
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<AppState>) {
|
||||||
|
state = { ...state, ...partial };
|
||||||
|
listeners.forEach((l) => l(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribe(listener: Listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
}
|
||||||
23
public/ts/ui/modal.ts
Normal file
23
public/ts/ui/modal.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
67
public/ts/ui/navigation.ts
Normal file
67
public/ts/ui/navigation.ts
Normal file
@@ -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 = `<span class="icon">${item.icon || ''}</span>${item.label}`;
|
||||||
|
nav.appendChild(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(brand);
|
||||||
|
container.appendChild(nav);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initNavigation(onChange: (section: string) => void) {
|
||||||
|
const navLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>('.nav a'));
|
||||||
|
const activate = (section: string) => {
|
||||||
|
navLinks.forEach((link) => link.classList.toggle('active', link.dataset.target === section));
|
||||||
|
document.querySelectorAll<HTMLElement>('.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
14
public/ts/ui/switch.ts
Normal file
14
public/ts/ui/switch.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
23
public/ts/ui/toast.ts
Normal file
23
public/ts/ui/toast.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
12
tsconfig.frontend.json
Normal file
12
tsconfig.frontend.json
Normal file
@@ -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/**/*"]
|
||||||
|
}
|
||||||
46
views/dashboard.ejs
Normal file
46
views/dashboard.ejs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Papo Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/public/styles/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="/public/styles/dashboard.sections.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"
|
||||||
|
data-view="dashboard"
|
||||||
|
data-base-root="<%= baseRoot %>"
|
||||||
|
data-base-dashboard="<%= baseDashboard %>"
|
||||||
|
data-base-auth="<%= baseAuth %>"
|
||||||
|
data-base-api="<%= baseApi %>"
|
||||||
|
data-guild-id="<%= guildId %>"
|
||||||
|
data-user-name="<%= user?.username || '' %>"
|
||||||
|
data-user-disc="<%= user?.discriminator || '' %>"
|
||||||
|
data-user-admin="<%= user?.isAdmin ? 'true' : 'false' %>">
|
||||||
|
<aside class="sidebar" id="sidebar-root"></aside>
|
||||||
|
<main class="content" id="content-root">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="muted">Papo Control</p>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p class="muted" id="userInfo"></p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<select id="guildSelect" class="select"></select>
|
||||||
|
<button id="logoutBtn" class="btn danger">Logout</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section id="section-overview" class="section active"></section>
|
||||||
|
<section id="section-tickets" class="section"></section>
|
||||||
|
<section id="section-modules" class="section"></section>
|
||||||
|
<section id="section-settings" class="section"></section>
|
||||||
|
<section id="section-events" class="section"></section>
|
||||||
|
<section id="section-admin" class="section"></section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div id="modal-root"></div>
|
||||||
|
<div id="toast-root"></div>
|
||||||
|
<script type="module" src="/public/ts-build/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
views/dashboardSelection.ejs
Normal file
36
views/dashboardSelection.ejs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Papo Dashboard - Auswahl</title>
|
||||||
|
<link rel="stylesheet" href="/public/styles/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="/public/styles/dashboard.sections.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"
|
||||||
|
data-view="selection"
|
||||||
|
data-base-root="<%= baseRoot %>"
|
||||||
|
data-base-dashboard="<%= baseDashboard %>"
|
||||||
|
data-base-auth="<%= baseAuth %>"
|
||||||
|
data-base-api="<%= baseApi %>">
|
||||||
|
<aside class="sidebar" id="sidebar-root"></aside>
|
||||||
|
<main class="content" id="content-root">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="muted">Papo Control</p>
|
||||||
|
<h1>Waehle eine Guild aus</h1>
|
||||||
|
<p class="muted">Nur Guilds, auf denen der Bot ist.</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="logoutBtn" class="btn danger">Logout</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section id="guild-grid-section">
|
||||||
|
<div id="guildGrid" class="grid cards-grid"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/public/ts-build/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user