Social_Cooking/js/event_overview.js

653 lines
26 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 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;
}
}
// 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();
return (firstName || `${firstName} ${lastName}`.trim() || 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;
}
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, 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 => {
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 = `
<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, 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);
// 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-gast"> <img src="${gastIconPath}" class="icon" alt="Gaeste Icon">${bookedSeats}/${totalCapacity} </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: 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
if (activeDiets.has(dietValue)) {
activeDiets.delete(dietValue);
} else {
activeDiets.add(dietValue);
}
} 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();
});
}
}
// 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 hasShownInfoModal = localStorage.getItem(INFO_MODAL_SHOWN_KEY);
if (!hasShownInfoModal) {
infoModal.classList.add('show');
localStorage.setItem(INFO_MODAL_SHOWN_KEY, 'true');
}
}
// Kick off initial load/render cycle.
fetchEvents();
});