Merge branch 'staging' into production
This commit is contained in:
		
						commit
						c9da3c1c54
					
				
					 21 changed files with 729 additions and 617 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,6 +1,3 @@
 | 
				
			||||||
[submodule "beautifulhugo"]
 | 
					 | 
				
			||||||
	path = themes/beautifulhugo
 | 
					 | 
				
			||||||
	url = https://github.com/cccb/beautifulhugo.git
 | 
					 | 
				
			||||||
[submodule "themes/blowfish"]
 | 
					[submodule "themes/blowfish"]
 | 
				
			||||||
	path = themes/blowfish
 | 
						path = themes/blowfish
 | 
				
			||||||
	url = https://github.com/nunocoracao/blowfish.git
 | 
						url = https://github.com/nunocoracao/blowfish.git
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1,5 @@
 | 
				
			||||||
 | 
					<<<<<<< HEAD
 | 
				
			||||||
--baseURL=https://berlin.ccc.de/
 | 
					--baseURL=https://berlin.ccc.de/
 | 
				
			||||||
 | 
					=======
 | 
				
			||||||
 | 
					--baseURL=https://berlin.ccc.de/
 | 
				
			||||||
 | 
					>>>>>>> staging
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										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
 | 
					RelativeURLs = true
 | 
				
			||||||
CanonifyURLs = 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
 | 
					enableRobotsTXT = true
 | 
				
			||||||
summaryLength = 0
 | 
					summaryLength = 0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,5 +9,5 @@
 | 
				
			||||||
  noClasses = false
 | 
					  noClasses = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tableOfContents]
 | 
					[tableOfContents]
 | 
				
			||||||
  startLevel = 2
 | 
					  startLevel = 1
 | 
				
			||||||
  endLevel = 4
 | 
					  endLevel = 5
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,8 +61,8 @@ forgejoDefaultServer = "https://git.berlin.ccc.de"
 | 
				
			||||||
  showDateUpdated = true
 | 
					  showDateUpdated = true
 | 
				
			||||||
  showAuthor = false
 | 
					  showAuthor = false
 | 
				
			||||||
  showAuthorBottom = true
 | 
					  showAuthorBottom = true
 | 
				
			||||||
  showHero = false
 | 
					  showHero = true
 | 
				
			||||||
  # heroStyle = "basic" # valid options: basic, big, background, thumbAndBackground
 | 
					  heroStyle = "big" # valid options: basic, big, background, thumbAndBackground
 | 
				
			||||||
  layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
 | 
					  layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
 | 
				
			||||||
  layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
 | 
					  layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
 | 
				
			||||||
  showBreadcrumbs = true
 | 
					  showBreadcrumbs = true
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,4 +6,4 @@ description: "Startseite CCCB mit Kurzkalender"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CALENDAR
 | 
					CALENDAR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Weitere Termine findest du [hier](/verein/calendar/).
 | 
					Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
categories: ["Datengarten"]
 | 
					categories: ["Datengarten"]
 | 
				
			||||||
tags: ["IOCCC", "Obfuscated C", "Wettbewerb", "Datengarten", "Veranstaltung"]
 | 
					tags: ["IOCCC", "Obfuscation", "Wettbewerb", "Datengarten", "Veranstaltung", "Programming"]
 | 
				
			||||||
series: "Datengarten"
 | 
					series: "Datengarten"
 | 
				
			||||||
title: "Datengarten 106"
 | 
					title: "Datengarten 106"
 | 
				
			||||||
