From f92463a5adfa4ab2132466604a4abda472706cab Mon Sep 17 00:00:00 2001 From: webfarben Date: Tue, 22 Sep 2026 12:12:07 +0200 Subject: [PATCH] Add calendar downloads for events --- css/style.css | 33 +++++++++++++++ index.html | 115 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/css/style.css b/css/style.css index 047c7f9..9e47227 100644 --- a/css/style.css +++ b/css/style.css @@ -949,6 +949,39 @@ section { margin-top: 4px; } +.date-card.cancelled { + opacity: 0.65; + background: #f0efec; +} + +.date-card.cancelled .date-number { + text-decoration: line-through; +} + +.date-status { + color: #a33a2b; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + margin-top: 5px; +} + +.calendar-link { + align-items: center; + color: var(--secondary-color); + display: inline-flex; + font-size: 0.8rem; + gap: 6px; + margin-top: 9px; + text-decoration: none; +} + +.calendar-link:hover, +.calendar-link:focus-visible { + color: var(--primary-color); + text-decoration: underline; +} + .events-cta { text-align: center; } diff --git a/index.html b/index.html index e32e09d..221773a 100644 --- a/index.html +++ b/index.html @@ -409,7 +409,7 @@
Samstag19. September
Samstag17. Oktober
-
Donnerstag30. Oktober
+
Donnerstag30. OktoberCANCELLED
Dienstag29. Dezember
@@ -675,6 +675,119 @@ }); } + // Create one downloadable iCalendar file for each scheduled event + const monthNumbers = { + 'Januar': '01', + 'Februar': '02', + 'März': '03', + 'April': '04', + 'Mai': '05', + 'Juni': '06', + 'Juli': '07', + 'August': '08', + 'September': '09', + 'Oktober': '10', + 'November': '11', + 'Dezember': '12' + }; + + const escapeIcsText = text => String(text) + .replace(/\\/g, '\\\\') + .replace(/;/g, '\\;') + .replace(/,/g, '\\,') + .replace(/\r?\n/g, '\\n'); + + const formatIcsDateTime = date => [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, '0'), + String(date.getDate()).padStart(2, '0') + ].join('') + 'T' + [ + String(date.getHours()).padStart(2, '0'), + String(date.getMinutes()).padStart(2, '0'), + '00' + ].join(''); + + const formatIcsUtcDateTime = date => [ + date.getUTCFullYear(), + String(date.getUTCMonth() + 1).padStart(2, '0'), + String(date.getUTCDate()).padStart(2, '0') + ].join('') + 'T' + [ + String(date.getUTCHours()).padStart(2, '0'), + String(date.getUTCMinutes()).padStart(2, '0'), + '00' + ].join(''); + + document.querySelectorAll('.date-card:not(.cancelled)').forEach(card => { + const dateNumber = card.querySelector('.date-number')?.textContent.trim(); + const yearText = card.closest('[data-year-accordion]')?.querySelector('summary')?.textContent; + const dateMatch = dateNumber?.match(/^(\d{1,2})\.\s+(.+)$/); + const yearMatch = yearText?.match(/\d{4}/); + const month = dateMatch ? monthNumbers[dateMatch[2]] : null; + + if (!dateMatch || !yearMatch || !month) { + return; + } + + const date = `${yearMatch[0]}-${month}-${dateMatch[1].padStart(2, '0')}`; + const timeText = card.querySelector('.date-time')?.textContent.trim(); + const timeMatch = timeText?.match(/^(\d{1,2}):(\d{2})/); + const isWineEvent = card.classList.contains('wine-event'); + const title = isWineEvent ? 'Weinreise bei Frida & Fred' : 'Käsetasting bei Frida & Fred'; + const slug = `${isWineEvent ? 'weinreise' : 'kaesetasting'}-${date}`; + const calendarLink = document.createElement('a'); + calendarLink.className = 'calendar-link'; + calendarLink.href = '#'; + calendarLink.setAttribute('aria-label', `${title} am ${dateNumber} ${yearMatch[0]} in den Kalender eintragen`); + calendarLink.innerHTML = 'In Kalender speichern'; + + calendarLink.addEventListener('click', event => { + event.preventDefault(); + let calendarContent; + + if (timeMatch) { + const start = new Date(`${date}T${timeMatch[1].padStart(2, '0')}:${timeMatch[2]}:00`); + const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); + calendarContent = [ + `DTSTART;TZID=Europe/Berlin:${formatIcsDateTime(start)}`, + `DTEND;TZID=Europe/Berlin:${formatIcsDateTime(end)}` + ]; + } else { + const start = new Date(`${date}T12:00:00`); + const end = new Date(start); + end.setDate(end.getDate() + 1); + calendarContent = [ + `DTSTART;VALUE=DATE:${date.replace(/-/g, '')}`, + `DTEND;VALUE=DATE:${end.getFullYear()}${String(end.getMonth() + 1).padStart(2, '0')}${String(end.getDate()).padStart(2, '0')}` + ]; + } + + const ics = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Frida & Fred//Events//DE', + 'CALSCALE:GREGORIAN', + 'BEGIN:VEVENT', + `UID:${slug}@frida-fred.com`, + `DTSTAMP:${formatIcsUtcDateTime(new Date())}Z`, + ...calendarContent, + `SUMMARY:${escapeIcsText(title)}`, + 'LOCATION:Frida & Fred', + `DESCRIPTION:${escapeIcsText('Anmeldung: info@frida-fred.com oder 0151 19426530')}`, + 'END:VEVENT', + 'END:VCALENDAR' + ].join('\r\n'); + const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' }); + const downloadUrl = URL.createObjectURL(blob); + const downloadLink = document.createElement('a'); + downloadLink.href = downloadUrl; + downloadLink.download = `${slug}.ics`; + downloadLink.click(); + URL.revokeObjectURL(downloadUrl); + }); + + card.querySelector('.date-info')?.appendChild(calendarLink); + }); + // Smooth scrolling for internal links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) {