Add calendar downloads for events

This commit is contained in:
2026-09-22 12:12:07 +02:00
parent 293386912d
commit f92463a5ad
2 changed files with 147 additions and 1 deletions
+33
View File
@@ -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;
}
+114 -1
View File
@@ -409,7 +409,7 @@
<div class="dates-grid">
<div class="date-card wine-event"><div class="date-icon"><i class="fas fa-wine-glass-alt" aria-hidden="true"></i></div><div class="date-info"><span class="date-day">Samstag</span><span class="date-number">19. September</span></div></div>
<div class="date-card wine-event"><div class="date-icon"><i class="fas fa-wine-glass-alt" aria-hidden="true"></i></div><div class="date-info"><span class="date-day">Samstag</span><span class="date-number">17. Oktober</span></div></div>
<div class="date-card wine-event"><div class="date-icon"><i class="fas fa-wine-glass-alt" aria-hidden="true"></i></div><div class="date-info"><span class="date-day">Donnerstag</span><span class="date-number">30. Oktober</span></div></div>
<div class="date-card wine-event cancelled"><div class="date-icon"><i class="fas fa-wine-glass-alt" aria-hidden="true"></i></div><div class="date-info"><span class="date-day">Donnerstag</span><span class="date-number">30. Oktober</span><span class="date-status">CANCELLED</span></div></div>
<div class="date-card wine-event"><div class="date-icon"><i class="fas fa-wine-glass-alt" aria-hidden="true"></i></div><div class="date-info"><span class="date-day">Dienstag</span><span class="date-number">29. Dezember</span></div></div>
</div>
</details>
@@ -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 = '<i class="fas fa-calendar-plus" aria-hidden="true"></i><span>In Kalender speichern</span>';
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) {