2025-02-26 19:47:07 +01:00
|
|
|
---
|
|
|
|
title: "Kalender"
|
|
|
|
subtitle: "Der Kalender des CCCB"
|
|
|
|
date: 2025-02-26T10:00:00+02:00
|
|
|
|
menu:
|
|
|
|
main:
|
2025-02-27 00:21:42 +01:00
|
|
|
parent: "Verein"
|
2025-02-26 19:47:07 +01:00
|
|
|
---
|
2025-02-27 00:21:42 +01:00
|
|
|

|
2025-02-26 19:47:07 +01:00
|
|
|
|
|
|
|
<!-- Kalender-Widget -->
|
|
|
|
<style>
|
|
|
|
.calendar-container {
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
width: 100%;
|
|
|
|
gap: 20px;
|
|
|
|
}
|
|
|
|
#calendar {
|
|
|
|
flex: 1;
|
|
|
|
min-width: 300px;
|
|
|
|
}
|
|
|
|
#event-panel {
|
|
|
|
flex: 1;
|
2025-03-10 04:31:53 +01:00
|
|
|
min-width: 30%;
|
2025-02-26 19:47:07 +01:00
|
|
|
background-color: var(--color-bg-secondary);
|
|
|
|
padding: 15px;
|
|
|
|
border-radius: 5px;
|
|
|
|
min-height: 300px;
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
#calendar-controls {
|
|
|
|
text-align: center;
|
|
|
|
margin-bottom: 10px;
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
#calendar-controls button {
|
|
|
|
background: none;
|
|
|
|
border: none;
|
|
|
|
font-size: 2em;
|
|
|
|
cursor: pointer;
|
|
|
|
padding: 5px 15px;
|
|
|
|
}
|
|
|
|
#calendar-table {
|
|
|
|
width: 100%;
|
|
|
|
border-collapse: collapse;
|
|
|
|
}
|
|
|
|
#calendar-table th, #calendar-table td {
|
2025-03-10 04:31:53 +01:00
|
|
|
border: 1px;
|
2025-02-26 19:47:07 +01:00
|
|
|
padding: 5px;
|
|
|
|
text-align: center;
|
|
|
|
vertical-align: top;
|
2025-03-10 04:31:53 +01:00
|
|
|
height: 50px;
|
|
|
|
width: 15%;
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
2025-03-10 04:31:53 +01:00
|
|
|
#calendar-table th
|
2025-02-26 19:47:07 +01:00
|
|
|
#calendar-table td {
|
|
|
|
position: relative;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
#calendar-table td:hover {
|
2025-03-10 04:31:53 +01:00
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
|
|
|
|
}
|
|
|
|
.event-dot-greenyellow {
|
|
|
|
height: 8px;
|
|
|
|
width: 8px;
|
|
|
|
background-color: #9acd32;
|
|
|
|
border-radius: 50%;
|
|
|
|
display: block;
|
|
|
|
margin: 5px auto 0;
|
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
|
|
|
}
|
|
|
|
.event-dot-orange {
|
|
|
|
height: 8px;
|
|
|
|
width: 8px;
|
|
|
|
background-color: #ffa500;
|
|
|
|
border-radius: 50%;
|
|
|
|
display: block;
|
|
|
|
margin: 5px auto 0;
|
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
2025-03-10 04:31:53 +01:00
|
|
|
.event-dot-red {
|
2025-02-26 19:47:07 +01:00
|
|
|
height: 8px;
|
|
|
|
width: 8px;
|
2025-03-10 04:31:53 +01:00
|
|
|
background-color: #ff4500;
|
2025-02-26 19:47:07 +01:00
|
|
|
border-radius: 50%;
|
|
|
|
display: block;
|
|
|
|
margin: 5px auto 0;
|
2025-03-10 04:31:53 +01:00
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
|
|
|
.has-event {
|
2025-03-10 04:31:53 +01:00
|
|
|
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
|
|
|
.selected-day {
|
2025-03-10 04:31:53 +01:00
|
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
|
|
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
|
|
|
|
border-radius: 8px;
|
|
|
|
width: 20px;
|
|
|
|
height: 20px;
|
|
|
|
border: 5px solid grey;
|
|
|
|
font-weight: lighter;
|
|
|
|
position: relative;
|
|
|
|
z-index: 1;
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
|
|
|
#event-date {
|
|
|
|
font-size: 1.2em;
|
|
|
|
margin-bottom: 15px;
|
|
|
|
}
|
|
|
|
.event-item {
|
|
|
|
margin-bottom: 15px;
|
|
|
|
padding-bottom: 15px;
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
}
|
|
|
|
.event-title {
|
|
|
|
font-weight: bold;
|
|
|
|
margin-bottom: 5px;
|
|
|
|
}
|
|
|
|
.event-time, .event-description {
|
|
|
|
font-size: 0.9em;
|
|
|
|
margin-bottom: 5px;
|
|
|
|
}
|
|
|
|
.no-events {
|
|
|
|
font-style: italic;
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<div class="calendar-container">
|
|
|
|
<div id="calendar">
|
|
|
|
<div id="calendar-controls">
|
|
|
|
<button id="prev-month">←</button>
|
|
|
|
<span id="current-month"></span>
|
|
|
|
<button id="next-month">→</button>
|
|
|
|
</div>
|
|
|
|
<table id="calendar-table">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Mo</th>
|
|
|
|
<th>Di</th>
|
|
|
|
<th>Mi</th>
|
|
|
|
<th>Do</th>
|
|
|
|
<th>Fr</th>
|
|
|
|
<th>Sa</th>
|
|
|
|
<th>So</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody id="calendar-body"></tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
<div id="event-panel">
|
|
|
|
<div id="event-date"></div>
|
|
|
|
<div id="event-details"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
(function(){
|
|
|
|
let events = [];
|
|
|
|
let eventsByDate = {};
|
|
|
|
|
|
|
|
// Funktion zum Parsen der ICS-Datei
|
|
|
|
function parseICS(icsText) {
|
|
|
|
let events = [];
|
|
|
|
let lines = icsText.split(/\r?\n/);
|
|
|
|
let event = null;
|
|
|
|
lines.forEach(line => {
|
|
|
|
if (line.startsWith("BEGIN:VEVENT")) {
|
|
|
|
event = {};
|
|
|
|
} else if (line.startsWith("END:VEVENT")) {
|
|
|
|
if (event) events.push(event);
|
|
|
|
event = null;
|
|
|
|
} else if (event) {
|
|
|
|
let colonIndex = line.indexOf(":");
|
|
|
|
if (colonIndex > -1) {
|
|
|
|
let key = line.substring(0, colonIndex);
|
|
|
|
let value = line.substring(colonIndex + 1);
|
2025-03-10 04:31:53 +01:00
|
|
|
|
|
|
|
// Handle properties with parameters (like TZID)
|
|
|
|
const baseKey = key.split(";")[0];
|
|
|
|
|
|
|
|
if (baseKey === "DTSTART") {
|
2025-02-26 19:47:07 +01:00
|
|
|
event.start = value;
|
2025-03-10 04:31:53 +01:00
|
|
|
event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
|
|
|
|
} else if (baseKey === "DTEND") {
|
2025-02-26 19:47:07 +01:00
|
|
|
event.end = value;
|
2025-03-10 04:31:53 +01:00
|
|
|
event.endParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
|
|
|
|
} else if (baseKey === "SUMMARY") {
|
2025-02-26 19:47:07 +01:00
|
|
|
event.summary = value;
|
2025-03-10 04:31:53 +01:00
|
|
|
} else if (baseKey === "DESCRIPTION") {
|
2025-02-26 19:47:07 +01:00
|
|
|
event.description = value;
|
2025-03-10 04:31:53 +01:00
|
|
|
} else if (baseKey === "RRULE") {
|
|
|
|
event.rrule = value;
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hilfsfunktion: Parst einen ICS-Datum-String ins Format "YYYY-MM-DD"
|
|
|
|
function parseDateString(icsDateStr) {
|
2025-03-10 04:31:53 +01:00
|
|
|
// Handle different date formats
|
|
|
|
if (!icsDateStr) return null;
|
|
|
|
|
|
|
|
// For basic date format: YYYYMMDD
|
2025-02-26 19:47:07 +01:00
|
|
|
if (icsDateStr.length === 8) {
|
|
|
|
let year = icsDateStr.substring(0, 4);
|
|
|
|
let month = icsDateStr.substring(4, 6);
|
|
|
|
let day = icsDateStr.substring(6, 8);
|
|
|
|
return `${year}-${month}-${day}`;
|
2025-03-10 04:31:53 +01:00
|
|
|
}
|
|
|
|
// For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss
|
|
|
|
else if (icsDateStr.includes("T")) {
|
2025-02-26 19:47:07 +01:00
|
|
|
let year = icsDateStr.substring(0, 4);
|
|
|
|
let month = icsDateStr.substring(4, 6);
|
|
|
|
let day = icsDateStr.substring(6, 8);
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
// Extract date components from different date formats
|
|
|
|
function getDateComponents(icsDateStr) {
|
|
|
|
if (!icsDateStr) return null;
|
|
|
|
|
|
|
|
// Basic handling - extract YYYY, MM, DD regardless of format
|
|
|
|
const year = parseInt(icsDateStr.substring(0, 4));
|
|
|
|
const month = parseInt(icsDateStr.substring(4, 6)) - 1; // 0-based months
|
|
|
|
const day = parseInt(icsDateStr.substring(6, 8));
|
|
|
|
|
|
|
|
return { year, month, day };
|
|
|
|
}
|
|
|
|
|
|
|
|
function expandRecurringEvents(event, year, month) {
|
|
|
|
if (!event.rrule) return [event];
|
|
|
|
|
|
|
|
const rruleStr = event.rrule;
|
|
|
|
|
|
|
|
// Get start date components
|
|
|
|
const startComponents = getDateComponents(event.start);
|
|
|
|
if (!startComponents) return [event];
|
|
|
|
|
|
|
|
const startDate = new Date(
|
|
|
|
startComponents.year,
|
|
|
|
startComponents.month,
|
|
|
|
startComponents.day
|
|
|
|
);
|
|
|
|
|
|
|
|
const rangeStart = new Date(year, month, 1);
|
|
|
|
const rangeEnd = new Date(year, month + 1, 0);
|
|
|
|
const expandedEvents = [];
|
|
|
|
|
|
|
|
if (rruleStr.includes("FREQ=WEEKLY") && rruleStr.includes("BYDAY")) {
|
|
|
|
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
|
|
|
|
if (bydayMatch) {
|
|
|
|
const dayCode = bydayMatch[1];
|
|
|
|
const dayMap = {
|
|
|
|
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
|
|
|
|
};
|
|
|
|
const targetDay = dayMap[dayCode];
|
|
|
|
|
|
|
|
if (targetDay !== undefined) {
|
|
|
|
// Create events for each matching day in the month
|
|
|
|
let day = 1;
|
|
|
|
while (day <= rangeEnd.getDate()) {
|
|
|
|
const testDate = new Date(year, month, day);
|
|
|
|
if (testDate.getDay() === targetDay && testDate >= startDate) {
|
|
|
|
const newEvent = {...event};
|
|
|
|
const eventDate = formatDateForICS(testDate);
|
|
|
|
|
|
|
|
// Preserve time portion from original event
|
|
|
|
const timePart = event.start.includes('T') ?
|
|
|
|
event.start.substring(event.start.indexOf('T')) : '';
|
|
|
|
const endTimePart = event.end.includes('T') ?
|
|
|
|
event.end.substring(event.end.indexOf('T')) : '';
|
|
|
|
|
|
|
|
newEvent.start = eventDate + timePart;
|
|
|
|
newEvent.end = eventDate + endTimePart;
|
|
|
|
expandedEvents.push(newEvent);
|
|
|
|
}
|
|
|
|
day++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (rruleStr.includes("FREQ=MONTHLY") && rruleStr.includes("BYDAY")) {
|
|
|
|
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
|
|
|
|
if (bydayMatch) {
|
|
|
|
const bydays = bydayMatch[1].split(',');
|
|
|
|
const dayMap = {
|
|
|
|
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
|
|
|
|
};
|
|
|
|
|
|
|
|
bydays.forEach(byday => {
|
|
|
|
const occurrence = parseInt(byday) || 1;
|
|
|
|
const dayCode = byday.slice(-2);
|
|
|
|
const dayIndex = dayMap[dayCode];
|
|
|
|
|
|
|
|
let day = 1;
|
|
|
|
let count = 0;
|
|
|
|
|
|
|
|
while (day <= rangeEnd.getDate()) {
|
|
|
|
const testDate = new Date(year, month, day);
|
|
|
|
if (testDate.getDay() === dayIndex) {
|
|
|
|
count++;
|
|
|
|
if (count === occurrence || (occurrence < 0 && day > rangeEnd.getDate() + occurrence * 7)) {
|
|
|
|
const newEvent = {...event};
|
|
|
|
const eventDate = new Date(year, month, day);
|
|
|
|
|
|
|
|
// Preserve time portion from original event
|
|
|
|
const timePart = event.start.includes('T') ?
|
|
|
|
event.start.substring(event.start.indexOf('T')) : '';
|
|
|
|
const endTimePart = event.end.includes('T') ?
|
|
|
|
event.end.substring(event.end.indexOf('T')) : '';
|
|
|
|
|
|
|
|
newEvent.start = formatDateForICS(eventDate) + timePart;
|
|
|
|
newEvent.end = formatDateForICS(eventDate) + endTimePart;
|
|
|
|
expandedEvents.push(newEvent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
day++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return expandedEvents.length > 0 ? expandedEvents : [event];
|
|
|
|
}
|
|
|
|
|
2025-02-26 19:47:07 +01:00
|
|
|
// Kalender initialisieren
|
|
|
|
let currentYear, currentMonth;
|
|
|
|
const currentMonthElem = document.getElementById("current-month");
|
|
|
|
const calendarBody = document.getElementById("calendar-body");
|
|
|
|
const eventPanel = document.getElementById("event-panel");
|
|
|
|
const eventDateElem = document.getElementById("event-date");
|
|
|
|
const eventDetailsElem = document.getElementById("event-details");
|
|
|
|
|
|
|
|
document.getElementById("prev-month").addEventListener("click", function(){
|
|
|
|
currentMonth--;
|
|
|
|
if (currentMonth < 0) {
|
|
|
|
currentMonth = 11;
|
|
|
|
currentYear--;
|
|
|
|
}
|
2025-03-10 04:31:53 +01:00
|
|
|
updateEventsForMonth(currentYear, currentMonth);
|
2025-02-26 19:47:07 +01:00
|
|
|
});
|
|
|
|
document.getElementById("next-month").addEventListener("click", function(){
|
|
|
|
currentMonth++;
|
|
|
|
if (currentMonth > 11) {
|
2025-03-10 04:31:53 +01:00
|
|
|
currentMonth = 0;
|
|
|
|
currentYear++;
|
2025-02-26 19:47:07 +01:00
|
|
|
}
|
2025-03-10 04:31:53 +01:00
|
|
|
updateEventsForMonth(currentYear, currentMonth);
|
2025-02-26 19:47:07 +01:00
|
|
|
});
|
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
function updateEventsForMonth(year, month) {
|
|
|
|
// Clear existing events for this month view
|
|
|
|
eventsByDate = {};
|
|
|
|
|
|
|
|
// Process each event, expanding recurring ones
|
|
|
|
events.forEach(ev => {
|
|
|
|
if (ev.rrule) {
|
|
|
|
// For recurring events, expand them for current month
|
|
|
|
const expandedEvents = expandRecurringEvents(ev, year, month);
|
|
|
|
expandedEvents.forEach(expandedEv => {
|
|
|
|
let dateKey = parseDateString(expandedEv.start);
|
|
|
|
if (dateKey) {
|
|
|
|
if (!eventsByDate[dateKey]) {
|
|
|
|
eventsByDate[dateKey] = [];
|
|
|
|
}
|
|
|
|
eventsByDate[dateKey].push(expandedEv);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// For regular events, check if they fall in current month
|
|
|
|
let dateKey = parseDateString(ev.start);
|
|
|
|
if (dateKey) {
|
|
|
|
// Check if this event belongs to current month view
|
|
|
|
const eventYear = parseInt(dateKey.split('-')[0]);
|
|
|
|
const eventMonth = parseInt(dateKey.split('-')[1]) - 1;
|
|
|
|
|
|
|
|
if (eventYear === year && eventMonth === month) {
|
|
|
|
if (!eventsByDate[dateKey]) {
|
|
|
|
eventsByDate[dateKey] = [];
|
|
|
|
}
|
|
|
|
eventsByDate[dateKey].push(ev);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
renderCalendar(year, month);
|
|
|
|
}
|
|
|
|
|
2025-02-26 19:47:07 +01:00
|
|
|
function renderCalendar(year, month) {
|
|
|
|
// Setze die Monatsbeschriftung (in Deutsch)
|
|
|
|
const monthNames = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
|
|
|
|
currentMonthElem.textContent = monthNames[month] + " " + year;
|
|
|
|
calendarBody.innerHTML = "";
|
|
|
|
|
|
|
|
let firstDay = new Date(year, month, 1);
|
|
|
|
let firstDayIndex = (firstDay.getDay() + 6) % 7; // Montag = 0, Dienstag = 1, etc.
|
|
|
|
let daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
|
|
|
|
|
|
let row = document.createElement("tr");
|
|
|
|
// Leere Zellen vor dem 1. Tag
|
|
|
|
for (let i = 0; i < firstDayIndex; i++){
|
|
|
|
let cell = document.createElement("td");
|
|
|
|
row.appendChild(cell);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tage hinzufügen
|
|
|
|
for (let day = 1; day <= daysInMonth; day++){
|
|
|
|
if (row.children.length === 7) {
|
|
|
|
calendarBody.appendChild(row);
|
|
|
|
row = document.createElement("tr");
|
|
|
|
}
|
|
|
|
let cell = document.createElement("td");
|
|
|
|
cell.innerHTML = "<strong>" + day + "</strong>";
|
|
|
|
|
|
|
|
let dayStr = day < 10 ? "0" + day : day;
|
|
|
|
let monthStr = (month + 1) < 10 ? "0" + (month + 1) : (month + 1);
|
|
|
|
let dateKey = year + "-" + monthStr + "-" + dayStr;
|
|
|
|
|
|
|
|
if (eventsByDate[dateKey]) {
|
2025-03-10 04:31:53 +01:00
|
|
|
let dotsContainer = document.createElement("div");
|
|
|
|
dotsContainer.className = "event-dots-container";
|
|
|
|
cell.classList.add("has-event");
|
|
|
|
|
|
|
|
// Gruppe Events nach Typ
|
|
|
|
const events = eventsByDate[dateKey];
|
|
|
|
const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only"));
|
|
|
|
const hasSubbotnik = events.some(e => e.summary.toLowerCase().includes("subbotnik"));
|
|
|
|
const hasBastelabend = events.some(e => e.summary.toLowerCase().includes("bastelabend"));
|
|
|
|
const hasSpieleabend = events.some(e => e.summary.toLowerCase().includes("spieleabend"));
|
|
|
|
const hasRegular = events.some(e => {
|
|
|
|
const title = e.summary.toLowerCase();
|
|
|
|
return !title.includes("members only") &&
|
|
|
|
!title.includes("subbotnik") &&
|
|
|
|
!title.includes("bastelabend") &&
|
|
|
|
!title.includes("spieleabend");
|
|
|
|
});
|
2025-02-26 19:47:07 +01:00
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
// Füge Dots entsprechend der Event-Typen hinzu
|
|
|
|
if (hasMembersOnly) {
|
|
|
|
let dot = document.createElement("div");
|
|
|
|
dot.className = "event-dot event-dot-red";
|
|
|
|
dotsContainer.appendChild(dot);
|
|
|
|
}
|
|
|
|
if (hasSubbotnik || hasBastelabend || hasSpieleabend) {
|
|
|
|
let dot = document.createElement("div");
|
|
|
|
dot.className = "event-dot event-dot-orange";
|
|
|
|
dotsContainer.appendChild(dot);
|
|
|
|
}
|
|
|
|
if (hasRegular) {
|
|
|
|
let dot = document.createElement("div");
|
|
|
|
dot.className = "event-dot event-dot-greenyellow";
|
|
|
|
dotsContainer.appendChild(dot);
|
|
|
|
}
|
|
|
|
|
|
|
|
cell.appendChild(dotsContainer);
|
2025-02-26 19:47:07 +01:00
|
|
|
cell.dataset.dateKey = dateKey;
|
|
|
|
cell.addEventListener("click", function() {
|
|
|
|
// Clear previous selections
|
|
|
|
document.querySelectorAll('.selected-day').forEach(el => {
|
|
|
|
el.classList.remove('selected-day');
|
|
|
|
});
|
|
|
|
cell.classList.add('selected-day');
|
|
|
|
showEventDetails(dateKey);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
row.appendChild(cell);
|
|
|
|
}
|
|
|
|
// Falls die letzte Zeile nicht komplett ist
|
|
|
|
while (row.children.length < 7) {
|
|
|
|
let cell = document.createElement("td");
|
|
|
|
row.appendChild(cell);
|
|
|
|
}
|
|
|
|
calendarBody.appendChild(row);
|
|
|
|
}
|
|
|
|
|
|
|
|
function createEventLink(eventTitle) {
|
|
|
|
if (eventTitle.startsWith("Datengarten")) {
|
|
|
|
// Extract the number after "Datengarten "
|
|
|
|
const match = eventTitle.match(/Datengarten\s+(\d+)/i);
|
|
|
|
if (match && match[1]) {
|
|
|
|
return `https://berlin.ccc.de/datengarten/${match[1]}/`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// For other titles, convert to lowercase and use as path
|
|
|
|
const slug = eventTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
|
|
|
|
return `https://berlin.ccc.de/page/${slug}/`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function showEventDetails(dateKey) {
|
|
|
|
const events = eventsByDate[dateKey];
|
|
|
|
eventDateElem.textContent = formatDate(dateKey);
|
|
|
|
eventDetailsElem.innerHTML = "";
|
|
|
|
|
|
|
|
if (events && events.length > 0) {
|
|
|
|
events.forEach(ev => {
|
|
|
|
let eventItem = document.createElement("div");
|
|
|
|
eventItem.className = "event-item";
|
|
|
|
|
|
|
|
let eventTitle = document.createElement("div");
|
|
|
|
eventTitle.className = "event-title";
|
|
|
|
|
|
|
|
// Create a link for the event title
|
2025-03-10 04:31:53 +01:00
|
|
|
let titleLink = document.createElement("h");
|
2025-02-26 19:47:07 +01:00
|
|
|
titleLink.textContent = ev.summary;
|
|
|
|
titleLink.target = "_blank";
|
|
|
|
eventTitle.appendChild(titleLink);
|
|
|
|
|
|
|
|
eventItem.appendChild(eventTitle);
|
|
|
|
|
|
|
|
let eventTime = document.createElement("div");
|
|
|
|
eventTime.className = "event-time";
|
|
|
|
eventTime.textContent = `Start: ${formatTime(ev.start)}, End: ${formatTime(ev.end)}`;
|
|
|
|
eventItem.appendChild(eventTime);
|
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
if (ev.description) {
|
|
|
|
let eventDescription = document.createElement("div");
|
|
|
|
eventDescription.className = "event-description";
|
|
|
|
|
|
|
|
// Check if the description is a URL and make it a clickable link
|
|
|
|
if (ev.description.trim().startsWith('http')) {
|
|
|
|
let linkElement = document.createElement("a");
|
|
|
|
linkElement.href = ev.description.trim();
|
|
|
|
linkElement.textContent = ev.description.trim();
|
|
|
|
linkElement.target = "_blank";
|
|
|
|
eventDescription.innerHTML = '';
|
|
|
|
eventDescription.appendChild(linkElement);
|
|
|
|
} else {
|
|
|
|
eventDescription.textContent = ev.description;
|
|
|
|
}
|
|
|
|
|
|
|
|
eventItem.appendChild(eventDescription);
|
|
|
|
}
|
2025-02-26 19:47:07 +01:00
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
eventDetailsElem.appendChild(eventItem);
|
2025-02-26 19:47:07 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
let noEvents = document.createElement("div");
|
|
|
|
noEvents.className = "no-events";
|
|
|
|
noEvents.textContent = "Keine Veranstaltungen an diesem Tag.";
|
|
|
|
eventDetailsElem.appendChild(noEvents);
|
|
|
|
}
|
|
|
|
|
|
|
|
eventPanel.style.display = "block";
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatDate(dateStr) {
|
|
|
|
// Convert YYYY-MM-DD to DD.MM.YYYY
|
|
|
|
const parts = dateStr.split("-");
|
|
|
|
return `${parts[2]}.${parts[1]}.${parts[0]}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatTime(icsTimeStr) {
|
|
|
|
// Format time for display
|
2025-03-10 04:31:53 +01:00
|
|
|
if (!icsTimeStr) return "";
|
|
|
|
|
2025-02-26 19:47:07 +01:00
|
|
|
if (icsTimeStr.length === 8) {
|
|
|
|
// All-day event
|
|
|
|
return "Ganztägig";
|
2025-03-10 04:31:53 +01:00
|
|
|
} else if (icsTimeStr.includes("T")) {
|
|
|
|
// Time-specific event (with or without timezone)
|
|
|
|
const timeStart = icsTimeStr.indexOf("T") + 1;
|
|
|
|
const hour = icsTimeStr.substring(timeStart, timeStart + 2);
|
|
|
|
const minute = icsTimeStr.substring(timeStart + 2, timeStart + 4);
|
2025-02-26 19:47:07 +01:00
|
|
|
return `${hour}:${minute}`;
|
|
|
|
}
|
|
|
|
return icsTimeStr;
|
|
|
|
}
|
|
|
|
|
2025-03-10 04:31:53 +01:00
|
|
|
function formatDateForICS(date) {
|
|
|
|
const year = date.getFullYear();
|
|
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
|
|
return `${year}${month}${day}`;
|
|
|
|
}
|
|
|
|
|
2025-02-26 19:47:07 +01:00
|
|
|
// ICS-Datei abrufen und Events verarbeiten
|
|
|
|
fetch('/all.ics')
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(data => {
|
|
|
|
events = parseICS(data);
|
2025-03-10 04:31:53 +01:00
|
|
|
|
|
|
|
// Initialize with current date
|
2025-02-26 19:47:07 +01:00
|
|
|
let today = new Date();
|
|
|
|
currentYear = today.getFullYear();
|
|
|
|
currentMonth = today.getMonth();
|
2025-03-10 04:31:53 +01:00
|
|
|
|
|
|
|
// Process events for current month
|
|
|
|
updateEventsForMonth(currentYear, currentMonth);
|
2025-02-26 19:47:07 +01:00
|
|
|
})
|
|
|
|
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
|
|
|
|
})();
|
|
|
|
</script>
|
2025-03-10 04:31:53 +01:00
|
|
|
|
|
|
|
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](all.ics)!
|