mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-16 07:13:28 +02:00
Erste Version: HCRB Spieltag-Kalender und Fahrtplaner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
10400eee59
30 files changed
+3771
No files matched your search
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
$, api, esc, sortByName, showError, renderHeader,
|
||||
toIso, fromIso, formatDate, getMe, setMe,
|
||||
} from './common.js';
|
||||
|
||||
renderHeader('kalender');
|
||||
|
||||
let members = [];
|
||||
let events = [];
|
||||
let me = getMe();
|
||||
|
||||
const todayIso = toIso(new Date());
|
||||
let selectedDay = todayIso;
|
||||
let viewMonth = fromIso(todayIso);
|
||||
viewMonth.setDate(1);
|
||||
|
||||
const memberName = (id) => members.find((m) => m.id === id)?.name ?? '(gelöscht)';
|
||||
|
||||
// ---------- "Ich bin" ----------
|
||||
function renderMe() {
|
||||
if (me && !members.some((m) => m.id === me)) me = null;
|
||||
$('#me-select').innerHTML = '<option value="">– bitte auswählen –</option>' +
|
||||
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;
|
||||
}
|
||||
|
||||
$('#me-select').addEventListener('change', (e) => {
|
||||
me = e.target.value || null;
|
||||
setMe(me);
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// ---------- Monatsansicht ----------
|
||||
function renderCalendar() {
|
||||
const title = viewMonth.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||
$('#month-title').textContent = title;
|
||||
|
||||
// Raster beginnt am Montag vor (oder am) Monatsersten
|
||||
const start = new Date(viewMonth);
|
||||
start.setDate(1 - ((start.getDay() + 6) % 7));
|
||||
|
||||
const byDate = new Map();
|
||||
for (const ev of events) {
|
||||
if (!byDate.has(ev.date)) byDate.set(ev.date, []);
|
||||
byDate.get(ev.date).push(ev);
|
||||
}
|
||||
|
||||
let html = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((d) => `<div class="dow">${d}</div>`).join('');
|
||||
const day = new Date(start);
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const iso = toIso(day);
|
||||
const classes = ['cal-day'];
|
||||
if (day.getMonth() !== viewMonth.getMonth()) classes.push('other');
|
||||
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('')}
|
||||
</button>`;
|
||||
day.setDate(day.getDate() + 1);
|
||||
}
|
||||
$('#calendar').innerHTML = html;
|
||||
}
|
||||
|
||||
$('#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' });
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
// ---------- Termine ----------
|
||||
function eventHtml(ev) {
|
||||
const going = ev.attendees.includes(me);
|
||||
const attendees = sortByName(ev.attendees.map((id) => ({ id, name: memberName(id) })));
|
||||
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>
|
||||
${ev.note ? `<div class="note">${esc(ev.note)}</div>` : ''}
|
||||
<div class="muted">${ev.attendees.length} zugesagt:</div>
|
||||
<div class="chips">
|
||||
${attendees.length
|
||||
? attendees.map((a) => `<span class="chip ${a.id === me ? 'me' : ''}">${esc(a.name)}</span>`).join('')
|
||||
: '<span class="muted">noch niemand</span>'}
|
||||
</div>
|
||||
<div class="row wrap">
|
||||
${me ? (going
|
||||
? `<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>
|
||||
<button class="link danger" data-action="delete" data-id="${ev.id}">Löschen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderDay() {
|
||||
$('#day-title').textContent = fromIso(selectedDay).toLocaleDateString('de-DE', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
|
||||
});
|
||||
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>';
|
||||
}
|
||||
|
||||
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>';
|
||||
}
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const { action, id } = btn.dataset;
|
||||
try {
|
||||
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;
|
||||
await api('DELETE', `/events/${id}`);
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
});
|
||||
|
||||
$('#event-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api('POST', '/events', {
|
||||
date: selectedDay,
|
||||
title: $('#event-title').value,
|
||||
note: $('#event-note').value,
|
||||
createdBy: me,
|
||||
});
|
||||
e.target.reset();
|
||||
await load();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Laden ----------
|
||||
function renderAll() {
|
||||
renderMe();
|
||||
renderCalendar();
|
||||
renderDay();
|
||||
renderUpcoming();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
[members, events] = await Promise.all([api('GET', '/members'), api('GET', '/events')]);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
load().catch(showError);
|
||||
Reference in new issue
Block a user