// ============================= // SETUP: Wichtige HTML-Elemente holen // Diese Konstanten verbinden unser JavaScript mit dem HTML. // So können wir später Buttons, Formularfelder und Bereiche steuern. // ============================= const form = document.getElementById("eventForm"); 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"); const flowFooter = document.getElementById("flowFooter"); const submissionSuccess = document.getElementById("submissionSuccess"); const EVENTS_STORAGE_KEY = "socialCookingEvents"; const CURRENT_USER_KEY = "socialCookingCurrentUser"; // ============================= // STATE: aktueller Schritt im Flow // currentStep merkt sich, auf welchem Schritt der User gerade ist. // lastStep ist der letzte Schritt im Formular. // ============================= let currentStep = 0; const lastStep = steps.length - 1; // Text für den Weiter-Button je nach Schritt const nextLabels = { 0: "Weiter", 1: "Weiter", 2: "Weiter", 3: "Weiter", 4: "Weiter", 5: "Weiter", 6: "Weiter", 7: "Event veröffentlichen" }; // Liest den aktiven Benutzer aus localStorage und setzt den Anzeigenamen im Header. function getCurrentUser() { try { const raw = localStorage.getItem(CURRENT_USER_KEY); return raw ? JSON.parse(raw) : null; } catch (error) { console.error("Aktueller Benutzer konnte nicht gelesen werden:", error); return null; } } const currentUser = getCurrentUser(); const displayName = currentUser?.vorname?.trim() || "Mia"; usernameElement.textContent = displayName; // ============================= // STEP 1: Kleine Hilfsfunktionen // Diese Funktionen helfen uns später an mehreren Stellen im Code. // ============================= /** * Gibt alle Eingabefelder eines bestimmten Schritts zurück. * Rückgabe: Array mit input-, textarea- und select-Feldern. */ function getStepFields(stepIndex) { return Array.from( steps[stepIndex].querySelectorAll("input, textarea, select") ); } /** * Zeigt eine Fehlermeldung im Formular an. * Wenn keine Nachricht übergeben wird, wird die Meldung geleert. */ function setErrorMessage(message = "") { errorMessage.textContent = message; } /** * Entfernt alle Fehlermarkierungen innerhalb eines Schritts. */ function clearStepInvalidState(stepIndex) { if (!steps[stepIndex]) return; steps[stepIndex] .querySelectorAll(".field-invalid, .option-card--invalid") .forEach(element => { element.classList.remove("field-invalid", "option-card--invalid"); }); } /** * Markiert ein einzelnes Feld visuell als ungültig. */ function markFieldInvalid(field) { field.classList.add("field-invalid"); } /** * Markiert eine ganze Radio-Gruppe visuell als ungültig. */ function markRadioGroupInvalid(group) { group.forEach(field => { const card = field.closest(".option-card"); if (card) { card.classList.add("option-card--invalid"); } }); } // ============================= // STEP 2: Schritt anzeigen & Oberfläche aktualisieren // showStep() ist eine der wichtigsten Funktionen: // Sie bestimmt, welcher Schritt sichtbar ist, // und aktualisiert gleichzeitig die restliche Oberfläche. // ============================= /** * Zeigt den gewünschten Schritt an. * Dabei werden auch Buttons, Progress Bar und Review aktualisiert. */ function showStep(index) { currentStep = index; submissionSuccess.hidden = true; clearStepInvalidState(index); // Nur der aktuelle Schritt soll sichtbar sein steps.forEach((step, stepIndex) => { step.classList.toggle("step--active", stepIndex === index); }); updateFlowVisibility(index); updateReviewIfNeeded(index); updateProgressBar(index, lastStep); setErrorMessage(""); // Für bessere UX: bei jedem Schritt wieder nach oben scrollen window.scrollTo({ top: 0, behavior: "smooth" }); } /** * Steuert Sichtbarkeit von Footer und Buttons. * Im Intro-Schritt werden Footer und Zurück-Button versteckt. * Zusätzlich bekommt der Weiter-Button je nach Schritt ein anderes Label. */ function updateFlowVisibility(stepIndex) { const isIntroStep = stepIndex === 0; flowFooter.hidden = isIntroStep; backButton.hidden = isIntroStep; nextButton.textContent = nextLabels[stepIndex]; } // ============================= // STEP 3: Fortschrittsanzeige // Die Progress Bar zeigt, wie weit der User im Flow ist. // Das Intro zählt noch nicht als eigentlicher Formularschritt. // ============================= /** * Berechnet den Fortschritt in Prozent und setzt die Breite der Progress Bar. * Beispiel: * - Intro = 0% * - erster echter Formularschritt = 0% * - letzter Schritt = 100% */ function updateProgressBar(stepIndex, 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; markerStep = stepIndex; } progressBar.style.width = `${progress}%`; progressMarker.style.left = `${markerPosition}%`; progressMarker.style.transform = markerTransform; progressMarker.hidden = stepIndex === 0; progressMarkerLabel.textContent = String(markerStep); } // ============================= // STEP 4: Review-Daten vorbereiten // Im letzten Schritt werden alle eingegebenen Daten nochmals angezeigt. // Dafür brauchen wir Funktionen, die Feldwerte sauber auslesen und formatieren. // ============================= /** * Führt das Update der Review nur aus, * wenn wirklich der letzte Schritt geöffnet ist. */ function updateReviewIfNeeded(stepIndex) { if (stepIndex === lastStep) { updateReview(); } } /** * Gibt den Wert eines Formularfeldes zurück. * Rückgabe: * - eingegebener Text / ausgewählte Option * - oder "–", falls nichts vorhanden ist */ function getFieldValue(name) { const field = form.elements[name]; if (!field) return "–"; // Spezialfall: Radio-Gruppen verhalten sich anders als normale Inputs if (field instanceof RadioNodeList) { return field.value || "–"; } return field.value.trim() || "–"; } /** * Gibt alle ausgewählten Checkbox-Werte als Text zurück. * Beispiel: "Vegetarisch, Vegan" * Falls nichts ausgewählt wurde: "Keine Angabe" */ function getCheckboxValues(name) { const checked = Array.from( form.querySelectorAll(`input[name="${name}"]:checked`) ); return checked.length ? checked.map(item => item.value).join(", ") : "Keine Angabe"; } /** * Formatiert ein Datum für die Review-Anzeige. * Beispiel: aus "2026-03-26" wird "26.03.2026" */ function formatDate(value) { if (!value || value === "–") return "–"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("de-CH", { day: "2-digit", month: "2-digit", year: "numeric" }).format(date); } /** * Schreibt einen Wert in das passende Feld der Review-Ansicht. * Gesucht wird ein HTML-Element mit data-review="..." */ function updateReviewField(fieldName, value) { const target = document.querySelector(`[data-review="${fieldName}"]`); if (target) { target.textContent = value; } } /** * Baut den Text für Allergien / Hinweise zusammen. * Dabei werden Checkboxen und zusätzliches Freitextfeld kombiniert. */ function buildAllergiesReviewValue() { const selected = getCheckboxValues("allergies"); const notes = getFieldValue("allergiesOther"); if (selected === "Keine Angabe" && notes === "–") return "Keine Angabe"; if (selected !== "Keine Angabe" && notes !== "–") { return `${selected}, ${notes}`; } return selected !== "Keine Angabe" ? selected : notes; } /** * Liest alle wichtigen Formularwerte aus * und schreibt sie gesammelt in die Review-Ansicht. */ function updateReview() { const reviewValues = getReviewValues(); Object.entries(reviewValues).forEach(([key, value]) => { updateReviewField(key, value); }); } /** * Liest alle wichtigen Formularwerte gesammelt aus. */ function getReviewValues() { return { eventTitle: getFieldValue("eventTitle"), eventType: getFieldValue("eventType"), menuDescription: getFieldValue("menuDescription"), eventDescription: getFieldValue("eventDescription"), maxGuests: getFieldValue("maxGuests"), dietType: getFieldValue("dietType"), allergies: buildAllergiesReviewValue(), eventDate: formatDate(getFieldValue("eventDate")), eventTime: getFieldValue("eventTime"), eventAddress: getFieldValue("eventAddress"), eventCity: getFieldValue("eventCity") }; } /** * Liest lokal gespeicherte Events robust aus dem Browser-Storage. */ function getStoredEvents() { try { const stored = localStorage.getItem(EVENTS_STORAGE_KEY); return stored ? JSON.parse(stored) : []; } catch (error) { console.error("Lokale Events konnten nicht gelesen werden:", error); return []; } } /** * Speichert die komplette Eventliste zurück in den Browser-Storage. */ function setStoredEvents(events) { localStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(events)); } /** * Formatiert ein ISO-Datum in das bestehende Eventformat der Demo-Daten. */ function formatDateForStorage(value) { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const monthMap = { 0: "JAN", 1: "FEB", 2: "MRZ", 3: "APR", 4: "MAI", 5: "JUN", 6: "JUL", 7: "AUG", 8: "SEP", 9: "OKT", 10: "NOV", 11: "DEZ" }; const day = String(date.getDate()).padStart(2, "0"); const month = monthMap[date.getMonth()]; const year = date.getFullYear(); return `${day}. ${month}. ${year}`; } /** * Formatiert die Zeit in das bestehende Eventformat der Demo-Daten. */ function formatTimeForStorage(value) { return value ? `${value} UHR` : ""; } /** * Zerlegt das Menü-Textarea in saubere Listenpunkte. */ function buildMenuItems(value) { return value .split("\n") .map(item => item.replace(/^[•-]\s*/, "").trim()) .filter(Boolean); } /** * Leitet den gewählten Eventtyp in die Kategorien der Übersicht über. */ function mapEventTypeToCategory(value) { const categoryMap = { Brunch: "BRUNCH", Lunch: "LUNCH", Dinner: "DINNER", "Kaffee + Kuchen": "COFFEE" }; return categoryMap[value] || value.toUpperCase(); } /** * Baut aus den Formulardaten ein lokal speicherbares Event-Objekt. */ function buildStoredEvent() { const eventType = getFieldValue("eventType"); const dietType = getFieldValue("dietType"); const menuDescription = form.elements.menuDescription.value.trim(); const eventDescription = form.elements.eventDescription.value.trim(); const eventDate = form.elements.eventDate.value; const eventTime = form.elements.eventTime.value; const eventCity = form.elements.eventCity.value.trim(); return { id: Date.now(), title: form.elements.eventTitle.value.trim(), location: eventCity, address: form.elements.eventAddress.value.trim(), date: formatDateForStorage(eventDate), time: formatTimeForStorage(eventTime), category: mapEventTypeToCategory(eventType), diet: dietType, spots: Number(form.elements.maxGuests.value), host: { name: usernameElement.textContent.trim() || "Host", initial: (usernameElement.textContent.trim().charAt(0) || "H").toUpperCase() }, hostEmail: currentUser?.email || "", hostMessage: [eventDescription], menu: buildMenuItems(menuDescription), specifications: getCheckboxValues("allergies") === "Keine Angabe" ? [] : getCheckboxValues("allergies").split(", ").filter(Boolean), allergiesNote: form.elements.allergiesOther.value.trim(), // Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt. participants: [], gallery: [], createdAt: new Date().toISOString(), source: "local" }; } /** * Speichert das aktuell erstellte Event lokal im Browser. */ function saveCurrentEvent() { const storedEvents = getStoredEvents(); const nextEvents = [buildStoredEvent(), ...storedEvents]; setStoredEvents(nextEvents); } // ============================= // STEP 5: Validierung // Bevor der User weitergehen darf, prüfen wir: // 1. Sind Pflichtfelder ausgefüllt? // 2. Wurde bei Pflicht-Radios etwas ausgewählt? // ============================= /** * Prüft, ob der aktuelle Schritt gültig ist. * Rückgabe: * - true = alles okay * - false = es gibt einen Fehler */ function validateCurrentStep() { // Intro und Review müssen nicht validiert werden if (currentStep === 0 || currentStep === lastStep) return true; const fields = getStepFields(currentStep); clearStepInvalidState(currentStep); // Zuerst Radio-Gruppen prüfen const radioCheck = validateRadioGroups(fields); if (!radioCheck.isValid) { setErrorMessage(radioCheck.message); return false; } // Danach normale Pflichtfelder prüfen const requiredCheck = validateRequiredFields(fields); if (!requiredCheck.isValid) { setErrorMessage(requiredCheck.message); return false; } setErrorMessage(""); return true; } /** * Prüft alle Radio-Gruppen eines Schritts. * Rückgabe: * - Objekt mit isValid: true/false * - bei Fehler zusätzlich eine passende Meldung */ function validateRadioGroups(fields) { const names = [...new Set(fields.filter(f => f.type === "radio").map(f => f.name))]; for (const name of names) { const group = Array.from(form.querySelectorAll(`input[name="${name}"]`)); const required = group.some(f => f.required); const selected = group.some(f => f.checked); if (required && !selected) { markRadioGroupInvalid(group); return { isValid: false, message: "Bitte wähle eine Option aus." }; } } return { isValid: true }; } /** * Prüft alle Pflichtfelder ausser Radios und Checkboxen. * Rückgabe: * - Objekt mit isValid: true/false * - bei Fehler zusätzlich eine passende Meldung */ function validateRequiredFields(fields) { for (const field of fields) { if (field.type === "radio" || field.type === "checkbox") continue; if (!field.checkValidity()) { markFieldInvalid(field); return { isValid: false, message: "Bitte fülle alle Pflichtfelder aus." }; } } return { isValid: true }; } // ============================= // STEP 6: Navigation mit Zurück / Weiter // Diese Funktionen bestimmen, was beim Klicken auf die Buttons passiert. // ============================= /** * Einen Schritt zurückgehen. * Wenn der User im ersten Formularschritt ist, geht es zurück zum Intro. */ function handleBackClick() { if (currentStep > 1) { showStep(currentStep - 1); } else { showStep(0); } } /** * Einen Schritt weitergehen. * Vorher wird geprüft, ob der aktuelle Schritt gültig ist. * Im letzten Schritt wird stattdessen das Formular abgeschickt. */ function handleNextClick() { if (!validateCurrentStep()) return; if (currentStep < lastStep) { showStep(currentStep + 1); } else { form.requestSubmit(); } } // ============================= // STEP 7: Submit // Aktuell ist das nur eine Demo. // Später könnte hier ein API-Call oder Speichern in einer Datenbank passieren. // ============================= /** * Reagiert auf das Absenden des Formulars. * preventDefault verhindert, dass die Seite neu lädt. */ function handleFormSubmit(event) { event.preventDefault(); saveCurrentEvent(); steps.forEach(step => step.classList.remove("step--active")); flowFooter.hidden = true; submissionSuccess.hidden = false; setErrorMessage(""); window.scrollTo({ top: 0, behavior: "smooth" }); } // ============================= // STEP 8: Counter-Felder (+ / -) // Für Zahlenfelder wie z. B. Anzahl Gäste. // ============================= /** * Sucht alle Counter-Komponenten * und verbindet die Plus-/Minus-Buttons mit den passenden Funktionen. */ function registerCounterHandlers() { document.querySelectorAll("[data-counter]").forEach(counter => { const input = counter.querySelector("input[type='number']"); const dec = counter.querySelector("[data-counter-action='decrease']"); const inc = counter.querySelector("[data-counter-action='increase']"); dec.addEventListener("click", () => updateCounterValue(input, -1)); inc.addEventListener("click", () => updateCounterValue(input, 1)); }); } /** * Erhöht oder verringert den Wert eines Zahlenfelds. * Der Wert darf dabei nie kleiner als das definierte Minimum werden. */ function updateCounterValue(input, change) { const min = Number(input.min || 1); const currentValue = Number(input.value || min); input.value = Math.max(min, currentValue + change); } /** * Macht aus "-"+Enter im Menüfeld eine einfache Bullet-Liste. */ function registerMenuBulletHandler() { const menuField = document.getElementById("menuDescription"); if (!menuField) return; menuField.addEventListener("keydown", event => { if (event.key !== "Enter") return; const { selectionStart, selectionEnd, value } = menuField; const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1; const lineEnd = value.indexOf("\n", selectionStart); const currentLineEnd = lineEnd === -1 ? value.length : lineEnd; const currentLine = value.slice(lineStart, currentLineEnd); const trimmedLine = currentLine.trim(); if (trimmedLine !== "-" && !currentLine.startsWith("• ")) return; event.preventDefault(); const isEmptyBullet = currentLine.trim() === "•"; if (isEmptyBullet) { const beforeLine = value.slice(0, lineStart); const afterLine = value.slice(currentLineEnd); const separator = beforeLine.endsWith("\n") || afterLine.startsWith("\n") ? "" : "\n"; const nextValue = `${beforeLine}${separator}${afterLine}`.replace(/\n{3,}/g, "\n\n"); menuField.value = nextValue; const caretPosition = Math.min(lineStart, nextValue.length); menuField.setSelectionRange(caretPosition, caretPosition); menuField.dispatchEvent(new Event("input", { bubbles: true })); return; } const bulletLine = currentLine.startsWith("• ") ? currentLine : currentLine.replace("-", "•"); const updatedLine = bulletLine.startsWith("• ") ? bulletLine : `• ${trimmedLine.slice(1).trimStart()}`; const beforeLine = value.slice(0, lineStart); const afterLine = value.slice(currentLineEnd); const nextValue = `${beforeLine}${updatedLine}\n• ${afterLine}`; const caretPosition = beforeLine.length + updatedLine.length + 3; menuField.value = nextValue; menuField.setSelectionRange(caretPosition, caretPosition); menuField.dispatchEvent(new Event("input", { bubbles: true })); }); } /** * Springt aus der Review zurück zum passenden Schritt * und fokussiert das gewünschte Feld für direktes Weiterbearbeiten. */ function registerReviewEditHandlers() { document.querySelectorAll(".review-item[data-edit-step]").forEach(item => { const activateEdit = () => { const stepIndex = Number(item.dataset.editStep); const fieldName = item.dataset.editField; showStep(stepIndex); focusFieldByName(fieldName); }; item.addEventListener("click", activateEdit); item.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activateEdit(); } }); }); } /** * Entfernt Fehlermarkierungen, sobald der User ein Feld korrigiert. */ function registerValidationFeedbackHandlers() { form.querySelectorAll("input, textarea, select").forEach(field => { const clearInvalidState = () => { field.classList.remove("field-invalid"); if (field.type === "radio") { const group = Array.from(form.querySelectorAll(`input[name="${field.name}"]`)); const hasSelection = group.some(item => item.checked); if (hasSelection) { group.forEach(item => { const card = item.closest(".option-card"); if (card) { card.classList.remove("option-card--invalid"); } }); } } }; field.addEventListener("input", clearInvalidState); field.addEventListener("change", clearInvalidState); }); } /** * Setzt den Fokus auf ein bestimmtes Feld oder die erste Option einer Radio-Gruppe. */ function focusFieldByName(fieldName) { const field = form.elements[fieldName]; if (!field) return; const focusTarget = field instanceof RadioNodeList ? field[0] : field; if (focusTarget && typeof focusTarget.focus === "function") { window.setTimeout(() => { focusTarget.focus(); }, 150); } } // ============================= // STEP 9: Alles starten // Hier werden alle Event Listener registriert // und der Flow startet mit dem Intro-Schritt. // ============================= /** * Initialisiert den kompletten Event-Erstellungs-Flow. * Diese Funktion wird einmal beim Laden der Seite aufgerufen. */ function initEventCreationFlow() { // Buttons, die den Flow starten document.querySelectorAll("[data-start-flow]").forEach(btn => { btn.addEventListener("click", () => showStep(1)); }); // Navigation backButton.addEventListener("click", handleBackClick); nextButton.addEventListener("click", handleNextClick); // Formular absenden form.addEventListener("submit", handleFormSubmit); // Counter aktivieren registerCounterHandlers(); registerMenuBulletHandler(); registerValidationFeedbackHandlers(); registerReviewEditHandlers(); // Startzustand: Intro anzeigen submissionSuccess.hidden = true; showStep(0); } // Startpunkt des Skripts initEventCreationFlow();