Anpassung Layout

This commit is contained in:
Simona Oswald 2026-04-21 22:00:11 +02:00
commit 17c26b1cb5
9 changed files with 605 additions and 42 deletions

View File

@ -243,6 +243,7 @@
background: var(--butter-light); background: var(--butter-light);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
font-family: var(--font-main); font-family: var(--font-main);
display: none;
font-weight: 400; font-weight: 400;
font-size: 1.25rem; font-size: 1.25rem;
place-items: center; place-items: center;
@ -381,3 +382,132 @@
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 400; font-weight: 400;
} }
/* --- FAQ Section: Akkordion --- */
.faq-section {
padding: var(--space-8) var(--space-4);
margin: var(--space-8) 0 var(--space-5);
}
.faq-section h2 {
text-align: center;
margin-bottom: var(--space-5);
color: var(--brown);
}
.faq-accordion {
width: 100%;
max-width: 56rem;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.faq-item {
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--white);
transition: background-color 0.2s ease, box-shadow var(--shadow-interaction);
}
.faq-item:hover {
box-shadow: var(--shadow-interaction);
}
.faq-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: transparent;
border: none;
cursor: pointer;
font-size: 1.25rem;
font-weight: 400;
color: var(--olive);
text-align: left;
transition: background-color 0.2s ease;
font-family: var(--font-main);
}
.faq-trigger:hover {
background-color: var(--butter-light);
}
.faq-trigger:focus-visible {
outline: 2px solid var(--olive);
outline-offset: -2px;
}
.faq-title {
flex: 1;
font-weight: 600;
}
.faq-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
font-size: 1.5rem;
font-weight: 300;
color: var(--olive);
transition: transform 0.3s ease;
flex-shrink: 0;
}
.faq-trigger[aria-expanded="true"] .faq-icon {
transform: rotate(45deg);
}
.faq-content {
padding: 0 var(--space-4);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
font-size: 1.125rem;
line-height: 1.7;
color: var(--black);
font-family: var(--font-main);
}
.faq-content p {
margin: 0;
padding: var(--space-3) 0;
}
.faq-trigger[aria-expanded="true"] + .faq-content {
display: block;
max-height: 500px;
padding: var(--space-3) var(--space-4);
}
/* --- Responsive: FAQ Section --- */
@media (max-width: 768px) {
.faq-section {
padding: var(--space-40) var(--space-4);
margin: var(--space-40) 0 var(--space-5);
}
.faq-trigger {
padding: var(--space-2) var(--space-3);
font-size: 1.125rem;
}
.faq-content {
padding: 0 var(--space-3);
font-size: 1rem;
}
.faq-content p {
padding: var(--space-2) 0;
}
}

View File

