280 lines
10 KiB
JavaScript
280 lines
10 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
|
|
// -------------------------------------------------------------
|
|
// 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/location-pin.svg';
|
|
|
|
// -------------------------------------------------------------
|
|
// In-memory state for fetched events and currently active category.
|
|
// -------------------------------------------------------------
|
|
let allEvents = [];
|
|
let activeCategory = 'ALLE';
|
|
|
|
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 [];
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 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') || '';
|
|
|
|
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 = '<p>Events konnten nicht geladen werden.</p>';
|
|
}
|
|
}
|
|
|
|
// 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`;
|
|
}
|
|
|
|
// 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') === activeCategory) {
|
|
btn.classList.add('active');
|
|
} else {
|
|
btn.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
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);
|
|
|
|
sessionStorage.setItem('activeFilter', activeCategory);
|
|
sessionStorage.setItem('activeLocation', selectedLocation);
|
|
sessionStorage.setItem('activeDate', selectedDate);
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="empty-state">
|
|
<p class="empty-state-kicker">Keine Treffer</p>
|
|
<h3>Schade, aktuell gibt es hier keine Events.</h3>
|
|
<p>Starte dein eigenes Dinner und bringe die Community an deinen Tisch.</p>
|
|
<a class="empty-state-link" href="event_create.html">
|
|
<button class="empty-state-btn" type="button">Event erstellen</button>
|
|
</a>
|
|
</div>
|
|
`;
|
|
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.onclick = () => {
|
|
window.location.href = `event_detail.html?id=${event.id}`;
|
|
};
|
|
|
|
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 => `<span class="event-tag">${spec}</span>`).join('')
|
|
: '';
|
|
|
|
card.innerHTML = `
|
|
<div class="event-main">
|
|
<div class="event-top-row">
|
|
<span class="event-location">
|
|
<img src="${locationIconPath}" alt="">
|
|
${event.location}
|
|
</span>
|
|
<p class="event-date-time">${displayDate} | ${displayTime} | ${bookedSeats}/${totalCapacity} Gaeste</p>
|
|
</div>
|
|
<h2 class="event-title">${event.title}</h2>
|
|
<div class="event-meta-row">
|
|
<span class="event-tag">${event.category}</span>
|
|
<span class="event-tag">${event.diet}</span>
|
|
${specsChips}
|
|
</div>
|
|
</div>
|
|
<div class="event-side${isFull ? ' event-side-full' : ''}">
|
|
<span class="event-spots${isFull ? ' event-spots-full' : ''}">${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`}</span>
|
|
${isFull ? '' : '<button class="btn-primary" type="button">Anmelden</button>'}
|
|
</div>
|
|
`;
|
|
|
|
eventGrid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
// Category filter interactions.
|
|
filterButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
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();
|
|
});
|