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
+84
-28
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
$, api, esc, sortByName, showError, renderHeader,
|
||||
toIso, fromIso, formatDate, getMe, setMe,
|
||||
toIso, fromIso, formatDate, formatTime, getMe, setMe,
|
||||
} from './common.js';
|
||||
|
||||
renderHeader('kalender');
|
||||
@@ -8,6 +8,7 @@ renderHeader('kalender');
|
||||
let members = [];
|
||||
let events = [];
|
||||
let me = getMe();
|
||||
let editingId = null; // Termin, der gerade im Formular bearbeitet wird
|
||||
|
||||
const todayIso = toIso(new Date());
|
||||
let selectedDay = todayIso;
|
||||
@@ -23,7 +24,7 @@ function renderMe() {
|
||||
sortByName(members).map((m) =>
|
||||
`<option value="${m.id}" ${m.id === me ? 'selected' : ''}>${esc(m.name)}</option>`).join('');
|
||||
$('#me-hint').hidden = !!me;
|
||||
$('#event-form button[type=submit]').disabled = !me;
|
||||
$('#event-submit').disabled = !me && !editingId;
|
||||
}
|
||||
|
||||
$('#me-select').addEventListener('change', (e) => {
|
||||
@@ -53,14 +54,15 @@ function renderCalendar() {
|
||||
const iso = toIso(day);
|
||||
const classes = ['cal-day'];
|
||||
if (day.getMonth() !== viewMonth.getMonth()) classes.push('other');
|
||||
if (day.getDay() === 0 || day.getDay() === 6) classes.push('weekend');
|
||||
if (iso === todayIso) classes.push('today');
|
||||
if (iso === selectedDay) classes.push('selected');
|
||||
const dayEvents = byDate.get(iso) || [];
|
||||
html += `
|
||||
<button class="${classes.join(' ')}" data-day="${iso}">
|
||||
<span class="num">${day.getDate()}</span>
|
||||
${dayEvents.map((ev) => `<span class="ev ${ev.attendees.includes(me) ? 'mine' : ''}"
|
||||
title="${esc(ev.title)} (${ev.attendees.length} zugesagt)">${esc(ev.title)} · ${ev.attendees.length}</span>`).join('')}
|
||||
${dayEvents.map((ev) => `<span class="ev ${ev.isGame ? '' : 'nogame'} ${ev.attendees.includes(me) ? 'mine' : ''}"
|
||||
title="${esc(ev.title)}${ev.isGame ? '' : ' (kein Spiel)'} · ${ev.attendees.length} zugesagt">${ev.time ? `${ev.time} ` : ''}${esc(ev.title)} · ${ev.attendees.length}</span>`).join('')}
|
||||
</button>`;
|
||||
day.setDate(day.getDate() + 1);
|
||||
}
|
||||
@@ -70,34 +72,43 @@ function renderCalendar() {
|
||||
$('#calendar').addEventListener('click', (e) => {
|
||||
const cell = e.target.closest('[data-day]');
|
||||
if (!cell) return;
|
||||
selectedDay = cell.dataset.day;
|
||||
const d = fromIso(selectedDay);
|
||||
if (d.getMonth() !== viewMonth.getMonth()) viewMonth = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
renderAll();
|
||||
$('#day-title').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
selectDay(cell.dataset.day);
|
||||
// Auf schmalen Bildschirmen stehen die Details unter dem Kalender
|
||||
if (matchMedia('(max-width: 999px)').matches) $('#day-title').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
|
||||
function selectDay(iso) {
|
||||
selectedDay = iso;
|
||||
const d = fromIso(iso);
|
||||
if (d.getMonth() !== viewMonth.getMonth()) viewMonth = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
if (!editingId) $('#event-date').value = iso;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function shiftMonth(delta) {
|
||||
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + delta, 1);
|
||||
renderCalendar();
|
||||
}
|
||||
$('#prev-month').addEventListener('click', () => shiftMonth(-1));
|
||||
$('#next-month').addEventListener('click', () => shiftMonth(1));
|
||||
$('#today-btn').addEventListener('click', () => {
|
||||
selectedDay = todayIso;
|
||||
viewMonth = fromIso(todayIso);
|
||||
viewMonth.setDate(1);
|
||||
renderAll();
|
||||
});
|
||||
$('#today-btn').addEventListener('click', () => selectDay(todayIso));
|
||||
|
||||
// ---------- Termine ----------
|
||||
const downloadIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M12 3v12M7 10l5 5 5-5M4 21h16"/></svg>';
|
||||
|
||||
function eventHtml(ev) {
|
||||
const going = ev.attendees.includes(me);
|
||||
const attendees = sortByName(ev.attendees.map((id) => ({ id, name: memberName(id) })));
|
||||
const when = [formatDate(ev.date, true), formatTime(ev)].filter(Boolean).join(' · ');
|
||||
return `
|
||||
<div class="event">
|
||||
<h3>${esc(ev.title)}</h3>
|
||||
<div class="when">${formatDate(ev.date, true)}${ev.createdBy ? ` · eingetragen von ${esc(memberName(ev.createdBy))}` : ''}</div>
|
||||
<div class="event ${ev.isGame ? '' : 'nogame'}">
|
||||
<div class="event-top">
|
||||
<h3>${esc(ev.title)}</h3>
|
||||
${ev.isGame
|
||||
? '<span class="badge game" title="Zählt für die Statistik">Spiel</span>'
|
||||
: '<span class="badge nogame" title="Zählt nicht für die Statistik">Kein Spiel</span>'}
|
||||
</div>
|
||||
<div class="when">${when}${ev.createdBy ? ` · eingetragen von ${esc(memberName(ev.createdBy))}` : ''}</div>
|
||||
${ev.note ? `<div class="note">${esc(ev.note)}</div>` : ''}
|
||||
<div class="muted">${ev.attendees.length} zugesagt:</div>
|
||||
<div class="chips">
|
||||
@@ -110,6 +121,8 @@ function eventHtml(ev) {
|
||||
? `<button data-action="leave" data-id="${ev.id}">Absagen</button>`
|
||||
: `<button class="primary" data-action="join" data-id="${ev.id}">Ich bin dabei</button>`) : ''}
|
||||
<a class="button" href="/fahrt?termin=${ev.id}">Fahrt planen</a>
|
||||
<a class="button" href="/api/events/${ev.id}/ics" download title="Termin als .ics-Datei in den eigenen Kalender übernehmen">${downloadIcon}Kalender</a>
|
||||
<button class="link" data-action="edit" data-id="${ev.id}">Bearbeiten</button>
|
||||
<button class="link danger" data-action="delete" data-id="${ev.id}">Löschen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -122,14 +135,14 @@ function renderDay() {
|
||||
const dayEvents = events.filter((e) => e.date === selectedDay);
|
||||
$('#day-events').innerHTML = dayEvents.length
|
||||
? dayEvents.map(eventHtml).join('')
|
||||
: '<p class="muted">An diesem Tag ist noch kein Spieltag eingetragen.</p>';
|
||||
: '<p class="muted">An diesem Tag ist noch nichts eingetragen.</p>';
|
||||
}
|
||||
|
||||
function renderUpcoming() {
|
||||
const upcoming = events.filter((e) => e.date >= todayIso);
|
||||
$('#upcoming').innerHTML = upcoming.length
|
||||
? upcoming.map(eventHtml).join('')
|
||||
: '<p class="muted">Keine kommenden Spieltage.</p>';
|
||||
: '<p class="muted">Keine kommenden Termine.</p>';
|
||||
}
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
@@ -137,12 +150,14 @@ document.addEventListener('click', async (e) => {
|
||||
if (!btn) return;
|
||||
const { action, id } = btn.dataset;
|
||||
try {
|
||||
if (action === 'edit') return startEdit(id);
|
||||
if (action === 'join') await api('PUT', `/events/${id}/attendees/${me}`);
|
||||
if (action === 'leave') await api('DELETE', `/events/${id}/attendees/${me}`);
|
||||
if (action === 'delete') {
|
||||
const ev = events.find((x) => x.id === id);
|
||||
if (!confirm(`Spieltag „${ev.title}“ am ${formatDate(ev.date)} wirklich löschen?`)) return;
|
||||
if (!confirm(`Termin „${ev.title}“ am ${formatDate(ev.date)} wirklich löschen?`)) return;
|
||||
await api('DELETE', `/events/${id}`);
|
||||
if (editingId === id) stopEdit();
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
@@ -150,17 +165,57 @@ document.addEventListener('click', async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Formular: eintragen und bearbeiten ----------
|
||||
function startEdit(id) {
|
||||
const ev = events.find((x) => x.id === id);
|
||||
if (!ev) return;
|
||||
editingId = id;
|
||||
$('#event-form-title').textContent = 'Termin bearbeiten';
|
||||
$('#event-submit').textContent = 'Speichern';
|
||||
$('#event-cancel').hidden = false;
|
||||
$('#event-title').value = ev.title;
|
||||
$('#event-game').checked = ev.isGame;
|
||||
$('#event-date').value = ev.date;
|
||||
$('#event-time').value = ev.time || '';
|
||||
$('#event-end').value = ev.endTime || '';
|
||||
$('#event-note').value = ev.note || '';
|
||||
renderMe();
|
||||
$('#event-form').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
$('#event-title').focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function stopEdit() {
|
||||
editingId = null;
|
||||
$('#event-form').reset();
|
||||
$('#event-form-title').textContent = 'Termin an diesem Tag eintragen';
|
||||
$('#event-submit').textContent = 'Eintragen';
|
||||
$('#event-cancel').hidden = true;
|
||||
$('#event-date').value = selectedDay;
|
||||
renderMe();
|
||||
}
|
||||
|
||||
$('#event-cancel').addEventListener('click', stopEdit);
|
||||
|
||||
$('#event-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
date: $('#event-date').value,
|
||||
title: $('#event-title').value,
|
||||
note: $('#event-note').value,
|
||||
isGame: $('#event-game').checked,
|
||||
time: $('#event-time').value,
|
||||
endTime: $('#event-end').value,
|
||||
};
|
||||
try {
|
||||
await api('POST', '/events', {
|
||||
date: selectedDay,
|
||||
title: $('#event-title').value,
|
||||
note: $('#event-note').value,
|
||||
createdBy: me,
|
||||
});
|
||||
e.target.reset();
|
||||
if (editingId) {
|
||||
await api('PUT', `/events/${editingId}`, body);
|
||||
} else {
|
||||
await api('POST', '/events', { ...body, createdBy: me });
|
||||
}
|
||||
const date = body.date;
|
||||
stopEdit();
|
||||
await load();
|
||||
if (date && date !== selectedDay) selectDay(date);
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
@@ -179,4 +234,5 @@ async function load() {
|
||||
renderAll();
|
||||
}
|
||||
|
||||
$('#event-date').value = selectedDay;
|
||||
load().catch(showError);
|
||||
Reference in new issue
Block a user