diff --git a/event-create.css b/event-create.css index a138e29..e118a3e 100644 --- a/event-create.css +++ b/event-create.css @@ -1,7 +1,9 @@ :root { --color-bg: #f7f7f2; --color-surface: #ffffff; + --color-surface-soft: #f0eee7; --color-text: #1f1f1f; + --color-text-secondary: #303030; --color-muted: #6b6b6b; --color-border: #d8d8d2; --color-border-strong: #202020; @@ -9,10 +11,16 @@ --color-primary-hover: #111111; --color-focus: #2f6fed; --color-error: #b42318; + --color-progress-bg: #ddddda; + --color-divider: #ecece7; + --shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.06); + --radius-sm: 0.875rem; --radius-md: 1.25rem; - --radius-lg: 999px; + --radius-lg: 1.5rem; + --radius-pill: 999px; + --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; @@ -21,8 +29,14 @@ --space-6: 2rem; --space-7: 3rem; --space-8: 4rem; + --max-width: 1120px; + --content-width: 760px; --header-height: 4.5rem; + + --control-min-height: 3rem; + --input-min-height: 3.5rem; + --card-min-height: 6rem; } *, @@ -130,7 +144,7 @@ a { } .step-layout { - width: min(100%, 760px); + width: min(100%, var(--content-width)); margin: 0 auto; display: grid; gap: var(--space-6); @@ -142,11 +156,30 @@ a { grid-template-columns: 1fr; } -.step-copy { +.step-copy, +.step-fields, +.form-field, +fieldset { display: grid; gap: var(--space-4); } +.step-copy { + gap: var(--space-4); +} + +.step-fields { + gap: var(--space-5); +} + +.form-field, +fieldset { + margin: 0; + padding: 0; + border: 0; + gap: var(--space-3); +} + .step-kicker { margin: 0; font-weight: 700; @@ -165,17 +198,22 @@ h2 { .step-text { margin: 0; max-width: 42rem; - color: #303030; + color: var(--color-text-secondary); font-size: clamp(1rem, 1.4vw, 1.2rem); } +.intro-card, +.review-card { + border: 1px solid var(--color-border); + background: var(--color-surface); + box-shadow: var(--shadow-soft); +} + .intro-card { max-width: 24rem; padding: var(--space-6); - border: 1px solid var(--color-border); border-radius: 1.75rem; - background: linear-gradient(135deg, #ffffff, #f0eee7); - box-shadow: var(--shadow-soft); + background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft)); } .intro-card-emoji { @@ -183,20 +221,6 @@ h2 { margin-bottom: var(--space-3); } -.step-fields { - display: grid; - gap: var(--space-5); -} - -.form-field, -fieldset { - margin: 0; - padding: 0; - border: 0; - display: grid; - gap: var(--space-3); -} - label, legend { font-weight: 700; @@ -214,7 +238,7 @@ input[type="time"], input[type="number"], textarea { width: 100%; - min-height: 3.5rem; + min-height: var(--input-min-height); padding: 0.95rem 1rem; border: 1px solid var(--color-border); border-radius: 1rem; @@ -241,7 +265,7 @@ textarea { position: relative; display: grid; gap: 0.15rem; - min-height: 6rem; + min-height: var(--card-min-height); padding: 1rem 1rem 1rem 1.05rem; border: 1px solid var(--color-border); border-radius: 1rem; @@ -280,9 +304,14 @@ textarea { text-align: center; } +.counter-button, +.button { + min-height: var(--control-min-height); +} + .counter-button { - width: 3rem; - height: 3rem; + width: var(--control-min-height); + height: var(--control-min-height); border: 1px solid var(--color-border); border-radius: 50%; background: var(--color-surface); @@ -291,10 +320,7 @@ textarea { .review-card { padding: var(--space-5); - border: 1px solid var(--color-border); - border-radius: 1.5rem; - background: var(--color-surface); - box-shadow: var(--shadow-soft); + border-radius: var(--radius-lg); } .review-list { @@ -307,7 +333,7 @@ textarea { display: grid; gap: var(--space-1); padding-bottom: var(--space-4); - border-bottom: 1px solid #ecece7; + border-bottom: 1px solid var(--color-divider); } .review-item:last-child { @@ -322,7 +348,7 @@ textarea { .review-item dd { margin: 0; white-space: pre-wrap; - color: #303030; + color: var(--color-text-secondary); } .flow-footer { @@ -338,7 +364,7 @@ textarea { .progress { width: 100%; height: 0.375rem; - background: #ddddda; + background: var(--color-progress-bg); } .progress-bar { @@ -374,9 +400,8 @@ textarea { display: inline-flex; align-items: center; justify-content: center; - min-height: 3rem; padding: 0.9rem 1.35rem; - border-radius: var(--radius-lg); + border-radius: var(--radius-pill); border: 1px solid var(--color-border); background: transparent; color: var(--color-text); @@ -430,6 +455,35 @@ textarea:focus-visible { outline-offset: 3px; } +@media (max-width: 767px) { + .site-nav { + flex-wrap: wrap; + padding: var(--space-3) 0; + } + + .site-nav-links { + gap: var(--space-3); + } + + .flow-actions, + .flow-actions-right { + flex-direction: column; + align-items: stretch; + } + + .button--text { + justify-content: flex-start; + } + + .button--primary { + width: 100%; + } + + .event-flow-header { + justify-content: flex-start; + } +} + @media (min-width: 768px) { .step-layout--intro { grid-template-columns: 1.25fr 0.8fr; diff --git a/event-create.html b/event-create.html index 5190f9f..da5d6b0 100644 --- a/event-create.html +++ b/event-create.html @@ -25,7 +25,11 @@
-
+

Event erstellen

@@ -39,8 +43,8 @@
-
@@ -64,7 +68,7 @@
- Art des Essens + Art des Essens / Eventtyp
@@ -189,6 +207,11 @@ ohne Nüsse + +
+ + +
@@ -312,7 +335,7 @@
- +
@@ -321,7 +344,7 @@
-

© Invité

+

© Social Cooking

diff --git a/event-create.js b/event-create.js index 34d0847..5735a3f 100644 --- a/event-create.js +++ b/event-create.js @@ -1,3 +1,8 @@ +// ============================= +// 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"); @@ -5,10 +10,17 @@ const nextButton = document.getElementById("nextButton"); const progressBar = document.getElementById("progressBar"); const errorMessage = document.getElementById("errorMessage"); const usernameElement = document.getElementById("username"); +const flowFooter = document.getElementById("flowFooter"); +// ============================= +// 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", @@ -18,92 +30,128 @@ const nextLabels = { 5: "Event veröffentlichen" }; -// Demo-Platzhalter; später aus Nutzerprofil setzen +// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen usernameElement.textContent = "Mia"; -const flowFooter = document.getElementById("flowFooter"); +// ============================= +// 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; +} + + +// ============================= +// 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; + // Nur der aktuelle Schritt soll sichtbar sein steps.forEach((step, stepIndex) => { step.classList.toggle("step--active", stepIndex === index); }); - const isIntroStep = index === 0; + 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[index]; - errorMessage.textContent = ""; + nextButton.textContent = nextLabels[stepIndex]; +} - if (index === lastStep) { - updateReview(); + +// ============================= +// 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; + + if (stepIndex > 0) { + progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100; } - function updateProgress(index) { - if (index === 0) { - progressBar.style.width = "0%"; - return; - } - - const totalSteps = lastStep; - const progress = ((index - 1) / (totalSteps - 1)) * 100; - progressBar.style.width = `${progress}%`; } - window.scrollTo({ top: 0, behavior: "smooth" }); - updateProgress(index); -} +// ============================= +// 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. +// ============================= -function stepFields(stepIndex) { - return Array.from(steps[stepIndex].querySelectorAll("input, textarea, select")); -} - -function validateCurrentStep() { - if (currentStep === 0 || currentStep === lastStep) { - return true; +/** + * Führt das Update der Review nur aus, + * wenn wirklich der letzte Schritt geöffnet ist. + */ +function updateReviewIfNeeded(stepIndex) { + if (stepIndex === lastStep) { + updateReview(); } - - const fields = stepFields(currentStep); - - for (const field of fields) { - if (field.type === "radio") { - const group = Array.from(form.querySelectorAll(`input[name="${field.name}"]`)); - const isRequiredGroup = group.some((item) => item.required); - const hasSelection = group.some((item) => item.checked); - - if (isRequiredGroup && !hasSelection) { - errorMessage.textContent = "Bitte wähle eine Option aus, bevor du weitergehst."; - group[0].focus(); - return false; - } - - continue; - } - - if (field.type === "checkbox") { - continue; - } - - if (!field.checkValidity()) { - errorMessage.textContent = "Bitte fülle alle Pflichtfelder dieses Schritts aus."; - field.reportValidity(); - return false; - } - } - - errorMessage.textContent = ""; - return true; } +/** + * 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 || "–"; } @@ -111,11 +159,25 @@ function getFieldValue(name) { 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"; + 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 "–"; @@ -129,74 +191,253 @@ function formatDate(value) { }).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 values = { + const reviewValues = { eventTitle: getFieldValue("eventTitle"), eventType: getFieldValue("eventType"), menuDescription: getFieldValue("menuDescription"), eventDescription: getFieldValue("eventDescription"), maxGuests: getFieldValue("maxGuests"), dietType: getFieldValue("dietType"), - allergies: getCheckboxValues("allergies"), + allergies: buildAllergiesReviewValue(), eventDate: formatDate(getFieldValue("eventDate")), eventTime: getFieldValue("eventTime"), eventAddress: getFieldValue("eventAddress"), eventCity: getFieldValue("eventCity") }; - Object.entries(values).forEach(([key, value]) => { - const target = document.querySelector(`[data-review="${key}"]`); - if (target) { - target.textContent = value; - } + Object.entries(reviewValues).forEach(([key, value]) => { + updateReviewField(key, value); }); } -document.querySelectorAll("[data-start-flow]").forEach((button) => { - button.addEventListener("click", () => showStep(1)); -}); -backButton.addEventListener("click", () => { +// ============================= +// 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); + + // 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) { + 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()) { + 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); } -}); +} -nextButton.addEventListener("click", () => { +/** + * 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); - return; + } else { + form.requestSubmit(); } +} - form.requestSubmit(); -}); -form.addEventListener("submit", (event) => { +// ============================= +// 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(); - alert("Dein Event wurde vorbereitet und würde jetzt veröffentlicht werden."); -}); + alert("Event würde jetzt veröffentlicht werden."); +} -document.querySelectorAll("[data-counter]").forEach((counter) => { - const input = counter.querySelector("input[type='number']"); - const decreaseButton = counter.querySelector("[data-counter-action='decrease']"); - const increaseButton = counter.querySelector("[data-counter-action='increase']"); - decreaseButton.addEventListener("click", () => { - const min = Number(input.min || 1); - const currentValue = Number(input.value || min); - input.value = Math.max(min, currentValue - 1); +// ============================= +// 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); +} + + +// ============================= +// 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)); }); - increaseButton.addEventListener("click", () => { - const currentValue = Number(input.value || 0); - input.value = currentValue + 1; - }); -}); + // Navigation + backButton.addEventListener("click", handleBackClick); + nextButton.addEventListener("click", handleNextClick); -showStep(0); + // Formular absenden + form.addEventListener("submit", handleFormSubmit); + // Counter aktivieren + registerCounterHandlers(); + // Startzustand: Intro anzeigen + showStep(0); +} + +// Startpunkt des Skripts +initEventCreationFlow(); \ No newline at end of file