From 5a893d7da8bfff4430baab8c6241d48acd84efc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABschmona=C2=BB?= <«simona.oswald@proton.me»> Date: Sun, 29 Mar 2026 23:33:38 +0200 Subject: [PATCH 01/14] Anpassung html Login --- login.html | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/login.html b/login.html index 295192f..f6c167f 100644 --- a/login.html +++ b/login.html @@ -7,22 +7,22 @@ - - - + - +
- Invité + Invite Logo -
@@ -35,30 +35,30 @@
-

Willkommen zurück

+

Login

- +
Bitte gib eine gültige E-Mail Adresse ein.
- +
Bitte gib dein Passwort ein.
- +
- + - + \ No newline at end of file From 8e1d591d5cefa5196d42c968a367b461ccc7546b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABschmona=C2=BB?= <«simona.oswald@proton.me»> Date: Sun, 29 Mar 2026 23:37:26 +0200 Subject: [PATCH 02/14] Anpassung html Sign up --- signup.html | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/signup.html b/signup.html index db2098b..80daf25 100644 --- a/signup.html +++ b/signup.html @@ -4,17 +4,27 @@ Kontaktseite - Invité + + + + + - -
- -
- - Login -
-
+ +
+
+ + Invite Logo + + +
+
@@ -27,7 +37,7 @@

Erstelle deinen Account

- Hinweis: Sichtbar auf der Plattform ist nur der Vorname, erst einer Anmeldung zum Event ist der Nachname für die Teilnehmenden sichtbar. + Hinweis: Sichtbar auf der Plattform ist nur dein Vorname. Erst einer Anmeldung zum Event ist der Nachname für die Teilnehmenden sichtbar.
@@ -44,7 +54,7 @@
- +
Bitte gib eine gültige E-Mail Adresse ein.
@@ -55,10 +65,11 @@
Dein Passwort muss mindestens 8 Zeichen lang sein.
- + +
- Du hast bereits einen Account? Hier geht's zum Log-in + Du hast bereits einen Account? Hier geht es zum Login.
@@ -82,4 +93,4 @@ - + \ No newline at end of file From 56ed78b4f7a2a372209eab3a299070bf7d8a911f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABschmona=C2=BB?= <«simona.oswald@proton.me»> Date: Sun, 29 Mar 2026 23:40:20 +0200 Subject: [PATCH 03/14] Anpassung css login_Signup --- css/login_signup.css | 151 +++++++------------------------------------ 1 file changed, 22 insertions(+), 129 deletions(-) diff --git a/css/login_signup.css b/css/login_signup.css index f2d2e93..a289f64 100644 --- a/css/login_signup.css +++ b/css/login_signup.css @@ -1,87 +1,7 @@ -/* Grundlegende Resets */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: Arial, sans-serif; - background-color: #f5f5f5; - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Header Styles */ -.header { - background-color: white; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - padding: 15px 30px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-logo { - font-size: 24px; - font-weight: bold; - color: #0084ff; - text-decoration: none; - letter-spacing: 1px; -} - -.header-buttons { - display: flex; - gap: 15px; - align-items: center; -} - -.header-btn { - padding: 10px 20px; - border: none; - border-radius: 4px; - font-size: 14px; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - text-decoration: none; - display: inline-block; -} - -.header-btn-secondary { - background-color: transparent; - color: #0084ff; - border: 2px solid #0084ff; -} - -.header-btn-secondary:hover { - background-color: #0084ff; - color: white; -} - -.header-btn-primary { - background-color: #0084ff; - color: white; -} - -.header-btn-primary:hover { - background-color: #0073e6; -} - -/* Main content wrapper */ -.main-content { - display: flex; - justify-content: center; - align-items: center; - flex: 1; - padding: 20px; -} - .container { - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-top: 5rem; + background-color: var(--white); + border-radius: var(--radius-lg); max-width: 1000px; width: 100%; display: flex; @@ -91,7 +11,7 @@ body { .image-section { flex: 1; - background-color: #e8f4f8; + background-color: var(--white); display: flex; align-items: center; justify-content: center; @@ -113,21 +33,14 @@ body { } /* Formularelemente */ -h1 { - color: #333; - margin-bottom: 20px; - text-align: center; -} - .info-box { - background-color: #e8f4f8; - border-left: 4px solid #0084ff; + background-color: var(--olive-light); padding: 15px; margin-bottom: 30px; - border-radius: 4px; - font-size: 14px; - color: #333; - line-height: 1.5; + border-radius: 8px; + font-size: 16px; + color: var(--black); + line-height: 1.4; } .form-group { @@ -137,8 +50,8 @@ h1 { label { display: block; margin-bottom: 6px; - color: #333; - font-weight: bold; + color: var(--black); + font-weight: 500; font-size: 14px; } @@ -151,66 +64,46 @@ input[type="password"] { border-radius: 4px; font-size: 14px; transition: border-color 0.3s ease; - font-family: Arial, sans-serif; } input:focus { outline: none; - border-color: #0084ff; - box-shadow: 0 0 5px rgba(0, 132, 255, 0.3); + border-color: var(--olive-dark); } -button { - width: 100%; - padding: 12px; - background-color: #0084ff; - color: white; - border: none; - border-radius: 4px; - font-size: 16px; - font-weight: bold; - cursor: pointer; - transition: background-color 0.3s ease; - margin-top: 10px; -} -button:hover { - background-color: #0073e6; -} - -button:active { - background-color: #0063cc; -} /* Hilfstexte & Fehler */ .signup-hint, .login-hint { - text-align: center; + text-align: left; margin-top: 20px; - color: #666; - font-size: 14px; + color: var(--black); + font-size: 1rem; + line-height: 130%; } .signup-hint a, .login-hint a { - color: #0084ff; + color: var(--blue); text-decoration: none; - font-weight: bold; + font-weight: 500; transition: color 0.3s ease; } .signup-hint a:hover, .login-hint a:hover { text-decoration: underline; + text-underline-offset: 2px; + color: var(--blue); } .error-message { - color: #d32f2f; + color: var(--error); font-size: 13px; margin-top: 5px; display: none; } .form-group.has-error input { - border-color: #d32f2f; - box-shadow: 0 0 5px rgba(211, 47, 47, 0.3); + border-color: var(--error); } .form-group.has-error .error-message { From 601d4163398e01c64dfbc78a7a813bb3d3881d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABschmona=C2=BB?= <«simona.oswald@proton.me»> Date: Mon, 30 Mar 2026 00:45:56 +0200 Subject: [PATCH 04/14] Anpassungen stylesheet global --- .vscode/launch.json | 15 --- css/stylesheet_global.css | 186 ++++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 94 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 2ba986f..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:8080", - "webRoot": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/css/stylesheet_global.css b/css/stylesheet_global.css index 5b2120b..6f9f57f 100644 --- a/css/stylesheet_global.css +++ b/css/stylesheet_global.css @@ -1,9 +1,7 @@ -/* Font Import */ - +/* Font Import */ @import url('https://fonts.googleapis.com/css2?family=Bagel+Fat+One&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Jost:wght@300;400;600&display=swap'); - /* Reset / Normalize */ *, *::before, @@ -15,43 +13,54 @@ /* Colors + Main Font */ :root { + --max-width: 1200px; + --padding: 1.5rem; + + --tomato: #D44B24; --tomato-dark: #D44B24; --olive: #6B6B05; --olive-dark: #5C5C05; - --olive-light: #E5E1B7; - --butter: #FFFBD1; - --butter-light: #FFFDE3; + --olive-light: #C8CC7A; + --butter: #F5F1BC; + --butter-light: #F7F6E6; --white: #ffffff; - --black: #221C1A; + --black: #22211A; --brown: #66340d; + --blue: #3489DA; + --blue-dark: #1D70BF; + --error: #FF3B30; --font-main: 'Jost', sans-serif; - --font-size-base: 16px; + --font-size-base: 1rem; -/* Spacing Scale */ - --space-1: 8px; - --space-2: 12px; - --space-3: 16px; - --space-4: 20px; - --space-5: 24px; - --space-6: 32px; + /* Spacing Scale */ + --space-1: 0.5rem; + --space-2: 0.75rem; + --space-3: 1rem; + --space-4: 1.25rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-7: 3rem; + --space-8: 4rem; -/* Radius Scale */ - --radius-pill: 999px; - --radius-md: 20px; - --radius-lg: 30px; -/* Letter Spacing Scale */ + /* Radius Scale */ + --radius-pill: 62.4375rem; + --radius-sm: 0.5rem; + --radius-md: 1.5rem; + --radius-lg: 1.875rem; + + /* Letter Spacing */ --ls-none: 0; - --ls-ui: 0.03em; - --ls-label: 0.045em; + --ls-sm: 2.5%; + --ls-label: 0.045rem; } /* Base Styles */ body { font-family: var(--font-main); - font-size: 1.125em; + font-size: 1.125rem; line-height: 1.5; color: var(--black); background-color: var(--butter); @@ -66,96 +75,139 @@ img { h1, h2 { font-family: 'Bagel Fat One'; - margin-bottom: 0.5em; +} + +h1 { + font-size: 2em; + font-weight: 600; + line-height: 120%; + letter-spacing: var(--ls-sm); + color: var(--brown); } p { font-family: 'Jost', sans-serif; - margin-bottom: 1em; + margin-bottom: 1rem; } /* Layout */ + +.main-content { + margin-top: var(--space-8); + +} + .container { width: 90%; - max-width: 1200px; + max-width: 75rem; margin: 0 auto; } /* Top Navigation */ .top-nav-wrap { - background: var(--butter); - padding: 6px 12px; + position: sticky; + height: 58px; + top: 1rem; + z-index: 1000; + background: none; + padding: 0 1rem; } .top-nav { - background: rgba(255, 255, 255, 0.95); + background: var(--white); + /*backdrop-filter: blur(3px) saturate(140%); + -webkit-backdrop-filter: blur(3px) saturate(140%);*/ + border-radius: var(--radius-lg); - box-shadow: 0 3px 12px rgba(102, 52, 13, 0.1); + box-shadow: 0 0.1875rem 0.75rem rgba(102, 52, 13, 0.1); display: flex; justify-content: space-between; align-items: center; - min-height: 58px; - padding: 3px 9px 3px var(--space-5); + min-height: 3.625rem; + padding: 0.1875rem 0.5625rem 0.1875rem var(--space-5); max-width: none; width: 100%; box-sizing: border-box; - margin: 0 auto; } .brand { display: inline-flex; align-items: center; - height: 50px; + height: 3.125rem; text-decoration: none; } .brand img { width: auto; height: 100%; - max-width: 104px; + max-width: 6.5rem; display: block; } -.top-nav-links { +.button-small-links { display: flex; align-items: center; - gap: 16px; - + gap: var(--space-1); } -.nav-link { + +/* Buttons */ +.button { + display: inline-block; + padding: 0.5rem 1.25rem; + background-color: var(--olive); + border: none; + border-radius: var(--radius-lg); + font-family: 'Jost', sans-serif; + font-weight: 400; + font-size: 1.25rem; + color: var(--butter-light); + cursor: pointer; +} + +.button:hover { + background-color: var(--olive-dark); +} + +.button-small { color: var(--black); - line-height: 1.3; + font-size: 1.125rem; + font-weight: 500; + letter-spacing: var(--ls-sm); + line-height: 1; text-decoration: none; + padding: var(--space-1) var(--space-4); border-radius: var(--radius-md); - border-width: 2px; - border-color: none; - border-radius: 20px } -.nav-link:hover, -.nav-link:focus-visible { +.button-small:hover, .button-small:active, +.button-small:focus-visible { background: var(--olive-light); color: var(--black); } -.login-pill { + +.button-login { background: var(--olive); - border-radius: var(--radius-md); - color: var(--butter); - line-height: 1.3; - padding: var(--space-1) var(--space-4); + color: var(--butter-light); + font-size: 1.125rem; + font-weight: 500; + letter-spacing: var(--ls-sm); + line-height: 1; text-decoration: none; + + padding: var(--space-1) var(--space-4); + border-radius: var(--radius-md); } .profile-pill { - width: 38px; - height: 38px; - border-radius: 19px; + width: 2.375rem; + height: 2.375rem; + border-radius: 1.1875rem; background: var(--tomato); color: var(--butter); - font-size: 17px; + font-size: 1.0625rem; font-weight: 500; letter-spacing: var(--ls-ui); line-height: 1.3; @@ -165,30 +217,6 @@ p { text-decoration: none; } -/* Components */ -.button { - display: inline-block; - padding: 0.5em 1.25em; - background-color: var(--olive-color); - font-family: 'Jost', sans-serif; - font-size: 1em; - color: var(--butter-light); - border: none; - border-radius: 2em; - cursor: pointer; - -} - -.button:hover { - background-color: var(--olive-dark); -} - -/* Navigation */ -.nav { - display: flex; - gap: 1rem; -} - /* Utilities */ .text-center { text-align: left; @@ -199,7 +227,7 @@ p { } /* Media Queries (Responsive) */ -@media (max-width: 768px) { +@media (max-width: 48rem) { .container { width: 95%; } From 5c0806dc67d5ca6849505ad63dfff6e601d8a5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABschmona=C2=BB?= <«simona.oswald@proton.me»> Date: Mon, 30 Mar 2026 01:16:04 +0200 Subject: [PATCH 05/14] Logo ersetzen --- assets/logo_invite.svg | 9 +++++++++ event_create.html | 2 +- event_detail.html | 2 +- event_overview.html | 2 +- index.html | 4 ++-- login.html | 2 +- signup.html | 2 +- 7 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 assets/logo_invite.svg diff --git a/assets/logo_invite.svg b/assets/logo_invite.svg new file mode 100644 index 0000000..ab87e7f --- /dev/null +++ b/assets/logo_invite.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/event_create.html b/event_create.html index 54b635b..4519f0e 100644 --- a/event_create.html +++ b/event_create.html @@ -16,7 +16,7 @@
- Invite Logo + Invite Logo
+ + + +
+
+
+

