Anpassungen event erstellung Sprint 1

This commit is contained in:
Ysabelle Moser 2026-04-09 17:01:37 +02:00
parent c6d8df790e
commit ad6b6c2c8c
6 changed files with 709 additions and 127 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -6,6 +6,25 @@
--control-min-height: 3rem;
--input-min-height: 3.5rem;
--card-min-height: 6rem;
--color-bg: var(--butter);
--color-surface: var(--white);
--color-surface-soft: var(--butter-light);
--color-text: var(--black);
--color-text-secondary: rgba(34, 33, 26, 0.8);
--color-muted: rgba(34, 33, 26, 0.68);
--color-border: rgba(102, 52, 13, 0.16);
--color-border-strong: var(--brown);
--color-divider: rgba(102, 52, 13, 0.14);
--color-primary: var(--olive);
--color-primary-hover: var(--olive-dark);
--color-progress-bg: rgba(212, 75, 36, 0.18);
--color-focus: var(--blue);
--color-error: var(--error);
--shadow-soft: 0 12px 30px rgba(102, 52, 13, 0.1);
--input-border-soft: rgba(102, 52, 13, 0.2);
--input-border-focus: rgba(107, 107, 5, 0.45);
--input-shadow-focus: 0 0 0 4px rgba(107, 107, 5, 0.12);
}
*,
@ -108,6 +127,10 @@ a {
padding: var(--space-4) 0 var(--space-7);
}
.submission-success {
padding: var(--space-4) 0 var(--space-7);
}
.step--active {
display: block;
}
@ -123,6 +146,7 @@ a {
min-height: 60vh;
align-content: center;
grid-template-columns: 1fr;
gap: var(--space-7);
}
.step-copy,
@ -185,9 +209,22 @@ h2 {
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
}
.intro-card-emoji {
font-size: 2rem;
margin-bottom: var(--space-3);
.intro-card--image {
width: 100%;
padding: 0;
border: 0;
overflow: hidden;
background: transparent;
box-shadow: none;
}
.intro-image {
width: 100%;
aspect-ratio: 16 / 10;
display: block;
object-fit: cover;
border-radius: 1.875rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
label,
@ -208,11 +245,13 @@ input[type="number"],
textarea {
width: 100%;
min-height: var(--input-min-height);
padding: 0.95rem 1rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
background: var(--color-surface);
padding: 1rem 1.1rem;
border: 1px solid var(--input-border-soft);
border-radius: 1.125rem;
background: var(--butter-light);
color: var(--color-text);
box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04);
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
textarea {
@ -220,6 +259,30 @@ textarea {
resize: vertical;
}
input[type="text"]:hover,
input[type="date"]:hover,
input[type="time"]:hover,
input[type="number"]:hover,
textarea:hover {
border-color: rgba(102, 52, 13, 0.28);
}
input[type="text"]:focus,
input[type="date"]:focus,
input[type="time"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: var(--input-border-focus);
box-shadow: var(--input-shadow-focus);
background: var(--butter-light);
outline: none;
}
.field-invalid {
border-color: var(--tomato) !important;
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
}
.field-row {
display: grid;
gap: var(--space-4);
@ -238,8 +301,8 @@ textarea {
padding: 1rem 1rem 1rem 1.05rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
background: var(--color-surface);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
background: var(--butter-light);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
.option-card small {
@ -247,6 +310,7 @@ textarea {
}
.option-card:hover {
background: var(--olive-light);
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
@ -259,7 +323,18 @@ textarea {
}
.option-card:has(input:checked) {
border: 2px solid var(--color-border-strong);
border: 1px solid var(--color-primary);
background: var(--color-primary);
color: var(--white);
}
.option-card:has(input:checked) small {
color: rgba(247, 246, 230, 0.88);
}
.option-card--invalid {
border-color: var(--tomato) !important;
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
}
.counter {
@ -281,15 +356,44 @@ textarea {
.counter-button {
width: var(--control-min-height);
height: var(--control-min-height);
border: 1px solid var(--color-border);
border: 1px solid var(--color-primary);
border-radius: 50%;
background: var(--color-surface);
background: var(--color-primary);
color: var(--white);
font-size: 1.5rem;
line-height: 1;
box-shadow: 0 6px 16px rgba(107, 107, 5, 0.18);
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.counter-button:hover {
background: var(--color-primary-hover);
transform: translateY(-1px);
}
.counter-button:focus-visible {
outline: 3px solid rgba(107, 107, 5, 0.22);
outline-offset: 3px;
}
.review-card {
padding: var(--space-5);
border-radius: var(--radius-lg);
display: grid;
gap: var(--space-4);
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.review-card--success {
display: grid;
gap: var(--space-5);
padding: var(--space-3) 0 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.review-list {
@ -300,14 +404,29 @@ textarea {
.review-item {
display: grid;
gap: var(--space-1);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-divider);
gap: var(--space-2);
padding: 1rem 1.1rem;
border: 1px solid var(--input-border-soft);
border-radius: 1.125rem;
background: var(--butter-light);
box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04);
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
.review-item:last-child {
border-bottom: 0;
padding-bottom: 0;
border-bottom: 1px solid var(--input-border-soft);
}
.review-item:hover {
border-color: rgba(102, 52, 13, 0.28);
background: rgba(247, 246, 230, 0.92);
transform: translateY(-1px);
}
.review-item:focus-visible {
outline: 3px solid rgba(107, 107, 5, 0.2);
outline-offset: 3px;
}
.review-item dt {
@ -320,16 +439,29 @@ textarea {
color: var(--color-text-secondary);
}
.submission-success-actions {
display: flex;
justify-content: center;
}
.flow-footer {
position: sticky;
bottom: 0;
z-index: 5;
margin-top: auto;
background: rgba(247, 247, 242, 0.96);
backdrop-filter: blur(8px);
background: var(--color-bg);
backdrop-filter: none;
padding-top: var(--space-4);
padding-bottom: env(safe-area-inset-bottom);
}
.progress-wrap {
position: relative;
width: min(100%, var(--content-width));
margin: 0 auto;
padding-top: 4.35rem;
}
.progress {
width: 100%;
height: 0.375rem;
@ -340,15 +472,50 @@ textarea {
display: block;
width: 0;
height: 100%;
background: var(--color-primary);
background: var(--tomato);
transition: width 0.25s ease;
}
.progress-marker {
position: absolute;
top: 0;
transform: translateX(-50%);
display: grid;
justify-items: center;
gap: 0.2rem;
pointer-events: none;
}
.progress-marker::after {
content: "";
width: 0.125rem;
height: 1rem;
background: var(--tomato);
border-radius: 999px;
}
.progress-marker__circle {
width: 2.9rem;
height: 2.9rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--tomato);
color: var(--butter-light);
font-size: 1.35rem;
font-weight: 600;
line-height: 1;
box-shadow: 0 10px 24px rgba(212, 75, 36, 0.18);
}
.flow-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
width: min(100%, var(--content-width));
margin: 0 auto;
padding: var(--space-4) 0;
}
@ -455,8 +622,10 @@ textarea:focus-visible {
@media (min-width: 768px) {
.step-layout--intro {
grid-template-columns: 1.25fr 0.8fr;
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: center;
gap: var(--space-8);
}
.field-row {

View File

@ -43,16 +43,19 @@
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<p class="step-text">
Erzähl uns von deiner Idee vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event Schritt für Schritt aufzubauen.
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
</p>
<button type="button" class="button button--primary button--intro" data-start-flow>
Los gehts!
</button>
</div>
<aside class="intro-card" aria-label="Hinweis zur Event-Erstellung">
<div class="intro-card-emoji" aria-hidden="true">🍽️</div>
<p>Aus einer Idee wird Schritt für Schritt dein Event.</p>
<aside class="intro-card intro-card--image" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable.jpg"
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
/>
</aside>
</div>
</section>
@ -69,11 +72,6 @@
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heißen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<fieldset class="form-field">
<legend>Art des Essens / Eventtyp</legend>
@ -99,47 +97,7 @@
</label>
</div>
</fieldset>
</div>
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text">
Mach uns neugierig. Was kochst du und wie fühlt sich dein Abend an?
Hier entsteht die Geschichte, auf die sich deine Gäste freuen.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="menuDescription">Was ist das Menü?</label>
<textarea id="menuDescription" name="menuDescription" rows="5" required></textarea>
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 3</p>
<h2 id="step3-title">Wen lädst du ein?</h2>
<p class="step-text">
Wie viele Gäste passen zu deinem Event? Und gibt es etwas, das du bei
Ernährung oder Unverträglichkeiten beachten möchtest?
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Maximale Personenanzahl</legend>
@ -171,7 +129,21 @@
</button>
</div>
</fieldset>
</div>
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text">
Mach uns neugierig. Was gibt es zu essen? Gibt es eine bestimmte Ernährungsform oder ein Motto? Je mehr du verrätst, desto besser können sich deine Gäste auf dein Event freuen.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Ernährungsform</legend>
@ -194,6 +166,25 @@
</div>
</fieldset>
<div class="form-field">
<label for="menuDescription">Was ist das Menü?</label>
<textarea id="menuDescription" name="menuDescription" rows="5" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 3</p>
<h2 id="step3-title">Gibt es etwas zu beachten?</h2>
<p class="step-text">
Gibt es Allergien, Unverträglichkeiten oder andere Hinweise, die für dein Event wichtig sind? So wissen deine Gäste gleich, worauf sie sich einstellen können.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Allergene / Unverträglichkeiten</legend>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p>
@ -214,12 +205,12 @@
<span>ohne Nüsse</span>
</label>
</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>
<div class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
</div>
</div>
</section>
@ -230,8 +221,7 @@
<p class="step-kicker">Schritt 4</p>
<h2 id="step4-title">Wann findet dein Event statt?</h2>
<p class="step-text">
Wähle Datum und Uhrzeit und sag uns, wo dein Event stattfindet.
Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
Wähle Datum und Uhrzeit für dein Event. So können deine Gäste direkt einschätzen, ob der Termin für sie passt.
</p>
</div>
@ -247,7 +237,21 @@
<input type="time" id="eventTime" name="eventTime" required />
</div>
</div>
</div>
</div>
</section>
<section class="step" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 5</p>
<h2 id="step5-title">Wo findet dein Event statt?</h2>
<p class="step-text">
Sag uns, wo dein Event stattfindet. Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventAddress">Adresse</label>
<input type="text" id="eventAddress" name="eventAddress" autocomplete="street-address" required />
@ -261,81 +265,111 @@
</div>
</section>
<section class="step" data-step="5" aria-labelledby="step5-title">
<section class="step" data-step="6" aria-labelledby="step6-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 5</p>
<h2 id="step5-title">Alles bereit für deine Gäste?</h2>
<p class="step-kicker">Schritt 6</p>
<h2 id="step6-title">Gib deinem Event den letzten Schliff.</h2>
<p class="step-text">
Schau dir dein Event nochmal in Ruhe an. Passt alles?
Dann kannst du es jetzt veröffentlichen und Gäste einladen.
Jetzt bekommt dein Event seinen Namen und die Atmosphäre, die Lust aufs Dabeisein macht.
Ein klarer Titel (z.B. "Italienische Tavolata") und ein guter Beschreibungstext (Ablauf etc.) machen den Unterschied.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heißen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="7" aria-labelledby="step7-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 7</p>
<h2 id="step7-title">Dein Event auf einen Blick.</h2>
<p class="step-text">
Schau dir alle Details nochmal in Ruhe an. Wenn alles passt,
kannst du dein Event jetzt veröffentlichen und Gäste einladen.
</p>
</div>
<div class="review-card" aria-live="polite">
<dl class="review-list">
<div class="review-item">
<dt>Eventtitel</dt>
<dd data-review="eventTitle"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="1" data-edit-field="eventType" role="button" tabindex="0" aria-label="Eventtyp bearbeiten">
<dt>Eventtyp</dt>
<dd data-review="eventType"></dd>
</div>
<div class="review-item">
<dt>Menü</dt>
<dd data-review="menuDescription"></dd>
</div>
<div class="review-item">
<dt>Event-Abend</dt>
<dd data-review="eventDescription"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="1" data-edit-field="maxGuests" role="button" tabindex="0" aria-label="Maximale Personenanzahl bearbeiten">
<dt>Maximale Personenanzahl</dt>
<dd data-review="maxGuests"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="2" data-edit-field="dietType" role="button" tabindex="0" aria-label="Ernährungsform bearbeiten">
<dt>Ernährungsform</dt>
<dd data-review="dietType"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="2" data-edit-field="menuDescription" role="button" tabindex="0" aria-label="Menü bearbeiten">
<dt>Menü</dt>
<dd data-review="menuDescription"></dd>
</div>
<div class="review-item" data-edit-step="3" data-edit-field="allergiesOther" role="button" tabindex="0" aria-label="Allergene und Unverträglichkeiten bearbeiten">
<dt>Allergene / Unverträglichkeiten</dt>
<dd data-review="allergies">Keine Angabe</dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="4" data-edit-field="eventDate" role="button" tabindex="0" aria-label="Datum bearbeiten">
<dt>Datum</dt>
<dd data-review="eventDate"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="4" data-edit-field="eventTime" role="button" tabindex="0" aria-label="Uhrzeit bearbeiten">
<dt>Uhrzeit</dt>
<dd data-review="eventTime"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="5" data-edit-field="eventAddress" role="button" tabindex="0" aria-label="Adresse bearbeiten">
<dt>Adresse</dt>
<dd data-review="eventAddress"></dd>
</div>
<div class="review-item">
<div class="review-item" data-edit-step="5" data-edit-field="eventCity" role="button" tabindex="0" aria-label="Ort bearbeiten">
<dt>Ort</dt>
<dd data-review="eventCity"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventTitle" role="button" tabindex="0" aria-label="Eventtitel bearbeiten">
<dt>Eventtitel</dt>
<dd data-review="eventTitle"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventDescription" role="button" tabindex="0" aria-label="Event-Abend bearbeiten">
<dt>Event-Abend</dt>
<dd data-review="eventDescription"></dd>
</div>
</dl>
</div>
</div>
</section>
<div class="flow-footer" id="flowFooter" hidden>
<div class="progress" aria-hidden="true">
<span id="progressBar" class="progress-bar"></span>
<div class="progress-wrap" aria-hidden="true">
<div class="progress-marker" id="progressMarker">
<span class="progress-marker__circle" id="progressMarkerLabel">1</span>
</div>
<div class="progress">
<span id="progressBar" class="progress-bar"></span>
</div>
</div>
<div class="flow-actions">
@ -347,6 +381,31 @@
</div>
</div>
</div>
<section
id="submissionSuccess"
class="submission-success"
aria-labelledby="success-title"
aria-live="polite"
hidden
>
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Event erstellt</p>
<h2 id="success-title">Dein Event ist ready.</h2>
<p class="step-text">
Sieht gut aus: Deine Idee ist jetzt live und bereit für Gäste.
Im Profil kannst du dein Event anschauen, verwalten oder direkt das nächste planen.
</p>
</div>
<div class="review-card review-card--success">
<div class="submission-success-actions">
<a class="button button--primary" href="event_overview.html">Weiter zu deinem Profil</a>
</div>
</div>
</div>
</section>
</form>
</main>

View File

@ -8,9 +8,13 @@ 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
@ -27,7 +31,9 @@ const nextLabels = {
2: "Weiter",
3: "Weiter",
4: "Weiter",
5: "Event veröffentlichen"
5: "Weiter",
6: "Weiter",
7: "Event veröffentlichen"
};
// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen
@ -57,6 +63,39 @@ 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
@ -71,6 +110,8 @@ function setErrorMessage(message = "") {
*/
function showStep(index) {
currentStep = index;
submissionSuccess.hidden = true;
clearStepInvalidState(index);
// Nur der aktuelle Schritt soll sichtbar sein
steps.forEach((step, stepIndex) => {
@ -115,12 +156,21 @@ function updateFlowVisibility(stepIndex) {
*/
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);
}
@ -225,7 +275,18 @@ function buildAllergiesReviewValue() {
* und schreibt sie gesammelt in die Review-Ansicht.
*/
function updateReview() {
const reviewValues = {
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"),
@ -238,12 +299,137 @@ function updateReview() {
eventAddress: getFieldValue("eventAddress"),
eventCity: getFieldValue("eventCity")
};
Object.entries(reviewValues).forEach(([key, value]) => {
updateReviewField(key, value);
});
}
/**
* 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
@ -263,6 +449,7 @@ function validateCurrentStep() {
if (currentStep === 0 || currentStep === lastStep) return true;
const fields = getStepFields(currentStep);
clearStepInvalidState(currentStep);
// Zuerst Radio-Gruppen prüfen
const radioCheck = validateRadioGroups(fields);
@ -297,6 +484,7 @@ function validateRadioGroups(fields) {
const selected = group.some(f => f.checked);
if (required && !selected) {
markRadioGroupInvalid(group);
return {
isValid: false,
message: "Bitte wähle eine Option aus."
@ -318,6 +506,7 @@ function validateRequiredFields(fields) {
if (field.type === "radio" || field.type === "checkbox") continue;
if (!field.checkValidity()) {
markFieldInvalid(field);
return {
isValid: false,
message: "Bitte fülle alle Pflichtfelder aus."
@ -374,11 +563,12 @@ function handleNextClick() {
*/
function handleFormSubmit(event) {
event.preventDefault();
// 1. Feedback geben
alert("Dein Event wurde erfolgreich veröffentlicht!");
// 2. Weiterleiten (z. B. zur Event-Übersicht)
window.location.href = "event_overview.html";
saveCurrentEvent();
steps.forEach(step => step.classList.remove("step--active"));
flowFooter.hidden = true;
submissionSuccess.hidden = false;
setErrorMessage("");
window.scrollTo({ top: 0, behavior: "smooth" });
}
@ -412,6 +602,126 @@ function updateCounterValue(input, change) {
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
@ -438,8 +748,12 @@ function initEventCreationFlow() {
// Counter aktivieren
registerCounterHandlers();
registerMenuBulletHandler();
registerValidationFeedbackHandlers();
registerReviewEditHandlers();
// Startzustand: Intro anzeigen
submissionSuccess.hidden = true;
showStep(0);
}

View File

@ -1,4 +1,5 @@
document.addEventListener('DOMContentLoaded', async () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
// -------------------------------------------------------------
// DOM entry point and shared asset path.
// -------------------------------------------------------------
@ -14,10 +15,21 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
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 [];
}
}
// Fetch data source and resolve the matching event record.
try {
const response = await fetch('data/events.json');
const allEvents = await response.json();
const apiEvents = await response.json();
const allEvents = [...getStoredEvents(), ...apiEvents];
const event = allEvents.find(e => e.id === eventId);
if (event) {

View File

@ -1,4 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
@ -14,6 +15,16 @@ document.addEventListener('DOMContentLoaded', () => {
let allEvents = [];
let activeCategory = 'ALLE';
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 [];
}
}
// -------------------------------------------------------------
// Initial data bootstrap:
// 1) fetch JSON,
@ -24,7 +35,9 @@ document.addEventListener('DOMContentLoaded', () => {
async function fetchEvents() {
try {
const response = await fetch('data/events.json');
allEvents = await response.json();
const apiEvents = await response.json();
const localEvents = getStoredEvents();
allEvents = [...localEvents, ...apiEvents];
populateMetaFilters();
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
@ -62,6 +75,10 @@ document.addEventListener('DOMContentLoaded', () => {
// Convert localized event date (e.g. 19. MÄR. 2026) into ISO format for date input comparison.
function parseEventDateToIso(dateString) {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
return dateString;
}
const months = {
JAN: '01',
FEB: '02',
@ -92,6 +109,11 @@ document.addEventListener('DOMContentLoaded', () => {
// Convert short month notation into full German month label for UI display.
function formatEventDate(dateString) {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-');
return `${Number(day)}. ${['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1]} ${year}`;
}
const labels = {
JAN: 'Januar',
FEB: 'Februar',
@ -122,7 +144,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Normalize time label from UHR to Uhr for consistent typography.
function formatEventTime(timeString) {
return timeString.replace('UHR', 'Uhr').trim();
if (!timeString) {
return '';
}
return timeString.includes('UHR')
? timeString.replace('UHR', 'Uhr').trim()
: `${timeString} Uhr`;
}
// Safely verify whether a value exists in the given select element.