Merge pull request 'Anpassungen event erstellung Sprint 1' (#10) from event_create into main

Reviewed-on: #10
This commit is contained in:
Ysabelle Moser 2026-04-09 17:24:38 +02:00
commit 46074df578
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; --control-min-height: 3rem;
--input-min-height: 3.5rem; --input-min-height: 3.5rem;
--card-min-height: 6rem; --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); padding: var(--space-4) 0 var(--space-7);
} }
.submission-success {
padding: var(--space-4) 0 var(--space-7);
}
.step--active { .step--active {
display: block; display: block;
} }
@ -123,6 +146,7 @@ a {
min-height: 60vh; min-height: 60vh;
align-content: center; align-content: center;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--space-7);
} }
.step-copy, .step-copy,
@ -185,9 +209,22 @@ h2 {
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft)); background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
} }
.intro-card-emoji { .intro-card--image {
font-size: 2rem; width: 100%;
margin-bottom: var(--space-3); 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, label,
@ -208,11 +245,13 @@ input[type="number"],
textarea { textarea {
width: 100%; width: 100%;
min-height: var(--input-min-height); min-height: var(--input-min-height);
padding: 0.95rem 1rem; padding: 1rem 1.1rem;
border: 1px solid var(--color-border); border: 1px solid var(--input-border-soft);
border-radius: 1rem; border-radius: 1.125rem;
background: var(--color-surface); background: var(--butter-light);
color: var(--color-text); 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 { textarea {
@ -220,6 +259,30 @@ textarea {
resize: vertical; 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 { .field-row {
display: grid; display: grid;
gap: var(--space-4); gap: var(--space-4);
@ -238,8 +301,8 @@ textarea {
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;
background: var(--color-surface); background: var(--butter-light);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; 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 { .option-card small {
@ -247,6 +310,7 @@ textarea {
} }
.option-card:hover { .option-card:hover {
background: var(--olive-light);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
} }
@ -259,7 +323,18 @@ textarea {
} }
.option-card:has(input:checked) { .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 { .counter {
@ -281,15 +356,44 @@ textarea {
.counter-button { .counter-button {
width: var(--control-min-height); width: var(--control-min-height);
height: var(--control-min-height); height: var(--control-min-height);
border: 1px solid var(--color-border); border: 1px solid var(--color-primary);
border-radius: 50%; border-radius: 50%;
background: var(--color-surface); background: var(--color-primary);
color: var(--white);
font-size: 1.5rem; 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 { .review-card {
padding: var(--space-5); display: grid;
border-radius: var(--radius-lg); 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 { .review-list {
@ -300,14 +404,29 @@ textarea {
.review-item { .review-item {
display: grid; display: grid;
gap: var(--space-1); gap: var(--space-2);
padding-bottom: var(--space-4); padding: 1rem 1.1rem;
border-bottom: 1px solid var(--color-divider); 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 { .review-item:last-child {
border-bottom: 0; border-bottom: 1px solid var(--input-border-soft);
padding-bottom: 0; }
.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 { .review-item dt {
@ -320,16 +439,29 @@ textarea {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.submission-success-actions {
display: flex;
justify-content: center;
}
.flow-footer { .flow-footer {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: 5; z-index: 5;
margin-top: auto; margin-top: auto;
background: rgba(247, 247, 242, 0.96); background: var(--color-bg);
backdrop-filter: blur(8px); backdrop-filter: none;
padding-top: var(--space-4);
padding-bottom: env(safe-area-inset-bottom); 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 { .progress {
width: 100%; width: 100%;
height: 0.375rem; height: 0.375rem;
@ -340,15 +472,50 @@ textarea {
display: block; display: block;
width: 0; width: 0;
height: 100%; height: 100%;
background: var(--color-primary); background: var(--tomato);
transition: width 0.25s ease; 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 { .flow-actions {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: var(--space-4); gap: var(--space-4);
width: min(100%, var(--content-width));
margin: 0 auto;
padding: var(--space-4) 0; padding: var(--space-4) 0;
} }
@ -455,8 +622,10 @@ textarea:focus-visible {
@media (min-width: 768px) { @media (min-width: 768px) {
.step-layout--intro { .step-layout--intro {
grid-template-columns: 1.25fr 0.8fr; width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: center; align-items: center;
gap: var(--space-8);
} }
.field-row { .field-row {
@ -470,4 +639,4 @@ textarea:focus-visible {
.option-grid--4 { .option-grid--4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
} }

View File

@ -43,16 +43,19 @@
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1> <h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<p class="step-text"> <p class="step-text">
Erzähl uns von deiner Idee vom Essen bis zur Stimmung. Ob Dinner, Brunch 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> </p>
<button type="button" class="button button--primary button--intro" data-start-flow> <button type="button" class="button button--primary button--intro" data-start-flow>
Los gehts! Los gehts!
</button> </button>
</div> </div>
<aside class="intro-card" aria-label="Hinweis zur Event-Erstellung"> <aside class="intro-card intro-card--image" aria-label="Stimmungsbild zur Event-Erstellung">
<div class="intro-card-emoji" aria-hidden="true">🍽️</div> <img
<p>Aus einer Idee wird Schritt für Schritt dein Event.</p> class="intro-image"
src="assets/eventcreate_foodtable.jpg"
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
/>
</aside> </aside>
</div> </div>
</section> </section>
@ -69,11 +72,6 @@
</div> </div>
<div class="step-fields"> <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"> <fieldset class="form-field">
<legend>Art des Essens / Eventtyp</legend> <legend>Art des Essens / Eventtyp</legend>
@ -99,47 +97,7 @@
</label> </label>
</div> </div>
</fieldset> </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"> <fieldset class="form-field">
<legend>Maximale Personenanzahl</legend> <legend>Maximale Personenanzahl</legend>
@ -171,7 +129,21 @@
</button> </button>
</div> </div>
</fieldset> </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"> <fieldset class="form-field">
<legend>Ernährungsform</legend> <legend>Ernährungsform</legend>
@ -194,6 +166,25 @@
</div> </div>
</fieldset> </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"> <fieldset class="form-field">
<legend>Allergene / Unverträglichkeiten</legend> <legend>Allergene / Unverträglichkeiten</legend>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p> <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> <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 class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -230,8 +221,7 @@
<p class="step-kicker">Schritt 4</p> <p class="step-kicker">Schritt 4</p>
<h2 id="step4-title">Wann findet dein Event statt?</h2> <h2 id="step4-title">Wann findet dein Event statt?</h2>
<p class="step-text"> <p class="step-text">
Wähle Datum und Uhrzeit und sag uns, wo dein Event stattfindet. 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.
Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p> </p>
</div> </div>
@ -247,7 +237,21 @@
<input type="time" id="eventTime" name="eventTime" required /> <input type="time" id="eventTime" name="eventTime" required />
</div> </div>
</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"> <div class="form-field">
<label for="eventAddress">Adresse</label> <label for="eventAddress">Adresse</label>
<input type="text" id="eventAddress" name="eventAddress" autocomplete="street-address" required /> <input type="text" id="eventAddress" name="eventAddress" autocomplete="street-address" required />
@ -261,81 +265,111 @@
</div> </div>
</section> </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-layout">
<div class="step-copy"> <div class="step-copy">
<p class="step-kicker">Schritt 5</p> <p class="step-kicker">Schritt 6</p>
<h2 id="step5-title">Alles bereit für deine Gäste?</h2> <h2 id="step6-title">Gib deinem Event den letzten Schliff.</h2>
<p class="step-text"> <p class="step-text">
Schau dir dein Event nochmal in Ruhe an. Passt alles? Jetzt bekommt dein Event seinen Namen und die Atmosphäre, die Lust aufs Dabeisein macht.
Dann kannst du es jetzt veröffentlichen und Gäste einladen. 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> </p>
</div> </div>
<div class="review-card" aria-live="polite"> <div class="review-card" aria-live="polite">
<dl class="review-list"> <dl class="review-list">
<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>Eventtitel</dt>
<dd data-review="eventTitle"></dd>
</div>
<div class="review-item">
<dt>Eventtyp</dt> <dt>Eventtyp</dt>
<dd data-review="eventType"></dd> <dd data-review="eventType"></dd>
</div> </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>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">
<dt>Maximale Personenanzahl</dt> <dt>Maximale Personenanzahl</dt>
<dd data-review="maxGuests"></dd> <dd data-review="maxGuests"></dd>
</div> </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> <dt>Ernährungsform</dt>
<dd data-review="dietType"></dd> <dd data-review="dietType"></dd>
</div> </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> <dt>Allergene / Unverträglichkeiten</dt>
<dd data-review="allergies">Keine Angabe</dd> <dd data-review="allergies">Keine Angabe</dd>
</div> </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> <dt>Datum</dt>
<dd data-review="eventDate"></dd> <dd data-review="eventDate"></dd>
</div> </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> <dt>Uhrzeit</dt>
<dd data-review="eventTime"></dd> <dd data-review="eventTime"></dd>
</div> </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> <dt>Adresse</dt>
<dd data-review="eventAddress"></dd> <dd data-review="eventAddress"></dd>
</div> </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> <dt>Ort</dt>
<dd data-review="eventCity"></dd> <dd data-review="eventCity"></dd>
</div> </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> </dl>
</div> </div>
</div> </div>
</section> </section>
<div class="flow-footer" id="flowFooter" hidden> <div class="flow-footer" id="flowFooter" hidden>
<div class="progress" aria-hidden="true"> <div class="progress-wrap" aria-hidden="true">
<span id="progressBar" class="progress-bar"></span> <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>
<div class="flow-actions"> <div class="flow-actions">
@ -347,6 +381,31 @@
</div> </div>
</div> </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> </form>
</main> </main>
@ -356,4 +415,4 @@
<script src="js/event_create.js"></script> <script src="js/event_create.js"></script>
</body> </body>
</html> </html>

View File

@ -8,9 +8,13 @@ const steps = Array.from(document.querySelectorAll(".step"));
const backButton = document.getElementById("backButton"); const backButton = document.getElementById("backButton");
const nextButton = document.getElementById("nextButton"); const nextButton = document.getElementById("nextButton");
const progressBar = document.getElementById("progressBar"); const progressBar = document.getElementById("progressBar");
const progressMarker = document.getElementById("progressMarker");
const progressMarkerLabel = document.getElementById("progressMarkerLabel");
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"); const flowFooter = document.getElementById("flowFooter");
const submissionSuccess = document.getElementById("submissionSuccess");
const EVENTS_STORAGE_KEY = "socialCookingEvents";
// ============================= // =============================
// STATE: aktueller Schritt im Flow // STATE: aktueller Schritt im Flow
@ -27,7 +31,9 @@ const nextLabels = {
2: "Weiter", 2: "Weiter",
3: "Weiter", 3: "Weiter",
4: "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 // 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; 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 // STEP 2: Schritt anzeigen & Oberfläche aktualisieren
@ -71,6 +110,8 @@ function setErrorMessage(message = "") {
*/ */
function showStep(index) { function showStep(index) {
currentStep = index; currentStep = index;
submissionSuccess.hidden = true;
clearStepInvalidState(index);
// Nur der aktuelle Schritt soll sichtbar sein // Nur der aktuelle Schritt soll sichtbar sein
steps.forEach((step, stepIndex) => { steps.forEach((step, stepIndex) => {
@ -115,12 +156,21 @@ function updateFlowVisibility(stepIndex) {
*/ */
function updateProgressBar(stepIndex, totalStepIndex) { function updateProgressBar(stepIndex, totalStepIndex) {
let progress = 0; let progress = 0;
let markerPosition = 0;
let markerStep = 1;
let markerTransform = "translateX(-50%)";
if (stepIndex > 0) { if (stepIndex > 0) {
progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100; progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
markerPosition = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
markerStep = stepIndex;
} }
progressBar.style.width = `${progress}%`; 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. * und schreibt sie gesammelt in die Review-Ansicht.
*/ */
function updateReview() { 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"), eventTitle: getFieldValue("eventTitle"),
eventType: getFieldValue("eventType"), eventType: getFieldValue("eventType"),
menuDescription: getFieldValue("menuDescription"), menuDescription: getFieldValue("menuDescription"),
@ -238,12 +299,137 @@ function updateReview() {
eventAddress: getFieldValue("eventAddress"), eventAddress: getFieldValue("eventAddress"),
eventCity: getFieldValue("eventCity") 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 // STEP 5: Validierung
@ -263,6 +449,7 @@ function validateCurrentStep() {
if (currentStep === 0 || currentStep === lastStep) return true; if (currentStep === 0 || currentStep === lastStep) return true;
const fields = getStepFields(currentStep); const fields = getStepFields(currentStep);
clearStepInvalidState(currentStep);
// Zuerst Radio-Gruppen prüfen // Zuerst Radio-Gruppen prüfen
const radioCheck = validateRadioGroups(fields); const radioCheck = validateRadioGroups(fields);
@ -297,6 +484,7 @@ function validateRadioGroups(fields) {
const selected = group.some(f => f.checked); const selected = group.some(f => f.checked);
if (required && !selected) { if (required && !selected) {
markRadioGroupInvalid(group);
return { return {
isValid: false, isValid: false,
message: "Bitte wähle eine Option aus." message: "Bitte wähle eine Option aus."
@ -318,6 +506,7 @@ function validateRequiredFields(fields) {
if (field.type === "radio" || field.type === "checkbox") continue; if (field.type === "radio" || field.type === "checkbox") continue;
if (!field.checkValidity()) { if (!field.checkValidity()) {
markFieldInvalid(field);
return { return {
isValid: false, isValid: false,
message: "Bitte fülle alle Pflichtfelder aus." message: "Bitte fülle alle Pflichtfelder aus."
@ -374,11 +563,12 @@ function handleNextClick() {
*/ */
function handleFormSubmit(event) { function handleFormSubmit(event) {
event.preventDefault(); event.preventDefault();
// 1. Feedback geben saveCurrentEvent();
alert("Dein Event wurde erfolgreich veröffentlicht!"); steps.forEach(step => step.classList.remove("step--active"));
flowFooter.hidden = true;
// 2. Weiterleiten (z. B. zur Event-Übersicht) submissionSuccess.hidden = false;
window.location.href = "event_overview.html"; setErrorMessage("");
window.scrollTo({ top: 0, behavior: "smooth" });
} }
@ -412,6 +602,126 @@ function updateCounterValue(input, change) {
input.value = Math.max(min, currentValue + 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 // STEP 9: Alles starten
@ -438,10 +748,14 @@ function initEventCreationFlow() {
// Counter aktivieren // Counter aktivieren
registerCounterHandlers(); registerCounterHandlers();
registerMenuBulletHandler();
registerValidationFeedbackHandlers();
registerReviewEditHandlers();
// Startzustand: Intro anzeigen // Startzustand: Intro anzeigen
submissionSuccess.hidden = true;
showStep(0); showStep(0);
} }
// Startpunkt des Skripts // Startpunkt des Skripts
initEventCreationFlow(); initEventCreationFlow();

View File

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

View File

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