Files
hcrb/public/js/common.js
T
guyg1474andClaude Opus 5.5 486e6f6784 Spiel-Events, Kalenderdateien, Patches-Seite und neues Design
- Kalender: Termine als Spiel-Event markieren (nur diese zählen in der
  Statistik), optionale Uhrzeiten, Termine bearbeiten
- .ics-Download für einzelne und alle kommenden Termine
- Neue Seite Patches: Upload (PNG, JPEG, WebP, geprüftes SVG),
  Status "Gibt es schon" / "In Planung", Bilder in data/patches/
- Volle Seitenbreite mit Spaltenlayouts, neues Design mit selbst
  gehosteten Schriften, Startseite mit Zoom und drehendem Zahnrad
- Bestehende Daten werden beim Laden ergänzt, keine Migration nötig

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:13:52 +02:00

161 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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' },
{ href: '/patches', key: 'patches', label: 'Patches' },
];
// overlay: Kopfzeile liegt durchsichtig über dem Hero und wird erst beim Scrollen fest (Startseite)
export function renderHeader(active, { overlay = false } = {}) {
const header = document.createElement('header');
header.className = overlay ? 'site-header overlay' : '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);
if (overlay) {
const update = () => header.classList.toggle('scrolled', scrollY > 40);
addEventListener('scroll', update, { passive: true });
update();
}
// 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 = `
<span class="f-brand"><img src="/img/logo.png" alt="" width="32" height="32">HCRB · Highcap Rasselbande · Berlin</span>
<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);
}
// Einmal pro Seite abfragen, ob jemand angemeldet ist
let mePromise;
export function currentUser() {
mePromise ??= api('GET', '/me').then(({ user }) => user || null).catch(() => null);
return mePromise;
}
async function renderAuthLink(nav) {
const user = await currentUser();
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);
}
// Kurzes Zeitformat für Termine: "09:00 Uhr" oder "09:00–15:00 Uhr"
export function formatTime(ev) {
if (!ev.time) return '';
return `${ev.time}${ev.endTime ? `–${ev.endTime}` : ''} Uhr`;
}