Zu Tisch mit Fremden – bereit für die nächste kulinarische Begegnung?

Dein Ort um neue Leute kennen zu lernen! Erstelle eigene Events und lade Gäste zu dir nach Hause ein.

- + Anmelden
diff --git a/js/event_detail.js b/js/event_detail.js index e34ebc6..ecabc45 100644 --- a/js/event_detail.js +++ b/js/event_detail.js @@ -1,7 +1,11 @@ document.addEventListener('DOMContentLoaded', async () => { + // ------------------------------------------------------------- + // DOM entry point and shared asset path. + // ------------------------------------------------------------- const detailContainer = document.getElementById('detail-view'); + const locationIconPath = 'assets/location-pin.svg'; - // 1. ID aus der URL lesen (z.B. detail.html?id=1) + // Read event id from query string (detail page deep-link support). const params = new URLSearchParams(window.location.search); const eventId = parseInt(params.get('id')); @@ -10,7 +14,7 @@ document.addEventListener('DOMContentLoaded', async () => { return; } - // 2. Daten laden und das richtige Event suchen + // Fetch data source and resolve the matching event record. try { const response = await fetch('data/events.json'); const allEvents = await response.json(); @@ -25,23 +29,232 @@ document.addEventListener('DOMContentLoaded', async () => { console.error("Fehler beim Laden der Details:", error); } + // Format localized date token into full readable date. + function formatEventDate(dateString) { + 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 casing for UI consistency. + function formatEventTime(timeString) { + return timeString.replace('UHR', 'Uhr').trim(); + } + + // Map diet keys to readable labels while keeping unknown values untouched. + function getDietLabel(diet) { + const labels = { + VEGGIE: 'Vegetarisch', + VEGAN: 'Vegan', + FLEISCH: 'Fleisch', + FISCH: 'Fisch' + }; + + return labels[diet] || diet; + } + + // Compose and inject the full detail UI for a single event. function renderDetailPage(event) { - //Layout Deatilseite der Events mit Rücklink zur Übersicht, Eventtitel, Infos und Bild + // Core display values and resilient fallbacks for optional data fields. + const displayDate = formatEventDate(event.date); + const displayTime = formatEventTime(event.time); + const dietLabel = getDietLabel(event.diet); + const eventCategory = event.category || 'EVENT'; + const hostName = event.host?.name || 'Host'; + const hostInitial = (event.host?.initial || hostName.charAt(0) || 'H').charAt(0).toUpperCase(); + const hostMessage = Array.isArray(event.hostMessage) && event.hostMessage.length > 0 + ? event.hostMessage + : ['Der Host hat für dieses Event noch keine Nachricht hinterlegt.']; + const menuItems = Array.isArray(event.menu) && event.menu.length > 0 + ? event.menu + : ['Menü wird in Kuerze bekannt gegeben.']; + const specifications = Array.isArray(event.specifications) && event.specifications.length > 0 + ? event.specifications + : []; + const participants = Array.isArray(event.participants) ? event.participants : []; + const galleryImages = Array.isArray(event.gallery) && event.gallery.length > 0 + ? event.gallery + : [event.image, event.image, event.image]; + const visibleParticipants = participants.slice(0, 6); + const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length); + const totalGuests = Number.isFinite(event.spots) ? event.spots : 0; + const confirmedGuests = participants.length; + const freePlaces = Math.max(0, totalGuests - confirmedGuests); + const isFull = freePlaces === 0; + const detailChips = [ + `${eventCategory}`, + `${dietLabel}`, + ...specifications.map(item => `${item}`) + ].join(''); + + // Render complete detail page layout including: + // hero metadata, host card, menu, participants, gallery and sticky action bar. detailContainer.innerHTML = ` -
- -

${event.title}

-
-
-
-

📍 ${event.location} | 📅 ${event.date} | 👤 Max. ${event.spots} Personen

-
-

Hier kommen die detaillierten Infos zu ${event.title} hin...

-
-
- ${event.title} +
+ + + Alle Events + + +
+
+ + + ${event.location} + +

${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste

+
+

${event.title}

+
+ ${detailChips} +
+
+ +
+
+
+
+ ${hostInitial} + ${hostName} + Host +
+ ${hostMessage.map(paragraph => `

${paragraph}

`).join('')} +
+ +
+

Menue

+
    + ${menuItems.map(item => `
  • ${item}
  • `).join('')} +
+
+ +
+
+

Teilnehmer

+ Alle ansehen +
+
+ ${visibleParticipants.map(name => `${name.charAt(0).toUpperCase()}`).join('')} + ${remainingParticipants > 0 ? `+${remainingParticipants}` : ''} +
+
+
+ + +
+ +
+
+ + + + ${event.location} + + | ${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste + + ${event.title} +
+ +
+ + ${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`} + + +
+
+ +
`; + + // --------------------------------------------------------- + // Lightbox behavior for gallery images: + // open on image click, close via backdrop, close button or ESC. + // --------------------------------------------------------- + const lightbox = detailContainer.querySelector('.detail-lightbox'); + const lightboxImage = detailContainer.querySelector('.detail-lightbox-image'); + const lightboxClose = detailContainer.querySelector('.detail-lightbox-close'); + const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item'); + + // Central close helper to keep all close paths consistent. + function closeLightbox() { + if (!lightbox) { + return; + } + + lightbox.classList.remove('is-open'); + lightbox.setAttribute('aria-hidden', 'true'); + } + + if (lightbox && lightboxImage) { + // Open with selected image source. + galleryButtons.forEach(button => { + button.addEventListener('click', () => { + const imageSrc = button.getAttribute('data-fullsrc'); + if (!imageSrc) { + return; + } + + lightboxImage.src = imageSrc; + lightbox.classList.add('is-open'); + lightbox.setAttribute('aria-hidden', 'false'); + }); + }); + + // Close when user clicks on backdrop. + lightbox.addEventListener('click', event => { + const target = event.target; + if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) { + closeLightbox(); + } + }); + + // Close via dedicated icon/button. + lightboxClose?.addEventListener('click', closeLightbox); + + // Close with keyboard for accessibility. + document.addEventListener('keydown', event => { + if (event.key === 'Escape') { + closeLightbox(); + } + }); + } } }); \ No newline at end of file diff --git a/js/event_overview.js b/js/event_overview.js index 2c199cc..2559a0f 100644 --- a/js/event_overview.js +++ b/js/event_overview.js @@ -1,111 +1,251 @@ document.addEventListener('DOMContentLoaded', () => { + // ------------------------------------------------------------- + // DOM references used throughout the page lifecycle. + // ------------------------------------------------------------- const eventGrid = document.getElementById('event-grid'); const filterButtons = document.querySelectorAll('.category-item'); - let allEvents = []; + const locationFilter = document.getElementById('location-filter'); + const dateFilter = document.getElementById('date-filter'); + const locationIconPath = 'assets/location-pin.svg'; - // 1. Daten laden aus JSOn file + // ------------------------------------------------------------- + // In-memory state for fetched events and currently active category. + // ------------------------------------------------------------- + let allEvents = []; + let activeCategory = 'ALLE'; + + // ------------------------------------------------------------- + // Initial data bootstrap: + // 1) fetch JSON, + // 2) populate select options, + // 3) restore filter state from sessionStorage, + // 4) render filtered list. + // ------------------------------------------------------------- async function fetchEvents() { try { - // Pfad zu JSON File angepasst an lokale Ordnerstruktur const response = await fetch('data/events.json'); allEvents = await response.json(); - renderEvents(allEvents); + populateMetaFilters(); - // Beim Laden prüfen, ob ein Filter gespeichert war - const savedFilter = sessionStorage.getItem('activeFilter') || 'ALLE'; - applyFilter(savedFilter); + const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE'; + const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE'; + const savedDate = sessionStorage.getItem('activeDate') || ''; - //checked ob Fehler beim Laden oder Parsen der Daten auftreten + activeCategory = savedCategory; + if (locationFilter) { + locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE'; + } + if (dateFilter) { + dateFilter.value = savedDate; + } + + applyFilters(); } catch (error) { - console.error("Fehler:", error); - eventGrid.innerHTML = "

Events konnten nicht geladen werden.

"; + console.error('Fehler:', error); + eventGrid.innerHTML = '

Events konnten nicht geladen werden.

'; } } - // Funktion um Filter anzuwenden und gleichzeitig UI zu aktualisieren - function applyFilter(category) { - // UI: Aktiven Button stylen + // 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) { + 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) { + 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) { + return timeString.replace('UHR', 'Uhr').trim(); + } + + // 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, location, date), update button state, render and persist. + function applyFilters() { + const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE'; + const selectedDate = dateFilter ? dateFilter.value : ''; + filterButtons.forEach(btn => { - if (btn.getAttribute('data-cat') === category) { + if (btn.getAttribute('data-cat') === activeCategory) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); - // Daten filtern - const filtered = category === 'ALLE' - ? allEvents - : allEvents.filter(e => e.category === category); - + const filtered = allEvents.filter(event => { + const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory; + const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation; + const eventDateIso = parseEventDateToIso(event.date); + const dateMatch = !selectedDate || eventDateIso === selectedDate; + + return categoryMatch && locationMatch && dateMatch; + }); + renderEvents(filtered); - - //Filter im Browser merken - sessionStorage.setItem('activeFilter', category); + sessionStorage.setItem('activeFilter', activeCategory); + sessionStorage.setItem('activeLocation', selectedLocation); + sessionStorage.setItem('activeDate', selectedDate); } - // 2. Events rendern + "Empty State" Logik - function renderEvents(events) { - eventGrid.innerHTML = ''; - // PRÜFUNG: Wenn keine Events vorhanden sind zeigt folgende Nachricht + // Render either: + // - empty state call-to-action when no results match, + // - or event cards with status and metadata. + function renderEvents(events) { + eventGrid.innerHTML = ''; + if (events.length === 0) { eventGrid.innerHTML = ` `; return; } - // Wenn Events da sind, Karten bauen events.forEach(event => { + // Card shell and click-through navigation to detail page. const card = document.createElement('article'); card.className = 'event-card'; - - //Klick auf die gesamte Karte leitet zur Detailseite weiter - card.style.cursor = "pointer"; + card.style.cursor = 'pointer'; card.onclick = () => { window.location.href = `event_detail.html?id=${event.id}`; }; - //internes HTML im Js zur Styling der Event-Karte (HIER CHECKEN OB SO OK NACH CLEAN CODE) + const displayDate = formatEventDate(event.date); + const displayTime = formatEventTime(event.time); + + // Capacity logic: + // spots = total capacity, participants.length = booked seats. + const bookedSeats = event.participants ? event.participants.length : 0; + const totalCapacity = event.spots; + const freePlaces = Math.max(0, totalCapacity - bookedSeats); + const isFull = freePlaces === 0; + + // Build optional specification chips only when data exists. + const specsChips = event.specifications && event.specifications.length > 0 + ? event.specifications.map(spec => `${spec}`).join('') + : ''; + card.innerHTML = ` -
-
- 📍 ${event.location} -

${event.title}

-
- ${event.cuisine} - ${event.category} +
+
+ + + ${event.location} + +

${displayDate} | ${displayTime} | ${bookedSeats}/${totalCapacity} Gaeste

+
+

${event.title}

+
+ ${event.category} + ${event.diet} + ${specsChips}
-
- ${event.date}
- 🕒 ${event.time} +
+ ${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`} + ${isFull ? '' : ''}
-
-
🥗 ${event.diet}
- -

${event.spots} PLÄTZE FREI

-
-
❤️
`; + eventGrid.appendChild(card); }); } - // 3. Filter-Logik basic anhand der Kategorien im JSON File + // Category filter interactions. filterButtons.forEach(button => { button.addEventListener('click', () => { - const selectedCat = button.getAttribute('data-cat'); - applyFilter(selectedCat); + activeCategory = button.getAttribute('data-cat'); + applyFilters(); }); }); + // Secondary filter interactions. + if (locationFilter) { + locationFilter.addEventListener('change', applyFilters); + } + + if (dateFilter) { + dateFilter.addEventListener('change', applyFilters); + } + + // Kick off initial load/render cycle. fetchEvents(); -}); \ No newline at end of file +});