event-erstellung #2
122
event-create.css
122
event-create.css
@ -1,7 +1,9 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-bg: #f7f7f2;
|
--color-bg: #f7f7f2;
|
||||||
--color-surface: #ffffff;
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-soft: #f0eee7;
|
||||||
--color-text: #1f1f1f;
|
--color-text: #1f1f1f;
|
||||||
|
--color-text-secondary: #303030;
|
||||||
--color-muted: #6b6b6b;
|
--color-muted: #6b6b6b;
|
||||||
--color-border: #d8d8d2;
|
--color-border: #d8d8d2;
|
||||||
--color-border-strong: #202020;
|
--color-border-strong: #202020;
|
||||||
@ -9,10 +11,16 @@
|
|||||||
--color-primary-hover: #111111;
|
--color-primary-hover: #111111;
|
||||||
--color-focus: #2f6fed;
|
--color-focus: #2f6fed;
|
||||||
--color-error: #b42318;
|
--color-error: #b42318;
|
||||||
|
--color-progress-bg: #ddddda;
|
||||||
|
--color-divider: #ecece7;
|
||||||
|
|
||||||
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.06);
|
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
--radius-sm: 0.875rem;
|
--radius-sm: 0.875rem;
|
||||||
--radius-md: 1.25rem;
|
--radius-md: 1.25rem;
|
||||||
--radius-lg: 999px;
|
--radius-lg: 1.5rem;
|
||||||
|
--radius-pill: 999px;
|
||||||
|
|
||||||
--space-1: 0.25rem;
|
--space-1: 0.25rem;
|
||||||
--space-2: 0.5rem;
|
--space-2: 0.5rem;
|
||||||
--space-3: 0.75rem;
|
--space-3: 0.75rem;
|
||||||
@ -21,8 +29,14 @@
|
|||||||
--space-6: 2rem;
|
--space-6: 2rem;
|
||||||
--space-7: 3rem;
|
--space-7: 3rem;
|
||||||
--space-8: 4rem;
|
--space-8: 4rem;
|
||||||
|
|
||||||
--max-width: 1120px;
|
--max-width: 1120px;
|
||||||
|
--content-width: 760px;
|
||||||
--header-height: 4.5rem;
|
--header-height: 4.5rem;
|
||||||
|
|
||||||
|
--control-min-height: 3rem;
|
||||||
|
--input-min-height: 3.5rem;
|
||||||
|
--card-min-height: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@ -130,7 +144,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-layout {
|
.step-layout {
|
||||||
width: min(100%, 760px);
|
width: min(100%, var(--content-width));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-6);
|
gap: var(--space-6);
|
||||||
@ -142,11 +156,30 @@ a {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-copy {
|
.step-copy,
|
||||||
|
.step-fields,
|
||||||
|
.form-field,
|
||||||
|
fieldset {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
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 {
|
.step-kicker {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -165,17 +198,22 @@ h2 {
|
|||||||
.step-text {
|
.step-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 42rem;
|
max-width: 42rem;
|
||||||
color: #303030;
|
color: var(--color-text-secondary);
|
||||||
font-size: clamp(1rem, 1.4vw, 1.2rem);
|
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 {
|
.intro-card {
|
||||||
max-width: 24rem;
|
max-width: 24rem;
|
||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 1.75rem;
|
border-radius: 1.75rem;
|
||||||
background: linear-gradient(135deg, #ffffff, #f0eee7);
|
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-card-emoji {
|
.intro-card-emoji {
|
||||||
@ -183,20 +221,6 @@ h2 {
|
|||||||
margin-bottom: var(--space-3);
|
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,
|
label,
|
||||||
legend {
|
legend {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -214,7 +238,7 @@ input[type="time"],
|
|||||||
input[type="number"],
|
input[type="number"],
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 3.5rem;
|
min-height: var(--input-min-height);
|
||||||
padding: 0.95rem 1rem;
|
padding: 0.95rem 1rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@ -241,7 +265,7 @@ textarea {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
min-height: 6rem;
|
min-height: var(--card-min-height);
|
||||||
padding: 1rem 1rem 1rem 1.05rem;
|
padding: 1rem 1rem 1rem 1.05rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@ -280,9 +304,14 @@ textarea {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.counter-button,
|
||||||
|
.button {
|
||||||
|
min-height: var(--control-min-height);
|
||||||
|
}
|
||||||
|
|
||||||
.counter-button {
|
.counter-button {
|
||||||
width: 3rem;
|
width: var(--control-min-height);
|
||||||
height: 3rem;
|
height: var(--control-min-height);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
@ -291,10 +320,7 @@ textarea {
|
|||||||
|
|
||||||
.review-card {
|
.review-card {
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
border: 1px solid var(--color-border);
|
border-radius: var(--radius-lg);
|
||||||
border-radius: 1.5rem;
|
|
||||||
background: var(--color-surface);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-list {
|
.review-list {
|
||||||
@ -307,7 +333,7 @@ textarea {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
padding-bottom: var(--space-4);
|
padding-bottom: var(--space-4);
|
||||||
border-bottom: 1px solid #ecece7;
|
border-bottom: 1px solid var(--color-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-item:last-child {
|
.review-item:last-child {
|
||||||
@ -322,7 +348,7 @@ textarea {
|
|||||||
.review-item dd {
|
.review-item dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
color: #303030;
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-footer {
|
.flow-footer {
|
||||||
@ -338,7 +364,7 @@ textarea {
|
|||||||
.progress {
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.375rem;
|
height: 0.375rem;
|
||||||
background: #ddddda;
|
background: var(--color-progress-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@ -374,9 +400,8 @@ textarea {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 3rem;
|
|
||||||
padding: 0.9rem 1.35rem;
|
padding: 0.9rem 1.35rem;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-pill);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@ -430,6 +455,35 @@ textarea:focus-visible {
|
|||||||
outline-offset: 3px;
|
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) {
|
@media (min-width: 768px) {
|
||||||
.step-layout--intro {
|
.step-layout--intro {
|
||||||
grid-template-columns: 1.25fr 0.8fr;
|
grid-template-columns: 1.25fr 0.8fr;
|
||||||
|
|||||||
@ -25,7 +25,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form id="eventForm" class="event-form" novalidate>
|
<form id="eventForm" class="event-form" novalidate>
|
||||||
<section class="step step--active step--intro" data-step="0" aria-labelledby="intro-title">
|
<section
|
||||||
|
class="step step--active step--intro"
|
||||||
|
data-step="0"
|
||||||
|
aria-labelledby="intro-title"
|
||||||
|
>
|
||||||
<div class="step-layout step-layout--intro">
|
<div class="step-layout step-layout--intro">
|
||||||
<div class="step-copy">
|
<div class="step-copy">
|
||||||
<p class="step-kicker">Event erstellen</p>
|
<p class="step-kicker">Event erstellen</p>
|
||||||
@ -39,8 +43,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="intro-card" aria-hidden="true">
|
<aside class="intro-card" aria-label="Hinweis zur Event-Erstellung">
|
||||||
<div class="intro-card-emoji">🍽️</div>
|
<div class="intro-card-emoji" aria-hidden="true">🍽️</div>
|
||||||
<p>Aus einer Idee wird Schritt für Schritt dein Event.</p>
|
<p>Aus einer Idee wird Schritt für Schritt dein Event.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="form-field">
|
<fieldset class="form-field">
|
||||||
<legend>Art des Essens</legend>
|
<legend>Art des Essens / Eventtyp</legend>
|
||||||
|
|
||||||
<div class="option-grid option-grid--4">
|
<div class="option-grid option-grid--4">
|
||||||
<label class="option-card">
|
<label class="option-card">
|
||||||
@ -133,7 +137,14 @@
|
|||||||
<legend>Maximale Personenanzahl</legend>
|
<legend>Maximale Personenanzahl</legend>
|
||||||
|
|
||||||
<div class="counter" data-counter>
|
<div class="counter" data-counter>
|
||||||
<button type="button" class="counter-button" data-counter-action="decrease" aria-label="Personenzahl verringern">−</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="counter-button"
|
||||||
|
data-counter-action="decrease"
|
||||||
|
aria-label="Personenzahl verringern"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="maxGuests"
|
id="maxGuests"
|
||||||
@ -143,7 +154,14 @@
|
|||||||
value="4"
|
value="4"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<button type="button" class="counter-button" data-counter-action="increase" aria-label="Personenzahl erhöhen">+</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="counter-button"
|
||||||
|
data-counter-action="increase"
|
||||||
|
aria-label="Personenzahl erhöhen"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@ -189,6 +207,11 @@
|
|||||||
<span>ohne Nüsse</span>
|
<span>ohne Nüsse</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
|
||||||
|
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -312,7 +335,7 @@
|
|||||||
<button type="button" id="backButton" class="button button--text">Zurück</button>
|
<button type="button" id="backButton" class="button button--text">Zurück</button>
|
||||||
|
|
||||||
<div class="flow-actions-right">
|
<div class="flow-actions-right">
|
||||||
<p id="errorMessage" class="error-message" role="alert"></p>
|
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>
|
||||||
<button type="button" id="nextButton" class="button button--primary">Weiter</button>
|
<button type="button" id="nextButton" class="button button--primary">Weiter</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -321,7 +344,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<p>© Invité</p>
|
<p>© Social Cooking</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="event-create.js"></script>
|
<script src="event-create.js"></script>
|
||||||
|
|||||||
425
event-create.js
425
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 form = document.getElementById("eventForm");
|
||||||
const steps = Array.from(document.querySelectorAll(".step"));
|
const steps = Array.from(document.querySelectorAll(".step"));
|
||||||
const backButton = document.getElementById("backButton");
|
const backButton = document.getElementById("backButton");
|
||||||
@ -5,10 +10,17 @@ const nextButton = document.getElementById("nextButton");
|
|||||||
const progressBar = document.getElementById("progressBar");
|
const progressBar = document.getElementById("progressBar");
|
||||||
const errorMessage = document.getElementById("errorMessage");
|
const errorMessage = document.getElementById("errorMessage");
|
||||||
const usernameElement = document.getElementById("username");
|
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;
|
let currentStep = 0;
|
||||||
const lastStep = steps.length - 1;
|
const lastStep = steps.length - 1;
|
||||||
|
|
||||||
|
// Text für den Weiter-Button je nach Schritt
|
||||||
const nextLabels = {
|
const nextLabels = {
|
||||||
0: "Weiter",
|
0: "Weiter",
|
||||||
1: "Weiter",
|
1: "Weiter",
|
||||||
@ -18,92 +30,128 @@ const nextLabels = {
|
|||||||
5: "Event veröffentlichen"
|
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";
|
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) {
|
function showStep(index) {
|
||||||
currentStep = index;
|
currentStep = index;
|
||||||
|
|
||||||
|
// Nur der aktuelle Schritt soll sichtbar sein
|
||||||
steps.forEach((step, stepIndex) => {
|
steps.forEach((step, stepIndex) => {
|
||||||
step.classList.toggle("step--active", stepIndex === index);
|
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;
|
flowFooter.hidden = isIntroStep;
|
||||||
backButton.hidden = isIntroStep;
|
backButton.hidden = isIntroStep;
|
||||||
nextButton.textContent = nextLabels[index];
|
nextButton.textContent = nextLabels[stepIndex];
|
||||||
errorMessage.textContent = "";
|
}
|
||||||
|
|
||||||
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}%`;
|
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"));
|
* Führt das Update der Review nur aus,
|
||||||
}
|
* wenn wirklich der letzte Schritt geöffnet ist.
|
||||||
|
*/
|
||||||
function validateCurrentStep() {
|
function updateReviewIfNeeded(stepIndex) {
|
||||||
if (currentStep === 0 || currentStep === lastStep) {
|
if (stepIndex === lastStep) {
|
||||||
return true;
|
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) {
|
function getFieldValue(name) {
|
||||||
const field = form.elements[name];
|
const field = form.elements[name];
|
||||||
|
|
||||||
if (!field) return "–";
|
if (!field) return "–";
|
||||||
|
|
||||||
|
// Spezialfall: Radio-Gruppen verhalten sich anders als normale Inputs
|
||||||
if (field instanceof RadioNodeList) {
|
if (field instanceof RadioNodeList) {
|
||||||
return field.value || "–";
|
return field.value || "–";
|
||||||
}
|
}
|
||||||
@ -111,11 +159,25 @@ function getFieldValue(name) {
|
|||||||
return field.value.trim() || "–";
|
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) {
|
function getCheckboxValues(name) {
|
||||||
const checked = Array.from(form.querySelectorAll(`input[name="${name}"]:checked`));
|
const checked = Array.from(
|
||||||
return checked.length ? checked.map((item) => item.value).join(", ") : "Keine Angabe";
|
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) {
|
function formatDate(value) {
|
||||||
if (!value || value === "–") return "–";
|
if (!value || value === "–") return "–";
|
||||||
|
|
||||||
@ -129,74 +191,253 @@ function formatDate(value) {
|
|||||||
}).format(date);
|
}).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() {
|
function updateReview() {
|
||||||
const values = {
|
const reviewValues = {
|
||||||
eventTitle: getFieldValue("eventTitle"),
|
eventTitle: getFieldValue("eventTitle"),
|
||||||
eventType: getFieldValue("eventType"),
|
eventType: getFieldValue("eventType"),
|
||||||
menuDescription: getFieldValue("menuDescription"),
|
menuDescription: getFieldValue("menuDescription"),
|
||||||
eventDescription: getFieldValue("eventDescription"),
|
eventDescription: getFieldValue("eventDescription"),
|
||||||
maxGuests: getFieldValue("maxGuests"),
|
maxGuests: getFieldValue("maxGuests"),
|
||||||
dietType: getFieldValue("dietType"),
|
dietType: getFieldValue("dietType"),
|
||||||
allergies: getCheckboxValues("allergies"),
|
allergies: buildAllergiesReviewValue(),
|
||||||
eventDate: formatDate(getFieldValue("eventDate")),
|
eventDate: formatDate(getFieldValue("eventDate")),
|
||||||
eventTime: getFieldValue("eventTime"),
|
eventTime: getFieldValue("eventTime"),
|
||||||
eventAddress: getFieldValue("eventAddress"),
|
eventAddress: getFieldValue("eventAddress"),
|
||||||
eventCity: getFieldValue("eventCity")
|
eventCity: getFieldValue("eventCity")
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(values).forEach(([key, value]) => {
|
Object.entries(reviewValues).forEach(([key, value]) => {
|
||||||
const target = document.querySelector(`[data-review="${key}"]`);
|
updateReviewField(key, value);
|
||||||
if (target) {
|
|
||||||
target.textContent = 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) {
|
if (currentStep > 1) {
|
||||||
showStep(currentStep - 1);
|
showStep(currentStep - 1);
|
||||||
} else {
|
} else {
|
||||||
showStep(0);
|
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 (!validateCurrentStep()) return;
|
||||||
|
|
||||||
if (currentStep < lastStep) {
|
if (currentStep < lastStep) {
|
||||||
showStep(currentStep + 1);
|
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();
|
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);
|
// STEP 8: Counter-Felder (+ / -)
|
||||||
const currentValue = Number(input.value || min);
|
// Für Zahlenfelder wie z. B. Anzahl Gäste.
|
||||||
input.value = Math.max(min, currentValue - 1);
|
// =============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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", () => {
|
// Navigation
|
||||||
const currentValue = Number(input.value || 0);
|
backButton.addEventListener("click", handleBackClick);
|
||||||
input.value = currentValue + 1;
|
nextButton.addEventListener("click", handleNextClick);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
showStep(0);
|
// Formular absenden
|
||||||
|
form.addEventListener("submit", handleFormSubmit);
|
||||||
|
|
||||||
|
// Counter aktivieren
|
||||||
|
registerCounterHandlers();
|
||||||
|
|
||||||
|
// Startzustand: Intro anzeigen
|
||||||
|
showStep(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startpunkt des Skripts
|
||||||
|
initEventCreationFlow();
|
||||||
Loading…
x
Reference in New Issue
Block a user