Social_Cooking/js/event_create.js
2026-04-09 17:01:37 +02:00

762 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================
// 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";
// =============================
// 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"
};
// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen
usernameElement.textContent = "Mia";
// =============================
// 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()
},
hostMessage: [eventDescription],
menu: buildMenuItems(menuDescription),
specifications: getCheckboxValues("allergies") === "Keine Angabe"
? []
: getCheckboxValues("allergies").split(", ").filter(Boolean),
allergiesNote: form.elements.allergiesOther.value.trim(),
participants: [usernameElement.textContent.trim() || "Host"],
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();