@ -250,6 +250,19 @@ label {
} }
} }
/*
Content pages with sticky nav require top padding to avoid overlap.
Used on event_overview, event_detail, and similar pages.
*/
.container.page-content-safe {
padding-top: 6.5rem;
}
/* Detail pages with back button need less top padding. */
.container.page-content-safe.detail-page {
padding-top: 3.5rem;
}
.icon { .icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -460,6 +473,29 @@ label {
border-color: var(--olive-dark); border-color: var(--olive-dark);
} }
/* Butter-colored back button for detail pages. */
.btn-back-to-overview {
display: inline-block;
padding: 0.5rem 1.5rem;
background-color: var(--butter);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.125rem;
color: var(--olive);
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
margin-bottom: var(--space-4);
}
.btn-back-to-overview:hover,
.btn-back-to-overview:focus-visible {
background-color: var(--butter-light);
border-color: var(--olive);
}
.button--outline { .button--outline {
background-color: transparent; background-color: transparent;
@ -648,6 +684,32 @@ label {
gap: 8px; gap: 8px;
} }
/* Info button for event overview page */
.btn-info {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 1.5px solid var(--olive-light);
border-radius: 999px;
background-color: var(--butter);
color: var(--olive);
font-family: var(--font-main);
font-size: 1.5rem;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
flex-shrink: 0;
}
.btn-info:hover,
.btn-info:focus-visible {
background-color: var(--butter-light);
border-color: var(--olive);
}
/* Modal / Popup */ /* Modal / Popup */
.modal { .modal {
display: none; display: none;
@ -712,6 +774,22 @@ label {
justify-content: center; justify-content: center;
} }
.modal-close {
position: absolute;
right: 0;
top: 0;
font-size: 28px;
color: var(--black);
background: none;
border: none;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body { .modal-body {
padding: var(--space-20) var(--space-20) var(--space-4) var(--space-20); padding: var(--space-20) var(--space-20) var(--space-4) var(--space-20);
} }

View File

@ -4,11 +4,11 @@
"title": "Italienische Tavolata", "title": "Italienische Tavolata",
"location": "Luzern", "location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern", "address": "Pilatusstrasse 18, 6003 Luzern",
"date": "11. APR. 2026", "date": "22. APR. 2026",
"time": "15:30 UHR", "time": "15:30 UHR",
"category": "Dinner", "category": "Dinner",
"diet": "Vegetarisch", "diet": "Vegetarisch",
"spots": 6, "spots": 8,
"host": { "host": {
"name": "Ferdinando", "name": "Ferdinando",
"initial": "F" "initial": "F"
@ -45,7 +45,7 @@
"title": "Noche Peruana", "title": "Noche Peruana",
"location": "Chur", "location": "Chur",
"address": "Obere Gasse 41, 7000 Chur", "address": "Obere Gasse 41, 7000 Chur",
"date": "16. Juni 2026", "date": "12. April 2026",
"time": "19:00 UHR", "time": "19:00 UHR",
"category": "Dinner", "category": "Dinner",
"diet": "Omnivore", "diet": "Omnivore",

View File

@ -28,7 +28,11 @@
</header> </header>
<!-- Main content: detail page gets fully injected by JavaScript --> <!-- Main content: detail page gets fully injected by JavaScript -->
<<<<<<< HEAD
<main class="container layout-wide"> <main class="container layout-wide">
=======
<main class="container page-content-safe detail-page">
>>>>>>> e3ac1a11f091bcc9b224ce4ae81743564e160e37
<!-- Render target: loading, error state or full detail layout --> <!-- Render target: loading, error state or full detail layout -->
<div id="detail-view"> <div id="detail-view">
<p>Lädt Event-Details...</p> <p>Lädt Event-Details...</p>

View File

@ -28,10 +28,19 @@
</header> </header>
<!-- Main content: page headline, filter controls and dynamic event list --> <!-- Main content: page headline, filter controls and dynamic event list -->
<<<<<<< HEAD
<main class="container layout-wide"> <main class="container layout-wide">
<!-- Page headline --> <!-- Page headline -->
<p class="badge margin-bottom-40">Event finden</p> <p class="badge margin-bottom-40">Event finden</p>
<h1>Was darf es sein?</h1> <h1>Was darf es sein?</h1>
=======
<main class="container page-content-safe">
<!-- Page headline with info button -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem;">
<h1 style="margin-bottom: 0;">Events</h1>
<button type="button" class="btn-info" id="info-button" aria-label="Information über kostenlose Events">?</button>
</div>
>>>>>>> e3ac1a11f091bcc9b224ce4ae81743564e160e37
<!-- Filter section: category chips + location/date filters --> <!-- Filter section: category chips + location/date filters -->
<section class="filter-section"> <section class="filter-section">
@ -64,9 +73,14 @@
</div> </div>
</div> </div>
<<<<<<< HEAD
<p class="filter-label">Ernährungsform</p> <p class="filter-label">Ernährungsform</p>
<div class="filter-row"> <div class="filter-row">
<!-- Primary category filter buttons --> <!-- Primary category filter buttons -->
=======
<div class="filter-row">
<!-- Diet filter buttons -->
>>>>>>> e3ac1a11f091bcc9b224ce4ae81743564e160e37
<div class="category-group"> <div class="category-group">
<button class="category-item" type="button" data-diet="Fleisch">Fleisch</button> <button class="category-item" type="button" data-diet="Fleisch">Fleisch</button>
<button class="category-item" type="button" data-diet="Fisch">Fisch</button> <button class="category-item" type="button" data-diet="Fisch">Fisch</button>
@ -77,11 +91,11 @@
<p class="filter-label">Allergene</p> <p class="filter-label">Allergene</p>
<div class="filter-row"> <div class="filter-row">
<!-- Primary category filter buttons --> <!-- Allergen filter buttons -->
<div class="category-group"> <div class="category-group">
<button class="category-item" type="button" data-cat="Fleisch">glutenfrei</button> <button class="category-item" type="button" data-allergie="glutenfrei">glutenfrei</button>
<button class="category-item" type="button" data-cat="Fisch">laktosefrei</button> <button class="category-item" type="button" data-allergie="laktosefrei">laktosefrei</button>
<button class="category-item" type="button" data-cat="Vegetarisch">ohne Nüsse</button> <button class="category-item" type="button" data-allergie="ohne Nüsse">ohne Nüsse</button>
</div> </div>
</div> </div>
@ -95,6 +109,19 @@
<!-- Seitenlogik: Daten laden, filtern und Event-Karten rendern --> <!-- Seitenlogik: Daten laden, filtern und Event-Karten rendern -->
<script src="js/event_overview.js"></script> <script src="js/event_overview.js"></script>
<!-- Info Modal: Kostenlose Events Info -->
<div id="info-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Warum Invité kostenlos ist</h2>
<button type="button" class="modal-close" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Alle Events bei uns sind komplett kostenlos. Invité basiert rein auf Freiwilligkeit und der Freude am Teilen. Kein Geldfluss, keine versteckten Kosten nur die pure Absicht, die Community zu stärken und den sozialen Zusammenhalt in unserer Nachbarschaft zu fördern. Egal ob du den Kochlöffel schwingst oder dich als Gast dazu gesellst: Bei uns zählt nur die menschliche Begegnung.</p>
</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

@ -36,7 +36,7 @@
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span> <span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1> <h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1>
<p>Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p> <p>Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p>
<a class="button-primary" href="signup.html">Anmelden</a> <a class="button-primary" href="signup.html">Registrieren</a>
</div> </div>
<div class="hero__right"> <div class="hero__right">
@ -149,6 +149,77 @@
<script src="js/index-carousel.js"></script> <script src="js/index-carousel.js"></script>
<!-- FAQ Section: Akkordion mit häufig gestellten Fragen -->
<section class="faq-section">
<div class="container">
<h2>Häufig gestellte Fragen</h2>
<div class="faq-accordion">
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie kann ich bei Invité anfangen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p><strong>Schritt 1: Kostenloses Konto erstellen</strong><br>Gehe auf Invité, klicke auf "Jetzt beitreten" und fülle das Anmeldeformular aus. Du benötigst nur deine E-Mail und ein Passwort.</p>
<p><strong>Schritt 2: Dein Profil ausfüllen</strong><br>Lade ein Profilfoto hoch, schreib ein bisschen über dich und gib deine Allergien/Ernährungspräferenzen an. Das hilft anderen, dich besser kennenzulernen.</p>
<p><strong>Schritt 3: Erkunde Events</strong><br>Browsing durch unsere Events, filtere nach Diät oder Allergie-Einstellungen, und melde dich zu den Events an, die dich interessieren!</p>
<p><strong>Schritt 4: Erstelle dein eigenes Event</strong><br>Du kannst auch selbst ein Kochevent hosten! Klick auf "Event erstellen", beschreib dein Menü, und lade Gäste ein.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Fallen bei Invité Kosten an?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Nein, Invité ist komplett kostenlos. Alle Events basieren auf Freiwilligkeit und der Freude am Teilen. Es gibt keine versteckten Kosten nur die pure Absicht, die Community zu stärken.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Kann ich ein eigenes Event erstellen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ja, absolut! Du kannst dein eigenes Kochevent erstellen und Gäste einladen. Beschreibe dein Menü, die Teilnehmerzahl und weitere Details. Es ist deine Küche, dein Event, deine Regeln.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie funktioniert die An-/Abmeldung?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Bei jedem Event sehen dich die verfügbaren Plätze. Du kannst dich mit einem Klick anmelden. Eine Abmeldung ist bis 24 Stunden vor dem Event möglich so respektieren wir den Aufwand des Gastgebers.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Was ist mit Allergien und Diäten?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ich kann Informationen zu Allergien und Ernährungseinstellungen in der Event-Beschreibung hinzufügen oder beim Anmelden angeben. So können Gastgeber und Gäste besser zusammenkommen und Überraschungen vermeiden.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Ist Invité sicher und vertrauenswürdig?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ja, dein Profil hilft anderen, dich besser kennenzulernen. Wir ermutigen zu Offenheit und gegenseitigem Vertrauen. Allerdings bleibt es deine Entscheidung, wem du deine Adresse mitteilst die erfolgt nur 12 Stunden vor dem Event.</p>
</div>
</div>
</div>
</div>
</section>
<div class="footer"> <div class="footer">
<div class="footer-left"> <div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p> <p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
@ -165,5 +236,30 @@
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a> <a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div> </div>
</div> </div>
<!-- FAQ Akkordion Toggle Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const faqTriggers = document.querySelectorAll('.faq-trigger');
faqTriggers.forEach((trigger) => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const isExpanded = this.getAttribute('aria-expanded') === 'true';
// Close all other items (optional: comment out to allow multiple open)
faqTriggers.forEach((otherTrigger) => {
if (otherTrigger !== trigger) {
otherTrigger.setAttribute('aria-expanded', 'false');
}
});
// Toggle current item
this.setAttribute('aria-expanded', !isExpanded);
});
});
});
</script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,7 @@
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// ------------------------------------------------------------- // -------------------------------------------------------------
// DOM entry point and shared asset path. // DOM entry point and shared asset path.
@ -50,6 +51,57 @@
} }
} }
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) { function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
} }
@ -114,7 +166,7 @@
} }
const msUntilStart = eventDateTime.getTime() - Date.now(); const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000; const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart <= twentyfourHoursInMs; return msUntilStart <= twentyfourHoursInMs;
} }
@ -138,7 +190,7 @@
return { daysLeft, isClosed: false }; return { daysLeft, isClosed: false };
} }
// Adresse ist nur im 12h-Fenster VOR Eventstart sichtbar. // 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())) {
@ -146,7 +198,7 @@
} }
const msUntilStart = eventDateTime.getTime() - Date.now(); const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000; const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs; return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
} }
@ -283,16 +335,26 @@
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 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 registrationMap = getRegistrationMap(); const registrationMap = getRegistrationMap();
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id); const participants = getResolvedParticipants(event, registrationMap);
const remainingParticipants = Math.max(0, participants.length + extraRegistrations - visibleParticipants.length); const galleryImages = Array.isArray(event.gallery)
? event.gallery.filter(Boolean)
: [];
const galleryMarkup = galleryImages.length > 0
? `
<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>
`
: '';
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 totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
const confirmedGuests = participants.length + extraRegistrations; const confirmedGuests = participants.length;
const freePlaces = Math.max(0, totalGuests - confirmedGuests); const freePlaces = Math.max(0, totalGuests - confirmedGuests);
const isFull = freePlaces === 0; const isFull = freePlaces === 0;
const isRegistrationClosed = isRegistrationClosedForEvent(event); const isRegistrationClosed = isRegistrationClosedForEvent(event);
@ -331,7 +393,12 @@
<p>${event.address}</p> <p>${event.address}</p>
</article> </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>`),
@ -340,7 +407,12 @@
// Render complete detail page layout including: // Render complete detail page layout including:
// hero metadata, host card, menu, participants, gallery and sticky action bar. // hero metadata, host card, menu, participants, gallery and sticky action bar.
<<<<<<< HEAD
detailcontainer.innerHTML = ` detailcontainer.innerHTML = `
=======
detailContainer.innerHTML = `
<button type="button" class="btn-back-to-overview" data-navigate-back>Zurück</button>
>>>>>>> e3ac1a11f091bcc9b224ce4ae81743564e160e37
<section class="detail-hero"> <section class="detail-hero">
<div class="detail-top-row"> <div class="detail-top-row">
@ -400,13 +472,7 @@
${addressPanelMarkup} ${addressPanelMarkup}
</div> </div>
<div class="detail-gallery detail-gallery-large"> ${galleryMarkup}
${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>
<section class="detail-action-bar"> <section class="detail-action-bar">
@ -474,11 +540,20 @@
// Lightbox behavior for gallery images: // Lightbox behavior for gallery images:
// open on image click, close via backdrop, close button or ESC. // open on image click, close via backdrop, close button or ESC.
// --------------------------------------------------------- // ---------------------------------------------------------
<<<<<<< HEAD
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]');
=======
const backButton = detailContainer.querySelector('[data-navigate-back]');
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]');
>>>>>>> e3ac1a11f091bcc9b224ce4ae81743564e160e37
// Harte Absicherung: Eigene Events sind auf der Detailseite immer deaktiviert. // Harte Absicherung: Eigene Events sind auf der Detailseite immer deaktiviert.
if (registerButton && isOwnEvent) { if (registerButton && isOwnEvent) {
@ -595,5 +670,12 @@
} }
}); });
} }
// Back button navigation: returns to event overview page.
if (backButton) {
backButton.addEventListener('click', () => {
window.location.href = 'event_overview.html';
});
}
} }
}); });

