Erste Version: HCRB Spieltag-Kalender und Fahrtplaner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-10 13:54:33 +02:00
commit 10400eee59
30 files changed
+3771

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
// Gemeinsame Helfer für alle Seiten
export const $ = (sel, root = document) => root.querySelector(sel);
export function esc(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[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 = `
<div class="inner">
<a class="brand" href="/"><img src="/img/logo.png" alt="" width="40" height="40">HCRB<small>Highcap Rasselbande</small></a>
<button type="button" class="nav-toggle" aria-expanded="false" aria-controls="site-nav" aria-label="Menü">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 6h18M3 12h18M3 18h18"/>
</svg>
</button>
<nav id="site-nav">
${NAV.map((n) => `<a href="${n.href}" class="${n.key === active ? 'active' : ''}">${n.label}</a>`).join('')}
</nav>
</div>`;
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', '<link rel="icon" type="image/png" href="/img/favicon.png">');
}
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
<div class="footer-links">
<a href="https://www.youtube.com/@highcaprasselbande" target="_blank" rel="noopener">YouTube</a>
<a href="https://www.instagram.com/hcrb.airsoft" target="_blank" rel="noopener">Instagram</a>
</div>`;
document.body.append(footer);
}
async function renderAuthLink(nav) {
const { user } = await api('GET', '/me').catch(() => ({}));
if (!user) {
nav.insertAdjacentHTML('beforeend', '<a href="/login">Anmelden</a>');
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);
}