273 lines
12 KiB
JavaScript
273 lines
12 KiB
JavaScript
document.addEventListener('DOMContentLoaded', async () => {
|
|
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
|
|
// -------------------------------------------------------------
|
|
// DOM entry point and shared asset path.
|
|
// -------------------------------------------------------------
|
|
const detailContainer = document.getElementById('detail-view');
|
|
const locationIconPath = 'assets/location-pin.svg';
|
|
|
|
// Read event id from query string (detail page deep-link support).
|
|
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 [];
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Format localized date token into full readable date.
|
|
function formatEventDate(dateString) {
|
|
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 casing for UI consistency.
|
|
function formatEventTime(timeString) {
|
|
return timeString.replace('UHR', 'Uhr').trim();
|
|
}
|
|
|
|
// Map diet keys to readable labels while keeping unknown values untouched.
|
|
function getDietLabel(diet) {
|
|
const labels = {
|
|
VEGGIE: 'Vegetarisch',
|
|
VEGAN: 'Vegan',
|
|
FLEISCH: 'Fleisch',
|
|
FISCH: 'Fisch'
|
|
};
|
|
|
|
return labels[diet] || diet;
|
|
}
|
|
|
|
// Compose and inject the full detail UI for a single event.
|
|
function renderDetailPage(event) {
|
|
// Core display values and resilient fallbacks for optional data fields.
|
|
const displayDate = formatEventDate(event.date);
|
|
const displayTime = formatEventTime(event.time);
|
|
const dietLabel = getDietLabel(event.diet);
|
|
const eventCategory = event.category || 'EVENT';
|
|
const hostName = event.host?.name || 'Host';
|
|
const hostInitial = (event.host?.initial || hostName.charAt(0) || 'H').charAt(0).toUpperCase();
|
|
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 Kuerze bekannt gegeben.'];
|
|
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
|
|
? event.specifications
|
|
: [];
|
|
const participants = Array.isArray(event.participants) ? event.participants : [];
|
|
const galleryImages = Array.isArray(event.gallery) && event.gallery.length > 0
|
|
? event.gallery
|
|
: [event.image, event.image, event.image];
|
|
const visibleParticipants = participants.slice(0, 6);
|
|
const remainingParticipants = Math.max(0, participants.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 detailChips = [
|
|
`<span class="event-tag">${eventCategory}</span>`,
|
|
`<span class="event-tag">${dietLabel}</span>`,
|
|
...specifications.map(item => `<span class="event-tag">${item}</span>`)
|
|
].join('');
|
|
|
|
// Render complete detail page layout including:
|
|
// hero metadata, host card, menu, participants, gallery and sticky action bar.
|
|
detailContainer.innerHTML = `
|
|
<div class="detail-page">
|
|
<a class="detail-back" href="event_overview.html">
|
|
<span aria-hidden="true">‹</span>
|
|
Alle Events
|
|
</a>
|
|
|
|
<section class="detail-hero">
|
|
<div class="detail-top-row">
|
|
<span class="event-location">
|
|
<img src="${locationIconPath}" alt="">
|
|
${event.location}
|
|
</span>
|
|
<p class="event-date-time">${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</p>
|
|
</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="host-card detail-panel">
|
|
<header class="host-header">
|
|
<span class="host-avatar">${hostInitial}</span>
|
|
<span class="host-name">${hostName}</span>
|
|
<span class="host-role">Host</span>
|
|
</header>
|
|
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
|
|
</article>
|
|
|
|
<article class="detail-panel detail-panel-compact">
|
|
<h2 class="detail-section-title">Menue</h2>
|
|
<ul class="detail-menu-list">
|
|
${menuItems.map(item => `<li>${item}</li>`).join('')}
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="detail-panel detail-panel-compact">
|
|
<div class="detail-participants-head">
|
|
<h2 class="detail-section-title">Teilnehmer</h2>
|
|
<a href="#" class="detail-participants-link">Alle ansehen</a>
|
|
</div>
|
|
<div class="detail-avatar-row">
|
|
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
|
|
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="detail-gallery detail-gallery-large">
|
|
${galleryImages.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>
|
|
</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="detail-action-meta-text">| ${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</span>
|
|
</small>
|
|
<strong>${event.title}</strong>
|
|
</div>
|
|
|
|
<div class="detail-action-buttons">
|
|
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
|
|
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
|
|
</span>
|
|
<button class="detail-primary-btn" type="button" ${isFull ? 'disabled' : ''}>
|
|
Anmelden
|
|
</button>
|
|
</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">×</button>
|
|
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
|
|
</figure>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// ---------------------------------------------------------
|
|
// Lightbox behavior for gallery images:
|
|
// open on image click, close via backdrop, close button or ESC.
|
|
// ---------------------------------------------------------
|
|
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');
|
|
|
|
// Central close helper to keep all close paths consistent.
|
|
function closeLightbox() {
|
|
if (!lightbox) {
|
|
return;
|
|
}
|
|
|
|
lightbox.classList.remove('is-open');
|
|
lightbox.setAttribute('aria-hidden', 'true');
|
|
}
|
|
|
|
if (lightbox && lightboxImage) {
|
|
// Open with selected image source.
|
|
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');
|
|
});
|
|
});
|
|
|
|
// Close when user clicks on backdrop.
|
|
lightbox.addEventListener('click', event => {
|
|
const target = event.target;
|
|
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
|
|
closeLightbox();
|
|
}
|
|
});
|
|
|
|
// Close via dedicated icon/button.
|
|
lightboxClose?.addEventListener('click', closeLightbox);
|
|
|
|
// Close with keyboard for accessibility.
|
|
document.addEventListener('keydown', event => {
|
|
if (event.key === 'Escape') {
|
|
closeLightbox();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|