This commit is contained in:
Ysabelle Moser 2026-04-23 11:50:27 +02:00
commit 61d84022cc
6 changed files with 469 additions and 436 deletions

View File

@ -233,6 +233,52 @@
object-fit: cover; object-fit: cover;
} }
/* =========================================
NEW INSTAGRAM HOVER STYLES START HERE
========================================= */
.ig-post-wrapper {
position: relative;
width: 100%;
height: 100%; /* Ensures it fills the existing gallery__item */
aspect-ratio: 1 / 1;
overflow: hidden;
cursor: pointer;
}
.ig-post-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.ig-overlay {
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-weight: 600;
font-size: 1.1rem;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.ig-post-wrapper:hover .ig-overlay {
opacity: 1;
}
.ig-overlay span {
display: flex;
align-items: center;
gap: 8px;
}
.gallery__arrow { .gallery__arrow {
position: absolute; position: absolute;
display: grid; display: grid;

View File

@ -38,6 +38,38 @@
<!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox --> <!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox -->
<script src="js/event_detail.js"></script> <script src="js/event_detail.js"></script>
<div id="register-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Ein Platz für dich am Tisch!</h2>
<button type="button" class="modal-close" id="register-modal-close" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
</div>
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
<button class="button-primary button--outline" type="button" id="register-modal-cancel">Abbrechen</button>
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
<button type="button" class="modal-close" id="unregister-modal-close" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
</div>
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
<button class="button-primary button--outline" type="button" id="unregister-modal-cancel">Abbrechen</button>
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Ja, abmelden</button>
</div>
</div>
</div>
<!-- Snackbar: Feedback bei An-/Abmeldung --> <!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div> <div class="snackbar" id="snackbar"></div>

View File

@ -110,6 +110,38 @@
</div> </div>
</div> </div>
<div id="register-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Ein Platz für dich am Tisch!</h2>
<button type="button" class="modal-close" onclick="closeRegisterModal()" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
</div>
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
<button class="button-primary button--outline" type="button" onclick="closeRegisterModal()">Abbrechen</button>
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
<button type="button" class="modal-close" onclick="closeUnregisterModal()" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
</div>
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
<button class="button-primary button--outline" type="button" onclick="closeUnregisterModal()">Abbrechen</button>
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Ja, abmelden</button>
</div>
</div>
</div>
<!-- Snackbar: Feedback bei An-/Abmeldung --> <!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div> <div class="snackbar" id="snackbar"></div>

View File

@ -82,7 +82,7 @@
<!-- Main Content: uses .gallery, .gallery__carousel, .gallery__track, .gallery__item, and .gallery__info to present event carousel content --> <!-- Main Content: uses .gallery, .gallery__carousel, .gallery__track, .gallery__item, and .gallery__info to present event carousel content -->
<section class="gallery" aria-label="Bildergalerie" aria-roledescription="Karussell"> <section class="gallery" aria-label="Bildergalerie" aria-roledescription="Karussell">
<h2>Einblick in Cooking-Erlebnisse</h2> <div class="gallery__header"> </div><h2>Einblick in Cooking-Erlebnisse</h2>
<p>#gemeinsam_invité auf Instagram</p> <p>#gemeinsam_invité auf Instagram</p>
</div> </div>
<div class="gallery__carousel"> <div class="gallery__carousel">
@ -91,44 +91,129 @@
</button> </button>
<div class="gallery__track" aria-live="polite"> <div class="gallery__track" aria-live="polite">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 1 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 1 von 12">
<img src="assets/index_Red checkered social eating.jpg" alt="Red checkered social eating"> <div class="ig-post-wrapper">
<img src="assets/index_Red checkered social eating.jpg" alt="Red checkered social eating">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 142</span>
<span><i class="fa-solid fa-comment"></i> 18</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 2 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 2 von 12">
<img src="assets/index_Pasta and many forks.jpg" alt="Pasta and many forks"> <div class="ig-post-wrapper">
<img src="assets/index_Pasta and many forks.jpg" alt="Pasta and many forks">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 89</span>
<span><i class="fa-solid fa-comment"></i> 5</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 3 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 3 von 12">
<img src="assets/index_Zoomed in asian eating.jpg" alt="Zoomed in asian eating"> <div class="ig-post-wrapper">
<img src="assets/index_Zoomed in asian eating.jpg" alt="Zoomed in asian eating">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 215</span>
<span><i class="fa-solid fa-comment"></i> 32</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 4 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 4 von 12">
<img src="assets/index_Burger eating together.jpg" alt="Burger eating together"> <div class="ig-post-wrapper">
<img src="assets/index_Burger eating together.jpg" alt="Burger eating together">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 304</span>
<span><i class="fa-solid fa-comment"></i> 41</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 5 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 5 von 12">
<img src="assets/index_Cake cutting figs.jpg" alt="Cake cutting figs"> <div class="ig-post-wrapper">
<img src="assets/index_Cake cutting figs.jpg" alt="Cake cutting figs">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 178</span>
<span><i class="fa-solid fa-comment"></i> 12</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 6 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 6 von 12">
<img src="assets/index_Cooking woman at home.jpg" alt="Cooking woman at home"> <div class="ig-post-wrapper">
<img src="assets/index_Cooking woman at home.jpg" alt="Cooking woman at home">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 95</span>
<span><i class="fa-solid fa-comment"></i> 8</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 7 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 7 von 12">
<img src="assets/index_Eating and laughing girls.jpg" alt="Eating and laughing girls"> <div class="ig-post-wrapper">
<img src="assets/index_Eating and laughing girls.jpg" alt="Eating and laughing girls">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 420</span>
<span><i class="fa-solid fa-comment"></i> 55</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 8 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 8 von 12">
<img src="assets/index_Pasta in cheese.jpg" alt="Pasta in cheese"> <div class="ig-post-wrapper">
<img src="assets/index_Pasta in cheese.jpg" alt="Pasta in cheese">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 267</span>
<span><i class="fa-solid fa-comment"></i> 29</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 9 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 9 von 12">
<img src="assets/index_Salad roommates.jpg" alt="Salad roommates"> <div class="ig-post-wrapper">
<img src="assets/index_Salad roommates.jpg" alt="Salad roommates">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 112</span>
<span><i class="fa-solid fa-comment"></i> 4</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 10 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 10 von 12">
<img src="assets/index_Sharing food table.jpg" alt="Sharing food table"> <div class="ig-post-wrapper">
<img src="assets/index_Sharing food table.jpg" alt="Sharing food table">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 389</span>
<span><i class="fa-solid fa-comment"></i> 47</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 11 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 11 von 12">
<img src="assets/index_Spicy food zoomed.jpg" alt="Spicy food zoomed"> <div class="ig-post-wrapper">
<img src="assets/index_Spicy food zoomed.jpg" alt="Spicy food zoomed">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 156</span>
<span><i class="fa-solid fa-comment"></i> 11</span>
</div>
</div>
</article> </article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 12 von 12"> <article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 12 von 12">
<img src="assets/index_cooking.jpg" alt="Cooking"> <div class="ig-post-wrapper">
<img src="assets/index_cooking.jpg" alt="Cooking">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 234</span>
<span><i class="fa-solid fa-comment"></i> 21</span>
</div>
</div>
</article> </article>
</div> </div>
<button type="button" class="gallery__arrow gallery__arrow--next" aria-label="Nächstes Bild"> <button type="button" class="gallery__arrow gallery__arrow--next" aria-label="Nächstes Bild">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</button> </button>

View File

@ -3,16 +3,13 @@
const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers'; const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// -------------------------------------------------------------
// DOM entry point and shared asset path.
// -------------------------------------------------------------
const detailcontainer = document.getElementById('detail-view'); const detailcontainer = document.getElementById('detail-view');
const locationIconPath = 'assets/icon_location.svg'; const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg'; const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg'; const gastIconPath = 'assets/icon_gast.svg';
const currentUser = getCurrentUser(); const currentUser = getCurrentUser();
// Read event id from query string (detail page deep-link support).
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const eventId = parseInt(params.get('id')); const eventId = parseInt(params.get('id'));
@ -62,14 +59,10 @@
} }
function getUserDisplayName(user) { function getUserDisplayName(user) {
if (!user) { if (!user) return '';
return '';
}
const firstName = String(user.vorname || '').trim(); const firstName = String(user.vorname || '').trim();
const lastName = String(user.nachname || '').trim(); const lastName = String(user.nachname || '').trim();
const fullName = `${firstName} ${lastName}`.trim(); const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || String(user.email || '').trim()).trim(); return (fullName || firstName || String(user.email || '').trim()).trim();
} }
@ -85,10 +78,7 @@
Object.entries(registrationMap || {}).forEach(([email, ids]) => { Object.entries(registrationMap || {}).forEach(([email, ids]) => {
const isRegisteredForEvent = Array.isArray(ids) const isRegisteredForEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(event.id)); && ids.map(id => Number(id)).includes(Number(event.id));
if (!isRegisteredForEvent) return;
if (!isRegisteredForEvent) {
return;
}
const user = usersByEmail.get(String(email || '').trim().toLowerCase()); const user = usersByEmail.get(String(email || '').trim().toLowerCase());
const displayName = getUserDisplayName(user) || String(email || '').trim(); const displayName = getUserDisplayName(user) || String(email || '').trim();
@ -105,19 +95,9 @@
function getParticipantNameForViewer(name, canSeeLastName) { function getParticipantNameForViewer(name, canSeeLastName) {
const rawName = String(name || '').trim(); const rawName = String(name || '').trim();
if (!rawName) { if (!rawName) return '';
return ''; if (canSeeLastName) return rawName;
} if (rawName.includes('@')) return rawName.split('@')[0].trim() || rawName;
if (canSeeLastName) {
return rawName;
}
// Bei E-Mail-Fallback nur den lokalen Teil anzeigen.
if (rawName.includes('@')) {
return rawName.split('@')[0].trim() || rawName;
}
return rawName.split(/\s+/)[0]; return rawName.split(/\s+/)[0];
} }
@ -126,153 +106,98 @@
} }
function parseEventDateTime(event) { function parseEventDateTime(event) {
if (!event?.date) { if (!event?.date) return null;
return null;
}
const dateValue = String(event.date).trim(); const dateValue = String(event.date).trim();
const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/); const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let year; let year, month, day;
let month;
let day;
if (isoDateMatch) { if (isoDateMatch) {
year = Number(isoDateMatch[1]); year = Number(isoDateMatch[1]);
month = Number(isoDateMatch[2]); month = Number(isoDateMatch[2]);
day = Number(isoDateMatch[3]); day = Number(isoDateMatch[3]);
} else { } else {
const monthMap = { 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 };
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})$/); const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (!localizedMatch) return null;
if (!localizedMatch) {
return null;
}
day = Number(localizedMatch[1]); day = Number(localizedMatch[1]);
month = monthMap[localizedMatch[2]]; month = monthMap[localizedMatch[2]];
year = Number(localizedMatch[3]); year = Number(localizedMatch[3]);
if (!month) return null;
if (!month) {
return null;
}
} }
const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/); const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? Number(timeMatch[1]) : 0; const hours = timeMatch ? Number(timeMatch[1]) : 0;
const minutes = timeMatch ? Number(timeMatch[2]) : 0; const minutes = timeMatch ? Number(timeMatch[2]) : 0;
return new Date(year, month - 1, day, hours, minutes, 0, 0); return new Date(year, month - 1, day, hours, minutes, 0, 0);
} }
function isRegistrationClosedForEvent(event) { function isRegistrationClosedForEvent(event) {
const eventDateTime = parseEventDateTime(event); const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
return false;
}
const msUntilStart = eventDateTime.getTime() - Date.now(); const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 24 * 60 * 60 * 1000; return msUntilStart <= 24 * 60 * 60 * 1000;
return msUntilStart <= twentyfourHoursInMs;
} }
// Abmeldefrist: 1 Tag (24 h) vor Eventstart.
function getDeregistrationInfo(event) { function getDeregistrationInfo(event) {
const eventDateTime = parseEventDateTime(event); const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return { daysLeft: null, isClosed: false };
return { daysLeft: null, isClosed: false };
}
const oneDayMs = 24 * 60 * 60 * 1000; const oneDayMs = 24 * 60 * 60 * 1000;
const deadlineMs = eventDateTime.getTime() - oneDayMs; const msUntilDeadline = (eventDateTime.getTime() - oneDayMs) - Date.now();
const msUntilDeadline = deadlineMs - Date.now(); if (msUntilDeadline <= 0) return { daysLeft: 0, isClosed: true };
return { daysLeft: Math.ceil(msUntilDeadline / oneDayMs), isClosed: false };
if (msUntilDeadline <= 0) {
return { daysLeft: 0, isClosed: true };
}
const daysLeft = Math.ceil(msUntilDeadline / oneDayMs);
return { daysLeft, isClosed: false };
} }
// Adresse ist nur im 24h-Fenster VOR Eventstart sichtbar.
function isAddressVisibleWindow(event) { function isAddressVisibleWindow(event) {
const eventDateTime = parseEventDateTime(event); const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
return false;
}
const msUntilStart = eventDateTime.getTime() - Date.now(); const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 24 * 60 * 60 * 1000; return msUntilStart >= 0 && msUntilStart <= 24 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
} }
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);
}
// Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde.
function isEventOwnedByCurrentUser(event, user) { function isEventOwnedByCurrentUser(event, user) {
if (!event || !user) { if (!event || !user) return false;
return false;
}
const userEmail = String(user.email || '').trim().toLowerCase(); const userEmail = String(user.email || '').trim().toLowerCase();
const hostEmail = String(event.hostEmail || '').trim().toLowerCase(); const hostEmail = String(event.hostEmail || '').trim().toLowerCase();
if (userEmail && hostEmail) return userEmail === hostEmail;
if (userEmail && hostEmail) {
return userEmail === hostEmail;
}
// Fallback für ältere Datensätze ohne hostEmail.
const userFirstName = String(user.vorname || '').trim().toLowerCase(); const userFirstName = String(user.vorname || '').trim().toLowerCase();
const hostName = String(event.host?.name || '').trim().toLowerCase(); const hostName = String(event.host?.name || '').trim().toLowerCase();
return Boolean(userFirstName && hostName && userFirstName === hostName); return Boolean(userFirstName && hostName && userFirstName === hostName);
} }
// Prüft, ob der aktuelle Benutzer bereits in der Teilnehmerliste des Events steht.
function isUserListedInEventParticipants(event, user) { function isUserListedInEventParticipants(event, user) {
if (!event || !user || !Array.isArray(event.participants)) { if (!event || !user || !Array.isArray(event.participants)) return false;
return false;
}
const participantSet = new Set( const participantSet = new Set(
event.participants event.participants.map(name => String(name || '').trim().toLowerCase()).filter(Boolean)
.map(name => String(name || '').trim().toLowerCase())
.filter(Boolean)
); );
const userFirstName = String(user.vorname || '').trim().toLowerCase(); const userFirstName = String(user.vorname || '').trim().toLowerCase();
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}` const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
.trim()
.toLowerCase();
return Boolean( return Boolean(
(userFirstName && participantSet.has(userFirstName)) (userFirstName && participantSet.has(userFirstName))
|| (userFullName && participantSet.has(userFullName)) || (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;
}
// Fetch data source and resolve the matching event record. // Fetch data source and resolve the matching event record.
try { try {
const response = await fetch('data/events.json'); const response = await fetch('data/events.json');
@ -289,62 +214,11 @@
console.error("Fehler beim Laden der Details:", 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 = {
FLEISCH: 'Fleisch',
FISCH: 'Fisch',
VEGGIE: 'Vegetarisch',
VEGAN: 'Vegan'
};
return labels[diet] || diet;
}
// Compose and inject the full detail UI for a single event.
function renderDetailPage(event) { function renderDetailPage(event) {
// Core display values and resilient fallbacks for optional data fields.
const displayDate = formatEventDate(event.date); const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time); const displayTime = formatEventTime(event.time);
const dietLabel = getDietLabel(event.diet);
const eventCategory = event.category || 'EVENT'; const eventCategory = event.category || 'EVENT';
const hostName = event.host?.name || 'Host'; 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 const hostMessage = Array.isArray(event.hostMessage) && event.hostMessage.length > 0
? event.hostMessage ? event.hostMessage
: ['Der Host hat für dieses Event noch keine Nachricht hinterlegt.']; : ['Der Host hat für dieses Event noch keine Nachricht hinterlegt.'];
@ -352,28 +226,23 @@
? event.menu ? event.menu
: ['Menü wird in Kürze bekannt gegeben.']; : ['Menü wird in Kürze bekannt gegeben.'];
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0 const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
? event.specifications ? event.specifications : [];
: [];
const registrationMap = getRegistrationMap(); const registrationMap = getRegistrationMap();
const participants = getResolvedParticipants(event, registrationMap); const participants = getResolvedParticipants(event, registrationMap);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const participantNamesForView = participants const participantNamesForView = participants
.map(name => getParticipantNameForViewer(name, isOwnEvent)) .map(name => getParticipantNameForViewer(name, isOwnEvent))
.filter(Boolean); .filter(Boolean);
const galleryImages = Array.isArray(event.gallery) const galleryImages = Array.isArray(event.gallery) ? event.gallery.filter(Boolean) : [];
? event.gallery.filter(Boolean)
: [];
const galleryMarkup = galleryImages.length > 0 const galleryMarkup = galleryImages.length > 0
? ` ? `<div class="detail-gallery detail-gallery-large">
<div class="detail-gallery detail-gallery-large"> ${galleryImages.slice(0, 9).map((img, index) => `
${galleryImages.slice(0, 9).map((img, index) => ` <button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
<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">
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image"> </button>
</button> `).join('')}
`).join('')} </div>` : '';
</div>
`
: '';
const visibleParticipants = participantNamesForView.slice(0, 6); const visibleParticipants = participantNamesForView.slice(0, 6);
const remainingParticipants = Math.max(0, participantNamesForView.length - visibleParticipants.length); const remainingParticipants = Math.max(0, participantNamesForView.length - visibleParticipants.length);
const totalGuests = Number.isFinite(event.spots) ? event.spots : 0; const totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
@ -383,245 +252,207 @@
const isRegistrationClosed = isRegistrationClosedForEvent(event); const isRegistrationClosed = isRegistrationClosedForEvent(event);
const deregInfo = getDeregistrationInfo(event); const deregInfo = getDeregistrationInfo(event);
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? registrationMap[currentUser.email].map(id => Number(id)) ? registrationMap[currentUser.email].map(id => Number(id)) : [];
: [];
const isRegistered = userRegistrations.includes(Number(event.id)); const isRegistered = userRegistrations.includes(Number(event.id));
const isListedParticipant = isUserListedInEventParticipants(event, currentUser); const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
const hasAddressAccess = isRegistered || isListedParticipant; const hasAddressAccess = isRegistered || isListedParticipant;
const actionButtonLabel = isOwnEvent
? 'Dein Event!' const actionButtonLabel = isOwnEvent ? 'Dein Event!'
: !currentUser : !currentUser ? 'Einloggen'
? 'Einloggen' : isRegistered ? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden')
: isRegistered : isRegistrationClosed ? 'Anmeldung geschlossen'
? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden') : 'Anmelden';
: isRegistrationClosed
? 'Anmeldung geschlossen'
: 'Anmelden';
const actionButtonDisabled = isOwnEvent const actionButtonDisabled = isOwnEvent
|| (!isRegistered && (isFull || isRegistrationClosed)) || (!isRegistered && (isFull || isRegistrationClosed))
|| (isRegistered && deregInfo.isClosed); || (isRegistered && deregInfo.isClosed);
const actionButtonVariantClass = isOwnEvent const actionButtonVariantClass = isOwnEvent ? ' button-primary-eigener-event'
? ' button-primary-eigener-event' : isRegistered ? ' button-primary-abmelden '
: isRegistered : ' button-primary ';
? ' button-primary-abmelden '
: isRegistrationClosed
? ' button-primary '
: ' button-primary ';
const shouldRevealAddress = Boolean(event.address) && isAddressVisibleWindow(event) && hasAddressAccess; const shouldRevealAddress = Boolean(event.address) && isAddressVisibleWindow(event) && hasAddressAccess;
const addressPanelMarkup = shouldRevealAddress const addressPanelMarkup = shouldRevealAddress
? ` ? `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${event.address}</p></article>`
<article class="detail-panel"> : `<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>`;
<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 = [ const detailChips = [
`<span class="event-tag">${eventCategory}</span>`, `<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>`), ...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>`) ...specifications.map(item => `<span class="event-tag">${item}</span>`)
].join(''); ].join('');
// Render complete detail page layout including:
// hero metadata, host card, menu, participants, gallery and sticky action bar.
detailcontainer.innerHTML = ` detailcontainer.innerHTML = `
<section class="detail-hero">
<section class="detail-hero"> <div class="detail-top-row">
<div class="detail-top-row"> <span class="event-location"><img src="${locationIconPath}" class="icon" alt="">${event.location}</span>
<span class="event-location"> <span class="event-date-time"><img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}</span>
<img src="${locationIconPath}" class="icon" alt="">${event.location} <span class="event-date-time"><img src="${gastIconPath}" class="icon" alt="">${confirmedGuests}/${totalGuests}</span>
</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> </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> </div>
`; `;
// --------------------------------------------------------- // DOM references after render
// Lightbox behavior for gallery images:
// open on image click, close via backdrop, close button or ESC.
// ---------------------------------------------------------
const lightbox = detailcontainer.querySelector('.detail-lightbox'); const lightbox = detailcontainer.querySelector('.detail-lightbox');
const lightboxImage = detailcontainer.querySelector('.detail-lightbox-image'); const lightboxImage = detailcontainer.querySelector('.detail-lightbox-image');
const lightboxClose = detailcontainer.querySelector('.detail-lightbox-close'); const lightboxClose = detailcontainer.querySelector('.detail-lightbox-close');
const galleryButtons = detailcontainer.querySelectorAll('.detail-gallery-item'); const galleryButtons = detailcontainer.querySelectorAll('.detail-gallery-item');
const registerButton = detailcontainer.querySelector('[data-register-button]'); const registerButton = detailcontainer.querySelector('[data-register-button]');
// Harte Absicherung: Eigene Events sind auf der Detailseite immer deaktiviert. // Eigene Events immer deaktiviert
if (registerButton && isOwnEvent) { if (registerButton && isOwnEvent) {
registerButton.disabled = true; registerButton.disabled = true;
registerButton.textContent = 'Dein Event!'; registerButton.textContent = 'Dein Event!';
registerButton.setAttribute('aria-disabled', 'true'); registerButton.setAttribute('aria-disabled', 'true');
} }
// Anmeldung toggeln und im lokalen Registrierungs-Store persistieren. // Anmeldung / Abmeldung mit Bestätigungs-Modal
if (registerButton) { if (registerButton) {
registerButton.addEventListener('click', () => { registerButton.addEventListener('click', () => {
if (isOwnEvent) { if (isOwnEvent) return;
return;
}
if (!currentUser || !currentUser.email) { if (!currentUser || !currentUser.email) {
window.location.href = 'login.html'; window.location.href = 'login.html';
return; return;
} }
const nextRegistrationMap = getRegistrationMap(); const alreadyRegistered = (() => {
const currentList = Array.isArray(nextRegistrationMap[currentUser.email]) const map = getRegistrationMap();
? nextRegistrationMap[currentUser.email].map(id => Number(id)) const ids = Array.isArray(map[currentUser.email])
: []; ? map[currentUser.email].map(id => Number(id)) : [];
const registrationSet = new Set(currentList); return ids.includes(Number(event.id));
})();
if (registrationSet.has(Number(event.id))) { if (alreadyRegistered) {
registrationSet.delete(Number(event.id)); 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'); });
// Snackbar: Feedback bei Abmeldung.
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);
}
} else if (!isFull && !isRegistrationClosed) { } else if (!isFull && !isRegistrationClosed) {
registrationSet.add(Number(event.id)); const modal = document.getElementById('register-confirm-modal');
if (modal) modal.classList.add('show');
// Snackbar: Feedback bei Anmeldung. document.getElementById('confirm-register-btn').onclick = () => {
const snackbar = document.getElementById('snackbar'); modal.classList.remove('show');
if (snackbar) { const map = getRegistrationMap();
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.'; const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
snackbar.classList.add('snackbar--visible'); ids.add(Number(event.id));
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000); 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'); });
} }
nextRegistrationMap[currentUser.email] = Array.from(registrationSet);
setRegistrationMap(nextRegistrationMap);
// Re-Render aktualisiert Buttonzustand und CTA ohne Seitenreload.
renderDetailPage(event);
}); });
} }
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen. // "Alle ansehen": Teilnehmerliste aufklappen / zuklappen
const showAllBtn = detailcontainer.querySelector('[data-show-all-participants]'); const showAllBtn = detailcontainer.querySelector('[data-show-all-participants]');
const avatarRow = detailcontainer.querySelector('[data-participants-row]'); const avatarRow = detailcontainer.querySelector('[data-participants-row]');
const fullList = detailcontainer.querySelector('[data-participants-full]'); const fullList = detailcontainer.querySelector('[data-participants-full]');
@ -635,32 +466,24 @@
}); });
} }
// Central close helper to keep all close paths consistent. // Lightbox
function closeLightbox() { function closeLightbox() {
if (!lightbox) { if (!lightbox) return;
return;
}
lightbox.classList.remove('is-open'); lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true'); lightbox.setAttribute('aria-hidden', 'true');
} }
if (lightbox && lightboxImage) { if (lightbox && lightboxImage) {
// Open with selected image source.
galleryButtons.forEach(button => { galleryButtons.forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const imageSrc = button.getAttribute('data-fullsrc'); const imageSrc = button.getAttribute('data-fullsrc');
if (!imageSrc) { if (!imageSrc) return;
return;
}
lightboxImage.src = imageSrc; lightboxImage.src = imageSrc;
lightbox.classList.add('is-open'); lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false'); lightbox.setAttribute('aria-hidden', 'false');
}); });
}); });
// Close when user clicks on backdrop.
lightbox.addEventListener('click', event => { lightbox.addEventListener('click', event => {
const target = event.target; const target = event.target;
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) { if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
@ -668,22 +491,11 @@
} }
}); });
// Close via dedicated icon/button.
lightboxClose?.addEventListener('click', closeLightbox); lightboxClose?.addEventListener('click', closeLightbox);
// Close with keyboard for accessibility.
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
if (event.key === 'Escape') { if (event.key === 'Escape') closeLightbox();
closeLightbox();
}
});
}
// Back button navigation: returns to event overview page.
if (backButton) {
backButton.addEventListener('click', () => {
window.location.href = 'event_overview.html';
}); });
} }
} }
}); });

View File

@ -536,29 +536,55 @@
: []; : [];
const idSet = new Set(currentIds); const idSet = new Set(currentIds);
// Abmeldung: Benutzer vom Event entfernen und Snackbar anzeigen. // Anmelde-Modal öffnen
if (action === 'unregister') { if (action === 'register' && !isFull && !isRegistrationClosed) {
idSet.delete(Number(event.id)); const modal = document.getElementById('register-confirm-modal');
const snackbar = document.getElementById('snackbar'); if (modal) {
if (snackbar) { modal.classList.add('show');
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.'; document.getElementById('confirm-register-btn').onclick = () => {
snackbar.classList.add('snackbar--danger', 'snackbar--visible'); modal.classList.remove('show');
setTimeout(() => { const map = getRegistrationMap();
snackbar.classList.remove('snackbar--visible'); const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400); ids.add(Number(event.id));
}, 3000); 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);
}
applyFilters();
};
} }
return;
} }
// Anmeldung: Benutzer zum Event hinzufügen und Snackbar anzeigen. // Abmelde-Modal öffnen
if (action === 'register' && !isFull && !isRegistrationClosed) { if (action === 'unregister') {
idSet.add(Number(event.id)); const modal = document.getElementById('unregister-confirm-modal');
const snackbar = document.getElementById('snackbar'); if (modal) {
if (snackbar) { modal.classList.add('show');
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.'; document.getElementById('confirm-unregister-btn').onclick = () => {
snackbar.classList.add('snackbar--visible'); modal.classList.remove('show');
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000); 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);
}
applyFilters();
};
} }
return;
} }
nextRegistrationMap[currentUser.email] = Array.from(idSet); nextRegistrationMap[currentUser.email] = Array.from(idSet);