Schritt 5

+

Wo findet dein Event statt?

+

+ Sag uns, wo dein Event stattfindet. Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung. +

+
+ +
@@ -261,81 +265,111 @@
-
+
-

Schritt 5

-

Alles bereit für deine Gäste?

+

Schritt 6

+

Gib deinem Event den letzten Schliff.

- 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. +

+
+ +
+
+ + +
+ +
+ + +
+
+
+
+ +
+
+
+

Schritt 7

+

Dein Event auf einen Blick.

+

+ Schau dir alle Details nochmal in Ruhe an. Wenn alles passt, + kannst du dein Event jetzt veröffentlichen und Gäste einladen.

-
-
Eventtitel
-
-
- -
+
Eventtyp
-
-
Menü
-
-
- -
-
Event-Abend
-
-
- -
+
Maximale Personenanzahl
-
+
Ernährungsform
-
+
+
Menü
+
+
+ +
Allergene / Unverträglichkeiten
Keine Angabe
-
+
Datum
-
+
Uhrzeit
-
+
Adresse
-
+
Ort
+ +
+
Eventtitel
+
+
+ +
+
Event-Abend
+
+
+ + @@ -356,4 +415,4 @@ - \ No newline at end of file + diff --git a/js/event_create.js b/js/event_create.js index a633a24..1428c64 100644 --- a/js/event_create.js +++ b/js/event_create.js @@ -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,10 +748,14 @@ function initEventCreationFlow() { // Counter aktivieren registerCounterHandlers(); + registerMenuBulletHandler(); + registerValidationFeedbackHandlers(); + registerReviewEditHandlers(); // Startzustand: Intro anzeigen + submissionSuccess.hidden = true; showStep(0); } // Startpunkt des Skripts -initEventCreationFlow(); \ No newline at end of file +initEventCreationFlow(); diff --git a/js/event_detail.js b/js/event_detail.js index ecabc45..5b4bd6c 100644 --- a/js/event_detail.js +++ b/js/event_detail.js @@ -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) { @@ -257,4 +269,4 @@ document.addEventListener('DOMContentLoaded', async () => { }); } } -}); \ No newline at end of file +}); diff --git a/js/event_overview.js b/js/event_overview.js index 2559a0f..e92701c 100644 --- a/js/event_overview.js +++ b/js/event_overview.js @@ -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. From 1efa4dcd39878df3708bd5cd64f39e31bbf8bb73 Mon Sep 17 00:00:00 2001 From: viiivo <«vivien.vonburg@outlook.com»> Date: Fri, 10 Apr 2026 16:28:44 +0200 Subject: [PATCH 12/14] feat: Implement user profile management and navigation updates - Added a new profile page (my_profil.html) for users to manage their events and personal information. - Introduced a new CSS file (my_profil.css) for styling the profile page. - Created a JavaScript file (my_profil.js) to handle profile data retrieval, event registration management, and form submission. - Updated navigation logic (navigation.js) to dynamically display login/signup or event management links based on user authentication status. - Enhanced event creation and detail pages to support user-specific actions (registration/unregistration). - Improved login and signup processes to handle user data more robustly, including fallback user creation. - Refactored event overview to show user-specific events and registrations. - Added error handling and validation for user input in profile management. --- css/event_overview.css | 17 ++ css/my_profil.css | 251 ++++++++++++++++++++++++++ css/stylesheet_global.css | 19 ++ event_create.html | 5 +- event_detail.html | 5 +- event_overview.html | 5 +- index.html | 6 +- js/event_create.js | 18 +- js/event_detail.js | 91 +++++++++- js/event_overview.js | 39 +++- js/login.js | 52 +++++- js/my_profil.js | 367 ++++++++++++++++++++++++++++++++++++++ js/navigation.js | 63 +++++++ js/signup.js | 53 +++++- login.html | 6 +- my_profil.html | 102 +++++++++++ signup.html | 6 +- 17 files changed, 1076 insertions(+), 29 deletions(-) create mode 100644 css/my_profil.css create mode 100644 js/my_profil.js create mode 100644 js/navigation.js create mode 100644 my_profil.html diff --git a/css/event_overview.css b/css/event_overview.css index 30901c9..0c3e203 100644 --- a/css/event_overview.css +++ b/css/event_overview.css @@ -281,6 +281,14 @@ filter: brightness(0.95); } +.btn-primary-own, +.btn-primary-own:disabled { + background: var(--olive-light); + color: var(--black); + opacity: 1; + cursor: not-allowed; +} + /* --------------------------------------------------------- Overview Empty State --------------------------------------------------------- */ @@ -693,6 +701,15 @@ cursor: not-allowed; } +.detail-primary-btn-own, +.detail-primary-btn-own:disabled { + border-color: var(--olive-light); + background: var(--olive-light); + color: var(--black); + opacity: 1; + cursor: not-allowed; +} + /* --------------------------------------------------------- Responsive: Tablet (<= 850px) diff --git a/css/my_profil.css b/css/my_profil.css new file mode 100644 index 0000000..825504a --- /dev/null +++ b/css/my_profil.css @@ -0,0 +1,251 @@ +.profile-page { + /* Reserve a large safe zone below sticky nav so title/actions are never covered. */ + margin-top: 0; + padding-top: 6.5rem; + margin-bottom: var(--space-8); +} + +/* Kopfbereich mit Titel und Logout-Aktion. */ +.profile-hero { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: var(--space-4); + margin-bottom: var(--space-5); +} + +.profile-kicker { + margin: 0; + color: var(--olive); + font-size: 1rem; + font-weight: 500; + letter-spacing: var(--ls-label); +} + +#profile-headline { + margin: 0.4rem 0; + color: var(--brown); + font-size: clamp(2rem, 4.4vw, 2.8rem); +} + +.profile-subline { + margin: 0; + max-width: 48rem; +} + +.profile-logout { + border: none; + cursor: pointer; +} + +.profile-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-4); +} + +/* Konsistentes Karten-Layout fuer alle Profilsektionen. */ +.profile-panel { + background: rgba(255, 255, 255, 0.88); + border-radius: var(--radius-lg); + box-shadow: 0 3px 12px rgba(102, 52, 13, 0.1); + padding: var(--space-5); +} + +.panel-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-3); +} + +.panel-title { + margin: 0; + color: var(--brown); + font-size: 1.8rem; +} + +.panel-count { + min-width: 2rem; + padding: 0.1rem 0.65rem; + border-radius: var(--radius-pill); + background: var(--olive-light); + color: var(--black); + font-size: 0.95rem; + font-weight: 600; + text-align: center; +} + +.profile-card-list { + display: grid; + gap: var(--space-2); +} + +/* Einzelne Eventkarte fuer "Meine Events" und "Meine Anmeldungen". */ +.profile-event-card { + border: 1px solid rgba(107, 107, 5, 0.25); + border-radius: var(--radius-md); + padding: var(--space-3); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-3); +} + +.profile-event-title { + margin: 0; + color: var(--black); + font-family: "Jost", sans-serif; + font-size: 1.25rem; + font-weight: 600; +} + +.profile-event-meta { + margin: 0.3rem 0 0; + font-size: 0.95rem; + color: var(--olive); +} + +.profile-event-link { + flex-shrink: 0; + color: var(--blue); + font-weight: 500; + text-decoration: none; +} + +.profile-event-link:hover, +.profile-event-link:focus-visible { + text-decoration: underline; + text-underline-offset: 3px; +} + +.profile-event-actions { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.profile-unregister-btn { + border: none; + border-radius: var(--radius-md); + background: var(--tomato); + color: var(--butter-light); + padding: 0.45rem 0.95rem; + font-family: "Jost", sans-serif; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; +} + +.profile-unregister-btn:hover, +.profile-unregister-btn:focus-visible { + background: var(--tomato-dark); +} + +.profile-empty { + margin: 0; + color: var(--black); +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); +} + +.form-group { + margin-bottom: var(--space-3); +} + +.form-group label { + display: block; + margin-bottom: 0.35rem; + font-size: 0.95rem; + font-weight: 500; +} + +.form-group input { + width: 100%; + border: 1px solid #d8d8d8; + border-radius: var(--radius-sm); + background: var(--white); + padding: 0.7rem 0.85rem; + font-size: 1rem; +} + +.form-group input:focus { + outline: 2px solid rgba(107, 107, 5, 0.35); + outline-offset: 1px; +} + +.input-hint { + margin: 0.4rem 0 0; + font-size: 0.9rem; + color: #535353; +} + +.input-error { + margin-top: 0.35rem; + color: var(--error); + font-size: 0.85rem; + display: none; +} + +.form-group.has-error .input-error { + display: block; +} + +.form-group.has-error input { + border-color: var(--error); +} + +.profile-feedback { + margin: 0.75rem 0 0; + font-size: 0.95rem; + color: var(--olive); + min-height: 1.3rem; +} + +.profile-cta-row { + display: flex; + gap: var(--space-2); + margin-top: var(--space-3); +} + +.profile-button-secondary { + background: var(--tomato); +} + +.profile-button-secondary:hover { + background: var(--tomato-dark); +} + +@media (max-width: 48rem) { + .profile-page { + padding-top: 5.5rem; + } + + .profile-hero { + flex-direction: column; + align-items: stretch; + } + + .profile-logout { + width: max-content; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .profile-event-card { + flex-direction: column; + align-items: flex-start; + } + + .profile-event-actions { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + } +} diff --git a/css/stylesheet_global.css b/css/stylesheet_global.css index 7c358a8..2957091 100644 --- a/css/stylesheet_global.css +++ b/css/stylesheet_global.css @@ -210,6 +210,25 @@ p { color: var(--butter-light); } +/* Auth-Links in ausgeloggter Navigation: klarer Aktiv-/Default-Zustand. */ +.auth-nav-button--default { + background: transparent; + color: var(--olive); + border: 2px solid var(--olive); +} + +.auth-nav-button--default:hover, +.auth-nav-button--default:focus-visible { + background: var(--olive-light); + color: var(--black); +} + +.auth-nav-button--active { + background: var(--olive); + color: var(--butter-light); + border: 2px solid var(--olive); +} + .profile-pill { width: 2.375rem; height: 2.375rem; diff --git a/event_create.html b/event_create.html index 5f669db..8277154 100644 --- a/event_create.html +++ b/event_create.html @@ -9,6 +9,7 @@ + @@ -19,9 +20,7 @@ Invite Logo
diff --git a/event_detail.html b/event_detail.html index 8a420a9..7820053 100644 --- a/event_detail.html +++ b/event_detail.html @@ -9,6 +9,7 @@ + @@ -19,9 +20,7 @@ Invite Logo diff --git a/event_overview.html b/event_overview.html index a289a24..c64e2e2 100644 --- a/event_overview.html +++ b/event_overview.html @@ -9,6 +9,7 @@ + @@ -19,9 +20,7 @@ Invite Logo diff --git a/index.html b/index.html index 8a9f43a..7adaa2d 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,7 @@ + @@ -22,9 +23,8 @@ Invite Logo diff --git a/js/event_create.js b/js/event_create.js index 1428c64..e72d82d 100644 --- a/js/event_create.js +++ b/js/event_create.js @@ -15,6 +15,7 @@ const usernameElement = document.getElementById("username"); const flowFooter = document.getElementById("flowFooter"); const submissionSuccess = document.getElementById("submissionSuccess"); const EVENTS_STORAGE_KEY = "socialCookingEvents"; +const CURRENT_USER_KEY = "socialCookingCurrentUser"; // ============================= // STATE: aktueller Schritt im Flow @@ -36,8 +37,20 @@ const nextLabels = { 7: "Event veröffentlichen" }; -// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen -usernameElement.textContent = "Mia"; +// Liest den aktiven Benutzer aus localStorage und setzt den Anzeigenamen im Header. +function getCurrentUser() { + try { + const raw = localStorage.getItem(CURRENT_USER_KEY); + return raw ? JSON.parse(raw) : null; + } catch (error) { + console.error("Aktueller Benutzer konnte nicht gelesen werden:", error); + return null; + } +} + +const currentUser = getCurrentUser(); +const displayName = currentUser?.vorname?.trim() || "Mia"; +usernameElement.textContent = displayName; // ============================= @@ -409,6 +422,7 @@ function buildStoredEvent() { name: usernameElement.textContent.trim() || "Host", initial: (usernameElement.textContent.trim().charAt(0) || "H").toUpperCase() }, + hostEmail: currentUser?.email || "", hostMessage: [eventDescription], menu: buildMenuItems(menuDescription), specifications: getCheckboxValues("allergies") === "Keine Angabe" diff --git a/js/event_detail.js b/js/event_detail.js index 5b4bd6c..53bb7e2 100644 --- a/js/event_detail.js +++ b/js/event_detail.js @@ -1,10 +1,13 @@ document.addEventListener('DOMContentLoaded', async () => { const EVENTS_STORAGE_KEY = 'socialCookingEvents'; + const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; // ------------------------------------------------------------- // DOM entry point and shared asset path. // ------------------------------------------------------------- const detailContainer = document.getElementById('detail-view'); const locationIconPath = 'assets/location-pin.svg'; + const currentUser = getCurrentUser(); // Read event id from query string (detail page deep-link support). const params = new URLSearchParams(window.location.search); @@ -25,6 +28,49 @@ document.addEventListener('DOMContentLoaded', async () => { } } + function getCurrentUser() { + try { + const stored = localStorage.getItem(CURRENT_USER_KEY); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.error('Aktueller Benutzer konnte nicht gelesen werden.', error); + return null; + } + } + + function getRegistrationMap() { + try { + const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY); + return stored ? JSON.parse(stored) : {}; + } catch (error) { + console.error('Anmeldedaten konnten nicht gelesen werden.', error); + return {}; + } + } + + function setRegistrationMap(registrationMap) { + localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); + } + + // Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde. + function isEventOwnedByCurrentUser(event, user) { + if (!event || !user) { + return false; + } + + const userEmail = String(user.email || '').trim().toLowerCase(); + const hostEmail = String(event.hostEmail || '').trim().toLowerCase(); + + if (userEmail && hostEmail) { + return userEmail === hostEmail; + } + + // Fallback fuer aeltere Datensaetze ohne hostEmail. + const userFirstName = String(user.vorname || '').trim().toLowerCase(); + const hostName = String(event.host?.name || '').trim().toLowerCase(); + return Boolean(userFirstName && hostName && userFirstName === hostName); + } + // Fetch data source and resolve the matching event record. try { const response = await fetch('data/events.json'); @@ -116,6 +162,14 @@ document.addEventListener('DOMContentLoaded', async () => { const confirmedGuests = participants.length; const freePlaces = Math.max(0, totalGuests - confirmedGuests); const isFull = freePlaces === 0; + const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); + const registrationMap = getRegistrationMap(); + const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) + ? registrationMap[currentUser.email].map(id => Number(id)) + : []; + const isRegistered = userRegistrations.includes(Number(event.id)); + const actionButtonLabel = isOwnEvent ? 'Dein Event' : !currentUser ? 'Einloggen' : isRegistered ? 'Abmelden' : 'Anmelden'; + const actionButtonDisabled = isOwnEvent || (!isRegistered && isFull); const detailChips = [ `${eventCategory}`, `${dietLabel}`, @@ -200,8 +254,8 @@ document.addEventListener('DOMContentLoaded', async () => { ${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`} - @@ -224,6 +278,39 @@ document.addEventListener('DOMContentLoaded', async () => { const lightboxImage = detailContainer.querySelector('.detail-lightbox-image'); const lightboxClose = detailContainer.querySelector('.detail-lightbox-close'); const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item'); + const registerButton = detailContainer.querySelector('[data-register-button]'); + + // Anmeldung toggeln und im lokalen Registrierungs-Store persistieren. + if (registerButton) { + registerButton.addEventListener('click', () => { + if (isOwnEvent) { + return; + } + + if (!currentUser || !currentUser.email) { + window.location.href = 'login.html'; + return; + } + + const nextRegistrationMap = getRegistrationMap(); + const currentList = Array.isArray(nextRegistrationMap[currentUser.email]) + ? nextRegistrationMap[currentUser.email].map(id => Number(id)) + : []; + const registrationSet = new Set(currentList); + + if (registrationSet.has(Number(event.id))) { + registrationSet.delete(Number(event.id)); + } else if (!isFull) { + registrationSet.add(Number(event.id)); + } + + nextRegistrationMap[currentUser.email] = Array.from(registrationSet); + setRegistrationMap(nextRegistrationMap); + + // Re-Render aktualisiert Buttonzustand und CTA ohne Seitenreload. + renderDetailPage(event); + }); + } // Central close helper to keep all close paths consistent. function closeLightbox() { diff --git a/js/event_overview.js b/js/event_overview.js index e92701c..15bf312 100644 --- a/js/event_overview.js +++ b/js/event_overview.js @@ -1,5 +1,6 @@ document.addEventListener('DOMContentLoaded', () => { const EVENTS_STORAGE_KEY = 'socialCookingEvents'; + const CURRENT_USER_KEY = 'socialCookingCurrentUser'; // ------------------------------------------------------------- // DOM references used throughout the page lifecycle. // ------------------------------------------------------------- @@ -14,6 +15,35 @@ document.addEventListener('DOMContentLoaded', () => { // ------------------------------------------------------------- let allEvents = []; let activeCategory = 'ALLE'; + const currentUser = getCurrentUser(); + + function getCurrentUser() { + try { + const stored = localStorage.getItem(CURRENT_USER_KEY); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.error('Aktueller Benutzer konnte nicht gelesen werden.', error); + return null; + } + } + + // Prueft, ob ein Event dem aktuellen Benutzer gehoert. + function isEventOwnedByCurrentUser(event, user) { + if (!event || !user) { + return false; + } + + const userEmail = String(user.email || '').trim().toLowerCase(); + const hostEmail = String(event.hostEmail || '').trim().toLowerCase(); + + if (userEmail && hostEmail) { + return userEmail === hostEmail; + } + + const userFirstName = String(user.vorname || '').trim().toLowerCase(); + const hostName = String(event.host?.name || '').trim().toLowerCase(); + return Boolean(userFirstName && hostName && userFirstName === hostName); + } function getStoredEvents() { try { @@ -225,12 +255,19 @@ document.addEventListener('DOMContentLoaded', () => { const totalCapacity = event.spots; const freePlaces = Math.max(0, totalCapacity - bookedSeats); const isFull = freePlaces === 0; + const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); // Build optional specification chips only when data exists. const specsChips = event.specifications && event.specifications.length > 0 ? event.specifications.map(spec => `${spec}`).join('') : ''; + const actionMarkup = isOwnEvent + ? '' + : isFull + ? '' + : ''; + card.innerHTML = `
@@ -249,7 +286,7 @@ document.addEventListener('DOMContentLoaded', () => {
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`} - ${isFull ? '' : ''} + ${actionMarkup}
`; diff --git a/js/login.js b/js/login.js index e1c059c..5ba97fe 100644 --- a/js/login.js +++ b/js/login.js @@ -4,6 +4,42 @@ const passwortInput = document.getElementById('passwort'); const emailError = document.getElementById('emailError'); const passwortError = document.getElementById('passwortError'); +const USERS_STORAGE_KEY = 'socialCookingUsers'; +const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + +// Liest alle registrierten Benutzer robust aus localStorage. +function getStoredUsers() { + try { + const raw = localStorage.getItem(USERS_STORAGE_KEY); + return raw ? JSON.parse(raw) : []; + } catch (error) { + console.error('Benutzerdaten konnten nicht gelesen werden.', error); + return []; + } +} + +// Speichert den aktiven Benutzer fuer nachfolgende Seiten. +function setCurrentUser(user) { + localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user)); +} + +// Erstellt einen Demo-Benutzer, falls fuer die E-Mail noch kein Account existiert. +function createFallbackUser(email, passwort) { + const localPart = email.split('@')[0] || 'Gast'; + const normalized = localPart.replace(/[._-]/g, ' ').trim(); + const guessedVorname = normalized ? normalized.split(' ')[0] : 'Gast'; + + return { + id: Date.now(), + vorname: guessedVorname.charAt(0).toUpperCase() + guessedVorname.slice(1), + nachname: '', + email, + passwort, + createdAt: new Date().toISOString(), + source: 'login-fallback' + }; +} + // Validierungsfunktion function validateForm(event) { event.preventDefault(); @@ -43,11 +79,21 @@ function validateForm(event) { passwortGroup.classList.remove('has-error'); } - // Wenn alle Validierungen bestanden, Form absenden + // Wenn alle Validierungen bestanden, Benutzer pruefen und Session speichern. if (isValid) { - //alert('Login erfolgreich! (Dies ist eine Demo)'); + const users = getStoredUsers(); + const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase()); - // Weiterleitung zur event overview Page + if (matchedUser && matchedUser.passwort !== passwortValue) { + passwortGroup.classList.add('has-error'); + passwortError.textContent = 'Das Passwort ist nicht korrekt.'; + return; + } + + const userToLogin = matchedUser || createFallbackUser(emailValue, passwortValue); + setCurrentUser(userToLogin); + + // Weiterleitung zur Event-Overview-Seite. window.location.href = 'event_overview.html'; } } diff --git a/js/my_profil.js b/js/my_profil.js new file mode 100644 index 0000000..9d7ee5f --- /dev/null +++ b/js/my_profil.js @@ -0,0 +1,367 @@ +document.addEventListener('DOMContentLoaded', () => { + const EVENTS_STORAGE_KEY = 'socialCookingEvents'; + const USERS_STORAGE_KEY = 'socialCookingUsers'; + const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; + + // Zentrale DOM-Referenzen fuer klare, testbare Funktionen. + const loggedOutState = document.getElementById('logged-out-state'); + const loggedInContent = document.getElementById('logged-in-content'); + const profileHeadline = document.getElementById('profile-headline'); + const profileSubline = document.getElementById('profile-subline'); + const logoutButton = document.getElementById('logout-button'); + + const myEventsCount = document.getElementById('my-events-count'); + const myRegistrationsCount = document.getElementById('my-registrations-count'); + const myEventsList = document.getElementById('my-events-list'); + const myRegistrationsList = document.getElementById('my-registrations-list'); + + const profileForm = document.getElementById('profile-form'); + const profileFeedback = document.getElementById('profile-feedback'); + const vornameInput = document.getElementById('vorname'); + const nachnameInput = document.getElementById('nachname'); + const emailInput = document.getElementById('email'); + const passwortInput = document.getElementById('passwort'); + + let currentUser = getCurrentUser(); + let allEvents = []; + + init(); + + async function init() { + if (!currentUser) { + renderLoggedOutState(); + return; + } + + renderLoggedInState(currentUser); + bindFormHandlers(); + + allEvents = await loadAllEvents(); + renderMyEvents(allEvents, currentUser); + renderMyRegistrations(allEvents, currentUser); + } + + // Liest den aktuell eingeloggten Benutzer robust aus dem Storage. + function getCurrentUser() { + try { + const raw = localStorage.getItem(CURRENT_USER_KEY); + return raw ? JSON.parse(raw) : null; + } catch (error) { + console.error('Der aktuelle Benutzer konnte nicht geladen werden.', error); + return null; + } + } + + // Liest lokal erstellte Events aus dem Storage. + function getStoredEvents() { + try { + const raw = localStorage.getItem(EVENTS_STORAGE_KEY); + return raw ? JSON.parse(raw) : []; + } catch (error) { + console.error('Lokale Events konnten nicht gelesen werden.', error); + return []; + } + } + + // Liest den Anmeldestatus pro Benutzer-E-Mail. + function getRegistrationMap() { + try { + const raw = localStorage.getItem(REGISTRATION_STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; + } catch (error) { + console.error('Anmeldedaten konnten nicht gelesen werden.', error); + return {}; + } + } + + // Schreibt den gesamten Registrierungszustand in localStorage. + function setRegistrationMap(registrationMap) { + localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); + } + + // Fuehrt JSON-Daten und lokal erstellte Events in einer Liste zusammen. + async function loadAllEvents() { + try { + const response = await fetch('data/events.json'); + const apiEvents = await response.json(); + return [...getStoredEvents(), ...apiEvents]; + } catch (error) { + console.error('Events konnten nicht geladen werden.', error); + return getStoredEvents(); + } + } + + // Schaltet in den ausgeloggten Zustand und blendet geschuetzte Inhalte aus. + function renderLoggedOutState() { + loggedOutState.classList.remove('hidden'); + loggedInContent.classList.add('hidden'); + logoutButton.classList.add('hidden'); + profileHeadline.textContent = 'Mein Profil'; + profileSubline.textContent = 'Bitte logge dich ein, um deinen Bereich zu sehen.'; + } + + // Fuellt Ueberschriften und Formular mit den aktuellen Benutzerdaten. + function renderLoggedInState(user) { + loggedOutState.classList.add('hidden'); + loggedInContent.classList.remove('hidden'); + logoutButton.classList.remove('hidden'); + + profileHeadline.textContent = `Hallo ${user.vorname || 'Gast'}`; + profileSubline.textContent = 'Hier kannst du deine Events und Anmeldungen verwalten.'; + + vornameInput.value = user.vorname || ''; + nachnameInput.value = user.nachname || ''; + emailInput.value = user.email || ''; + } + + // Bindet Submit-, Input- und Logout-Verhalten an die Profilseite. + function bindFormHandlers() { + profileForm.addEventListener('submit', handleProfileSubmit); + myRegistrationsList.addEventListener('click', handleRegistrationListClick); + + [vornameInput, nachnameInput, emailInput, passwortInput].forEach(input => { + input.addEventListener('input', () => { + input.parentElement.classList.remove('has-error'); + profileFeedback.textContent = ''; + }); + }); + + logoutButton.addEventListener('click', () => { + localStorage.removeItem(CURRENT_USER_KEY); + window.location.href = 'login.html'; + }); + } + + // Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation. + function handleRegistrationListClick(event) { + const target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + + const unregisterButton = target.closest('[data-unregister-id]'); + if (!unregisterButton || !currentUser?.email) { + return; + } + + const eventId = Number(unregisterButton.getAttribute('data-unregister-id')); + if (!Number.isFinite(eventId)) { + return; + } + + unregisterFromEvent(eventId, currentUser.email); + } + + // Entfernt eine Event-ID aus der Benutzerliste und aktualisiert die UI sofort. + function unregisterFromEvent(eventId, userEmail) { + const registrationMap = getRegistrationMap(); + const currentIds = Array.isArray(registrationMap[userEmail]) ? registrationMap[userEmail] : []; + const nextIds = currentIds + .map(id => Number(id)) + .filter(id => Number.isFinite(id) && id !== eventId); + + registrationMap[userEmail] = nextIds; + setRegistrationMap(registrationMap); + + renderMyRegistrations(allEvents, currentUser); + profileFeedback.textContent = 'Du wurdest von dem Event abgemeldet.'; + } + + // Validiert Profildaten konsistent und liefert true/false zur Submit-Steuerung. + function validateProfileForm() { + let isValid = true; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!vornameInput.value.trim()) { + vornameInput.parentElement.classList.add('has-error'); + isValid = false; + } + + if (!nachnameInput.value.trim()) { + nachnameInput.parentElement.classList.add('has-error'); + isValid = false; + } + + if (!emailRegex.test(emailInput.value.trim())) { + emailInput.parentElement.classList.add('has-error'); + isValid = false; + } + + if (passwortInput.value && passwortInput.value.length < 6) { + passwortInput.parentElement.classList.add('has-error'); + isValid = false; + } + + return isValid; + } + + // Speichert Profilaenderungen lokal und synchronisiert auch den Benutzerkatalog. + function handleProfileSubmit(event) { + event.preventDefault(); + + if (!validateProfileForm()) { + profileFeedback.textContent = 'Bitte pruefe die markierten Felder.'; + return; + } + + const previousEmail = currentUser.email; + const nextUser = { + ...currentUser, + vorname: vornameInput.value.trim(), + nachname: nachnameInput.value.trim(), + email: emailInput.value.trim(), + passwort: passwortInput.value ? passwortInput.value : currentUser.passwort, + updatedAt: new Date().toISOString() + }; + + currentUser = nextUser; + localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(nextUser)); + syncUserInUserStore(previousEmail, nextUser); + + // Falls sich die E-Mail geaendert hat, verschieben wir bestehende Anmeldungen auf die neue E-Mail. + migrateRegistrationEmail(previousEmail, nextUser.email); + + passwortInput.value = ''; + profileHeadline.textContent = `Hallo ${nextUser.vorname}`; + profileFeedback.textContent = 'Profil erfolgreich gespeichert.'; + } + + // Synchronisiert einen Benutzer im zentralen User-Array. + function syncUserInUserStore(previousEmail, nextUser) { + let users = []; + + try { + const raw = localStorage.getItem(USERS_STORAGE_KEY); + users = raw ? JSON.parse(raw) : []; + } catch (error) { + console.error('Benutzerdaten konnten nicht gelesen werden.', error); + } + + const nextUsers = users.filter(user => user.email !== previousEmail && user.email !== nextUser.email); + nextUsers.unshift(nextUser); + localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(nextUsers)); + } + + // Migriert bestehende Registrierungen, falls die E-Mail aktualisiert wurde. + function migrateRegistrationEmail(previousEmail, nextEmail) { + if (!previousEmail || !nextEmail || previousEmail === nextEmail) { + return; + } + + const map = getRegistrationMap(); + const existingRegistrations = Array.isArray(map[previousEmail]) ? map[previousEmail] : []; + const alreadyPresent = Array.isArray(map[nextEmail]) ? map[nextEmail] : []; + + map[nextEmail] = Array.from(new Set([...alreadyPresent, ...existingRegistrations])); + delete map[previousEmail]; + + localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map)); + } + + // Ermittelt gehostete Events anhand Host-E-Mail oder Host-Vorname. + function getMyHostedEvents(events, user) { + const userFirstName = normalizeText(user.vorname); + + return events.filter(event => { + const hostEmail = normalizeText(event.hostEmail || ''); + const hostName = normalizeText(event.host?.name || ''); + + if (hostEmail && hostEmail === normalizeText(user.email)) { + return true; + } + + return userFirstName && hostName === userFirstName; + }); + } + + // Ermittelt angemeldete Events ueber die Registration-Map. + function getMyRegisteredEvents(events, user) { + const registrationMap = getRegistrationMap(); + const registeredIds = Array.isArray(registrationMap[user.email]) ? registrationMap[user.email] : []; + const idSet = new Set(registeredIds.map(id => Number(id))); + + return events.filter(event => idSet.has(Number(event.id))); + } + + // Rendert gehostete Events inkl. Zaehler. + function renderMyEvents(events, user) { + const hostedEvents = getMyHostedEvents(events, user); + myEventsCount.textContent = String(hostedEvents.length); + renderEventCards(myEventsList, hostedEvents, 'Du hast noch kein eigenes Event erstellt.', false); + } + + // Rendert angemeldete Events inkl. Zaehler. + function renderMyRegistrations(events, user) { + const registeredEvents = getMyRegisteredEvents(events, user); + myRegistrationsCount.textContent = String(registeredEvents.length); + renderEventCards(myRegistrationsList, registeredEvents, 'Du bist aktuell bei keinem Event angemeldet.', true); + } + + // Baut die Eventkarten fuer beide Listen in einheitlichem Markup. + function renderEventCards(container, events, emptyText, withUnregisterButton) { + container.innerHTML = ''; + + if (events.length === 0) { + const emptyElement = document.createElement('p'); + emptyElement.className = 'profile-empty'; + emptyElement.textContent = emptyText; + container.appendChild(emptyElement); + return; + } + + events.forEach(event => { + const card = document.createElement('article'); + card.className = 'profile-event-card'; + + const actionMarkup = withUnregisterButton + ? ` +
+ Zum Event + +
+ ` + : `Zum Event`; + + card.innerHTML = ` +
+

${event.title}

+

${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}

+
+ ${actionMarkup} + `; + + container.appendChild(card); + }); + } + + // Formatiert ein Eventdatum konsistent fuer die Profilkarten. + function formatEventDate(dateString) { + if (!dateString) { + return 'Kein Datum'; + } + + if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + const [year, month, day] = dateString.split('-'); + const monthLabel = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1]; + return `${Number(day)}. ${monthLabel} ${year}`; + } + + return dateString; + } + + // Vereinheitlicht die Zeitanzeige fuer die Profilseite. + function formatEventTime(timeString) { + if (!timeString) { + return 'Keine Uhrzeit'; + } + + return timeString.includes('UHR') ? timeString.replace('UHR', 'Uhr').trim() : timeString; + } + + // Normalisiert Vergleichswerte fuer robuste String-Matches. + function normalizeText(value) { + return String(value || '').trim().toLowerCase(); + } +}); diff --git a/js/navigation.js b/js/navigation.js new file mode 100644 index 0000000..ffe8264 --- /dev/null +++ b/js/navigation.js @@ -0,0 +1,63 @@ +document.addEventListener('DOMContentLoaded', () => { + const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + const navContainers = document.querySelectorAll('.nav-tab-links'); + const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase(); + + // Beendet frueh, falls auf einer Seite keine Hauptnavigation vorhanden ist. + if (!navContainers.length) { + return; + } + + // Liest den aktiven Benutzer robust aus localStorage. + function getCurrentUser() { + try { + const stored = localStorage.getItem(CURRENT_USER_KEY); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.error('Aktueller Benutzer konnte nicht gelesen werden.', error); + return null; + } + } + + // Baut die Navigation fuer ausgeloggte Besucher. + function buildLoggedOutNavigation() { + const loginIsActive = currentPage === 'login.html'; + const signupIsActive = currentPage === 'signup.html'; + + return ` + + Login + + + Signup + + `; + } + + // Baut die Navigation fuer eingeloggte Benutzer. + function buildLoggedInNavigation() { + return ` + Event finden + Event erstellen + Mein Profil + `; + } + + const currentUser = getCurrentUser(); + const nextMarkup = currentUser ? buildLoggedInNavigation() : buildLoggedOutNavigation(); + + // Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an. + navContainers.forEach(container => { + container.innerHTML = nextMarkup; + }); +}); diff --git a/js/signup.js b/js/signup.js index f158cd4..87a1b64 100644 --- a/js/signup.js +++ b/js/signup.js @@ -5,6 +5,30 @@ const emailInput = document.getElementById('email'); const passwortInput = document.getElementById('passwort'); const welcomeModal = document.getElementById('welcomeModal'); +const USERS_STORAGE_KEY = 'socialCookingUsers'; +const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + +// Liest bestehende Benutzerliste robust aus localStorage. +function getStoredUsers() { + try { + const raw = localStorage.getItem(USERS_STORAGE_KEY); + return raw ? JSON.parse(raw) : []; + } catch (error) { + console.error('Benutzerdaten konnten nicht gelesen werden.', error); + return []; + } +} + +// Schreibt die komplette Benutzerliste in localStorage. +function setStoredUsers(users) { + localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); +} + +// Speichert den aktiven Benutzer fuer nachfolgende Seiten. +function setCurrentUser(user) { + localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user)); +} + // Funktion zum Öffnen des Welcome Modals function openWelcomeModal() { welcomeModal.classList.add('show'); @@ -78,12 +102,35 @@ function validateForm(event) { passwortGroup.classList.remove('has-error'); } - // Wenn alle Validierungen bestanden, Modal anzeigen + // Wenn alle Validierungen bestanden, Benutzer speichern und Session setzen. if (isValid) { + const existingUsers = getStoredUsers(); + const emailLower = emailValue.toLowerCase(); + const emailAlreadyUsed = existingUsers.some(user => user.email?.toLowerCase() === emailLower); + + if (emailAlreadyUsed) { + emailGroup.classList.add('has-error'); + document.getElementById('emailError').textContent = 'Diese E-Mail ist bereits registriert. Bitte nutze den Login.'; + return; + } + + const newUser = { + id: Date.now(), + vorname: vornameValue, + nachname: nachnameValue, + email: emailValue, + passwort: passwortValue, + createdAt: new Date().toISOString(), + source: 'signup' + }; + + setStoredUsers([newUser, ...existingUsers]); + setCurrentUser(newUser); + openWelcomeModal(); - // Hier würde später die Registrierung zum Backend gesendet + // Hier würde spaeter die Registrierung zum Backend gesendet. - // Weiterleitung zur event overview Page + // Weiterleitung zur Event-Overview-Seite. window.location.href = 'event_overview.html'; } } diff --git a/login.html b/login.html index 16d2ea7..67f3daa 100644 --- a/login.html +++ b/login.html @@ -9,6 +9,7 @@ + @@ -20,9 +21,8 @@ Invite Logo
diff --git a/my_profil.html b/my_profil.html new file mode 100644 index 0000000..f5610a9 --- /dev/null +++ b/my_profil.html @@ -0,0 +1,102 @@ + + + + + + Mein Profil | Invité + + + + + + + + + +
+
+ + Invite Logo + + +
+
+ +
+
+
+

Mein Bereich

+

Mein Profil

+

Hier findest du deine Events, deine Anmeldungen und kannst deine Profildaten verwalten.

+
+ +
+ + + +
+
+
+

Meine Events

+ 0 +
+
+
+ +
+
+

Meine Anmeldungen

+ 0 +
+
+
+ +
+

Profil verwalten

+
+
+
+ + +

Bitte gib deinen Vornamen ein.

+
+ +
+ + +

Bitte gib deinen Nachnamen ein.

+
+
+ +
+ + +

Bitte gib eine gültige E-Mail-Adresse ein.

+
+ +
+ + +

Nur ausfüllen, wenn du dein Passwort ändern möchtest.

+

Das Passwort muss mindestens 6 Zeichen lang sein.

+
+ + +

+
+
+
+
+ + + + diff --git a/signup.html b/signup.html index f54de99..22e258d 100644 --- a/signup.html +++ b/signup.html @@ -9,6 +9,7 @@ + @@ -19,9 +20,8 @@ Invite Logo From c3bea2817c2368cffc9487d69409ec9568e46ec4 Mon Sep 17 00:00:00 2001 From: viiivo <«vivien.vonburg@outlook.com»> Date: Fri, 10 Apr 2026 18:05:46 +0200 Subject: [PATCH 13/14] feat: Enhance profile page with tab navigation and event management features Fix Event anmeldung und abmeldung Button. 12h vor Event beginn wird Adresse angezeigt. --- css/event_overview.css | 73 +++++++++++++++-- css/my_profil.css | 129 +++++++++++++++++++++++++++++ data/events.json | 7 +- js/event_create.js | 3 +- js/event_detail.js | 148 +++++++++++++++++++++++++++++++-- js/event_overview.js | 173 +++++++++++++++++++++++++++++++++++++-- js/my_profil.js | 182 +++++++++++++++++++++++++++++++++++++---- my_profil.html | 12 ++- 8 files changed, 686 insertions(+), 41 deletions(-) diff --git a/css/event_overview.css b/css/event_overview.css index 0c3e203..5f99e94 100644 --- a/css/event_overview.css +++ b/css/event_overview.css @@ -266,7 +266,6 @@ } .btn-primary { - background: var(--olive); color: #fffde8; border: none; border-radius: var(--radius-pill); @@ -275,10 +274,44 @@ line-height: 1.3; cursor: pointer; white-space: nowrap; + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; } -.btn-primary:hover { - filter: brightness(0.95); +.btn-primary-register { + background: var(--olive); +} + +.btn-primary-register:hover, +.btn-primary-register:focus-visible { + background: #575704; + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28); +} + +.btn-primary-register:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(107, 107, 5, 0.25); +} + +.btn-primary-danger { + background: var(--tomato); +} + +.btn-primary-danger:hover, +.btn-primary-danger:focus-visible { + background: var(--tomato-dark); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28); +} + +.btn-primary-danger:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; } .btn-primary-own, @@ -688,12 +721,42 @@ } .detail-primary-btn { - border: 2px solid var(--tomato); border-radius: var(--radius-pill); - background: var(--tomato); color: var(--white); padding: 10px 22px; cursor: pointer; + transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} + +.detail-primary-btn-register { + border: 2px solid var(--olive); + background: var(--olive); +} + +.detail-primary-btn-register:not(:disabled):hover, +.detail-primary-btn-register:not(:disabled):focus-visible { + background: #575704; + border-color: #575704; + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28); +} + +.detail-primary-btn-danger { + border: 2px solid var(--tomato); + background: var(--tomato); +} + +.detail-primary-btn-danger:not(:disabled):hover, +.detail-primary-btn-danger:not(:disabled):focus-visible { + background: var(--tomato-dark); + border-color: var(--tomato-dark); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28); +} + +.detail-primary-btn:not(:disabled):active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(102, 52, 13, 0.22); } .detail-primary-btn:disabled { diff --git a/css/my_profil.css b/css/my_profil.css index 825504a..28c9e15 100644 --- a/css/my_profil.css +++ b/css/my_profil.css @@ -44,6 +44,38 @@ gap: var(--space-4); } +.profile-tabs { + display: inline-flex; + flex-wrap: wrap; + gap: var(--space-2); +} + +.profile-tab { + border: 2px solid var(--olive); + border-radius: var(--radius-md); + background: var(--butter); + color: var(--black); + padding: 0.45rem 1rem; + min-height: 2.5rem; + font-family: "Jost", sans-serif; + font-size: 1rem; + font-weight: 500; + letter-spacing: var(--ls-ui); + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} + +.profile-tab:hover, +.profile-tab:focus-visible { + background: #faf8e8; +} + +.profile-tab.is-active { + border-color: transparent; + background: var(--olive); + color: var(--white); +} + /* Konsistentes Karten-Layout fuer alle Profilsektionen. */ .profile-panel { background: rgba(255, 255, 255, 0.88); @@ -92,6 +124,16 @@ gap: var(--space-3); } +.profile-event-card-clickable { + cursor: pointer; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.profile-event-card-clickable:hover { + box-shadow: 0 6px 16px rgba(102, 52, 13, 0.14); + transform: translateY(-1px); +} + .profile-event-title { margin: 0; color: var(--black); @@ -106,6 +148,31 @@ color: var(--olive); } +.profile-event-address-block { + margin-top: 0.55rem; + padding: 0.6rem 0.75rem; + border-radius: var(--radius-sm); + border-left: 4px solid var(--tomato); + background: rgba(232, 237, 209, 0.65); +} + +.profile-event-address-label { + margin: 0; + color: var(--olive); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: var(--ls-label); + text-transform: uppercase; +} + +.profile-event-address { + margin: 0.2rem 0 0; + font-size: 0.95rem; + color: var(--black); + font-weight: 600; + line-height: 1.35; +} + .profile-event-link { flex-shrink: 0; color: var(--blue); @@ -135,11 +202,44 @@ font-size: 0.95rem; font-weight: 500; cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} + +.profile-cancel-btn { + border: none; + border-radius: var(--radius-md); + background: var(--tomato); + color: var(--butter-light); + padding: 0.45rem 0.95rem; + font-family: "Jost", sans-serif; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} + +.profile-cancel-btn:hover, +.profile-cancel-btn:focus-visible { + background: var(--tomato-dark); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28); +} + +.profile-cancel-btn:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25); } .profile-unregister-btn:hover, .profile-unregister-btn:focus-visible { background: var(--tomato-dark); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28); +} + +.profile-unregister-btn:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25); } .profile-empty { @@ -147,6 +247,35 @@ color: var(--black); } +.profile-empty-state { + text-align: center; + padding: 2.4rem 1.3rem; + border: 2px solid var(--olive-light); + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 3px 12px rgba(102, 52, 13, 0.08); +} + +.profile-empty-kicker { + margin: 0 0 0.5rem; + color: var(--olive); + font-size: 0.8rem; + font-weight: 600; + letter-spacing: var(--ls-label); + text-transform: uppercase; +} + +.profile-empty-state h3 { + margin: 0; + font-size: 1.5rem; + color: var(--brown); +} + +.profile-empty-state p { + margin: 0.65rem auto 1rem; + max-width: 36rem; +} + .form-grid { display: grid; grid-template-columns: 1fr 1fr; diff --git a/data/events.json b/data/events.json index c5d1502..7f84219 100644 --- a/data/events.json +++ b/data/events.json @@ -3,8 +3,9 @@ "id": 1, "title": "Italienische Tavolata", "location": "Luzern", - "date": "19. MÄR. 2026", - "time": "18:30 UHR", + "address": "Pilatusstrasse 18, 6003 Luzern", + "date": "11. APR. 2026", + "time": "3:30 UHR", "category": "DINNER", "diet": "VEGGIE", "spots": 6, @@ -43,6 +44,7 @@ "id": 2, "title": "Noche Peruana", "location": "Chur", + "address": "Obere Gasse 41, 7000 Chur", "date": "11. APR. 2026", "time": "19:00 UHR", "category": "DINNER", @@ -84,6 +86,7 @@ "id": 3, "title": "Japanese Delight", "location": "ZÜRICH", + "address": "Limmatquai 92, 8001 Zürich", "date": "02. MAI. 2026", "time": "12:30 UHR", "category": "LUNCH", diff --git a/js/event_create.js b/js/event_create.js index e72d82d..d3cef97 100644 --- a/js/event_create.js +++ b/js/event_create.js @@ -429,7 +429,8 @@ function buildStoredEvent() { ? [] : getCheckboxValues("allergies").split(", ").filter(Boolean), allergiesNote: form.elements.allergiesOther.value.trim(), - participants: [usernameElement.textContent.trim() || "Host"], + // Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt. + participants: [], gallery: [], createdAt: new Date().toISOString(), source: "local" diff --git a/js/event_detail.js b/js/event_detail.js index 53bb7e2..2ca06ac 100644 --- a/js/event_detail.js +++ b/js/event_detail.js @@ -52,6 +52,80 @@ document.addEventListener('DOMContentLoaded', async () => { localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); } + function parseEventDateTime(event) { + if (!event?.date) { + return null; + } + + const dateValue = String(event.date).trim(); + const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/); + let year; + let month; + let day; + + if (isoDateMatch) { + year = Number(isoDateMatch[1]); + month = Number(isoDateMatch[2]); + day = Number(isoDateMatch[3]); + } else { + const monthMap = { + JAN: 1, + FEB: 2, + 'MÄR': 3, + MRZ: 3, + APR: 4, + MAI: 5, + JUN: 6, + JUL: 7, + AUG: 8, + SEP: 9, + OKT: 10, + NOV: 11, + DEZ: 12 + }; + const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/); + + if (!localizedMatch) { + return null; + } + + day = Number(localizedMatch[1]); + month = monthMap[localizedMatch[2]]; + year = Number(localizedMatch[3]); + + if (!month) { + return null; + } + } + + const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/); + const hours = timeMatch ? Number(timeMatch[1]) : 0; + const minutes = timeMatch ? Number(timeMatch[2]) : 0; + + return new Date(year, month - 1, day, hours, minutes, 0, 0); + } + + function isRegistrationClosedForEvent(event) { + const eventDateTime = parseEventDateTime(event); + if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { + return false; + } + + const msUntilStart = eventDateTime.getTime() - Date.now(); + const twelveHoursInMs = 12 * 60 * 60 * 1000; + + return msUntilStart <= twelveHoursInMs; + } + + function countRegistrationsForEvent(registrationMap, eventId) { + return Object.values(registrationMap).reduce((count, ids) => { + const hasEvent = Array.isArray(ids) + && ids.map(id => Number(id)).includes(Number(eventId)); + + return hasEvent ? count + 1 : count; + }, 0); + } + // Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde. function isEventOwnedByCurrentUser(event, user) { if (!event || !user) { @@ -71,6 +145,29 @@ document.addEventListener('DOMContentLoaded', async () => { return Boolean(userFirstName && hostName && userFirstName === hostName); } + // Prueft, ob der aktuelle Benutzer bereits in der Teilnehmerliste des Events steht. + function isUserListedInEventParticipants(event, user) { + if (!event || !user || !Array.isArray(event.participants)) { + return false; + } + + const participantSet = new Set( + event.participants + .map(name => String(name || '').trim().toLowerCase()) + .filter(Boolean) + ); + + const userFirstName = String(user.vorname || '').trim().toLowerCase(); + const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}` + .trim() + .toLowerCase(); + + return Boolean( + (userFirstName && participantSet.has(userFirstName)) + || (userFullName && participantSet.has(userFullName)) + ); + } + // Fetch data source and resolve the matching event record. try { const response = await fetch('data/events.json'); @@ -157,19 +254,47 @@ document.addEventListener('DOMContentLoaded', async () => { ? event.gallery : [event.image, event.image, event.image]; const visibleParticipants = participants.slice(0, 6); - const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length); + const registrationMap = getRegistrationMap(); + const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id); + const remainingParticipants = Math.max(0, participants.length + extraRegistrations - visibleParticipants.length); const totalGuests = Number.isFinite(event.spots) ? event.spots : 0; - const confirmedGuests = participants.length; + const confirmedGuests = participants.length + extraRegistrations; const freePlaces = Math.max(0, totalGuests - confirmedGuests); const isFull = freePlaces === 0; + const isRegistrationClosed = isRegistrationClosedForEvent(event); const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); - const registrationMap = getRegistrationMap(); const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) ? registrationMap[currentUser.email].map(id => Number(id)) : []; const isRegistered = userRegistrations.includes(Number(event.id)); - const actionButtonLabel = isOwnEvent ? 'Dein Event' : !currentUser ? 'Einloggen' : isRegistered ? 'Abmelden' : 'Anmelden'; - const actionButtonDisabled = isOwnEvent || (!isRegistered && isFull); + const isListedParticipant = isUserListedInEventParticipants(event, currentUser); + const hasAddressAccess = isRegistered || isListedParticipant; + const actionButtonLabel = isOwnEvent + ? 'Dein Event!' + : !currentUser + ? 'Einloggen' + : isRegistered + ? 'Abmelden' + : isRegistrationClosed + ? 'Anmeldung geschlossen' + : 'Anmelden'; + const actionButtonDisabled = isOwnEvent || (!isRegistered && (isFull || isRegistrationClosed)); + const actionButtonVariantClass = isOwnEvent + ? ' detail-primary-btn-own' + : isRegistered + ? ' detail-primary-btn-danger' + : isRegistrationClosed + ? ' detail-primary-btn-danger' + : ' detail-primary-btn-register'; + const shouldRevealAddress = Boolean(event.address) && isRegistrationClosed && hasAddressAccess; + const addressPanelMarkup = shouldRevealAddress + ? ` +
+

Adresse

+

${event.address}

+
+ ` + : ''; const detailChips = [ `${eventCategory}`, `${dietLabel}`, @@ -227,6 +352,8 @@ document.addEventListener('DOMContentLoaded', async () => { ${remainingParticipants > 0 ? `+${remainingParticipants}` : ''} + + ${addressPanelMarkup} @@ -280,6 +407,13 @@ document.addEventListener('DOMContentLoaded', async () => { const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item'); const registerButton = detailContainer.querySelector('[data-register-button]'); + // Harte Absicherung: Eigene Events sind auf der Detailseite immer deaktiviert. + if (registerButton && isOwnEvent) { + registerButton.disabled = true; + registerButton.textContent = 'Dein Event!'; + registerButton.setAttribute('aria-disabled', 'true'); + } + // Anmeldung toggeln und im lokalen Registrierungs-Store persistieren. if (registerButton) { registerButton.addEventListener('click', () => { @@ -300,7 +434,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (registrationSet.has(Number(event.id))) { registrationSet.delete(Number(event.id)); - } else if (!isFull) { + } else if (!isFull && !isRegistrationClosed) { registrationSet.add(Number(event.id)); } diff --git a/js/event_overview.js b/js/event_overview.js index 15bf312..723a753 100644 --- a/js/event_overview.js +++ b/js/event_overview.js @@ -1,6 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; // ------------------------------------------------------------- // DOM references used throughout the page lifecycle. // ------------------------------------------------------------- @@ -55,6 +56,20 @@ document.addEventListener('DOMContentLoaded', () => { } } + function getRegistrationMap() { + try { + const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY); + return stored ? JSON.parse(stored) : {}; + } catch (error) { + console.error('Anmeldedaten konnten nicht gelesen werden.', error); + return {}; + } + } + + function setRegistrationMap(registrationMap) { + localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); + } + // ------------------------------------------------------------- // Initial data bootstrap: // 1) fetch JSON, @@ -183,6 +198,83 @@ document.addEventListener('DOMContentLoaded', () => { : `${timeString} Uhr`; } + // Baut aus Eventdatum/-zeit ein Date-Objekt fuer Fristlogik und Vergleiche. + function parseEventDateTime(event) { + if (!event?.date) { + return null; + } + + const dateValue = String(event.date).trim(); + const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/); + let year; + let month; + let day; + + if (isoDateMatch) { + year = Number(isoDateMatch[1]); + month = Number(isoDateMatch[2]); + day = Number(isoDateMatch[3]); + } else { + const monthMap = { + JAN: 1, + FEB: 2, + 'MÄR': 3, + MRZ: 3, + APR: 4, + MAI: 5, + JUN: 6, + JUL: 7, + AUG: 8, + SEP: 9, + OKT: 10, + NOV: 11, + DEZ: 12 + }; + const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/); + + if (!localizedMatch) { + return null; + } + + day = Number(localizedMatch[1]); + month = monthMap[localizedMatch[2]]; + year = Number(localizedMatch[3]); + + if (!month) { + return null; + } + } + + const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/); + const hours = timeMatch ? Number(timeMatch[1]) : 0; + const minutes = timeMatch ? Number(timeMatch[2]) : 0; + + return new Date(year, month - 1, day, hours, minutes, 0, 0); + } + + // Zaehlt eindeutige Registrierungen eines Events ueber alle Benutzer. + function countRegistrationsForEvent(registrationMap, eventId) { + return Object.values(registrationMap).reduce((count, ids) => { + const hasEvent = Array.isArray(ids) + && ids.map(id => Number(id)).includes(Number(eventId)); + + return hasEvent ? count + 1 : count; + }, 0); + } + + // Schliesst neue Anmeldungen ab 12h vor Start (inkl. bereits gestarteter Events). + function isRegistrationClosedForEvent(event) { + const eventDateTime = parseEventDateTime(event); + if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) { + return false; + } + + const msUntilStart = eventDateTime.getTime() - Date.now(); + const twelveHoursInMs = 12 * 60 * 60 * 1000; + + return msUntilStart <= twelveHoursInMs; + } + // Safely verify whether a value exists in the given select element. function hasOption(selectElement, value) { return Array.from(selectElement.options).some(option => option.value === value); @@ -202,6 +294,11 @@ document.addEventListener('DOMContentLoaded', () => { }); const filtered = allEvents.filter(event => { + // Lokal erstellte Events werden nicht in der allgemeinen Event-Uebersicht angezeigt. + if (event.source === 'local') { + return false; + } + const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory; const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation; const eventDateIso = parseEventDateToIso(event.date); @@ -222,6 +319,10 @@ document.addEventListener('DOMContentLoaded', () => { // - or event cards with status and metadata. function renderEvents(events) { eventGrid.innerHTML = ''; + const registrationMap = getRegistrationMap(); + const userRegistrationSet = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) + ? new Set(registrationMap[currentUser.email].map(id => Number(id))) + : new Set(); if (events.length === 0) { eventGrid.innerHTML = ` @@ -242,20 +343,28 @@ document.addEventListener('DOMContentLoaded', () => { const card = document.createElement('article'); card.className = 'event-card'; card.style.cursor = 'pointer'; - card.onclick = () => { + card.addEventListener('click', clickedEvent => { + if (clickedEvent.target instanceof HTMLElement && clickedEvent.target.closest('button')) { + return; + } + window.location.href = `event_detail.html?id=${event.id}`; - }; + }); const displayDate = formatEventDate(event.date); const displayTime = formatEventTime(event.time); // Capacity logic: // spots = total capacity, participants.length = booked seats. - const bookedSeats = event.participants ? event.participants.length : 0; + const baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0; + const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id); + const bookedSeats = baseParticipants + extraRegistrations; const totalCapacity = event.spots; const freePlaces = Math.max(0, totalCapacity - bookedSeats); const isFull = freePlaces === 0; const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); + const isRegistered = userRegistrationSet.has(Number(event.id)); + const isRegistrationClosed = isRegistrationClosedForEvent(event); // Build optional specification chips only when data exists. const specsChips = event.specifications && event.specifications.length > 0 @@ -263,10 +372,16 @@ document.addEventListener('DOMContentLoaded', () => { : ''; const actionMarkup = isOwnEvent - ? '' - : isFull - ? '' - : ''; + ? '' + : isRegistered + ? '' + : isRegistrationClosed + ? '' + : isFull + ? '' + : !currentUser + ? '' + : ''; card.innerHTML = `
@@ -290,6 +405,50 @@ document.addEventListener('DOMContentLoaded', () => {
`; + const actionButton = card.querySelector('[data-registration-action]'); + if (actionButton) { + actionButton.addEventListener('click', clickEvent => { + clickEvent.stopPropagation(); + + const action = actionButton.getAttribute('data-registration-action'); + if (action === 'own') { + return; + } + + if (action === 'closed') { + return; + } + + if (action === 'login') { + window.location.href = 'login.html'; + return; + } + + if (!currentUser?.email) { + window.location.href = 'login.html'; + return; + } + + const nextRegistrationMap = getRegistrationMap(); + const currentIds = Array.isArray(nextRegistrationMap[currentUser.email]) + ? nextRegistrationMap[currentUser.email].map(id => Number(id)) + : []; + const idSet = new Set(currentIds); + + if (action === 'unregister') { + idSet.delete(Number(event.id)); + } + + if (action === 'register' && !isFull && !isRegistrationClosed) { + idSet.add(Number(event.id)); + } + + nextRegistrationMap[currentUser.email] = Array.from(idSet); + setRegistrationMap(nextRegistrationMap); + applyFilters(); + }); + } + eventGrid.appendChild(card); }); } diff --git a/js/my_profil.js b/js/my_profil.js index 9d7ee5f..9d47a5b 100644 --- a/js/my_profil.js +++ b/js/my_profil.js @@ -10,6 +10,8 @@ document.addEventListener('DOMContentLoaded', () => { const profileHeadline = document.getElementById('profile-headline'); const profileSubline = document.getElementById('profile-subline'); const logoutButton = document.getElementById('logout-button'); + const profileTabButtons = Array.from(document.querySelectorAll('[data-profile-tab]')); + const profileTabPanels = Array.from(document.querySelectorAll('[data-profile-panel]')); const myEventsCount = document.getElementById('my-events-count'); const myRegistrationsCount = document.getElementById('my-registrations-count'); @@ -36,6 +38,7 @@ document.addEventListener('DOMContentLoaded', () => { renderLoggedInState(currentUser); bindFormHandlers(); + activateProfileTab('hosting'); allEvents = await loadAllEvents(); renderMyEvents(allEvents, currentUser); @@ -80,6 +83,11 @@ document.addEventListener('DOMContentLoaded', () => { localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); } + // Schreibt die lokal erstellten Events in den Storage. + function setStoredEvents(events) { + localStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(events)); + } + // Fuehrt JSON-Daten und lokal erstellte Events in einer Liste zusammen. async function loadAllEvents() { try { @@ -119,6 +127,18 @@ document.addEventListener('DOMContentLoaded', () => { function bindFormHandlers() { profileForm.addEventListener('submit', handleProfileSubmit); myRegistrationsList.addEventListener('click', handleRegistrationListClick); + myEventsList.addEventListener('click', handleHostedListClick); + + profileTabButtons.forEach(button => { + button.addEventListener('click', () => { + const tabName = button.getAttribute('data-profile-tab'); + if (!tabName) { + return; + } + + activateProfileTab(tabName); + }); + }); [vornameInput, nachnameInput, emailInput, passwortInput].forEach(input => { input.addEventListener('input', () => { @@ -133,6 +153,53 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Reagiert auf Aktionen in der Liste "Meine Events" per Event Delegation. + function handleHostedListClick(event) { + const target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + + const cancelButton = target.closest('[data-cancel-event-id]'); + if (cancelButton && currentUser?.email) { + const eventId = Number(cancelButton.getAttribute('data-cancel-event-id')); + if (Number.isFinite(eventId)) { + cancelHostedEvent(eventId, currentUser.email); + } + return; + } + + if (target.closest('a, button')) { + return; + } + + const card = target.closest('[data-event-id]'); + if (!card) { + return; + } + + const eventId = Number(card.getAttribute('data-event-id')); + if (!Number.isFinite(eventId)) { + return; + } + + window.location.href = `event_detail.html?id=${eventId}`; + } + + // Schaltet den sichtbaren Profilbereich per Tabname um. + function activateProfileTab(tabName) { + profileTabButtons.forEach(button => { + const isActive = button.getAttribute('data-profile-tab') === tabName; + button.classList.toggle('is-active', isActive); + button.setAttribute('aria-selected', isActive ? 'true' : 'false'); + }); + + profileTabPanels.forEach(panel => { + const isActive = panel.getAttribute('data-profile-panel') === tabName; + panel.classList.toggle('hidden', !isActive); + }); + } + // Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation. function handleRegistrationListClick(event) { const target = event.target; @@ -141,18 +208,68 @@ document.addEventListener('DOMContentLoaded', () => { } const unregisterButton = target.closest('[data-unregister-id]'); - if (!unregisterButton || !currentUser?.email) { + if (unregisterButton) { + if (!currentUser?.email) { + return; + } + + const eventId = Number(unregisterButton.getAttribute('data-unregister-id')); + if (!Number.isFinite(eventId)) { + return; + } + + unregisterFromEvent(eventId, currentUser.email); return; } - const eventId = Number(unregisterButton.getAttribute('data-unregister-id')); + if (target.closest('a, button')) { + return; + } + + const card = target.closest('[data-event-id]'); + if (!card) { + return; + } + + const eventId = Number(card.getAttribute('data-event-id')); if (!Number.isFinite(eventId)) { return; } - unregisterFromEvent(eventId, currentUser.email); + window.location.href = `event_detail.html?id=${eventId}`; } + + // Sagt ein gehostetes Event ab (aus eigener Profilansicht entfernen). + function cancelHostedEvent(eventId, userEmail) { + // Lokal erstellte, eigene Events werden direkt aus dem Storage geloescht. + const storedEvents = getStoredEvents(); + const nextStoredEvents = storedEvents.filter(event => { + const isTargetEvent = Number(event.id) === eventId; + const isOwnedByUser = normalizeText(event.hostEmail || '') === normalizeText(userEmail) + || normalizeText(event.host?.name || '') === normalizeText(currentUser?.vorname || ''); + + return !(isTargetEvent && isOwnedByUser); + }); + setStoredEvents(nextStoredEvents); + + // Event-ID fuer alle Benutzer aus den Anmeldungen entfernen. + const registrationMap = getRegistrationMap(); + Object.keys(registrationMap).forEach(email => { + const ids = Array.isArray(registrationMap[email]) + ? registrationMap[email].map(id => Number(id)).filter(Number.isFinite) + : []; + + registrationMap[email] = ids.filter(id => id !== eventId); + }); + setRegistrationMap(registrationMap); + + allEvents = allEvents.filter(event => Number(event.id) !== eventId); + + renderMyEvents(allEvents, currentUser); + renderMyRegistrations(allEvents, currentUser); + profileFeedback.textContent = 'Event wurde abgesagt und aus deinem Hosting entfernt.'; + } // Entfernt eine Event-ID aus der Benutzerliste und aktualisiert die UI sofort. function unregisterFromEvent(eventId, userEmail) { const registrationMap = getRegistrationMap(); @@ -260,15 +377,20 @@ document.addEventListener('DOMContentLoaded', () => { localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map)); } - // Ermittelt gehostete Events anhand Host-E-Mail oder Host-Vorname. + // Ermittelt gehostete Events aus lokal erstellten Daten des aktuellen Benutzers. function getMyHostedEvents(events, user) { - const userFirstName = normalizeText(user.vorname); + const userFirstName = normalizeText(user.vorname || ''); + const userEmail = normalizeText(user.email || ''); return events.filter(event => { + if (event.source !== 'local') { + return false; + } + const hostEmail = normalizeText(event.hostEmail || ''); const hostName = normalizeText(event.host?.name || ''); - if (hostEmail && hostEmail === normalizeText(user.email)) { + if (hostEmail && hostEmail === userEmail) { return true; } @@ -289,45 +411,73 @@ document.addEventListener('DOMContentLoaded', () => { function renderMyEvents(events, user) { const hostedEvents = getMyHostedEvents(events, user); myEventsCount.textContent = String(hostedEvents.length); - renderEventCards(myEventsList, hostedEvents, 'Du hast noch kein eigenes Event erstellt.', false); + renderEventCards(myEventsList, hostedEvents, { + title: 'Noch kein eigenes Event', + text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.', + buttonLabel: 'Event erstellen', + href: 'event_create.html' + }, 'hosting'); } // Rendert angemeldete Events inkl. Zaehler. function renderMyRegistrations(events, user) { const registeredEvents = getMyRegisteredEvents(events, user); myRegistrationsCount.textContent = String(registeredEvents.length); - renderEventCards(myRegistrationsList, registeredEvents, 'Du bist aktuell bei keinem Event angemeldet.', true); + renderEventCards(myRegistrationsList, registeredEvents, { + title: 'Noch keine Anmeldungen', + text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.', + buttonLabel: 'Events entdecken', + href: 'event_overview.html' + }, 'registrations'); } // Baut die Eventkarten fuer beide Listen in einheitlichem Markup. - function renderEventCards(container, events, emptyText, withUnregisterButton) { + function renderEventCards(container, events, emptyStateConfig, mode) { container.innerHTML = ''; if (events.length === 0) { - const emptyElement = document.createElement('p'); - emptyElement.className = 'profile-empty'; - emptyElement.textContent = emptyText; + const emptyElement = document.createElement('div'); + emptyElement.className = 'profile-empty-state'; + emptyElement.innerHTML = ` +

Keine Treffer

+

${emptyStateConfig.title}

+

${emptyStateConfig.text}

+ ${emptyStateConfig.buttonLabel} + `; container.appendChild(emptyElement); return; } events.forEach(event => { const card = document.createElement('article'); - card.className = 'profile-event-card'; + card.className = 'profile-event-card profile-event-card-clickable'; + card.setAttribute('data-event-id', String(event.id)); + const addressMarkup = mode === 'registrations' && event.address + ? ` +
+

Adresse

+

${event.address}

+
+ ` + : ''; - const actionMarkup = withUnregisterButton + const actionMarkup = mode === 'registrations' ? `
- Zum Event
` - : `Zum Event`; + : ` +
+ +
+ `; card.innerHTML = `

${event.title}

${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}

+ ${addressMarkup}
${actionMarkup} `; diff --git a/my_profil.html b/my_profil.html index f5610a9..da5bb7f 100644 --- a/my_profil.html +++ b/my_profil.html @@ -44,7 +44,13 @@
-
+ + +

Meine Events

0 @@ -52,7 +58,7 @@
-
+ -
+