feat: lightbox gallery, event create UX improvements, cancel modal, footer consistency, Swiss German fixes

This commit is contained in:
Estelle Köhler 2026-04-12 15:18:40 +02:00
parent 4b54c48311
commit e805abbf12
16 changed files with 307 additions and 77 deletions

View File

@ -187,6 +187,7 @@ h2 {
font-size: clamp(2rem, 4vw, 4rem);
line-height: 1.03;
letter-spacing: -0.03em;
color: var(--brown);
}
.step-text {
@ -204,7 +205,6 @@ h2 {
}
.intro-card {
max-width: 24rem;
padding: var(--space-6);
border-radius: 1.75rem;
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
@ -221,7 +221,7 @@ h2 {
.intro-image {
width: 100%;
aspect-ratio: 16 / 10;
height: 100%;
display: block;
object-fit: cover;
border-radius: 1.875rem;
@ -457,57 +457,36 @@ textarea:focus {
}
.progress-wrap {
position: relative;
width: min(100%, var(--content-width));
margin: 0 auto;
padding-top: 4.35rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.progress-label {
flex-shrink: 0;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
white-space: nowrap;
}
.progress {
width: 100%;
height: 0.375rem;
flex: 1;
height: 0.3rem;
background: var(--color-progress-bg);
border-radius: 999px;
overflow: hidden;
}
.progress-bar {
display: block;
width: 0;
height: 100%;
background: var(--tomato);
transition: width 0.25s ease;
}
.progress-marker {
position: absolute;
top: 0;
transform: translateX(-50%);
display: grid;
justify-items: center;
gap: 0.2rem;
pointer-events: none;
}
.progress-marker::after {
content: "";
width: 0.125rem;
height: 1rem;
background: var(--tomato);
background: var(--olive);
border-radius: 999px;
}
.progress-marker__circle {
width: 2.9rem;
height: 2.9rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--tomato);
color: var(--butter-light);
font-size: 1.35rem;
font-weight: 600;
line-height: 1;
box-shadow: 0 10px 24px rgba(212, 75, 36, 0.18);
transition: width 0.25s ease;
}
.flow-actions {
@ -550,8 +529,15 @@ textarea:focus {
}
.button--text {
border: 0;
padding-left: 0;
border: 2px solid var(--olive);
color: var(--olive);
background: transparent;
padding-left: 1.35rem;
}
.button--text:hover {
background: var(--olive-light);
color: var(--black);
}
.button--primary {
@ -625,7 +611,7 @@ textarea:focus-visible {
.step-layout--intro {
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: center;
align-items: stretch;
gap: var(--space-8);
}

View File

@ -492,6 +492,7 @@
color: var(--white);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
}
@ -668,6 +669,40 @@
opacity: 0.7;
}
.detail-participants-full {
display: flex;
flex-direction: column;
gap: 10px;
}
.detail-participant-item {
display: flex;
align-items: center;
gap: 10px;
}
.participant-name {
font-size: 0.95rem;
font-weight: 500;
color: var(--black);
}
.detail-participants-link {
background: none;
border: none;
color: var(--olive);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
padding: 0;
text-decoration: underline;
text-underline-offset: 3px;
}
.detail-participants-link:hover {
color: var(--olive-dark);
}
.detail-action-bar {
/* Sticky bottom CTA bar with summary and booking controls. */
display: flex;
@ -733,8 +768,8 @@
.detail-spots-pill {
border: 2px solid var(--olive-light);
border-radius: var(--radius-pill);
opacity: 0.5;
font-size: 14px;
font-weight: 500;
letter-spacing: var(--ls-ui);
padding: 7px 14px;
color: var(--olive);

View File

@ -247,6 +247,7 @@
background: var(--white);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
aspect-ratio: 2 / 3;
cursor: pointer;
}
.gallery__item img {

View File

@ -435,6 +435,56 @@ p {
background: var(--tomato);
}
/* Lightbox */
.lightbox {
position: fixed;
inset: 0;
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
}
.lightbox.is-open {
display: flex;
}
.lightbox__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
}
.lightbox__content {
position: relative;
margin: 0;
max-width: min(96vw, 1100px);
max-height: 90vh;
z-index: 1;
}
.lightbox__image {
display: block;
width: 100%;
max-height: 90vh;
object-fit: contain;
border-radius: 16px;
background: #111;
}
.lightbox__close {
position: absolute;
top: -42px;
right: 0;
border: 0;
background: transparent;
color: var(--white);
font-size: 40px;
line-height: 1;
cursor: pointer;
}
/* Footer */
.footer {
display: flex;

View File

@ -57,7 +57,7 @@
"hostMessage": [
"¡Hola a todos! Ich lade euch ein auf eine kulinarische Reise nach Peru.",
"Ich koche für euch ein authentisches peruanisches Sharing-Menü, das vor Lebensfreude nur so sprüht. Freut euch auf eine Explosion aus leuchtenden Farben, fein abgestimmter Schärfe und der unverwechselbaren Frische verschiedenster Kräuter.",
"Wir genießen den Abend gemeinsam in mehreren kleinen Gängen, ganz nach dem Sharing-Prinzip. Dabei entdecken wir die klassischen Aromen meiner Heimatstadt Lima von traditionell bis modern interpretiert.",
"Wir geniessen den Abend gemeinsam in mehreren kleinen Gängen, ganz nach dem Sharing-Prinzip. Dabei entdecken wir die klassischen Aromen meiner Heimatstadt Lima von traditionell bis modern interpretiert.",
"Es wird gesellig, aromatisch und ein echtes Erlebnis für alle Sinne. ¡Buen provecho!"
],
"menu": [

View File

@ -41,8 +41,8 @@
<p class="step-kicker">Event erstellen</p>
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<p class="step-text">
Erzähl uns von deiner Idee vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
Erzähl uns von deiner Idee, vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
</p>
<button type="button" class="button button--primary button--intro" data-start-flow>
Los gehts!
@ -66,7 +66,7 @@
<h2 id="step1-title">Was hast du vor?</h2>
<p class="step-text">
Erzähl uns, was für ein Event du planst. Ist es ein gemütlicher Brunch,
ein Dinner mit Wow-Effekt oder einfach ein entspannter Abend mit gutem Essen?
ein Dinner mit Wow-Effekt oder einfach ein entspanntes Mittagessen mit gutem Essen?
</p>
</div>
@ -277,12 +277,12 @@
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heißen?</label>
<label for="eventTitle">Wie soll dein Event heissen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<label for="eventDescription">Beschreibung des Events</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
</div>
@ -363,9 +363,7 @@
<div class="flow-footer" id="flowFooter" hidden>
<div class="progress-wrap" aria-hidden="true">
<div class="progress-marker" id="progressMarker">
<span class="progress-marker__circle" id="progressMarkerLabel">1</span>
</div>
<span class="progress-label" id="progressMarkerLabel">Schritt 1 von 7</span>
<div class="progress">
<span id="progressBar" class="progress-bar"></span>
</div>
@ -400,7 +398,7 @@
<div class="review-card review-card--success">
<div class="submission-success-actions">
<a class="button button--primary" href="event_overview.html">Weiter zu deinem Profil</a>
<a class="button button--primary" href="my_profil.html">Weiter zu deinem Profil</a>
</div>
</div>
</div>

View File

@ -35,6 +35,11 @@
<!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox -->
<script src="js/event_detail.js"></script>
<!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div>
<!-- Instagram Einladung -->
<div class="instagram-invite">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite__icon" />

View File

@ -148,6 +148,15 @@
</section>
</main>
<!-- Lightbox: Bildansicht vergrössert -->
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
<div class="lightbox__backdrop" data-close-lightbox></div>
<figure class="lightbox__content" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="lightbox__close" type="button" aria-label="Schliessen">&times;</button>
<img class="lightbox__image" src="" alt="Grossansicht">
</figure>
</div>
<script src="js/index-carousel.js"></script>
<footer class="footer">
<a href="impressum.html" class="footer__link">Impressum</a>

View File

@ -8,7 +8,6 @@ const steps = Array.from(document.querySelectorAll(".step"));
const backButton = document.getElementById("backButton");
const nextButton = document.getElementById("nextButton");
const progressBar = document.getElementById("progressBar");
const progressMarker = document.getElementById("progressMarker");
const progressMarkerLabel = document.getElementById("progressMarkerLabel");
const errorMessage = document.getElementById("errorMessage");
const usernameElement = document.getElementById("username");
@ -121,7 +120,7 @@ function markRadioGroupInvalid(group) {
* Zeigt den gewünschten Schritt an.
* Dabei werden auch Buttons, Progress Bar und Review aktualisiert.
*/
function showStep(index) {
function showStep(index, pushHistory = true) {
currentStep = index;
submissionSuccess.hidden = true;
clearStepInvalidState(index);
@ -136,6 +135,11 @@ function showStep(index) {
updateProgressBar(index, lastStep);
setErrorMessage("");
// Browser-History aktualisieren, damit Zurück-Taste funktioniert
if (pushHistory) {
history.pushState({ step: index }, "");
}
// Für bessere UX: bei jedem Schritt wieder nach oben scrollen
window.scrollTo({ top: 0, behavior: "smooth" });
}
@ -168,22 +172,17 @@ function updateFlowVisibility(stepIndex) {
* - letzter Schritt = 100%
*/
function updateProgressBar(stepIndex, totalStepIndex) {
const totalFormSteps = totalStepIndex;
let progress = 0;
let markerPosition = 0;
let markerStep = 1;
let markerTransform = "translateX(-50%)";
if (stepIndex > 0) {
progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
markerPosition = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
progress = ((stepIndex) / totalFormSteps) * 100;
markerStep = stepIndex;
}
progressBar.style.width = `${progress}%`;
progressMarker.style.left = `${markerPosition}%`;
progressMarker.style.transform = markerTransform;
progressMarker.hidden = stepIndex === 0;
progressMarkerLabel.textContent = String(markerStep);
progressMarkerLabel.textContent = `Schritt ${markerStep} von ${totalFormSteps}`;
}
@ -767,9 +766,21 @@ function initEventCreationFlow() {
registerValidationFeedbackHandlers();
registerReviewEditHandlers();
// Browser-Zurück-Taste: vorherigen Schritt wiederherstellen
window.addEventListener("popstate", (e) => {
if (e.state && typeof e.state.step === "number") {
showStep(e.state.step, false);
} else {
showStep(0, false);
}
});
// Startzustand: Intro anzeigen
submissionSuccess.hidden = true;
showStep(0);
// Initialen History-Eintrag ersetzen, damit Step 0 im Verlauf ist
history.replaceState({ step: 0 }, "");
}
// Startpunkt des Skripts

View File

@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// DOM entry point and shared asset path.
// -------------------------------------------------------------
const detailContainer = document.getElementById('detail-view');
const locationIconPath = 'assets/location-pin.svg';
const locationIconPath = 'assets/icon_location-pin.svg';
const currentUser = getCurrentUser();
// Read event id from query string (detail page deep-link support).
@ -358,12 +358,20 @@ document.addEventListener('DOMContentLoaded', async () => {
<article class="detail-panel detail-panel-compact">
<div class="detail-participants-head">
<h2 class="detail-section-title">Teilnehmer</h2>
<a href="#" class="detail-participants-link">Alle ansehen</a>
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
</div>
<div class="detail-avatar-row">
<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>
${participants.map(name => `
<div class="detail-participant-item">
<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>
<span class="participant-name">${name}</span>
</div>
`).join('')}
</div>
</article>
${addressPanelMarkup}
@ -392,7 +400,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<div class="detail-action-buttons">
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze frei`}
</span>
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
@ -447,8 +455,27 @@ document.addEventListener('DOMContentLoaded', async () => {
if (registrationSet.has(Number(event.id))) {
registrationSet.delete(Number(event.id));
// 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) {
registrationSet.add(Number(event.id));
// Snackbar: Feedback bei Anmeldung.
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);
}
}
nextRegistrationMap[currentUser.email] = Array.from(registrationSet);
@ -459,6 +486,20 @@ document.addEventListener('DOMContentLoaded', async () => {
});
}
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen.
const showAllBtn = detailContainer.querySelector('[data-show-all-participants]');
const avatarRow = detailContainer.querySelector('[data-participants-row]');
const fullList = detailContainer.querySelector('[data-participants-full]');
if (showAllBtn && avatarRow && fullList) {
showAllBtn.addEventListener('click', () => {
const isExpanded = !fullList.classList.contains('hidden');
fullList.classList.toggle('hidden');
avatarRow.classList.toggle('hidden');
showAllBtn.textContent = isExpanded ? 'Alle ansehen' : 'Weniger anzeigen';
});
}
// Central close helper to keep all close paths consistent.
function closeLightbox() {
if (!lightbox) {

View File

@ -294,11 +294,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
const filtered = allEvents.filter(event => {
// Lokal erstellte Events werden nicht in der allgemeinen Event-Uebersicht angezeigt.
if (event.source === 'local') {
return false;
}
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date);

View File

@ -97,4 +97,47 @@ if (carouselTrack) {
buildDots();
updateTrack();
});
// =============================================
// Lightbox: Bild vergroessern bei Klick
// =============================================
const lightbox = document.getElementById('gallery-lightbox');
const lightboxImage = lightbox ? lightbox.querySelector('.lightbox__image') : null;
function openLightbox(src, alt) {
if (!lightbox || !lightboxImage) return;
lightboxImage.src = src;
lightboxImage.alt = alt || 'Grossansicht';
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
}
function closeLightbox() {
if (!lightbox) return;
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
lightboxImage.src = '';
}
// Klick auf Galerie-Bild oeffnet die Lightbox.
items.forEach(function(item) {
var img = item.querySelector('img');
if (img) {
item.addEventListener('click', function() {
openLightbox(img.src, img.alt);
});
}
});
// Lightbox schliessen: Backdrop, Close-Button oder ESC-Taste.
if (lightbox) {
lightbox.querySelector('.lightbox__close').addEventListener('click', closeLightbox);
lightbox.querySelector('[data-close-lightbox]').addEventListener('click', closeLightbox);
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && lightbox.classList.contains('is-open')) {
closeLightbox();
}
});
}
}

View File

@ -177,7 +177,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (cancelButton && currentUser?.email) {
const eventId = Number(cancelButton.getAttribute('data-cancel-event-id'));
if (Number.isFinite(eventId)) {
cancelHostedEvent(eventId, currentUser.email);
openCancelEventModal(eventId);
}
return;
}
@ -265,6 +265,34 @@ document.addEventListener('DOMContentLoaded', () => {
// Sagt ein gehostetes Event ab (aus eigener Profilansicht entfernen).
let pendingCancelEventId = null;
function openCancelEventModal(eventId) {
pendingCancelEventId = eventId;
const modal = document.getElementById('cancelEventModal');
modal.classList.add('show');
}
window.closeCancelEventModal = function() {
pendingCancelEventId = null;
const modal = document.getElementById('cancelEventModal');
modal.classList.remove('show');
};
document.getElementById('confirmCancelEventBtn').addEventListener('click', function() {
if (pendingCancelEventId !== null && currentUser?.email) {
cancelHostedEvent(pendingCancelEventId, currentUser.email);
}
closeCancelEventModal();
});
// Schliesst das Modal bei Klick ausserhalb des Inhalts.
document.getElementById('cancelEventModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCancelEventModal();
}
});
function cancelHostedEvent(eventId, userEmail) {
// Lokal erstellte, eigene Events werden direkt aus dem Storage geloescht.
const storedEvents = getStoredEvents();

View File

@ -42,7 +42,7 @@ function openWelcomeModal() {
document.body.style.overflow = 'hidden';
}
// Funktion zum Schließen des Welcome Modals
// Funktion zum Schliessen des Welcome Modals
function closeWelcomeModal() {
welcomeModal.classList.remove('show');
document.body.style.overflow = 'auto';
@ -172,7 +172,7 @@ passwortInput.addEventListener('input', function() {
}
});
// Modal schließen wenn außerhalb geklickt wird
// Modal schliessen wenn ausserhalb geklickt wird
welcomeModal.addEventListener('click', function(event) {
if (event.target === welcomeModal) {
closeWelcomeModal();

View File

@ -123,6 +123,34 @@
<!-- Snackbar: Feedback bei Abmeldung von Events -->
<div class="snackbar" id="snackbar"></div>
<!-- Event-Absage Confirmation Modal -->
<div id="cancelEventModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<button class="close-btn" onclick="closeCancelEventModal()">&times;</button>
<h2>Event absagen?</h2>
</div>
<div class="modal-body">
Bist du sicher, dass du dieses Event absagen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
</div>
<div class="modal-footer">
<button class="button button--outline" type="button" onclick="closeCancelEventModal()">Abbrechen</button>
<button class="button" type="button" id="confirmCancelEventBtn" style="background-color: var(--tomato); border-color: var(--tomato);">Event absagen</button>
</div>
</div>
</div>
<div class="instagram-invite">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite__icon" />
<img src="assets/logo_invite.svg" alt="Invité Logo" class="instagram-invite__logo" />
</a>
</div>
<footer class="footer">
<a href="impressum.html" class="footer__link">Impressum</a>
<a href="datenschutz.html" class="footer__link">Datenschutz</a>
</footer>
<script src="js/my_profil.js"></script>
</body>
</html>

View File

@ -73,7 +73,7 @@
</form>
</div>
</div>
</div> <!-- Schließt main-content -->
</div> <!-- Schliesst main-content -->
<!-- Welcome Modal -->
<div id="welcomeModal" class="modal">