View File

@ -1,7 +1,9 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const INFO_MODAL_SHOWN_KEY = 'infoModalShownOnFirstLogin';
// ------------------------------------------------------------- // -------------------------------------------------------------
// DOM references used throughout the page lifecycle. // DOM references used throughout the page lifecycle.
// ------------------------------------------------------------- // -------------------------------------------------------------
@ -15,10 +17,13 @@
// ------------------------------------------------------------- // -------------------------------------------------------------
// In-memory state for fetched events and currently active category. // In-memory state for fetched events and currently active filters.
// Separate state for category, diet, and allergie selections.
// ------------------------------------------------------------- // -------------------------------------------------------------
let allEvents = []; let allEvents = [];
let activeCategory = 'ALLE'; let activeCategory = 'ALLE';
let activeDiets = new Set();
let activeAllergies = new Set();
const currentUser = getCurrentUser(); const currentUser = getCurrentUser();
function getCurrentUser() { function getCurrentUser() {
@ -69,6 +74,57 @@
} }
} }
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) { function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
} }
@ -91,8 +147,13 @@
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE'; const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE'; const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE';
const savedDate = sessionStorage.getItem('activeDate') || ''; const savedDate = sessionStorage.getItem('activeDate') || '';
const savedDiets = sessionStorage.getItem('activeDiets') || '';
const savedAllergies = sessionStorage.getItem('activeAllergies') || '';
activeCategory = savedCategory; activeCategory = savedCategory;
activeDiets = new Set(savedDiets ? savedDiets.split(',') : []);
activeAllergies = new Set(savedAllergies ? savedAllergies.split(',') : []);
if (locationFilter) { if (locationFilter) {
locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE'; locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE';
} }
@ -283,26 +344,54 @@
return Array.from(selectElement.options).some(option => option.value === value); return Array.from(selectElement.options).some(option => option.value === value);
} }
// Apply all filters together (category, location, date), update button state, render and persist. // Apply all filters together (category, diet, allergie, location, date), update button state, render and persist.
function applyFilters() { function applyFilters() {
const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE'; const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE';
const selectedDate = dateFilter ? dateFilter.value : ''; const selectedDate = dateFilter ? dateFilter.value : '';
// Update active states for all filter types
filterButtons.forEach(btn => { filterButtons.forEach(btn => {
if (btn.getAttribute('data-cat') === activeCategory) { const isCategoryButton = btn.getAttribute('data-cat') !== null;
btn.classList.add('active'); const isDietButton = btn.getAttribute('data-diet') !== null;
} else { const isAllergieButton = btn.getAttribute('data-allergie') !== null;
btn.classList.remove('active');
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 filtered = allEvents.filter(event => {
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory; 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 locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date); const eventDateIso = parseEventDateToIso(event.date);
const dateMatch = !selectedDate || eventDateIso === selectedDate; const dateMatch = !selectedDate || eventDateIso === selectedDate;
return categoryMatch && locationMatch && dateMatch; return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch;
}); });
renderEvents(filtered); renderEvents(filtered);
@ -310,6 +399,8 @@
sessionStorage.setItem('activeFilter', activeCategory); sessionStorage.setItem('activeFilter', activeCategory);
sessionStorage.setItem('activeLocation', selectedLocation); sessionStorage.setItem('activeLocation', selectedLocation);
sessionStorage.setItem('activeDate', selectedDate); sessionStorage.setItem('activeDate', selectedDate);
sessionStorage.setItem('activeDiets', Array.from(activeDiets).join(','));
sessionStorage.setItem('activeAllergies', Array.from(activeAllergies).join(','));
} }
// Render either: // Render either:
@ -353,10 +444,9 @@
const displayTime = formatEventTime(event.time); const displayTime = formatEventTime(event.time);
// Capacity logic: // Capacity logic:
// spots = total capacity, participants.length = booked seats. // spots = total capacity, resolved participants = booked seats.
const baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0; const resolvedParticipants = getResolvedParticipants(event, registrationMap);
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id); const bookedSeats = resolvedParticipants.length;
const bookedSeats = baseParticipants + extraRegistrations;
const totalCapacity = event.spots; const totalCapacity = event.spots;
const freePlaces = Math.max(0, totalCapacity - bookedSeats); const freePlaces = Math.max(0, totalCapacity - bookedSeats);
const isFull = freePlaces === 0; const isFull = freePlaces === 0;
@ -475,10 +565,32 @@
}); });
} }
// Category filter interactions. // Category filter interactions: mutually exclusive (radio button behavior).
filterButtons.forEach(button => { filterButtons.forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
activeCategory = button.getAttribute('data-cat'); 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(); applyFilters();
}); });
}); });
@ -501,6 +613,40 @@
} }
} }
// 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. // Kick off initial load/render cycle.
fetchEvents(); fetchEvents();
}); });

View File

@ -549,7 +549,7 @@
}); });
} }
// Gibt true zurück, wenn ein Event innerhalb der nächsten 12 Stunden startet. // Gibt true zurück, wenn ein Event innerhalb der nächsten 24 Stunden startet.
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())) {
@ -557,7 +557,7 @@
} }
const msUntilStart = eventDateTime.getTime() - Date.now(); const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000; const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs; return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
} }