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/assets/eventcreate_foodtable.jpg b/assets/eventcreate_foodtable.jpg new file mode 100644 index 0000000..07e8aeb Binary files /dev/null and b/assets/eventcreate_foodtable.jpg differ diff --git a/assets/logo_invite.svg b/assets/logo_invite.svg index ff16a2c..ab87e7f 100644 --- a/assets/logo_invite.svg +++ b/assets/logo_invite.svg @@ -1,13 +1,9 @@ - - - - - - - - - - - - + + + + + + + + diff --git a/css/event_create.css b/css/event_create.css index 9bccef6..a58da91 100644 --- a/css/event_create.css +++ b/css/event_create.css @@ -6,6 +6,25 @@ --control-min-height: 3rem; --input-min-height: 3.5rem; --card-min-height: 6rem; + + --color-bg: var(--butter); + --color-surface: var(--white); + --color-surface-soft: var(--butter-light); + --color-text: var(--black); + --color-text-secondary: rgba(34, 33, 26, 0.8); + --color-muted: rgba(34, 33, 26, 0.68); + --color-border: rgba(102, 52, 13, 0.16); + --color-border-strong: var(--brown); + --color-divider: rgba(102, 52, 13, 0.14); + --color-primary: var(--olive); + --color-primary-hover: var(--olive-dark); + --color-progress-bg: rgba(212, 75, 36, 0.18); + --color-focus: var(--blue); + --color-error: var(--error); + --shadow-soft: 0 12px 30px rgba(102, 52, 13, 0.1); + --input-border-soft: rgba(102, 52, 13, 0.2); + --input-border-focus: rgba(107, 107, 5, 0.45); + --input-shadow-focus: 0 0 0 4px rgba(107, 107, 5, 0.12); } *, @@ -20,7 +39,7 @@ html { body { margin: 0; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: var(--font-main); background: var(--color-bg); color: var(--color-text); line-height: 1.5; @@ -108,6 +127,10 @@ a { padding: var(--space-4) 0 var(--space-7); } +.submission-success { + padding: var(--space-4) 0 var(--space-7); +} + .step--active { display: block; } @@ -123,6 +146,7 @@ a { min-height: 60vh; align-content: center; grid-template-columns: 1fr; + gap: var(--space-7); } .step-copy, @@ -159,6 +183,7 @@ fieldset { h1, h2 { margin: 0; + font-family: "Bagel Fat One", cursive; font-size: clamp(2rem, 4vw, 4rem); line-height: 1.03; letter-spacing: -0.03em; @@ -185,9 +210,22 @@ h2 { background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft)); } -.intro-card-emoji { - font-size: 2rem; - margin-bottom: var(--space-3); +.intro-card--image { + width: 100%; + padding: 0; + border: 0; + overflow: hidden; + background: transparent; + box-shadow: none; +} + +.intro-image { + width: 100%; + aspect-ratio: 16 / 10; + display: block; + object-fit: cover; + border-radius: 1.875rem; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12); } label, @@ -208,11 +246,13 @@ input[type="number"], textarea { width: 100%; min-height: var(--input-min-height); - padding: 0.95rem 1rem; - border: 1px solid var(--color-border); - border-radius: 1rem; - background: var(--color-surface); + padding: 1rem 1.1rem; + border: 1px solid var(--input-border-soft); + border-radius: 1.125rem; + background: var(--butter-light); color: var(--color-text); + box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04); + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; } textarea { @@ -220,6 +260,30 @@ textarea { resize: vertical; } +input[type="text"]:hover, +input[type="date"]:hover, +input[type="time"]:hover, +input[type="number"]:hover, +textarea:hover { + border-color: rgba(102, 52, 13, 0.28); +} + +input[type="text"]:focus, +input[type="date"]:focus, +input[type="time"]:focus, +input[type="number"]:focus, +textarea:focus { + border-color: var(--input-border-focus); + box-shadow: var(--input-shadow-focus); + background: var(--butter-light); + outline: none; +} + +.field-invalid { + border-color: var(--tomato) !important; + box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14); +} + .field-row { display: grid; gap: var(--space-4); @@ -238,8 +302,8 @@ textarea { padding: 1rem 1rem 1rem 1.05rem; border: 1px solid var(--color-border); border-radius: 1rem; - background: var(--color-surface); - transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + background: var(--butter-light); + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease, color 0.2s ease; } .option-card small { @@ -247,6 +311,7 @@ textarea { } .option-card:hover { + background: var(--olive-light); transform: translateY(-1px); box-shadow: var(--shadow-soft); } @@ -259,7 +324,18 @@ textarea { } .option-card:has(input:checked) { - border: 2px solid var(--color-border-strong); + border: 1px solid var(--color-primary); + background: var(--color-primary); + color: var(--white); +} + +.option-card:has(input:checked) small { + color: rgba(247, 246, 230, 0.88); +} + +.option-card--invalid { + border-color: var(--tomato) !important; + box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14); } .counter { @@ -281,15 +357,44 @@ textarea { .counter-button { width: var(--control-min-height); height: var(--control-min-height); - border: 1px solid var(--color-border); + border: 1px solid var(--color-primary); border-radius: 50%; - background: var(--color-surface); + background: var(--color-primary); + color: var(--white); font-size: 1.5rem; + line-height: 1; + box-shadow: 0 6px 16px rgba(107, 107, 5, 0.18); + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} + +.counter-button:hover { + background: var(--color-primary-hover); + transform: translateY(-1px); +} + +.counter-button:focus-visible { + outline: 3px solid rgba(107, 107, 5, 0.22); + outline-offset: 3px; } .review-card { - padding: var(--space-5); - border-radius: var(--radius-lg); + display: grid; + gap: var(--space-4); + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.review-card--success { + display: grid; + gap: var(--space-5); + padding: var(--space-3) 0 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } .review-list { @@ -300,14 +405,29 @@ textarea { .review-item { display: grid; - gap: var(--space-1); - padding-bottom: var(--space-4); - border-bottom: 1px solid var(--color-divider); + gap: var(--space-2); + padding: 1rem 1.1rem; + border: 1px solid var(--input-border-soft); + border-radius: 1.125rem; + background: var(--butter-light); + box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04); + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease; } .review-item:last-child { - border-bottom: 0; - padding-bottom: 0; + border-bottom: 1px solid var(--input-border-soft); +} + +.review-item:hover { + border-color: rgba(102, 52, 13, 0.28); + background: rgba(247, 246, 230, 0.92); + transform: translateY(-1px); +} + +.review-item:focus-visible { + outline: 3px solid rgba(107, 107, 5, 0.2); + outline-offset: 3px; } .review-item dt { @@ -320,16 +440,29 @@ textarea { color: var(--color-text-secondary); } +.submission-success-actions { + display: flex; + justify-content: center; +} + .flow-footer { position: sticky; bottom: 0; z-index: 5; margin-top: auto; - background: rgba(247, 247, 242, 0.96); - backdrop-filter: blur(8px); + background: var(--color-bg); + backdrop-filter: none; + padding-top: var(--space-4); padding-bottom: env(safe-area-inset-bottom); } +.progress-wrap { + position: relative; + width: min(100%, var(--content-width)); + margin: 0 auto; + padding-top: 4.35rem; +} + .progress { width: 100%; height: 0.375rem; @@ -340,15 +473,50 @@ textarea { display: block; width: 0; height: 100%; - background: var(--color-primary); + background: var(--tomato); transition: width 0.25s ease; } +.progress-marker { + position: absolute; + top: 0; + transform: translateX(-50%); + display: grid; + justify-items: center; + gap: 0.2rem; + pointer-events: none; +} + +.progress-marker::after { + content: ""; + width: 0.125rem; + height: 1rem; + background: var(--tomato); + border-radius: 999px; +} + +.progress-marker__circle { + width: 2.9rem; + height: 2.9rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--tomato); + color: var(--butter-light); + font-size: 1.35rem; + font-weight: 600; + line-height: 1; + box-shadow: 0 10px 24px rgba(212, 75, 36, 0.18); +} + .flow-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); + width: min(100%, var(--content-width)); + margin: 0 auto; padding: var(--space-4) 0; } @@ -455,8 +623,10 @@ textarea:focus-visible { @media (min-width: 768px) { .step-layout--intro { - grid-template-columns: 1.25fr 0.8fr; + width: min(100%, 56rem); + grid-template-columns: 1fr 1fr; align-items: center; + gap: var(--space-8); } .field-row { @@ -470,4 +640,4 @@ textarea:focus-visible { .option-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } -} \ No newline at end of file +} diff --git a/css/event_overview.css b/css/event_overview.css index 30901c9..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,52 @@ 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, +.btn-primary-own:disabled { + background: var(--olive-light); + color: var(--black); + opacity: 1; + cursor: not-allowed; } /* --------------------------------------------------------- @@ -680,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 { @@ -693,6 +764,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/login_signup.css b/css/login_signup.css index eeb7809..632b901 100644 --- a/css/login_signup.css +++ b/css/login_signup.css @@ -161,14 +161,14 @@ button[type="submit"]:active { } .error-message { - color: var(--tomato); + color: var(--error); font-size: 0.8rem; margin-top: 5px; display: none; } .form-group.has-error input { - border-color: var(--tomato); + border-color: var(--error); box-shadow: 0 0 5px rgba(212, 75, 36, 0.3); } diff --git a/css/my_profil.css b/css/my_profil.css new file mode 100644 index 0000000..28c9e15 --- /dev/null +++ b/css/my_profil.css @@ -0,0 +1,380 @@ +.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); +} + +.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); + 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-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); + 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-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); + 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; + 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 { + margin: 0; + 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; + 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 5b2120b..e1ef140 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); @@ -64,98 +73,169 @@ img { /* Typography */ -h1, h2 { +h1, h2, h3, h4, h5, h6 { 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: 3rem; + padding: 0.1875rem 0.75rem 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 { +.nav-tab { + color: var(--black); + font-size: 1.125rem; + font-weight: 500; + letter-spacing: var(--ls-sm); + line-height: 1; + text-decoration: none; +} + +.nav-tab:hover, .nav-tab:active, +.nav-tab:focus-visible { + text-decoration: underline; + text-underline-offset: 4px; +} + +.button-small:hover, .button-small:active, +.button-small:focus-visible { + background: var(--olive-dark); + color: var(--black); +} + +.nav-tab-links { display: flex; align-items: center; - gap: 16px; - + gap: var(--space-5); } -.nav-link { - color: var(--black); - line-height: 1.3; + +/* 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 { + background: var(--olive); + 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); - 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-dark); + 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); } -.login-pill { +.auth-nav-button--active { background: var(--olive); - border-radius: var(--radius-md); - color: var(--butter); - line-height: 1.3; - padding: var(--space-1) var(--space-4); - text-decoration: none; + color: var(--butter-light); + border: 2px solid var(--olive); } .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 +245,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; @@ -198,8 +254,26 @@ p { display: none; } +/* Footer */ +.footer { + display: flex; + justify-content: center; + align-items: center; + gap: 1.5rem; + padding: var(--space-3) var(--space-5); + border: none; + margin-top: 2.5rem; +} + +.footer__link { + color: var(--black); + text-decoration: underline; + font-size: 0.8rem; + font-weight: 400; +} + /* Media Queries (Responsive) */ -@media (max-width: 768px) { +@media (max-width: 48rem) { .container { width: 95%; } 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/datenschutz.html b/datenschutz.html index c695f91..aef1b6c 100644 --- a/datenschutz.html +++ b/datenschutz.html @@ -5,20 +5,19 @@ Invité | Datenschutz - + -
+
-
+

Datenschutzerklärung

diff --git a/event_create.html b/event_create.html index ef3176e..ececc6e 100644 --- a/event_create.html +++ b/event_create.html @@ -5,30 +5,29 @@ Event erstellen | Invité - - - + + + + - +
+
- Abbrechen
@@ -43,16 +42,19 @@

Hey {{username}}, was hast du vor?

Erzähl uns von deiner Idee – vom Essen bis zur Stimmung. Ob Dinner, Brunch - oder etwas ganz Eigenes – wir helfen dir dabei, dein Event Schritt für Schritt aufzubauen. + oder etwas ganz Eigenes – wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.

-
@@ -357,4 +415,4 @@ - \ No newline at end of file + diff --git a/event_detail.html b/event_detail.html index 4d315f8..1a0cc45 100644 --- a/event_detail.html +++ b/event_detail.html @@ -4,24 +4,23 @@ Event-Detail - - - - - - - - + + + + + + + + +
diff --git a/event_overview.html b/event_overview.html index 8a7c85c..ddd97e4 100644 --- a/event_overview.html +++ b/event_overview.html @@ -5,24 +5,22 @@ Event-Overview - - + + - - + + - +
diff --git a/impressum.html b/impressum.html index ae9967d..dc76f26 100644 --- a/impressum.html +++ b/impressum.html @@ -5,20 +5,19 @@ Invité | Impressum - + -
+
-
+

Impressum

diff --git a/index.html b/index.html index 5e9b39f..ba30bfa 100644 --- a/index.html +++ b/index.html @@ -4,24 +4,27 @@ Invité | Events entdecken - - - - - - -
-
- - Invité - -
+
diff --git a/js/event_create.js b/js/event_create.js index a633a24..d3cef97 100644 --- a/js/event_create.js +++ b/js/event_create.js @@ -8,9 +8,14 @@ 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"; +const CURRENT_USER_KEY = "socialCookingCurrentUser"; // ============================= // STATE: aktueller Schritt im Flow @@ -27,11 +32,25 @@ 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 -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; // ============================= @@ -57,6 +76,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 +123,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 +169,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 +288,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 +312,139 @@ 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() + }, + hostEmail: currentUser?.email || "", + hostMessage: [eventDescription], + menu: buildMenuItems(menuDescription), + specifications: getCheckboxValues("allergies") === "Keine Angabe" + ? [] + : getCheckboxValues("allergies").split(", ").filter(Boolean), + allergiesNote: form.elements.allergiesOther.value.trim(), + // Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt. + participants: [], + 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 +464,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 +499,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 +521,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 +578,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 +617,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 +763,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..8481cc7 100644 --- a/js/event_detail.js +++ b/js/event_detail.js @@ -1,9 +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); @@ -14,10 +18,174 @@ 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 []; + } + } + + 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)); + } + + 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; + } + + // Adresse ist nur im 12h-Fenster VOR Eventstart sichtbar. + function isAddressVisibleWindow(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 >= 0 && 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) { + 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); + } + + // 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'); - const allEvents = await response.json(); + const apiEvents = await response.json(); + const allEvents = [...getStoredEvents(), ...apiEvents]; const event = allEvents.find(e => e.id === eventId); if (event) { @@ -99,11 +267,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 userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) + ? registrationMap[currentUser.email].map(id => Number(id)) + : []; + const isRegistered = userRegistrations.includes(Number(event.id)); + 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) && isAddressVisibleWindow(event) && hasAddressAccess; + const addressPanelMarkup = shouldRevealAddress + ? ` +
+

Adresse

+

${event.address}

+
+ ` + : ''; const detailChips = [ `${eventCategory}`, `${dietLabel}`, @@ -161,6 +365,8 @@ document.addEventListener('DOMContentLoaded', async () => { ${remainingParticipants > 0 ? `+${remainingParticipants}` : ''} + + ${addressPanelMarkup} @@ -212,6 +418,46 @@ 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]'); + + // 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', () => { + 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 && !isRegistrationClosed) { + 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() { @@ -257,4 +503,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..723a753 100644 --- a/js/event_overview.js +++ b/js/event_overview.js @@ -1,4 +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. // ------------------------------------------------------------- @@ -13,6 +16,59 @@ 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 { + const stored = localStorage.getItem(EVENTS_STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error('Lokale Events konnten nicht gelesen werden.', error); + return []; + } + } + + 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: @@ -24,7 +80,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 +120,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 +154,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 +189,90 @@ 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`; + } + + // 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. @@ -144,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); @@ -164,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 = ` @@ -184,25 +343,46 @@ 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 ? event.specifications.map(spec => `${spec}`).join('') : ''; + const actionMarkup = isOwnEvent + ? '' + : isRegistered + ? '' + : isRegistrationClosed + ? '' + : isFull + ? '' + : !currentUser + ? '' + : ''; + card.innerHTML = `
@@ -221,10 +401,54 @@ document.addEventListener('DOMContentLoaded', () => {
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`} - ${isFull ? '' : ''} + ${actionMarkup}
`; + 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/index-carousel.js b/js/index-carousel.js index bedc437..88045a7 100644 --- a/js/index-carousel.js +++ b/js/index-carousel.js @@ -1,10 +1,20 @@ +// ============================================= +// Galerie-Karussell (Startseite) +// Diese Datei steuert die Foto-Galerie mit Pfeilen. +// ============================================= + +// Wichtige Elemente aus dem HTML holen. const carouselTrack = document.querySelector('.gallery__track'); const prevArrow = document.querySelector('.gallery__arrow--prev'); const nextArrow = document.querySelector('.gallery__arrow--next'); const dotsContainer = document.querySelector('.gallery__dots'); +// Nur ausfuehren, wenn die Galerie auf der Seite vorhanden ist. if (carouselTrack) { + // Alle einzelnen Karten/Bilder im Track sammeln. const items = Array.from(carouselTrack.querySelectorAll('.gallery__item')); + + // Auf Mobile zeigen wir 1 Bild, auf Desktop 3 Bilder pro "Seite". const getItemsPerPage = () => (window.matchMedia('(max-width: 900px)').matches ? 1 : 3); let itemsPerPage = getItemsPerPage(); let pageCount = Math.ceil(items.length / itemsPerPage); @@ -52,11 +62,13 @@ if (carouselTrack) { updateTrack(); } + // Geht zur naechsten Seite (mit Wrap-around am Ende). function showNext() { activePage = (activePage + 1) % pageCount; updateTrack(); } + // Geht zur vorherigen Seite (mit Wrap-around zum Ende). function showPrev() { activePage = (activePage - 1 + pageCount) % pageCount; updateTrack(); @@ -64,14 +76,17 @@ if (carouselTrack) { buildDots(); + // Klick-Steuerung der Pfeile. if (nextArrow) nextArrow.addEventListener('click', showNext); if (prevArrow) prevArrow.addEventListener('click', showPrev); + // Tastatur-Support fuer Barrierefreiheit. document.addEventListener('keydown', function(event) { if (event.key === 'ArrowRight') showNext(); if (event.key === 'ArrowLeft') showPrev(); }); + // Reagiert auf Bildschirmgroessen-Aenderungen. window.addEventListener('resize', function() { var newPerPage = getItemsPerPage(); if (newPerPage !== itemsPerPage) { diff --git a/js/landingpage.js b/js/landingpage.js index c70097e..46c015c 100644 --- a/js/landingpage.js +++ b/js/landingpage.js @@ -1,8 +1,18 @@ +// ============================================= +// Mini-Galerie auf der Landingpage +// Diese Datei hebt immer ein Bild hervor und +// erlaubt Navigation mit Pfeilen/Tastatur. +// ============================================= + +// Elemente aus dem DOM lesen. const prevBtn = document.querySelector('.arrow--prev'); const nextBtn = document.querySelector('.arrow--next'); const items = Array.from(document.querySelectorAll('.gallery__item')); let activeIndex = 0; +// Aktualisiert die Darstellung aller Bilder: +// - aktives Bild ist klar sichtbar +// - inaktive Bilder sind abgeblendet function updateGallery() { items.forEach((item, i) => { item.style.opacity = i === activeIndex ? '1' : '0.35'; @@ -10,20 +20,25 @@ function updateGallery() { }); } +// Ein Schritt nach rechts. function showNext() { + if (!items.length) return; activeIndex = (activeIndex + 1) % items.length; updateGallery(); } +// Ein Schritt nach links. function showPrev() { + if (!items.length) return; activeIndex = (activeIndex - 1 + items.length) % items.length; updateGallery(); } -nextBtn.addEventListener('click', showNext); -prevBtn.addEventListener('click', showPrev); +// Event-Handler nur registrieren, wenn die Buttons existieren. +if (nextBtn) nextBtn.addEventListener('click', showNext); +if (prevBtn) prevBtn.addEventListener('click', showPrev); -// keyboard support +// Tastatursteuerung fuer bessere Bedienbarkeit. document.addEventListener('keydown', (event) => { if (event.key === 'ArrowRight') { showNext(); @@ -33,4 +48,5 @@ document.addEventListener('keydown', (event) => { } }); +// Initialen Zustand einmal setzen. updateGallery(); \ No newline at end of file diff --git a/js/login.js b/js/login.js index f4dcb41..ad1c486 100644 --- a/js/login.js +++ b/js/login.js @@ -1,9 +1,52 @@ +// ============================================= +// Login-Logik +// Diese Datei validiert die Eingaben, sucht den +// Benutzer im localStorage und legt die Session an. +// ============================================= + +// Formular und Felder aus dem HTML holen. const loginForm = document.getElementById('loginForm'); const emailInput = document.getElementById('email'); 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,13 +86,33 @@ function validateForm(event) { passwortGroup.classList.remove('has-error'); } - // Wenn alle Validierungen bestanden, Form absenden + // Wenn alle Validierungen bestanden, pruefen wir: + // 1) gibt es den Benutzer schon? + // 2) ist das Passwort korrekt? + // Danach speichern wir die aktive Session. if (isValid) { + const users = getStoredUsers(); + const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase()); + + 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); + + // Snackbar anzeigen und dann zur Event-Uebersicht weiterleiten. var snackbar = document.getElementById('snackbar'); - snackbar.classList.add('snackbar--visible'); - setTimeout(function() { + if (snackbar) { + snackbar.classList.add('snackbar--visible'); + setTimeout(function() { + window.location.href = 'event_overview.html'; + }, 2000); + } else { window.location.href = 'event_overview.html'; - }, 2000); + } } } diff --git a/js/my_profil.js b/js/my_profil.js new file mode 100644 index 0000000..68cfdb4 --- /dev/null +++ b/js/my_profil.js @@ -0,0 +1,584 @@ +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 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'); + 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(); + activateProfileTab('hosting'); + + 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)); + } + + // 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 { + 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); + 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', () => { + 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 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; + if (!(target instanceof HTMLElement)) { + return; + } + + const unregisterButton = target.closest('[data-unregister-id]'); + if (unregisterButton) { + if (!currentUser?.email) { + return; + } + + const eventId = Number(unregisterButton.getAttribute('data-unregister-id')); + if (!Number.isFinite(eventId)) { + return; + } + + unregisterFromEvent(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}`; + } + + + // 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(); + 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 aus lokal erstellten Daten des aktuellen Benutzers. + function getMyHostedEvents(events, user) { + 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 === userEmail) { + 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, { + 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, { + 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, emptyStateConfig, mode) { + container.innerHTML = ''; + + if (events.length === 0) { + 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 profile-event-card-clickable'; + card.setAttribute('data-event-id', String(event.id)); + const addressMarkup = mode === 'registrations' && event.address && isAddressVisibleWindow(event) + ? ` +
+

Adresse

+

${event.address}

+
+ ` + : ''; + + const actionMarkup = mode === 'registrations' + ? ` +
+ +
+ ` + : ` +
+ +
+ `; + + card.innerHTML = ` +
+

${event.title}

+

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

+ ${addressMarkup} +
+ ${actionMarkup} + `; + + container.appendChild(card); + }); + } + + // Gibt true zurueck, wenn ein Event innerhalb der naechsten 12 Stunden startet. + function isAddressVisibleWindow(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 >= 0 && msUntilStart <= twelveHoursInMs; + } + + // Parse fuer ISO- und lokalisierte Datumsformate aus den Eventdaten. + 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); + } + + // 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..08a4b95 --- /dev/null +++ b/js/navigation.js @@ -0,0 +1,69 @@ +// ============================================= +// Dynamische Navigation +// Je nach Login-Status wird die Kopfzeile fuer +// alle Seiten mit passendem Markup aufgebaut. +// ============================================= + +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..21a6fc0 100644 --- a/js/signup.js +++ b/js/signup.js @@ -1,3 +1,10 @@ +// ============================================= +// Signup-Logik +// Diese Datei validiert das Formular, speichert +// neue Benutzer lokal und startet direkt die Session. +// ============================================= + +// Formular und Felder aus dem HTML holen. const signupForm = document.getElementById('signupForm'); const vornameInput = document.getElementById('vorname'); const nachnameInput = document.getElementById('nachname'); @@ -5,6 +12,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'); @@ -17,7 +48,7 @@ function closeWelcomeModal() { document.body.style.overflow = 'auto'; } -// Validierungsfunktion +// Hauptfunktion fuer Formularvalidierung und Speicherung. function validateForm(event) { event.preventDefault(); @@ -78,12 +109,38 @@ function validateForm(event) { passwortGroup.classList.remove('has-error'); } - // Wenn alle Validierungen bestanden, Modal anzeigen + // Wenn alles gueltig ist: + // 1) auf doppelte E-Mail pruefen + // 2) neuen Benutzer speichern + // 3) als aktuellen Benutzer einloggen 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 koennte spaeter ein echter API-Call zum Backend stehen. - // 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 f4e11c2..96fa159 100644 --- a/login.html +++ b/login.html @@ -9,18 +9,19 @@ - + - +
@@ -33,25 +34,25 @@
-

Willkommen zurück

+

Login

- +
Bitte gib eine gültige E-Mail Adresse ein.
- +
Bitte gib dein Passwort ein.
- +
@@ -64,4 +65,4 @@ Datenschutz - + \ No newline at end of file diff --git a/my_profil.html b/my_profil.html new file mode 100644 index 0000000..da5bb7f --- /dev/null +++ b/my_profil.html @@ -0,0 +1,108 @@ + + + + + + Mein Profil | Invité + + + + + + + + + +
+
+ + Invite Logo + + +
+
+ +
+
+
+

Mein Bereich

+

Mein Profil

+

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

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

Meine Events

+ 0 +
+
+
+ + + + +
+
+ + + + diff --git a/signup.html b/signup.html index 23d0072..3fcf26e 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.
- + + @@ -86,4 +97,4 @@ Datenschutz - + \ No newline at end of file