no: 106
 | 
					no: 106
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										87
									
								
								content/datengarten/112/feature-dg-standard-logo.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								content/datengarten/112/feature-dg-standard-logo.svg
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 46 KiB  | 
							
								
								
									
										19
									
								
								content/datengarten/112/index.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								content/datengarten/112/index.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					categories: ["Datengarten"]
 | 
				
			||||||
 | 
					tags: ["Alltag", "Kompression", "Mediendateien", "Datengarten", "Veranstaltung"]
 | 
				
			||||||
 | 
					series: "Datengarten"
 | 
				
			||||||
 | 
					title: "Datengarten 112"
 | 
				
			||||||
 | 
					no: 112
 | 
				
			||||||
 | 
					subtitle: "Ich hab den kleinsten ... Anhang! - Erste Hilfe für sinnvolle Mediendateigrößen"
 | 
				
			||||||
 | 
					speaker: "Volker Diels-Grabsch"
 | 
				
			||||||
 | 
					date: 2025-03-12T19:00:00+01:00
 | 
				
			||||||
 | 
					event:
 | 
				
			||||||
 | 
					  start: 2025-03-20T20:00:00+01:00
 | 
				
			||||||
 | 
					  end:   2025-03-20T21:30:00+01:00
 | 
				
			||||||
 | 
					location: CCCB
 | 
				
			||||||
 | 
					language: de
 | 
				
			||||||
 | 
					streaming: true
 | 
				
			||||||
 | 
					recording: https://streaming.media.ccc.de/datengarten/cccb
 | 
				
			||||||
 | 
					draft: false
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					to be done
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
categories: ["Datengarten"]
 | 
					categories: ["Datengarten"]
 | 
				
			||||||
tags: ["Compiler", "Interpreter", "Programming Language", "Trust", "Datengarten", "Veranstaltung"]
 | 
					tags: ["Compiler", "Interpreter", "Programming Language", "Trust", "Programming", "Datengarten", "Veranstaltung"]
 | 
				
			||||||
series: "Datengarten"
 | 
					series: "Datengarten"
 | 
				
			||||||
title: "Datengarten 96"
 | 
					title: "Datengarten 96"
 | 
				
			||||||
no: 96
 | 
					no: 96
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ subtitle: "Der CCCB lädt ein zum Kennenlernen und Entdecken."
 | 
				
			||||||
date: 2025-02-10T12:00:00+02:00
 | 
					date: 2025-02-10T12:00:00+02:00
 | 
				
			||||||
dtstart:  20250329T130000
 | 
					dtstart:  20250329T130000
 | 
				
			||||||
dtend:    20250329T200000
 | 
					dtend:    20250329T200000
 | 
				
			||||||
 | 
					showHero: false
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{< figure
 | 
					{{< figure
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ menu:
 | 
				
			||||||
  main:
 | 
					  main:
 | 
				
			||||||
    parent: "Veranstaltungen"
 | 
					    parent: "Veranstaltungen"
 | 
				
			||||||
tag: ["Veranstaltung"]
 | 
					tag: ["Veranstaltung"]
 | 
				
			||||||
 | 
					draft: true
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ dtstart: 20180704T200000
 | 
				
			||||||
dtend:   20180704T220000
 | 
					dtend:   20180704T220000
 | 
				
			||||||
rrule:   "FREQ=MONTHLY;BYDAY=2TU,4TU;INTERVAL=1;WKST=MO"
 | 
					rrule:   "FREQ=MONTHLY;BYDAY=2TU,4TU;INTERVAL=1;WKST=MO"
 | 
				
			||||||
tags: ["Mitgliedschaft", "Veranstaltung"]
 | 
					tags: ["Mitgliedschaft", "Veranstaltung"]
 | 
				
			||||||
 | 
					draft: true
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Das Plenum findet in der Regel am 2. und 4. Dienstag im Monat ab 20:00 Uhr in den Clubräumen
 | 
					Das Plenum findet in der Regel am 2. und 4. Dienstag im Monat ab 20:00 Uhr in den Clubräumen
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ menu:
 | 
				
			||||||
  main:
 | 
					  main:
 | 
				
			||||||
    parent: "Veranstaltungen"
 | 
					    parent: "Veranstaltungen"
 | 
				
			||||||
tag: ["Veranstaltung"]
 | 
					tag: ["Veranstaltung"]
 | 
				
			||||||
 | 
					draft: true
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
Subproject commit 0699f2797d2b8c2da8a6a28da9ef4a8013f5f858
 | 
					 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue