forked from cccb-website-team/www
		
	improved calendar and fixed url temporarily
This commit is contained in:
		
							parent
							
								
									4a60651ce7
								
							
						
					
					
						commit
						4068fab565
					
				
					 9 changed files with 610 additions and 608 deletions
				
			
		|  | @ -1 +1 @@ | |||
| --baseURL=https://staging.berlin.ccc.de/ | ||||
| --baseURL=https://berlin.ccc.de/ | ||||
|  |  | |||
							
								
								
									
										113
									
								
								assets/css/calendar.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								assets/css/calendar.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
|   .calendar-container { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     width: 100%; | ||||
|     gap: 20px; | ||||
|   } | ||||
|   #calendar { | ||||
|     flex: 1; | ||||
|     min-width: 300px; | ||||
|   } | ||||
|   #event-panel { | ||||
|     flex: 1; | ||||
|     min-width: 30%; | ||||
|     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 { | ||||
|     border: 1px; | ||||
|     padding: 5px; | ||||
|     text-align: center; | ||||
|     vertical-align: top; | ||||
|     height: 50px; | ||||
|     width: 15%; | ||||
|   } | ||||
|   #calendar-table th | ||||
|   #calendar-table td { | ||||
|     position: relative; | ||||
|     cursor: pointer; | ||||
|   } | ||||
|   #calendar-table td:hover { | ||||
|     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); | ||||
|   } | ||||
|   .event-dot-red { | ||||
|     height: 8px; | ||||
|     width: 8px; | ||||
|     background-color: #ff4500; | ||||
|     border-radius: 50%; | ||||
|     display: block; | ||||
|     margin: 5px auto 0; | ||||
|     box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); | ||||
|   } | ||||
|   .selected-day { | ||||
|     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; | ||||
|   } | ||||
|   #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); | ||||
|   } | ||||
							
								
								
									
										445
									
								
								assets/js/calendar.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								assets/js/calendar.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,445 @@ | |||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     (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); | ||||
|                          | ||||
|                         // Handle properties with parameters (like TZID)
 | ||||
|                         const baseKey = key.split(";")[0]; | ||||
|                          | ||||
|                         if (baseKey === "DTSTART") { | ||||
|                             event.start = value; | ||||
|                             event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null; | ||||
|                         } else if (baseKey === "DTEND") { | ||||
|                             event.end = value; | ||||
|                             event.endParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null; | ||||
|                         } else if (baseKey === "SUMMARY") { | ||||
|                             event.summary = value; | ||||
|                         } else if (baseKey === "DESCRIPTION") { | ||||
|                             event.description = value; | ||||
|                         } else if (baseKey === "RRULE") { | ||||
|                             event.rrule = value; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             return events; | ||||
|         } | ||||
| 
 | ||||
|         // Hilfsfunktion: Parst einen ICS-Datum-String ins Format "YYYY-MM-DD"
 | ||||
|         function parseDateString(icsDateStr) { | ||||
|             // Handle different date formats
 | ||||
|             if (!icsDateStr) return null; | ||||
|              | ||||
|             // For basic date format: YYYYMMDD
 | ||||
|             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}`; | ||||
|             }  | ||||
|             // For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss
 | ||||
|             else if (icsDateStr.includes("T")) { | ||||
|                 let year = icsDateStr.substring(0, 4); | ||||
|                 let month = icsDateStr.substring(4, 6); | ||||
|                 let day = icsDateStr.substring(6, 8); | ||||
|                 return `${year}-${month}-${day}`; | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         // 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]; | ||||
|         } | ||||
| 
 | ||||
|         // 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--; | ||||
|              } | ||||
|              updateEventsForMonth(currentYear, currentMonth); | ||||
|         }); | ||||
|         document.getElementById("next-month").addEventListener("click", function(){ | ||||
|              currentMonth++; | ||||
|              if (currentMonth > 11) { | ||||
|               currentMonth = 0; | ||||
|               currentYear++; | ||||
|              } | ||||
|              updateEventsForMonth(currentYear, currentMonth); | ||||
|         }); | ||||
| 
 | ||||
|         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); | ||||
|         } | ||||
| 
 | ||||
|         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]) { | ||||
|                     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"); | ||||
|                     }); | ||||
|                      | ||||
|                     // 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); | ||||
|                     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
 | ||||
|                     let titleLink = document.createElement("h"); | ||||
|                     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); | ||||
| 
 | ||||
|                             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); | ||||
|                             } | ||||
| 
 | ||||
|                               eventDetailsElem.appendChild(eventItem); | ||||
|                 }); | ||||
|             } 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
 | ||||
|             if (!icsTimeStr) return ""; | ||||
|              | ||||
|             if (icsTimeStr.length === 8) { | ||||
|                 // All-day event
 | ||||
|                 return "Ganztägig"; | ||||
|             } 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); | ||||
|                 return `${hour}:${minute}`; | ||||
|             } | ||||
|             return icsTimeStr; | ||||
|         } | ||||
| 
 | ||||
|         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}`; | ||||
|         } | ||||
| 
 | ||||
|         // ICS-Datei abrufen und Events verarbeiten
 | ||||
|         fetch('/all.ics') | ||||
|         .then(response => response.text()) | ||||
|         .then(data => { | ||||
|             events = parseICS(data); | ||||
|              | ||||
|             // Initialize with current date
 | ||||
|             let today = new Date(); | ||||
|             currentYear = today.getFullYear(); | ||||
|             currentMonth = today.getMonth(); | ||||
|              | ||||
|             // Process events for current month
 | ||||
| 
 | ||||
|                 updateEventsForMonth(currentYear, currentMonth); | ||||
| 
 | ||||
|         }) | ||||
|         .catch(err => console.error('Fehler beim Laden der ICS-Datei:', err)); | ||||
|     })(); | ||||
| }); | ||||
|  | @ -8,7 +8,7 @@ defaultContentLanguage = "de" | |||
| RelativeURLs = true | ||||
| CanonifyURLs = true | ||||
| 
 | ||||
| pluralizeListTitles = "true" # hugo function useful for non-english languages, find out more in  https://gohugo.io/getting-started/configuration/#pluralizelisttitles | ||||
| pluralizeListTitles = "false" # hugo function useful for non-english languages, find out more in  https://gohugo.io/getting-started/configuration/#pluralizelisttitles | ||||
| 
 | ||||
| enableRobotsTXT = true | ||||
| summaryLength = 0 | ||||
|  |  | |||
|  | @ -9,5 +9,5 @@ | |||
|   noClasses = false | ||||
| 
 | ||||
| [tableOfContents] | ||||
|   startLevel = 2 | ||||
|   endLevel = 4 | ||||
|   startLevel = 1 | ||||
|   endLevel = 5 | ||||
|  |  | |||
|  | @ -1,604 +0,0 @@ | |||
| --- | ||||
| title: "Kalender" | ||||
| subtitle: "Der Kalender des CCCB" | ||||
| date: 2025-02-26T10:00:00+02:00 | ||||
| menu: | ||||
|   main: | ||||
|     parent: "Verein" | ||||
| tag: ["Verein"] | ||||
| --- | ||||
|  | ||||
| 
 | ||||
| <!-- Kalender-Widget --> | ||||
| <style> | ||||
|   .calendar-container { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     width: 100%; | ||||
|     gap: 20px; | ||||
|   } | ||||
|   #calendar { | ||||
|     flex: 1; | ||||
|     min-width: 300px; | ||||
|   } | ||||
|   #event-panel { | ||||
|     flex: 1; | ||||
|     min-width: 30%; | ||||
|     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 { | ||||
|     border: 1px; | ||||
|     padding: 5px; | ||||
|     text-align: center; | ||||
|     vertical-align: top; | ||||
|     height: 50px; | ||||
|     width: 15%; | ||||
|   } | ||||
|   #calendar-table th | ||||
|   #calendar-table td { | ||||
|     position: relative; | ||||
|     cursor: pointer; | ||||
|   } | ||||
|   #calendar-table td:hover { | ||||
|     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); | ||||
|   } | ||||
|   .event-dot-red { | ||||
|     height: 8px; | ||||
|     width: 8px; | ||||
|     background-color: #ff4500; | ||||
|     border-radius: 50%; | ||||
|     display: block; | ||||
|     margin: 5px auto 0; | ||||
|     box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); | ||||
|   } | ||||
|   .has-event { | ||||
|      | ||||
|   } | ||||
|   .selected-day { | ||||
|     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; | ||||
|   } | ||||
|   #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); | ||||
|                      | ||||
|                     // Handle properties with parameters (like TZID) | ||||
|                     const baseKey = key.split(";")[0]; | ||||
|                      | ||||
|                     if (baseKey === "DTSTART") { | ||||
|                         event.start = value; | ||||
|                         event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null; | ||||
|                     } else if (baseKey === "DTEND") { | ||||
|                         event.end = value; | ||||
|                         event.endParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null; | ||||
|                     } else if (baseKey === "SUMMARY") { | ||||
|                         event.summary = value; | ||||
|                     } else if (baseKey === "DESCRIPTION") { | ||||
|                         event.description = value; | ||||
|                     } else if (baseKey === "RRULE") { | ||||
|                         event.rrule = value; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         return events; | ||||
|     } | ||||
| 
 | ||||
