mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-16 07:13:28 +02:00
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>
This commit is contained in:
1 parent
43356a120c
commit
486e6f6784
23 files changed
+1757
-387
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import { renderHeader, renderFooter, api, esc, formatDate, fromIso, toIso, formatTime } from './common.js';
|
||||
|
||||
renderHeader('home', { overlay: true });
|
||||
renderFooter();
|
||||
|
||||
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
|
||||
// ---------- Scrollen: Flagge zoomt heran ----------
|
||||
// Eine einzige rAF-Schleife, die nur läuft, wenn gescrollt wurde.
|
||||
const stage = document.getElementById('hero-stage');
|
||||
const hero = stage.parentElement;
|
||||
let ticking = false;
|
||||
|
||||
function frame() {
|
||||
ticking = false;
|
||||
if (reduceMotion.matches) {
|
||||
stage.style.setProperty('--p', 0);
|
||||
return;
|
||||
}
|
||||
const range = Math.max(1, hero.offsetHeight - innerHeight);
|
||||
const p = Math.min(1, Math.max(0, scrollY / range));
|
||||
stage.style.setProperty('--p', p.toFixed(4));
|
||||
}
|
||||
|
||||
function requestFrame() {
|
||||
if (!ticking) {
|
||||
ticking = true;
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
}
|
||||
addEventListener('scroll', requestFrame, { passive: true });
|
||||
addEventListener('resize', requestFrame);
|
||||
reduceMotion.addEventListener('change', requestFrame);
|
||||
frame();
|
||||
|
||||
// ---------- Abschnitte gleiten beim ersten Sichtbarwerden herein ----------
|
||||
// Nur was beim Laden unterhalb des Bildschirms liegt, wird erst versteckt – sichtbarer Inhalt bleibt sichtbar.
|
||||
if (!reduceMotion.matches && 'IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
entry.target.classList.add('reveal-in');
|
||||
entry.target.classList.remove('reveal-pending');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
}, { rootMargin: '0px 0px -10% 0px' });
|
||||
for (const el of document.querySelectorAll('.reveal')) {
|
||||
if (el.getBoundingClientRect().top > innerHeight) {
|
||||
el.classList.add('reveal-pending');
|
||||
observer.observe(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Nächster Einsatz + nächste Termine ----------
|
||||
function daysUntil(iso) {
|
||||
return Math.round((fromIso(iso) - fromIso(toIso(new Date()))) / 86400000);
|
||||
}
|
||||
|
||||
function renderNext(events) {
|
||||
const next = events.find((e) => e.isGame) || events[0];
|
||||
const title = document.getElementById('next-title');
|
||||
const when = document.getElementById('next-when');
|
||||
const box = document.getElementById('next-countdown');
|
||||
if (!next) {
|
||||
title.textContent = 'Noch nichts geplant';
|
||||
when.textContent = 'Der nächste Spieltag steht noch nicht im Kalender.';
|
||||
return;
|
||||
}
|
||||
title.textContent = next.title;
|
||||
when.textContent = [formatDate(next.date, true), formatTime(next)].filter(Boolean).join(' · ');
|
||||
const days = daysUntil(next.date);
|
||||
const label = next.attendeeCount === 1 ? '1 Zusage' : `${next.attendeeCount} Zusagen`;
|
||||
box.innerHTML = days <= 0
|
||||
? `<span class="big">Heute</span><span class="unit">${label}</span>`
|
||||
: `<span class="big">${days}</span>
|
||||
<span class="unit">${days === 1 ? 'Tag' : 'Tage'}<small>bis zum Spiel · ${label}</small></span>`;
|
||||
box.hidden = false;
|
||||
}
|
||||
|
||||
const box = document.getElementById('upcoming');
|
||||
try {
|
||||
const events = await api('GET', '/upcoming');
|
||||
renderNext(events);
|
||||
box.innerHTML = events.length
|
||||
? `<ul class="list">${events.map((e) => `
|
||||
<li>
|
||||
<span class="name"><strong>${esc(e.title)}</strong>
|
||||
<span class="meta">· ${formatDate(e.date, true)}${e.time ? `, ${e.time} Uhr` : ''}</span></span>
|
||||
${e.isGame ? '' : '<span class="badge nogame">Kein Spiel</span>'}
|
||||
<span class="badge">${e.attendeeCount} zugesagt</span>
|
||||
</li>`).join('')}</ul>`
|
||||
: '<p class="muted">Noch keine Termine eingetragen.</p>';
|
||||
} catch {
|
||||
box.innerHTML = '<p class="muted">Termine konnten nicht geladen werden.</p>';
|
||||
document.getElementById('next-title').textContent = 'Termine nicht erreichbar';
|
||||
}
|
||||
|
||||
// ---------- Pile of Shame: wer dieses Jahr noch nicht spielen war ----------
|
||||
const shame = document.getElementById('shame');
|
||||
try {
|
||||
const { year, entries } = await api('GET', '/stats');
|
||||
const names = entries.filter((s) => s.total === 0).map((s) => s.name).sort((a, b) => a.localeCompare(b, 'de'));
|
||||
shame.innerHTML = names.length
|
||||
? `<p class="muted">Waren ${year} noch kein einziges Mal spielen:</p>
|
||||
<div class="chips">${names.map((n) => `<span class="chip">${esc(n)}</span>`).join('')}</div>`
|
||||
: `<p class="muted">Leer, alle waren ${year} schon spielen.</p>`;
|
||||
} catch {
|
||||
shame.innerHTML = '<p class="muted">Konnte nicht geladen werden.</p>';
|
||||
}
|
||||
|
||||
// ---------- Patches (neueste zuerst) ----------
|
||||
try {
|
||||
const patches = await api('GET', '/patches');
|
||||
if (patches.length) {
|
||||
document.getElementById('patch-rail').innerHTML = patches.slice(0, 12).map((p) => `
|
||||
<a href="/patches#${p.id}">
|
||||
<figure>
|
||||
<div class="patch-img"><img src="/patch-img/${encodeURIComponent(p.image)}" alt="" loading="lazy"></div>
|
||||
<figcaption>${esc(p.name)}${p.status === 'geplant' ? ' <span class="badge nogame">In Planung</span>' : ''}</figcaption>
|
||||
</figure>
|
||||
</a>`).join('');
|
||||
document.getElementById('patch-section').hidden = false;
|
||||
}
|
||||
} catch { /* Abschnitt bleibt ausgeblendet */ }
|
||||
Reference in new issue
Block a user