document.addEventListener('DOMContentLoaded', () => { const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const USERS_STORAGE_KEY = 'socialCookingUsers'; const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; const INFO_MODAL_SHOWN_KEY = 'infoModalShownOnFirstLogin'; // ------------------------------------------------------------- // DOM references used throughout the page lifecycle. // ------------------------------------------------------------- const eventGrid = document.getElementById('event-grid'); const filterButtons = document.querySelectorAll('.category-item'); const locationFilter = document.getElementById('location-filter'); const dateFilter = document.getElementById('date-filter'); const locationIconPath = 'assets/icon_location.svg'; const calendarIconPath = 'assets/icon_calendar.svg'; const gastIconPath = 'assets/icon_gast.svg'; // ------------------------------------------------------------- // In-memory state for fetched events and currently active filters. // Separate state for category, diet, and allergie selections. // ------------------------------------------------------------- let allEvents = []; let activeCategory = 'ALLE'; let activeDiets = new Set(); let activeAllergies = new Set(); const currentUser = getCurrentUser(); function getCurrentUser() { try { const stored = localStorage.getItem(CURRENT_USER_KEY); return stored ? JSON.parse(stored) : null; } catch (error) { console.error('Aktueller Benutzer konnte nicht gelesen werden.', error); return null; } } function getInfoModalShownKeyForUser(user) { const email = String(user?.email || '').trim().toLowerCase(); return email ? `${INFO_MODAL_SHOWN_KEY}:${email}` : INFO_MODAL_SHOWN_KEY; } // Prüft, ob ein Event dem aktuellen Benutzer gehört. function isEventOwnedByCurrentUser(event, user) { if (!event || !user) { return false; } const userEmail = String(user.email || '').trim().toLowerCase(); const hostEmail = String(event.hostEmail || '').trim().toLowerCase(); if (userEmail && hostEmail) { return userEmail === hostEmail; } const userFirstName = String(user.vorname || '').trim().toLowerCase(); const hostName = String(event.host?.name || '').trim().toLowerCase(); return Boolean(userFirstName && hostName && userFirstName === hostName); } function getStoredEvents() { try { const stored = localStorage.getItem(EVENTS_STORAGE_KEY); return stored ? JSON.parse(stored) : []; } catch (error) { console.error('Lokale Events konnten nicht gelesen werden.', error); return []; } } function getRegistrationMap() { try { const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY); return stored ? JSON.parse(stored) : {}; } catch (error) { console.error('Anmeldedaten konnten nicht gelesen werden.', error); return {}; } } function getStoredUsers() { try { const stored = localStorage.getItem(USERS_STORAGE_KEY); return stored ? JSON.parse(stored) : []; } catch (error) { console.error('Benutzerdaten konnten nicht gelesen werden.', error); return []; } } function getUserDisplayName(user) { if (!user) { return ''; } const firstName = String(user.vorname || '').trim(); const lastName = String(user.nachname || '').trim(); const fullName = `${firstName} ${lastName}`.trim(); return (fullName || firstName || String(user.email || '').trim()).trim(); } function getResolvedParticipants(event, registrationMap) { const baseParticipants = Array.isArray(event.participants) ? event.participants.map(name => String(name || '').trim()).filter(Boolean) : []; const usersByEmail = new Map( getStoredUsers().map(user => [String(user.email || '').trim().toLowerCase(), user]) ); const participantLookup = new Set(baseParticipants.map(name => name.toLowerCase())); Object.entries(registrationMap || {}).forEach(([email, ids]) => { const isRegisteredForEvent = Array.isArray(ids) && ids.map(id => Number(id)).includes(Number(event.id)); if (!isRegisteredForEvent) { return; } const user = usersByEmail.get(String(email || '').trim().toLowerCase()); const displayName = getUserDisplayName(user) || String(email || '').trim(); const normalizedName = displayName.toLowerCase(); if (displayName && !participantLookup.has(normalizedName)) { baseParticipants.push(displayName); participantLookup.add(normalizedName); } }); return baseParticipants; } function setRegistrationMap(registrationMap) { localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); } // ------------------------------------------------------------- // Initial data bootstrap: // 1) fetch JSON, // 2) populate select options, // 3) restore filter state from sessionStorage, // 4) render filtered list. // ------------------------------------------------------------- async function fetchEvents() { try { const response = await fetch('data/events.json'); const apiEvents = await response.json(); const localEvents = getStoredEvents(); allEvents = [...localEvents, ...apiEvents]; populateMetaFilters(); const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE'; const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE'; const savedDate = sessionStorage.getItem('activeDate') || ''; const savedDiets = sessionStorage.getItem('activeDiets') || ''; const savedAllergies = sessionStorage.getItem('activeAllergies') || ''; activeCategory = savedCategory; activeDiets = new Set(savedDiets ? savedDiets.split(',') : []); activeAllergies = new Set(savedAllergies ? savedAllergies.split(',') : []); if (locationFilter) { locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE'; } if (dateFilter) { dateFilter.value = savedDate; } updateDietAvailability(); applyFilters(); } catch (error) { console.error('Fehler:', error); eventGrid.innerHTML = '

Events konnten nicht geladen werden.

'; } } // Build location options dynamically from loaded events. function populateMetaFilters() { const locations = [...new Set(allEvents.map(event => event.location))].sort(); if (locationFilter) { locations.forEach(location => { const option = document.createElement('option'); option.value = location; option.textContent = location; locationFilter.appendChild(option); }); } } // Convert localized event date (e.g. 19. MÄR. 2026) into ISO format for date input comparison. function parseEventDateToIso(dateString) { if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { return dateString; } const months = { JAN: '01', FEB: '02', 'MÄR': '03', MRZ: '03', APR: '04', MAI: '05', JUN: '06', JUL: '07', AUG: '08', SEP: '09', OKT: '10', NOV: '11', DEZ: '12' }; const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/); if (!match) { return ''; } const day = String(match[1]).padStart(2, '0'); const month = months[match[2]]; const year = match[3]; return month ? `${year}-${month}-${day}` : ''; } // Convert short month notation into full German month label for UI display. function formatEventDate(dateString) { if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { const [year, month, day] = dateString.split('-'); return `${Number(day)}. ${['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1]} ${year}`; } const labels = { JAN: 'Januar', FEB: 'Februar', 'MÄR': 'März', MRZ: 'März', APR: 'April', MAI: 'Mai', JUN: 'Juni', JUL: 'Juli', AUG: 'August', SEP: 'September', OKT: 'Oktober', NOV: 'November', DEZ: 'Dezember' }; const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/); if (!match) { return dateString; } const day = Number(match[1]); const monthLabel = labels[match[2]]; const year = match[3]; return monthLabel ? `${day}. ${monthLabel} ${year}` : dateString; } // Normalize time label from UHR to Uhr for consistent typography. function formatEventTime(timeString) { if (!timeString) { return ''; } return timeString.includes('UHR') ? timeString.replace('UHR', 'Uhr').trim() : `${timeString} Uhr`; } // Baut aus Eventdatum/-zeit ein Date-Objekt für Fristlogik und Vergleiche. function parseEventDateTime(event) { if (!event?.date) { return null; } const dateValue = String(event.date).trim(); const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/); let year; let month; let day; if (isoDateMatch) { year = Number(isoDateMatch[1]); month = Number(isoDateMatch[2]); day = Number(isoDateMatch[3]); } else { const monthMap = { jan: 1, januar: 1, feb: 2, februar: 2, 'mär': 3, mrz: 3, mar: 3, maerz: 3, märz: 3, apr: 4, april: 4, mai: 5, jun: 6, juni: 6, jul: 7, juli: 7, aug: 8, august: 8, sep: 9, sept: 9, september: 9, okt: 10, oktober: 10, nov: 11, november: 11, dez: 12, dezember: 12 }; const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/); if (!localizedMatch) { return null; } day = Number(localizedMatch[1]); month = monthMap[String(localizedMatch[2]).toLowerCase()]; year = Number(localizedMatch[3]); if (!month) { return null; } } const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/); const hours = timeMatch ? Number(timeMatch[1]) : 0; const minutes = timeMatch ? Number(timeMatch[2]) : 0; return new Date(year, month - 1, day, hours, minutes, 0, 0); } // Zählt eindeutige Registrierungen eines Events über alle Benutzer. function countRegistrationsForEvent(registrationMap, eventId) { return Object.values(registrationMap).reduce((count, ids) => { const hasEvent = Array.isArray(ids) && ids.map(id => Number(id)).includes(Number(eventId)); return hasEvent ? count + 1 : count; }, 0); } // Schliesst neue Anmeldungen ab 24h vor Start (inkl. bereits gestarteter Events). function isRegistrationClosedForEvent(event) { const eventDateTime = parseEventDateTime(event); if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { return false; } const msUntilStart = eventDateTime.getTime() - Date.now(); const twentyfourHoursInMs = 24 * 60 * 60 * 1000; return msUntilStart <= twentyfourHoursInMs; } // Safely verify whether a value exists in the given select element. function hasOption(selectElement, value) { return Array.from(selectElement.options).some(option => option.value === value); } // Apply all filters together (category, diet, allergie, location, date), update button state, render and persist. function applyFilters() { const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE'; const selectedDate = dateFilter ? dateFilter.value : ''; // Update active states for all filter types filterButtons.forEach(btn => { const isCategoryButton = btn.getAttribute('data-cat') !== null; const isDietButton = btn.getAttribute('data-diet') !== null; const isAllergieButton = btn.getAttribute('data-allergie') !== null; if (isCategoryButton) { if (btn.getAttribute('data-cat') === activeCategory) { btn.classList.add('active'); } else { btn.classList.remove('active'); } } else if (isDietButton) { if (activeDiets.has(btn.getAttribute('data-diet'))) { btn.classList.add('active'); } else { btn.classList.remove('active'); } } else if (isAllergieButton) { if (activeAllergies.has(btn.getAttribute('data-allergie'))) { btn.classList.add('active'); } else { btn.classList.remove('active'); } } }); const filtered = allEvents.filter(event => { if (event.status === 'canceled') return false; const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory; // Diet filter: if no diets selected, show all. Otherwise, event MUST have at least one selected diet. const dietMatch = activeDiets.size === 0 || (event.diet && event.diet.split(', ').some(d => activeDiets.has(d.trim()))); // Allergie filter: if no allergies selected, show all. Otherwise, event MUST have at least one selected allergie. const allergieMatch = activeAllergies.size === 0 || (event.specifications && event.specifications.some(spec => activeAllergies.has(spec))); const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation; const eventDateIso = parseEventDateToIso(event.date); const dateMatch = !selectedDate || eventDateIso === selectedDate; return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch; }); renderEvents(filtered); sessionStorage.setItem('activeFilter', activeCategory); sessionStorage.setItem('activeLocation', selectedLocation); sessionStorage.setItem('activeDate', selectedDate); sessionStorage.setItem('activeDiets', Array.from(activeDiets).join(',')); sessionStorage.setItem('activeAllergies', Array.from(activeAllergies).join(',')); } // Render either: // - empty state call-to-action when no results match, // - or event cards with status and metadata. function renderEvents(events) { eventGrid.innerHTML = ''; const registrationMap = getRegistrationMap(); const userRegistrationSet = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) ? new Set(registrationMap[currentUser.email].map(id => Number(id))) : new Set(); if (events.length === 0) { eventGrid.innerHTML = `

