// =============================
// 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 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, pushHistory = true) {
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("");
// 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" });
}
/**
* 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) {
const totalFormSteps = totalStepIndex;
let progress = 0;
let markerStep = 1;
if (stepIndex > 0) {
progress = ((stepIndex) / totalFormSteps) * 100;
markerStep = stepIndex;
}
progressBar.style.width = `${progress}%`;
progressMarkerLabel.textContent = `Schritt ${markerStep} von ${totalFormSteps}`;
}
// =============================
// 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 geführt und nicht als angemeldeter Gast gezählt.
participants: [],
gallery: [...galleryImages],
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 8b: Galerie-Upload (Fotos hinzufügen)
// =============================
const galleryImages = [];
function registerGalleryHandlers() {
const addBtn = document.getElementById("galleryAddBtn");
const fileInput = document.getElementById("galleryFileInput");
const preview = document.getElementById("galleryPreview");
if (!addBtn) return;
// Klick auf + öffnet direkt den Datei-Dialog
addBtn.addEventListener("click", () => {
fileInput.click();
});
// Datei(en) hochladen
fileInput.addEventListener("change", () => {
Array.from(fileInput.files).forEach(file => {
if (!file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.onload = (e) => {
addGalleryImage(e.target.result);
};
reader.readAsDataURL(file);
});
fileInput.value = "";
});
function addGalleryImage(src) {
galleryImages.push(src);
renderGalleryPreview();
}
function removeGalleryImage(index) {
galleryImages.splice(index, 1);
renderGalleryPreview();
}
function renderGalleryPreview() {
preview.innerHTML = "";
galleryImages.forEach((src, i) => {
const thumb = document.createElement("div");
thumb.className = "gallery-thumb";
thumb.innerHTML = `
`;
thumb.querySelector(".gallery-thumb-remove").addEventListener("click", () => {
removeGalleryImage(i);
});
preview.appendChild(thumb);
});
}
}
// =============================
// 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();
registerGalleryHandlers();
// 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
initEventCreationFlow();