// Gemeinsame Helfer für alle Seiten export const $ = (sel, root = document) => root.querySelector(sel); export function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } export function sortByName(list) { return [...list].sort((a, b) => a.name.localeCompare(b.name, 'de')); } export function seatsLabel(n) { return `${n} ${n === 1 ? 'Platz' : 'Plätze'}`; } // ---------- API ---------- export async function api(method, path, body) { const res = await fetch('/api' + path, { method, headers: body !== undefined ? { 'Content-Type': 'application/json' } : {}, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (res.status === 401 && path !== '/login') { // Sitzung abgelaufen → zur Anmeldung und danach hierher zurück location.href = `/login?next=${encodeURIComponent(location.pathname + location.search)}`; return new Promise(() => {}); } if (res.status === 204) return null; const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Fehler ${res.status}`); return data; } export function showError(err) { alert(err.message || 'Es ist ein Fehler aufgetreten.'); } // ---------- Datum ---------- // Datumswerte werden als "YYYY-MM-DD" gespeichert (ohne Zeitzone) export function toIso(d) { const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } export function fromIso(iso) { const [y, m, d] = iso.split('-').map(Number); return new Date(y, m - 1, d); } export function formatDate(iso, withWeekday = false) { if (!iso) return ''; return fromIso(iso).toLocaleDateString('de-DE', { weekday: withWeekday ? 'short' : undefined, day: '2-digit', month: '2-digit', year: 'numeric', }); } // ---------- "Ich bin ..." (pro Browser gemerkt) ---------- const ME_KEY = 'hcrb.me'; export function getMe() { try { return localStorage.getItem(ME_KEY); } catch { return null; } } export function setMe(id) { try { id ? localStorage.setItem(ME_KEY, id) : localStorage.removeItem(ME_KEY); } catch { /* ignorieren */ } } // ---------- Kopfzeile ---------- const NAV = [ { href: '/', key: 'home', label: 'Start' }, { href: '/kalender', key: 'kalender', label: 'Kalender' }, { href: '/fahrt', key: 'fahrt', label: 'Fahrtplaner' }, { href: '/gruppe', key: 'gruppe', label: 'Gruppe' }, { href: '/statistik', key: 'statistik', label: 'Statistik' }, ]; export function renderHeader(active) { const header = document.createElement('header'); header.className = 'site-header'; header.innerHTML = `
HCRBHighcap Rasselbande
`; document.body.prepend(header); // Auf schmalen Bildschirmen klappt das Menü über die Schaltfläche auf const toggle = header.querySelector('.nav-toggle'); toggle.addEventListener('click', () => { const open = header.classList.toggle('open'); toggle.setAttribute('aria-expanded', open); }); if (!document.querySelector('link[rel="icon"]')) { document.head.insertAdjacentHTML('beforeend', ''); } if (active !== 'login') renderAuthLink(header.querySelector('nav')); } // ---------- Fußzeile (öffentliche Seiten) ---------- export function renderFooter() { const footer = document.createElement('footer'); footer.innerHTML = ` HCRB · Highcap Rasselbande · Berlin `; document.body.append(footer); } async function renderAuthLink(nav) { const { user } = await api('GET', '/me').catch(() => ({})); if (!user) { nav.insertAdjacentHTML('beforeend', 'Anmelden'); return; } const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Abmelden'; btn.title = `Angemeldet als ${user}`; btn.addEventListener('click', async () => { await api('POST', '/logout').catch(() => {}); location.href = '/'; }); nav.append(btn); }