Keine Treffer

Schade, aktuell gibt es hier keine Events.

Starte dein eigenes Event und bringe die Community an deinen Tisch.

`; return; } events.forEach(event => { // Card shell and click-through navigation to detail page. const card = document.createElement('article'); card.className = 'event-card'; card.style.cursor = 'pointer'; card.addEventListener('click', clickedEvent => { if (clickedEvent.target instanceof HTMLElement && clickedEvent.target.closest('button')) { return; } window.location.href = `event_detail.html?id=${event.id}`; }); const displayDate = formatEventDate(event.date); const displayTime = formatEventTime(event.time); // Capacity logic: // spots = total capacity, resolved participants = booked seats. const resolvedParticipants = getResolvedParticipants(event, registrationMap); const bookedSeats = resolvedParticipants.length; const totalCapacity = event.spots; const freePlaces = Math.max(0, totalCapacity - bookedSeats); const isFull = freePlaces === 0; const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); const isRegistered = userRegistrationSet.has(Number(event.id)); const isRegistrationClosed = isRegistrationClosedForEvent(event); const isCanceled = event.status === 'canceled'; if (isCanceled) { card.style.opacity = '0.6'; } // Build optional specification chips only when data exists. const specsChips = event.specifications && event.specifications.length > 0 ? event.specifications.map(spec => `${spec}`).join('') : ''; // Build diet tags: split by comma and create individual tags const dietTags = event.diet && event.diet !== 'Keine Angabe' && event.diet !== '–' ? event.diet.split(', ').map(d => `${d.trim()}`).join('') : ''; let actionMarkup = ''; if (isCanceled) { actionMarkup = ''; } else if (isOwnEvent) { actionMarkup = ''; } else if (isRegistered) { actionMarkup = isRegistrationClosed ? '' : ''; } else if (isRegistrationClosed) { actionMarkup = ''; } else if (!isFull) { if (!currentUser) { actionMarkup = ''; } else { actionMarkup = ''; } } let sideInfoMarkup = ''; if (isCanceled) { sideInfoMarkup = 'Event abgesagt'; } else if (!isRegistrationClosed) { sideInfoMarkup = `${isFull ? 'Ausgebucht' : `${freePlaces} Plätze frei`}`; } card.innerHTML = `
${event.location} ${displayDate} | ${displayTime} Gaeste Icon${bookedSeats}/${totalCapacity}