|     // Hilfsfunktion: Parst einen ICS-Datum-String ins Format "YYYY-MM-DD" | ||||
|     function parseDateString(icsDateStr) { | ||||
|         // Handle different date formats | ||||
|         if (!icsDateStr) return null; | ||||
|          | ||||
|         // For basic date format: YYYYMMDD | ||||
|         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}`; | ||||
|         }  | ||||
|         // For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss | ||||
|         else if (icsDateStr.includes("T")) { | ||||
|             let year = icsDateStr.substring(0, 4); | ||||
|             let month = icsDateStr.substring(4, 6); | ||||
|             let day = icsDateStr.substring(6, 8); | ||||
|             return `${year}-${month}-${day}`; | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     // 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]; | ||||
|     } | ||||
| 
 | ||||
|     // 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--; | ||||
|          } | ||||
|          updateEventsForMonth(currentYear, currentMonth); | ||||
|     }); | ||||
|     document.getElementById("next-month").addEventListener("click", function(){ | ||||
|          currentMonth++; | ||||
|          if (currentMonth > 11) { | ||||
|           currentMonth = 0; | ||||
|           currentYear++; | ||||
|          } | ||||
|          updateEventsForMonth(currentYear, currentMonth); | ||||
|     }); | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     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]) { | ||||
|                 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"); | ||||
|                 }); | ||||
|                  | ||||
|                 // 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); | ||||
|                 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 | ||||
|                 let titleLink = document.createElement("h"); | ||||
|                 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); | ||||
| 
 | ||||
|                         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); | ||||
|                         } | ||||
| 
 | ||||
|                           eventDetailsElem.appendChild(eventItem); | ||||
|             }); | ||||
|         } 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 | ||||
|         if (!icsTimeStr) return ""; | ||||
|          | ||||
|         if (icsTimeStr.length === 8) { | ||||
|             // All-day event | ||||
|             return "Ganztägig"; | ||||
|         } 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); | ||||
|             return `${hour}:${minute}`; | ||||
|         } | ||||
|         return icsTimeStr; | ||||
|     } | ||||
| 
 | ||||
|     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}`; | ||||
|     } | ||||
| 
 | ||||
|     // ICS-Datei abrufen und Events verarbeiten | ||||
|     fetch('/all.ics') | ||||
|       .then(response => response.text()) | ||||
|       .then(data => { | ||||
|          events = parseICS(data); | ||||
|           | ||||
|          // Initialize with current date | ||||
|          let today = new Date(); | ||||
|          currentYear = today.getFullYear(); | ||||
|          currentMonth = today.getMonth(); | ||||
|           | ||||
|          // Process events for current month | ||||
|          updateEventsForMonth(currentYear, currentMonth); | ||||
|       }) | ||||
|       .catch(err => console.error('Fehler beim Laden der ICS-Datei:', err)); | ||||
| })(); | ||||
| </script> | ||||
| 
 | ||||
| Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](all.ics)! | ||||
							
								
								
									
										
											BIN
										
									
								
								content/verein/calendar/feature-calendar-nachts-geschlossen.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								content/verein/calendar/feature-calendar-nachts-geschlossen.jpg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.7 MiB | 
							
								
								
									
										15
									
								
								content/verein/calendar/index.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								content/verein/calendar/index.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| --- | ||||
| title: "Kalender" | ||||
| subtitle: "Der Kalender des CCCB" | ||||
| date: 2025-02-26T10:00:00+02:00 | ||||
| menu: | ||||
|   main: | ||||
|     parent: "Verein" | ||||
| tag: ["Verein"] | ||||
| herostyle: big | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| {{< calendar >}} | ||||
| 
 | ||||
| Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](all.ics)! | ||||
							
								
								
									
										33
									
								
								layouts/shortcodes/calendar.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								layouts/shortcodes/calendar.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| {{ $style := resources.Get "css/calendar.css" | resources.ToCSS | resources.Fingerprint }} | ||||
| <link rel="stylesheet" href="{{ $style.RelPermalink }}"> | ||||
| 
 | ||||
| {{ $js := resources.Get "js/calendar.js" | resources.Fingerprint }} | ||||
| <script src="{{ $js.RelPermalink }}" defer></script> | ||||
| 
 | ||||
| <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> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Marek Krug
						Marek Krug