Social_Cooking/js/event_detail.js

540 lines
26 KiB
JavaScript

document.addEventListener('DOMContentLoaded', async () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const detailcontainer = document.getElementById('detail-view');
const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg';
const currentUser = getCurrentUser();
const params = new URLSearchParams(window.location.search);
const eventId = parseInt(params.get('id'));
if (!eventId) {
window.location.href = 'event_overview.html';
return;
}
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 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;
}
}
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();
const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || 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 getParticipantNameForViewer(name, canSeeLastName) {
const rawName = String(name || '').trim();
if (!rawName) return '';
if (canSeeLastName) return rawName;
if (rawName.includes('@')) return rawName.split('@')[0].trim() || rawName;
return rawName.split(/\s+/)[0];
}
function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
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, month, 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);
}
function isRegistrationClosedForEvent(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const msUntilStart = eventDateTime.getTime() - Date.now();
return msUntilStart <= 24 * 60 * 60 * 1000;
}
function getDeregistrationInfo(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return { daysLeft: null, isClosed: false };
const oneDayMs = 24 * 60 * 60 * 1000;
const msUntilDeadline = (eventDateTime.getTime() - oneDayMs) - Date.now();
if (msUntilDeadline <= 0) return { daysLeft: 0, isClosed: true };
return { daysLeft: Math.ceil(msUntilDeadline / oneDayMs), isClosed: false };
}
function isAddressVisibleWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const msUntilStart = eventDateTime.getTime() - Date.now();
return msUntilStart >= 0 && msUntilStart <= 24 * 60 * 60 * 1000;
}
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 isUserListedInEventParticipants(event, user) {
if (!event || !user || !Array.isArray(event.participants)) return false;
const participantSet = new Set(
event.participants.map(name => String(name || '').trim().toLowerCase()).filter(Boolean)
);
const userFirstName = String(user.vorname || '').trim().toLowerCase();
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
return Boolean(
(userFirstName && participantSet.has(userFirstName))
|| (userFullName && participantSet.has(userFullName))
);
}
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 monthLabel = labels[match[2]];
return monthLabel ? `${Number(match[1])}. ${monthLabel} ${match[3]}` : dateString;
}
function formatEventTime(timeString) {
return timeString.replace('UHR', 'Uhr').trim();
}
function getDietLabel(diet) {
const labels = { FLEISCH:'Fleisch', FISCH:'Fisch', VEGGIE:'Vegetarisch', VEGAN:'Vegan' };
return labels[diet] || diet;
}
function getPlaceholderImageByEventType(event) {
const rawType = String(event?.eventType || event?.category || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[+&/_-]/g, ' ')
.replace(/\s+/g, ' ');
if (rawType.includes('brunch')) {
return 'assets/platzhalter_brunch.jpeg';
}
if (rawType.includes('lunch')) {
return 'assets/platzhalter_lunch.jpeg';
}
if (
rawType.includes('kaffee')
|| rawType.includes('coffee')
|| rawType.includes('cafe')
|| rawType.includes('kuchen')
) {
return 'assets/platzhalter_kaffee.jpeg';
}
if (rawType.includes('dinner')) {
return 'assets/platzhalter_dinner.jpeg';
}
return 'assets/platzhalter_dinner.jpeg';
}
// Fetch data source and resolve the matching event record.
try {
const response = await fetch('data/events.json');
const apiEvents = await response.json();
const allEvents = [...getStoredEvents(), ...apiEvents];
const event = allEvents.find(e => e.id === eventId);
if (event) {
renderDetailPage(event);
} else {
detailcontainer.innerHTML = "<h1>Event wurde nicht gefunden.</h1><a href='event_overview.html'>Zurück zur Übersicht</a>";
}
} catch (error) {
console.error("Fehler beim Laden der Details:", error);
}
function renderDetailPage(event) {
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
const eventCategory = event.category || 'EVENT';
const hostName = event.host?.name || 'Host';
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 Kürze bekannt gegeben.'];
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
? event.specifications : [];
const registrationMap = getRegistrationMap();
const participants = getResolvedParticipants(event, registrationMap);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const participantNamesForView = participants
.map(name => getParticipantNameForViewer(name, isOwnEvent))
.filter(Boolean);
const galleryImages = Array.isArray(event.gallery) ? event.gallery.filter(Boolean) : [];
const resolvedGalleryImages = galleryImages.length > 0
? galleryImages
: [getPlaceholderImageByEventType(event)];
const galleryLayoutClass = resolvedGalleryImages.length === 1
? 'detail-gallery detail-gallery-large detail-gallery-large--single'
: 'detail-gallery detail-gallery-large';
const galleryMarkup = resolvedGalleryImages.length > 0
? `<div class="${galleryLayoutClass}">
${resolvedGalleryImages.slice(0, 9).map((img, index) => `
<button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image">
</button>
`).join('')}
</div>` : '';
const visibleParticipants = participantNamesForView.slice(0, 6);
const remainingParticipants = Math.max(0, participantNamesForView.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 isRegistrationClosed = isRegistrationClosedForEvent(event);
const deregInfo = getDeregistrationInfo(event);
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? registrationMap[currentUser.email].map(id => Number(id)) : [];
const isRegistered = userRegistrations.includes(Number(event.id));
const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
const hasAddressAccess = isRegistered || isListedParticipant;
const actionButtonLabel = isOwnEvent ? 'Dein Event!'
: !currentUser ? 'Einloggen'
: isRegistered ? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden')
: isRegistrationClosed ? 'Anmeldung geschlossen'
: 'Anmelden';
const actionButtonDisabled = isOwnEvent
|| (!isRegistered && (isFull || isRegistrationClosed))
|| (isRegistered && deregInfo.isClosed);
const actionButtonVariantClass = isOwnEvent ? ' button-primary-eigener-event'
: isRegistered ? ' button-primary-abmelden '
: ' button-primary ';
const shouldRevealAddress = Boolean(event.address) && isAddressVisibleWindow(event) && hasAddressAccess;
const addressPanelMarkup = shouldRevealAddress
? `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${event.address}</p></article>`
: `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.</p></article>`;
const detailChips = [
`<span class="event-tag">${eventCategory}</span>`,
...event.diet.split(', ').filter(d => d.trim() && d !== 'Keine Angabe').map(d => `<span class="event-tag">${getDietLabel(d.trim())}</span>`),
...specifications.map(item => `<span class="event-tag">${item}</span>`)
].join('');
detailcontainer.innerHTML = `
<section class="detail-hero">
<div class="detail-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"><img src="${gastIconPath}" class="icon" alt="">${confirmedGuests}/${totalGuests}</span>
</div>
<h1 class="detail-title">${event.title}</h1>
<div class="event-meta-row detail-chip-row">${detailChips}</div>
</section>
<section class="detail-content-grid">
<div class="detail-side-stack">
<article class="detail-panel">
<header class="host-header">
<span class="host-role">Host</span>
<span class="host-name">${hostName}</span>
</header>
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
</article>
<article class="detail-panel">
<h2 class="detail-section-title">Menu</h2>
<ul class="detail-menu-list">
${menuItems.map(item => `<li>${item}</li>`).join('')}
</ul>
</article>
<article class="detail-panel">
<div class="detail-participants-head">
<h2 class="detail-section-title">Teilnehmer</h2>
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
</div>
<div class="detail-avatar-row" data-participants-row>
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
</div>
<div class="detail-participants-full hidden" data-participants-full>
${participantNamesForView.map(name => `
<div class="detail-participant-item">
<span class="participant-name">${name}</span>
</div>
`).join('')}
</div>
</article>
${addressPanelMarkup}
</div>
${galleryMarkup}
</section>
<section class="detail-action-bar">
<div class="detail-action-summary">
<small class="detail-action-meta">
<span class="event-location detail-action-location"><img src="${locationIconPath}" alt="">${event.location}</span>
<span class="event-date-time detail-action-location"><img src="${calendarIconPath}" alt=""> ${displayDate} | ${displayTime}</span>
<span class="event-gast detail-action-location"><img src="${gastIconPath}" alt="">${confirmedGuests}/${totalGuests}</span>
</small>
<strong>${event.title}</strong>
</div>
<div class="detail-action-btn-wrap">
${isFull ? `
<button class="detail-primary-btn detail-spots-pill-full" type="button" disabled>Ausgebucht</button>
` : `
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
</button>
`}
${isRegistered && deregInfo.daysLeft !== null ? `
<small class="detail-dereg-hint${deregInfo.isClosed ? ' detail-dereg-hint--closed' : ''}">
${deregInfo.isClosed ? 'Abmeldefrist abgelaufen'
: deregInfo.daysLeft === 1 ? 'Noch 1 Tag zur Abmeldung'
: `Noch ${deregInfo.daysLeft} Tage zur Abmeldung`}
</small>
` : ''}
</div>
</section>
<div class="detail-lightbox" aria-hidden="true">
<div class="detail-lightbox-backdrop" data-close-lightbox="true"></div>
<figure class="detail-lightbox-content" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="detail-lightbox-close" type="button" aria-label="Schliessen">&times;</button>
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
</figure>
</div>
`;
// DOM references after render
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');
const registerButton = detailcontainer.querySelector('[data-register-button]');
// Eigene Events immer deaktiviert
if (registerButton && isOwnEvent) {
registerButton.disabled = true;
registerButton.textContent = 'Dein Event!';
registerButton.setAttribute('aria-disabled', 'true');
}
// Anmeldung / Abmeldung mit Bestätigungs-Modal
if (registerButton) {
registerButton.addEventListener('click', () => {
if (isOwnEvent) return;
if (!currentUser || !currentUser.email) {
window.location.href = 'login.html';
return;
}
const alreadyRegistered = (() => {
const map = getRegistrationMap();
const ids = Array.isArray(map[currentUser.email])
? map[currentUser.email].map(id => Number(id)) : [];
return ids.includes(Number(event.id));
})();
if (alreadyRegistered) {
const modal = document.getElementById('unregister-confirm-modal');
if (modal) modal.classList.add('show');
document.getElementById('confirm-unregister-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.delete(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
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);
}
renderDetailPage(event);
};
document.getElementById('unregister-modal-close').onclick = () => modal.classList.remove('show');
document.getElementById('unregister-modal-cancel').onclick = () => modal.classList.remove('show');
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('show'); });
} else if (!isFull && !isRegistrationClosed) {
const modal = document.getElementById('register-confirm-modal');
if (modal) modal.classList.add('show');
document.getElementById('confirm-register-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.add(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
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);
}
renderDetailPage(event);
};
document.getElementById('register-modal-close').onclick = () => modal.classList.remove('show');
document.getElementById('register-modal-cancel').onclick = () => modal.classList.remove('show');
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('show'); });
}
});
}
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen
const showAllBtn = detailcontainer.querySelector('[data-show-all-participants]');
const avatarRow = detailcontainer.querySelector('[data-participants-row]');
const fullList = detailcontainer.querySelector('[data-participants-full]');
if (showAllBtn && avatarRow && fullList) {
showAllBtn.addEventListener('click', () => {
const isExpanded = !fullList.classList.contains('hidden');
fullList.classList.toggle('hidden');
avatarRow.classList.toggle('hidden');
showAllBtn.textContent = isExpanded ? 'Alle ansehen' : 'Weniger anzeigen';
});
}
// Lightbox
function closeLightbox() {
if (!lightbox) return;
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
}
if (lightbox && lightboxImage) {
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');
});
});
lightbox.addEventListener('click', event => {
const target = event.target;
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
closeLightbox();
}
});
lightboxClose?.addEventListener('click', closeLightbox);
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeLightbox();
});
}
}
});