${event.title}

${event.category} ${dietTags} ${specsChips}
${actionMarkup} ${sideInfoMarkup}
`; const actionButton = card.querySelector('[data-registration-action]'); if (actionButton) { actionButton.addEventListener('click', clickEvent => { clickEvent.stopPropagation(); const action = actionButton.getAttribute('data-registration-action'); if (action === 'own') { return; } if (action === 'closed') { return; } if (action === 'login') { window.location.href = 'login.html'; return; } if (!currentUser?.email) { window.location.href = 'login.html'; return; } const nextRegistrationMap = getRegistrationMap(); const currentIds = Array.isArray(nextRegistrationMap[currentUser.email]) ? nextRegistrationMap[currentUser.email].map(id => Number(id)) : []; const idSet = new Set(currentIds); // Anmelde-Modal öffnen if (action === 'register' && !isFull && !isRegistrationClosed) { const modal = document.getElementById('register-confirm-modal'); if (modal) { modal.classList.add('show'); document.getElementById('confirm-register-btn').onclick = () => { modal.classList.remove('show'); const map = getRegistrationMap(); const ids = new Set((map[currentUser.email] || []).map(id => Number(id))); ids.add(Number(event.id)); map[currentUser.email] = Array.from(ids); setRegistrationMap(map); const snackbar = document.getElementById('snackbar'); if (snackbar) { snackbar.textContent = 'Du wurdest erfolgreich angemeldet.'; snackbar.classList.add('snackbar--visible'); setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000); } applyFilters(); }; } return; } // Abmelde-Modal öffnen if (action === 'unregister') { const modal = document.getElementById('unregister-confirm-modal'); if (modal) { modal.classList.add('show'); document.getElementById('confirm-unregister-btn').onclick = () => { modal.classList.remove('show'); const map = getRegistrationMap(); const ids = new Set((map[currentUser.email] || []).map(id => Number(id))); ids.delete(Number(event.id)); map[currentUser.email] = Array.from(ids); setRegistrationMap(map); const snackbar = document.getElementById('snackbar'); if (snackbar) { snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.'; snackbar.classList.add('snackbar--danger', 'snackbar--visible'); setTimeout(() => { snackbar.classList.remove('snackbar--visible'); setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400); }, 3000); } applyFilters(); }; } return; } nextRegistrationMap[currentUser.email] = Array.from(idSet); setRegistrationMap(nextRegistrationMap); applyFilters(); }); } eventGrid.appendChild(card); }); } // Verhindert widersprüchliche Ernährungsformen: // Fleisch/Fisch schliessen Vegetarisch/Vegan aus. // Vegan schliesst alles andere (Fleisch, Fisch, Vegetarisch) aus. // Vegetarisch schliesst Fleisch/Fisch aus, aber nicht Vegan. function updateDietAvailability() { const dietButtons = Array.from(filterButtons).filter(btn => btn.getAttribute('data-diet') !== null); const meatFishButtons = dietButtons.filter(btn => ['Fleisch', 'Fisch'].includes(btn.getAttribute('data-diet'))); const plantButtons = dietButtons.filter(btn => ['Vegetarisch', 'Vegan'].includes(btn.getAttribute('data-diet'))); const vegetarischBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegetarisch'); const veganBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegan'); const hasVegan = activeDiets.has('Vegan'); const hasVegetarisch = activeDiets.has('Vegetarisch'); const hasMeatOrFish = meatFishButtons.some(btn => activeDiets.has(btn.getAttribute('data-diet'))); // If Vegan is selected, disable everything else if (hasVegan) { meatFishButtons.forEach(btn => { btn.classList.add('disabled'); }); if (vegetarischBtn) { vegetarischBtn.classList.add('disabled'); } } // If Vegetarisch is selected, disable only Fleisch/Fisch else if (hasVegetarisch) { meatFishButtons.forEach(btn => { btn.classList.add('disabled'); }); if (veganBtn) { veganBtn.classList.remove('disabled'); } } // If Fleisch/Fisch is selected, disable both Vegetarisch and Vegan else if (hasMeatOrFish) { if (vegetarischBtn) { vegetarischBtn.classList.add('disabled'); } if (veganBtn) { veganBtn.classList.add('disabled'); } } // No conflicts, enable everything else { plantButtons.forEach(btn => { btn.classList.remove('disabled'); }); meatFishButtons.forEach(btn => { btn.classList.remove('disabled'); }); } } // Category filter interactions: mutually exclusive (radio button behavior). filterButtons.forEach(button => { button.addEventListener('click', () => { const categoryValue = button.getAttribute('data-cat'); const dietValue = button.getAttribute('data-diet'); const allergieValue = button.getAttribute('data-allergie'); if (categoryValue !== null) { // Category filter: exclusive selection activeCategory = categoryValue; } else if (dietValue !== null) { // Diet filter: toggle selection with conflict handling const isCurrentlySelected = activeDiets.has(dietValue); if (!isCurrentlySelected) { // Adding a diet - handle conflicts if (dietValue === 'Vegetarisch') { // Vegetarisch removes Fleisch/Fisch but not Vegan activeDiets.delete('Fleisch'); activeDiets.delete('Fisch'); activeDiets.add('Vegetarisch'); } else if (dietValue === 'Vegan') { // Vegan removes all other options activeDiets.delete('Vegetarisch'); activeDiets.delete('Fleisch'); activeDiets.delete('Fisch'); activeDiets.add('Vegan'); } else if (dietValue === 'Fleisch' || dietValue === 'Fisch') { // Fleisch/Fisch remove Vegetarisch/Vegan activeDiets.delete('Vegetarisch'); activeDiets.delete('Vegan'); activeDiets.add(dietValue); } } else { // Removing a diet activeDiets.delete(dietValue); } updateDietAvailability(); } else if (allergieValue !== null) { // Allergie filter: toggle selection if (activeAllergies.has(allergieValue)) { activeAllergies.delete(allergieValue); } else { activeAllergies.add(allergieValue); } } applyFilters(); }); }); // Secondary filter interactions. if (locationFilter) { locationFilter.addEventListener('change', applyFilters); } if (dateFilter) { dateFilter.addEventListener('change', applyFilters); // Make calendar icon clickable to focus the date input const calendarIcon = document.querySelector('.calendar-icon'); if (calendarIcon) { calendarIcon.addEventListener('click', () => { dateFilter.focus(); dateFilter.click(); }); } } // Clear all filters button const clearAllFiltersBtn = document.getElementById('clear-all-filters'); if (clearAllFiltersBtn) { clearAllFiltersBtn.addEventListener('click', () => { activeCategory = 'ALLE'; activeDiets.clear(); activeAllergies.clear(); if (locationFilter) { locationFilter.value = 'ALLE_ORTE'; } if (dateFilter) { dateFilter.value = ''; } updateDietAvailability(); applyFilters(); }); } // Info button modal behavior const infoButton = document.getElementById('info-button'); const infoModal = document.getElementById('info-modal'); const modalClose = infoModal?.querySelector('.modal-close'); if (infoButton && infoModal) { infoButton.addEventListener('click', () => { infoModal.classList.add('show'); }); } if (modalClose && infoModal) { modalClose.addEventListener('click', () => { infoModal.classList.remove('show'); }); } if (infoModal) { infoModal.addEventListener('click', (event) => { if (event.target === infoModal) { infoModal.classList.remove('show'); } }); } // Auto-open info modal on first login if (currentUser && infoModal) { const userInfoModalKey = getInfoModalShownKeyForUser(currentUser); const hasShownInfoModal = localStorage.getItem(userInfoModalKey); if (!hasShownInfoModal) { infoModal.classList.add('show'); localStorage.setItem(userInfoModalKey, 'true'); } } // Kick off initial load/render cycle. fetchEvents(); }); // Modal closing helper functions function closeRegisterModal() { const modal = document.getElementById('register-confirm-modal'); if (modal) { modal.classList.remove('show'); } } function closeUnregisterModal() { const modal = document.getElementById('unregister-confirm-modal'); if (modal) { modal.classList.remove('show'); } } function closeInfoModal() { const modal = document.getElementById('info-modal'); if (modal) { modal.classList.remove('show'); } }