[deploy]
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s

This commit is contained in:
Pascal Prießnitz
2025-12-04 16:43:38 +01:00
parent 311f5a87f1
commit 22caa79b54
60 changed files with 2652 additions and 2999 deletions

23
public/ts/ui/modal.ts Normal file
View 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;
}

View 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
View 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
View 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');
}