Add FAQ section, back button, and info modal to event pages; update styles and scripts

This commit is contained in:
viiivo 2026-04-21 19:39:15 +02:00
parent 24dc61a887
commit fa8a7f1fc2
7 changed files with 444 additions and 20 deletions

View File

@ -254,6 +254,7 @@
background: var(--butter-light);
border-radius: var(--radius-lg);
font-family: var(--font-main);
display: none;
font-weight: 400;
font-size: 1.25rem;
place-items: center;
@ -391,4 +392,133 @@
text-decoration: underline;
font-size: 0.8rem;
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

@ -199,6 +199,19 @@ p {
margin: 0 auto;
}
/*
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 {
width: 20px;
height: 20px;
@ -409,6 +422,29 @@ p {
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 {
background-color: transparent;
@ -594,6 +630,32 @@ p {
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 {
display: none;
@ -658,6 +720,22 @@ p {
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 {
padding: var(--space-20) var(--space-20) var(--space-4) var(--space-20);
}

View File

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

View File

@ -28,9 +28,12 @@
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container">
<!-- Page headline -->
<h1>Events</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>
<!-- Filter section: category chips + location/date filters -->
<section class="filter-section">
@ -63,8 +66,8 @@
</div>
</div>
<div class="filter-row">
<!-- Primary category filter buttons -->
<div class="filter-row">
<!-- Diet filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-diet="Fleisch">Fleisch</button>
<button class="category-item" type="button" data-diet="Fisch">Fisch</button>
@ -74,11 +77,11 @@
</div>
<div class="filter-row">
<!-- Primary category filter buttons -->
<!-- Allergen filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-cat="Fleisch">glutenfrei</button>
<button class="category-item" type="button" data-cat="Fisch">laktosefrei</button>
<button class="category-item" type="button" data-cat="Vegetarisch">ohne Nüsse</button>
<button class="category-item" type="button" data-allergie="glutenfrei">glutenfrei</button>
<button class="category-item" type="button" data-allergie="laktosefrei">laktosefrei</button>
<button class="category-item" type="button" data-allergie="ohne Nüsse">ohne Nüsse</button>
</div>
</div>
@ -92,6 +95,19 @@
<!-- Seitenlogik: Daten laden, filtern und Event-Karten rendern -->
<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 -->
<div class="snackbar" id="snackbar"></div>

View File

@ -36,7 +36,7 @@
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<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>
<a class="button-primary" href="signup.html">Anmelden</a>
<a class="button-primary" href="signup.html">Registrieren</a>
</div>
<div class="hero__right">
@ -149,6 +149,77 @@
<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-left">
<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>
</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>
</html>

View File

@ -341,6 +341,7 @@
// Render complete detail page layout including:
// hero metadata, host card, menu, participants, gallery and sticky action bar.
detailContainer.innerHTML = `
<button type="button" class="btn-back-to-overview" data-navigate-back>Zurück</button>
<section class="detail-hero">
<div class="detail-top-row">
@ -465,6 +466,7 @@
// Lightbox behavior for gallery images:
// open on image click, close via backdrop, close button or ESC.
// ---------------------------------------------------------
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');
@ -586,5 +588,12 @@
}
});
}
// Back button navigation: returns to event overview page.
if (backButton) {
backButton.addEventListener('click', () => {
window.location.href = 'event_overview.html';
});
}
}
});

View File

@ -2,6 +2,7 @@
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const INFO_MODAL_SHOWN_KEY = 'infoModalShownOnFirstLogin';
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
@ -15,10 +16,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 activeCategory = 'ALLE';
let activeDiets = new Set();
let activeAllergies = new Set();
const currentUser = getCurrentUser();
function getCurrentUser() {
@ -91,8 +95,13 @@
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE';
const savedDate = sessionStorage.getItem('activeDate') || '';
const savedDiets = sessionStorage.getItem('activeDiets') || '';
const savedAllergies = sessionStorage.getItem('activeAllergies') || '';
activeCategory = savedCategory;
activeDiets = new Set(savedDiets ? savedDiets.split(',') : []);
activeAllergies = new Set(savedAllergies ? savedAllergies.split(',') : []);
if (locationFilter) {
locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE';
}
@ -283,26 +292,54 @@
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() {
const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE';
const selectedDate = dateFilter ? dateFilter.value : '';
// Update active states for all filter types
filterButtons.forEach(btn => {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
const isCategoryButton = btn.getAttribute('data-cat') !== null;
const isDietButton = btn.getAttribute('data-diet') !== null;
const isAllergieButton = btn.getAttribute('data-allergie') !== null;
if (isCategoryButton) {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isDietButton) {
if (activeDiets.has(btn.getAttribute('data-diet'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isAllergieButton) {
if (activeAllergies.has(btn.getAttribute('data-allergie'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
});
const filtered = allEvents.filter(event => {
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
// Diet filter: if no diets selected, show all. Otherwise, event MUST have at least one selected diet.
const dietMatch = activeDiets.size === 0 ||
(event.diet && event.diet.split(', ').some(d => activeDiets.has(d.trim())));
// Allergie filter: if no allergies selected, show all. Otherwise, event MUST have at least one selected allergie.
const allergieMatch = activeAllergies.size === 0 ||
(event.specifications && event.specifications.some(spec => activeAllergies.has(spec)));
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date);
const dateMatch = !selectedDate || eventDateIso === selectedDate;
return categoryMatch && locationMatch && dateMatch;
return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch;
});
renderEvents(filtered);
@ -310,6 +347,8 @@
sessionStorage.setItem('activeFilter', activeCategory);
sessionStorage.setItem('activeLocation', selectedLocation);
sessionStorage.setItem('activeDate', selectedDate);
sessionStorage.setItem('activeDiets', Array.from(activeDiets).join(','));
sessionStorage.setItem('activeAllergies', Array.from(activeAllergies).join(','));
}
// Render either:
@ -475,10 +514,32 @@
});
}
// Category filter interactions.
// Category filter interactions: mutually exclusive (radio button behavior).
filterButtons.forEach(button => {
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();
});
});
@ -501,6 +562,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.
fetchEvents();
});