476 lines
18 KiB
JavaScript
476 lines
18 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
|
|
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
|
|
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
|
|
// -------------------------------------------------------------
|
|
// 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';
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Prueft, ob ein Event dem aktuellen Benutzer gehoert.
|
|
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 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') || '';
|
|
|
|
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`;
|
|
}
|
|
|
|
// Baut aus Eventdatum/-zeit ein Date-Objekt fuer 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,
|
|
FEB: 2,
|
|
'MÄR': 3,
|
|
MRZ: 3,
|
|
APR: 4,
|
|
MAI: 5,
|
|
JUN: 6,
|
|
JUL: 7,
|
|
AUG: 8,
|
|
SEP: 9,
|
|
OKT: 10,
|
|
NOV: 11,
|
|
DEZ: 12
|
|
};
|
|
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
|
|
|
if (!localizedMatch) {
|
|
return null;
|
|
}
|
|
|
|
day = Number(localizedMatch[1]);
|
|
month = monthMap[localizedMatch[2]];
|
|
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);
|
|
}
|
|
|
|
// Zaehlt eindeutige Registrierungen eines Events ueber 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 12h 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 twelveHoursInMs = 12 * 60 * 60 * 1000;
|
|
|
|
return msUntilStart <= twelveHoursInMs;
|
|
}
|
|
|
|
// 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 => {
|
|
// Lokal erstellte Events werden nicht in der allgemeinen Event-Uebersicht angezeigt.
|
|
if (event.source === 'local') {
|
|
return false;
|
|
}
|
|
|
|
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 = '';
|
|
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 = `
|
|
<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.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, participants.length = booked seats.
|
|
const baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0;
|
|
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
|
|
const bookedSeats = baseParticipants + extraRegistrations;
|
|
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);
|
|
|
|
// 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('')
|
|
: '';
|
|
|
|
const actionMarkup = isOwnEvent
|
|
? '<button class="btn-primary btn-primary-own" type="button" data-registration-action="own" disabled>Dein Event!</button>'
|
|
: isRegistered
|
|
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="unregister">Abmelden</button>'
|
|
: isRegistrationClosed
|
|
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
|
|
: isFull
|
|
? ''
|
|
: !currentUser
|
|
? '<button class="btn-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
|
|
: '<button class="btn-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
|
|
|
|
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>
|
|
${actionMarkup}
|
|
</div>
|
|
`;
|
|
|
|
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);
|
|
|
|
if (action === 'unregister') {
|
|
idSet.delete(Number(event.id));
|
|
}
|
|
|
|
if (action === 'register' && !isFull && !isRegistrationClosed) {
|
|
idSet.add(Number(event.id));
|
|
}
|
|
|
|
nextRegistrationMap[currentUser.email] = Array.from(idSet);
|
|
setRegistrationMap(nextRegistrationMap);
|
|
applyFilters();
|
|
});
|
|
}
|
|
|
|
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();
|
|
});
|