Social_Cooking/js/event_overview.js

505 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.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;
}
}
// 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 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 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,
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);
}
// 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, 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 = '';
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 Event und bringe die Community an deinen Tisch.</p>
<a class="empty-state-link" href="event_create.html">
<button class="button-primary" 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('')
: '';
// 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 => `<span class="event-tag">${d.trim()}</span>`).join('')
: '';
const actionMarkup = isOwnEvent
? '<button class="button-primary-eigener-event" type="button" data-registration-action="own" disabled>Dein Event!</button>'
: isRegistered
? '<button class="button-primary button-primary-abmelden" type="button" data-registration-action="unregister">Abmelden</button>'
: isRegistrationClosed
? '<button class="button-primary" button-plaetze" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
: isFull
? ''
: !currentUser
? '<button class="button-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
: '<button class="button-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}" class="icon" alt="">
${event.location}
</span>
<span class="event-date-time"> <img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}
</span>
<span class="event-date-time"> ${bookedSeats}/${totalCapacity} Gäste</span>
</div>
<h2>${event.title}</h2>
<div class="event-meta-row">
<span class="event-tag">${event.category}</span>
${dietTags}
${specsChips}
</div>
</div>
<div class="event-side${isFull ? ' event-side-full' : ''}">
${isRegistrationClosed ? '' : `<span class="button-plaetze${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);
// Abmeldung: Benutzer vom Event entfernen und Snackbar anzeigen.
if (action === 'unregister') {
idSet.delete(Number(event.id));
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);
}
}
// Anmeldung: Benutzer zum Event hinzufügen und Snackbar anzeigen.
if (action === 'register' && !isFull && !isRegistrationClosed) {
idSet.add(Number(event.id));
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);
}
}
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);
// Make calendar icon clickable to focus the date input
const calendarIcon = document.querySelector('.calendar-icon');
if (calendarIcon) {
calendarIcon.addEventListener('click', () => {
dateFilter.focus();
dateFilter.click();
});
}
}
// Kick off initial load/render cycle.
fetchEvents();
});