Compare commits

..

2 Commits

Author SHA1 Message Date
«schmona»
6a7fbc969d clean up branch css-main 2026-03-30 00:24:37 +02:00
«schmona»
4e1b5a5495 Globales Stylesheet und clean up 2026-03-29 23:17:37 +02:00
25 changed files with 267 additions and 2772 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// 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}"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -6,25 +6,6 @@
--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);
}
*,
@ -37,72 +18,12 @@ html {
font-size: 100%;
}
body {
margin: 0;
font-family: var(--font-main);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.5;
}
a,
button,
input,
textarea {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
.site-header {
background: var(--color-bg);
border-top: 2px solid #232323;
border-bottom: 1px solid var(--color-border);
}
.site-nav {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
min-height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.site-logo {
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.02em;
}
.site-nav-links {
display: flex;
align-items: center;
gap: var(--space-5);
margin: 0;
padding: 0;
list-style: none;
}
.site-nav-links a {
font-weight: 500;
}
.site-nav-links li:last-child a {
width: 2.25rem;
height: 2.25rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #231f20;
color: #ffffff;
font-weight: 700;
}
.event-create-page {
width: min(100% - 2rem, var(--max-width));
@ -127,10 +48,6 @@ a {
padding: var(--space-4) 0 var(--space-7);
}
.submission-success {
padding: var(--space-4) 0 var(--space-7);
}
.step--active {
display: block;
}
@ -146,7 +63,6 @@ a {
min-height: 60vh;
align-content: center;
grid-template-columns: 1fr;
gap: var(--space-7);
}
.step-copy,
@ -180,14 +96,6 @@ fieldset {
letter-spacing: 0.02em;
}
h1,
h2 {
margin: 0;
font-family: "Bagel Fat One", cursive;
font-size: clamp(2rem, 4vw, 4rem);
line-height: 1.03;
letter-spacing: -0.03em;
}
.step-text {
margin: 0;
@ -210,22 +118,9 @@ h2 {
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
}
.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);
.intro-card-emoji {
font-size: 2rem;
margin-bottom: var(--space-3);
}
label,
@ -246,13 +141,11 @@ input[type="number"],
textarea {
width: 100%;
min-height: var(--input-min-height);
padding: 1rem 1.1rem;
border: 1px solid var(--input-border-soft);
border-radius: 1.125rem;
background: var(--butter-light);
padding: 0.95rem 1rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
background: var(--color-surface);
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 {
@ -260,30 +153,6 @@ 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);
@ -302,8 +171,8 @@ textarea:focus {
padding: 1rem 1rem 1rem 1.05rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
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;
background: var(--color-surface);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.option-card small {
@ -311,7 +180,6 @@ textarea:focus {
}
.option-card:hover {
background: var(--olive-light);
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
@ -324,18 +192,7 @@ textarea:focus {
}
.option-card:has(input:checked) {
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);
border: 2px solid var(--color-border-strong);
}
.counter {
@ -357,44 +214,15 @@ textarea:focus {
.counter-button {
width: var(--control-min-height);
height: var(--control-min-height);
border: 1px solid var(--color-primary);
border: 1px solid var(--color-border);
border-radius: 50%;
background: var(--color-primary);
color: var(--white);
background: var(--color-surface);
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 {
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;
padding: var(--space-5);
border-radius: var(--radius-lg);
}
.review-list {
@ -405,29 +233,14 @@ textarea:focus {
.review-item {
display: grid;
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;
gap: var(--space-1);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-divider);
}
.review-item:last-child {
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;
border-bottom: 0;
padding-bottom: 0;
}
.review-item dt {
@ -440,29 +253,16 @@ textarea:focus {
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: var(--color-bg);
backdrop-filter: none;
padding-top: var(--space-4);
background: rgba(247, 247, 242, 0.96);
backdrop-filter: blur(8px);
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;
@ -473,50 +273,15 @@ textarea:focus {
display: block;
width: 0;
height: 100%;
background: var(--tomato);
background: var(--color-primary);
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;
}
@ -623,10 +388,8 @@ textarea:focus-visible {
@media (min-width: 768px) {
.step-layout--intro {
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
grid-template-columns: 1.25fr 0.8fr;
align-items: center;
gap: var(--space-8);
}
.field-row {

View File

@ -266,6 +266,7 @@
}
.btn-primary {
background: var(--olive);
color: #fffde8;
border: none;
border-radius: var(--radius-pill);
@ -274,52 +275,10 @@
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-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;
.btn-primary:hover {
filter: brightness(0.95);
}
/* ---------------------------------------------------------
@ -721,42 +680,12 @@
}
.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 {
@ -764,15 +693,6 @@
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)

View File

@ -20,17 +20,12 @@
* { box-sizing: border-box; }
:root {
--black: #22211A;
--black: #000000;
--white: #ffffff;
--button-green: var(--olive);
--button-green-dark: var(--olive-dark);
--button-green: #6b6b05;
--button-green-dark: #514c04;
}
body {
margin: 0;
font-family: var(--font-main);
background: #FFFDE3; /* butter background color from stylesheet */
}
.page-wrapper {
max-width: 1440px;
@ -184,10 +179,9 @@ body {
align-items: center;
gap: 16px;
padding: 28px 20px;
background: var(--white) !important;
border: 2px solid var(--tomato) !important;
background: #6B6B05;
border-radius: 28px;
box-shadow: 0 12px 30px rgba(212, 75, 36, 0.08);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.06);
}
.how-step__icon {
@ -226,7 +220,7 @@ body {
}
.how-step__label--brown {
color: var(--tomato);
color: #FFFDE3;
}
.how-step__label--big {
@ -247,7 +241,7 @@ body {
}
.how-step__corner-number--brown {
color: var(--tomato);
color: #FFFDE3;
}
.how-step__png {
@ -257,7 +251,7 @@ body {
}
.how-step__png--brown {
filter: brightness(0) saturate(100%) invert(39%) sepia(84%) saturate(1682%) hue-rotate(349deg) brightness(93%) contrast(86%);
filter: brightness(0) saturate(100%) invert(99%) sepia(6%) saturate(1200%) hue-rotate(10deg) brightness(104%) contrast(97%);
}
@ -405,19 +399,12 @@ body {
justify-content: center;
border-radius: 999px;
font-size: 0.95rem;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn:hover,
.btn:focus-visible {
.btn:hover {
background-color: var(--button-green-dark);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(107, 107, 5, 0.25);
}
.nav__link {
@ -519,7 +506,7 @@ body {
}
.gallery__brand {
font-family: var(--font-main);
font-family: 'Inter', sans-serif;
color: #DD541A;
}

View File

@ -1,380 +0,0 @@
.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;
}
}

View File

@ -73,7 +73,7 @@ img {
/* Typography */
h1, h2, h3, h4, h5, h6 {
h1, h2 {
font-family: 'Bagel Fat One';
}
@ -123,8 +123,8 @@ p {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 3rem;
padding: 0.1875rem 0.75rem 0.1875rem var(--space-5);
min-height: 3.625rem;
padding: 0.1875rem 0.5625rem 0.1875rem var(--space-5);
max-width: none;
width: 100%;
box-sizing: border-box;
@ -144,31 +144,10 @@ p {
display: block;
}
.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 {
.button-small-links {
display: flex;
align-items: center;
gap: var(--space-5);
gap: var(--space-1);
}
@ -190,10 +169,8 @@ p {
background-color: var(--olive-dark);
}
.button-small {
background: var(--olive);
color: var(--butter-light);
color: var(--black);
font-size: 1.125rem;
font-weight: 500;
letter-spacing: var(--ls-sm);
@ -206,27 +183,22 @@ p {
.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);
}
.auth-nav-button--active {
.button-login {
background: var(--olive);
color: var(--butter-light);
border: 2px solid var(--olive);
font-size: 1.125rem;
font-weight: 500;
letter-spacing: var(--ls-sm);
line-height: 1;
text-decoration: none;
padding: var(--space-1) var(--space-4);
border-radius: var(--radius-md);
}
.profile-pill {

View File

@ -3,9 +3,8 @@
"id": 1,
"title": "Italienische Tavolata",
"location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern",
"date": "11. APR. 2026",
"time": "3:30 UHR",
"date": "19. MÄR. 2026",
"time": "18:30 UHR",
"category": "DINNER",
"diet": "VEGGIE",
"spots": 6,
@ -44,7 +43,6 @@
"id": 2,
"title": "Noche Peruana",
"location": "Chur",
"address": "Obere Gasse 41, 7000 Chur",
"date": "11. APR. 2026",
"time": "19:00 UHR",
"category": "DINNER",
@ -86,7 +84,6 @@
"id": 3,
"title": "Japanese Delight",
"location": "ZÜRICH",
"address": "Limmatquai 92, 8001 Zürich",
"date": "02. MAI. 2026",
"time": "12:30 UHR",
"category": "LUNCH",

View File

@ -9,23 +9,23 @@
<link rel="stylesheet" href="css/event_create.css" />
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
<nav class="top-nav-links" aria-label="Hauptnavigation">
<a class="button-small" href="event_overview.html" aria-current="page">Event finden</a>
<a class="button-small:active" href="event_create.html">Event erstellen</a>
<a class="button-small" href="login.html" aria-label="Profil">M</a>
</nav>
</div>
</header>
<main class="event-create-page">
<section class="event-flow-header" aria-label="Event erstellen Aktionen">
</section>
@ -38,24 +38,16 @@
>
<div class="step-layout step-layout--intro">
<div class="step-copy">
<p class="step-kicker">Event erstellen</p>
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<h1 class="step-kicker">Event erstellen</h1>
<h2 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h2>
<p class="step-text">
Erzähl uns von deiner Idee vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
oder etwas ganz Eigenes wir helfen dir dabei, dein Event Schritt für Schritt aufzubauen.
</p>
<button type="button" class="button button--primary button--intro" data-start-flow>
Los gehts!
</button>
</div>
<aside class="intro-card intro-card--image" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable.jpg"
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
/>
</aside>
</div>
</section>
@ -71,6 +63,11 @@
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heissen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<fieldset class="form-field">
<legend>Art des Essens / Eventtyp</legend>
@ -96,7 +93,47 @@
</label>
</div>
</fieldset>
</div>
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text">
Mach uns neugierig. Was kochst du und wie fühlt sich dein Abend an?
Hier entsteht die Geschichte, auf die sich deine Gäste freuen.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="menuDescription">Was ist das Menü?</label>
<textarea id="menuDescription" name="menuDescription" rows="5" required></textarea>
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 3</p>
<h2 id="step3-title">Wen lädst du ein?</h2>
<p class="step-text">
Wie viele Gäste passen zu deinem Event? Und gibt es etwas, das du bei
Ernährung oder Unverträglichkeiten beachten möchtest?
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Maximale Personenanzahl</legend>
@ -128,21 +165,7 @@
</button>
</div>
</fieldset>
</div>
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text">
Mach uns neugierig. Was gibt es zu essen? Gibt es eine bestimmte Ernährungsform oder ein Motto? Je mehr du verrätst, desto besser können sich deine Gäste auf dein Event freuen.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Ernährungsform</legend>
@ -165,25 +188,6 @@
</div>
</fieldset>
<div class="form-field">
<label for="menuDescription">Was ist das Menü?</label>
<textarea id="menuDescription" name="menuDescription" rows="5" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 3</p>
<h2 id="step3-title">Gibt es etwas zu beachten?</h2>
<p class="step-text">
Gibt es Allergien, Unverträglichkeiten oder andere Hinweise, die für dein Event wichtig sind? So wissen deine Gäste gleich, worauf sie sich einstellen können.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Allergene / Unverträglichkeiten</legend>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p>
@ -204,12 +208,12 @@
<span>ohne Nüsse</span>
</label>
</div>
</fieldset>
<div class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
<div class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
</fieldset>
</div>
</div>
</section>
@ -220,7 +224,8 @@
<p class="step-kicker">Schritt 4</p>
<h2 id="step4-title">Wann findet dein Event statt?</h2>
<p class="step-text">
Wähle Datum und Uhrzeit für dein Event. So können deine Gäste direkt einschätzen, ob der Termin für sie passt.
Wähle Datum und Uhrzeit und sag uns, wo dein Event stattfindet.
Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p>
</div>
@ -236,21 +241,7 @@
<input type="time" id="eventTime" name="eventTime" required />
</div>
</div>
</div>
</div>
</section>
<section class="step" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 5</p>
<h2 id="step5-title">Wo findet dein Event statt?</h2>
<p class="step-text">
Sag uns, wo dein Event stattfindet. Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventAddress">Adresse</label>
<input type="text" id="eventAddress" name="eventAddress" autocomplete="street-address" required />
@ -264,111 +255,81 @@
</div>
</section>
<section class="step" data-step="6" aria-labelledby="step6-title">
<section class="step" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 6</p>
<h2 id="step6-title">Gib deinem Event den letzten Schliff.</h2>
<p class="step-kicker">Schritt 5</p>
<h2 id="step5-title">Alles bereit für deine Gäste?</h2>
<p class="step-text">
Jetzt bekommt dein Event seinen Namen und die Atmosphäre, die Lust aufs Dabeisein macht.
Ein klarer Titel (z.B. "Italienische Tavolata") und ein guter Beschreibungstext (Ablauf etc.) machen den Unterschied.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heißen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="7" aria-labelledby="step7-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 7</p>
<h2 id="step7-title">Dein Event auf einen Blick.</h2>
<p class="step-text">
Schau dir alle Details nochmal in Ruhe an. Wenn alles passt,
kannst du dein Event jetzt veröffentlichen und Gäste einladen.
Schau dir dein Event nochmal in Ruhe an. Passt alles?
Dann kannst du es jetzt veröffentlichen und Gäste einladen.
</p>
</div>
<div class="review-card" aria-live="polite">
<dl class="review-list">
<div class="review-item" data-edit-step="1" data-edit-field="eventType" role="button" tabindex="0" aria-label="Eventtyp bearbeiten">
<dt>Eventtyp</dt>
<dd data-review="eventType"></dd>
</div>
<div class="review-item" data-edit-step="1" data-edit-field="maxGuests" role="button" tabindex="0" aria-label="Maximale Personenanzahl bearbeiten">
<dt>Maximale Personenanzahl</dt>
<dd data-review="maxGuests"></dd>
</div>
<div class="review-item" data-edit-step="2" data-edit-field="dietType" role="button" tabindex="0" aria-label="Ernährungsform bearbeiten">
<dt>Ernährungsform</dt>
<dd data-review="dietType"></dd>
</div>
<div class="review-item" data-edit-step="2" data-edit-field="menuDescription" role="button" tabindex="0" aria-label="Menü bearbeiten">
<dt>Menü</dt>
<dd data-review="menuDescription"></dd>
</div>
<div class="review-item" data-edit-step="3" data-edit-field="allergiesOther" role="button" tabindex="0" aria-label="Allergene und Unverträglichkeiten bearbeiten">
<dt>Allergene / Unverträglichkeiten</dt>
<dd data-review="allergies">Keine Angabe</dd>
</div>
<div class="review-item" data-edit-step="4" data-edit-field="eventDate" role="button" tabindex="0" aria-label="Datum bearbeiten">
<dt>Datum</dt>
<dd data-review="eventDate"></dd>
</div>
<div class="review-item" data-edit-step="4" data-edit-field="eventTime" role="button" tabindex="0" aria-label="Uhrzeit bearbeiten">
<dt>Uhrzeit</dt>
<dd data-review="eventTime"></dd>
</div>
<div class="review-item" data-edit-step="5" data-edit-field="eventAddress" role="button" tabindex="0" aria-label="Adresse bearbeiten">
<dt>Adresse</dt>
<dd data-review="eventAddress"></dd>
</div>
<div class="review-item" data-edit-step="5" data-edit-field="eventCity" role="button" tabindex="0" aria-label="Ort bearbeiten">
<dt>Ort</dt>
<dd data-review="eventCity"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventTitle" role="button" tabindex="0" aria-label="Eventtitel bearbeiten">
<div class="review-item">
<dt>Eventtitel</dt>
<dd data-review="eventTitle"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventDescription" role="button" tabindex="0" aria-label="Event-Abend bearbeiten">
<div class="review-item">
<dt>Eventtyp</dt>
<dd data-review="eventType"></dd>
</div>
<div class="review-item">
<dt>Menü</dt>
<dd data-review="menuDescription"></dd>
</div>
<div class="review-item">
<dt>Event-Abend</dt>
<dd data-review="eventDescription"></dd>
</div>
<div class="review-item">
<dt>Maximale Personenanzahl</dt>
<dd data-review="maxGuests"></dd>
</div>
<div class="review-item">
<dt>Ernährungsform</dt>
<dd data-review="dietType"></dd>
</div>
<div class="review-item">
<dt>Allergene / Unverträglichkeiten</dt>
<dd data-review="allergies">Keine Angabe</dd>
</div>
<div class="review-item">
<dt>Datum</dt>
<dd data-review="eventDate"></dd>
</div>
<div class="review-item">
<dt>Uhrzeit</dt>
<dd data-review="eventTime"></dd>
</div>
<div class="review-item">
<dt>Adresse</dt>
<dd data-review="eventAddress"></dd>
</div>
<div class="review-item">
<dt>Ort</dt>
<dd data-review="eventCity"></dd>
</div>
</dl>
</div>
</div>
</section>
<div class="flow-footer" id="flowFooter" hidden>
<div class="progress-wrap" aria-hidden="true">
<div class="progress-marker" id="progressMarker">
<span class="progress-marker__circle" id="progressMarkerLabel">1</span>
</div>
<div class="progress">
<span id="progressBar" class="progress-bar"></span>
</div>
<div class="progress" aria-hidden="true">
<span id="progressBar" class="progress-bar"></span>
</div>
<div class="flow-actions">
@ -380,31 +341,6 @@
</div>
</div>
</div>
<section
id="submissionSuccess"
class="submission-success"
aria-labelledby="success-title"
aria-live="polite"
hidden
>
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Event erstellt</p>
<h2 id="success-title">Dein Event ist ready.</h2>
<p class="step-text">
Sieht gut aus: Deine Idee ist jetzt live und bereit für Gäste.
Im Profil kannst du dein Event anschauen, verwalten oder direkt das nächste planen.
</p>
</div>
<div class="review-card review-card--success">
<div class="submission-success-actions">
<a class="button button--primary" href="event_overview.html">Weiter zu deinem Profil</a>
</div>
</div>
</div>
</section>
</form>
</main>

View File

@ -4,23 +4,23 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event-Detail</title>
<!-- Stylesheet für diese Seite-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bagel+Fat+One&family=Jost:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/event_overview.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
<nav class="top-nav-links" aria-label="Hauptnavigation">
<a class="nav-link active" href="event_overview.html" aria-current="page">Event finden</a>
<a class="nav-link" href="event_create.html">Event erstellen</a>
<a class="profile-pill" href="login.html" aria-label="Profil">M</a>
</nav>
</div>
</header>

View File

@ -6,21 +6,23 @@
<title>Event-Overview</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="stylesheet.css">
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
<nav class="top-nav-links" aria-label="Hauptnavigation">
<a class="nav-link active" href="event_overview.html" aria-current="page">Event finden</a>
<a class="nav-link" href="event_create.html">Event erstellen</a>
<a class="profile-pill" href="login.html" aria-label="Profil">M</a>
</nav>
</div>
</header>

View File

@ -3,30 +3,20 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Events entdecken</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/landingpage.css?v=2" />
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css?v=2">
<script src="js/navigation.js" defer></script>
<!-- Font Awesome
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-dU7ZrF1pFq5kVnPzlV9+04YhARzNjCX5Q5P1shgMpuN4s5I8mI8QD4981h7kYtV7sSgNldR0z5pZW5bS2ZpY3Q==" crossorigin="anonymous" referrerpolicy="no-referrer" /> -->
<title>Social Cooking Wireframe</title>
<link rel="stylesheet" href="css/landingpage.css" />
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--default" href="login.html" aria-label="Login">Login</a>
<a class="button-small auth-nav-button auth-nav-button--default" href="signup.html" aria-label="Signup">Signup</a>
</nav>
</div>
<div class="page-wrapper">
<header class="header">
<div class="header__brand">LOGO</div>
<div class="header__actions">
<nav class="nav">
<a class="nav__link btn btn--outline" href="event_overview.html">Event finden</a>
</nav>
<a class="btn btn--outline" href="login.html">Login</a>
</div>
</header>
<main class="main-content">
@ -120,7 +110,7 @@
<div class="gallery__info">
<div class="gallery__handle" style="display: flex; align-items: center; gap: 16px;">
<img src="assets/instagram.png" alt="Instagram" class="gallery__icon--instagram" />
<img src="assets/logo_invite.svg" alt="Invité Logo" class="gallery__icon--invite" />
<img src="assets/invite-logo.svg" alt="Invité Logo" class="gallery__icon--invite" />
</div>
</div>
</section>

View File

@ -8,14 +8,9 @@ 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
@ -32,25 +27,11 @@ const nextLabels = {
2: "Weiter",
3: "Weiter",
4: "Weiter",
5: "Weiter",
6: "Weiter",
7: "Event veröffentlichen"
5: "Event veröffentlichen"
};
// 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;
// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen
usernameElement.textContent = "Mia";
// =============================
@ -76,39 +57,6 @@ 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
@ -123,8 +71,6 @@ function markRadioGroupInvalid(group) {
*/
function showStep(index) {
currentStep = index;
submissionSuccess.hidden = true;
clearStepInvalidState(index);
// Nur der aktuelle Schritt soll sichtbar sein
steps.forEach((step, stepIndex) => {
@ -169,21 +115,12 @@ 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);
}
@ -288,18 +225,7 @@ function buildAllergiesReviewValue() {
* und schreibt sie gesammelt in die Review-Ansicht.
*/
function updateReview() {
const reviewValues = getReviewValues();
Object.entries(reviewValues).forEach(([key, value]) => {
updateReviewField(key, value);
});
}
/**
* Liest alle wichtigen Formularwerte gesammelt aus.
*/
function getReviewValues() {
return {
const reviewValues = {
eventTitle: getFieldValue("eventTitle"),
eventType: getFieldValue("eventType"),
menuDescription: getFieldValue("menuDescription"),
@ -312,139 +238,12 @@ function getReviewValues() {
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
@ -464,7 +263,6 @@ function validateCurrentStep() {
if (currentStep === 0 || currentStep === lastStep) return true;
const fields = getStepFields(currentStep);
clearStepInvalidState(currentStep);
// Zuerst Radio-Gruppen prüfen
const radioCheck = validateRadioGroups(fields);
@ -499,7 +297,6 @@ 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."
@ -521,7 +318,6 @@ 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."
@ -578,12 +374,11 @@ function handleNextClick() {
*/
function handleFormSubmit(event) {
event.preventDefault();
saveCurrentEvent();
steps.forEach(step => step.classList.remove("step--active"));
flowFooter.hidden = true;
submissionSuccess.hidden = false;
setErrorMessage("");
window.scrollTo({ top: 0, behavior: "smooth" });
// 1. Feedback geben
alert("Dein Event wurde erfolgreich veröffentlicht!");
// 2. Weiterleiten (z. B. zur Event-Übersicht)
window.location.href = "event_overview.html";
}
@ -617,126 +412,6 @@ 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
@ -763,12 +438,8 @@ function initEventCreationFlow() {
// Counter aktivieren
registerCounterHandlers();
registerMenuBulletHandler();
registerValidationFeedbackHandlers();
registerReviewEditHandlers();
// Startzustand: Intro anzeigen
submissionSuccess.hidden = true;
showStep(0);
}

View File

@ -1,13 +1,9 @@
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);
@ -18,174 +14,10 @@ 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 apiEvents = await response.json();
const allEvents = [...getStoredEvents(), ...apiEvents];
const allEvents = await response.json();
const event = allEvents.find(e => e.id === eventId);
if (event) {
@ -267,47 +99,11 @@ document.addEventListener('DOMContentLoaded', async () => {
? event.gallery
: [event.image, event.image, event.image];
const visibleParticipants = participants.slice(0, 6);
const registrationMap = getRegistrationMap();
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
const remainingParticipants = Math.max(0, participants.length + extraRegistrations - visibleParticipants.length);
const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length);
const totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
const confirmedGuests = participants.length + extraRegistrations;
const confirmedGuests = participants.length;
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
? `
<article class="detail-panel detail-panel-compact">
<h2 class="detail-section-title">Adresse</h2>
<p>${event.address}</p>
</article>
`
: '';
const detailChips = [
`<span class="event-tag">${eventCategory}</span>`,
`<span class="event-tag">${dietLabel}</span>`,
@ -365,8 +161,6 @@ document.addEventListener('DOMContentLoaded', async () => {
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
</div>
</article>
${addressPanelMarkup}
</div>
<div class="detail-gallery detail-gallery-large">
@ -394,8 +188,8 @@ document.addEventListener('DOMContentLoaded', async () => {
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
</span>
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
<button class="detail-primary-btn" type="button" ${isFull ? 'disabled' : ''}>
Anmelden
</button>
</div>
</section>
@ -418,46 +212,6 @@ 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() {

View File

@ -1,7 +1,4 @@
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.
// -------------------------------------------------------------
@ -16,59 +13,6 @@ 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:
@ -80,9 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
async function fetchEvents() {
try {
const response = await fetch('data/events.json');
const apiEvents = await response.json();
const localEvents = getStoredEvents();
allEvents = [...localEvents, ...apiEvents];
allEvents = await response.json();
populateMetaFilters();
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
@ -120,10 +62,6 @@ 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',
@ -154,11 +92,6 @@ 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',
@ -189,90 +122,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Normalize time label from UHR to Uhr for consistent typography.
function formatEventTime(timeString) {
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;
return timeString.replace('UHR', 'Uhr').trim();
}
// Safely verify whether a value exists in the given select element.
@ -294,11 +144,6 @@ 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);
@ -319,10 +164,6 @@ 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 = `
@ -343,46 +184,25 @@ document.addEventListener('DOMContentLoaded', () => {
const card = document.createElement('article');
card.className = 'event-card';
card.style.cursor = 'pointer';
card.addEventListener('click', clickedEvent => {
if (clickedEvent.target instanceof HTMLElement && clickedEvent.target.closest('button')) {
return;
}
card.onclick = () => {
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 baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0;
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
const bookedSeats = baseParticipants + extraRegistrations;
const bookedSeats = event.participants ? event.participants.length : 0;
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 => `<span class="event-tag">${spec}</span>`).join('')
: '';
const actionMarkup = isOwnEvent
? '<button class="btn-primary btn-primary-own" type="button" data-registration-action="own" disabled>Dein Event!</button>'
: isRegistered
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="unregister">Abmelden</button>'
: isRegistrationClosed
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
: isFull
? ''
: !currentUser
? '<button class="btn-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
: '<button class="btn-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
card.innerHTML = `
<div class="event-main">
<div class="event-top-row">
@ -401,54 +221,10 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="event-side${isFull ? ' event-side-full' : ''}">
<span class="event-spots${isFull ? ' event-spots-full' : ''}">${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`}</span>
${actionMarkup}
${isFull ? '' : '<button class="btn-primary" type="button">Anmelden</button>'}
</div>
`;
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);
});
}

View File

@ -1,45 +1,30 @@
// =============================================
// 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');
// 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();
const pageCount = Math.ceil(items.length / itemsPerPage);
let activePage = 0;
// Scrollt den Track auf eine bestimmte Seite.
function scrollToPage(page) {
activePage = page;
const pageWidth = carouselTrack.clientWidth;
carouselTrack.scrollTo({ left: pageWidth * page, behavior: 'smooth' });
}
// Geht zur naechsten Seite (mit Wrap-around am Ende).
function showNext() {
activePage = (activePage + 1) % pageCount;
scrollToPage(activePage);
}
// Geht zur vorherigen Seite (mit Wrap-around zum Ende).
function showPrev() {
activePage = (activePage - 1 + pageCount) % pageCount;
scrollToPage(activePage);
}
// Wenn sich bei Resize die Karten-Anzahl pro Seite aendert,
// laden wir die Seite neu, damit Layout und Seitenzahl wieder stimmen.
function refreshCarousel() {
const responsiveItemsPerPage = getItemsPerPage();
if (responsiveItemsPerPage !== itemsPerPage) {
@ -48,11 +33,9 @@ if (carouselTrack) {
}
}
// Klick-Steuerung der Pfeile.
if (nextArrow) nextArrow.addEventListener('click', showNext);
if (prevArrow) prevArrow.addEventListener('click', showPrev);
// Tastatur-Support fuer Barrierefreiheit.
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
showNext();
@ -62,7 +45,6 @@ if (carouselTrack) {
}
});
// Reagiert auf Bildschirmgroessen-Aenderungen.
window.addEventListener('resize', () => {
refreshCarousel();
scrollToPage(activePage);

View File

@ -1,18 +1,8 @@
// =============================================
// 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';
@ -20,25 +10,20 @@ 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();
}
// Event-Handler nur registrieren, wenn die Buttons existieren.
if (nextBtn) nextBtn.addEventListener('click', showNext);
if (prevBtn) prevBtn.addEventListener('click', showPrev);
nextBtn.addEventListener('click', showNext);
prevBtn.addEventListener('click', showPrev);
// Tastatursteuerung fuer bessere Bedienbarkeit.
// keyboard support
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
showNext();
@ -48,5 +33,4 @@ document.addEventListener('keydown', (event) => {
}
});
// Initialen Zustand einmal setzen.
updateGallery();

View File

@ -1,52 +1,9 @@
// =============================================
// 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();
@ -86,24 +43,11 @@ function validateForm(event) {
passwortGroup.classList.remove('has-error');
}
// Wenn alle Validierungen bestanden, pruefen wir:
// 1) gibt es den Benutzer schon?
// 2) ist das Passwort korrekt?
// Danach speichern wir die aktive Session.
// Wenn alle Validierungen bestanden, Form absenden
if (isValid) {
const users = getStoredUsers();
const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase());
//alert('Login erfolgreich! (Dies ist eine Demo)');
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);
// Nach erfolgreichem Login geht es zur Event-Uebersicht.
// Weiterleitung zur event overview Page
window.location.href = 'event_overview.html';
}
}

View File

@ -1,584 +0,0 @@
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 = `
<p class="profile-empty-kicker">Keine Treffer</p>
<h3>${emptyStateConfig.title}</h3>
<p>${emptyStateConfig.text}</p>
<a class="button" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
`;
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)
? `
<div class="profile-event-address-block" aria-label="Event Adresse">
<p class="profile-event-address-label">Adresse</p>
<p class="profile-event-address">${event.address}</p>
</div>
`
: '';
const actionMarkup = mode === 'registrations'
? `
<div class="profile-event-actions">
<button class="profile-unregister-btn" type="button" data-unregister-id="${event.id}">Abmelden</button>
</div>
`
: `
<div class="profile-event-actions">
<button class="profile-cancel-btn" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
</div>
`;
card.innerHTML = `
<div>
<h3 class="profile-event-title">${event.title}</h3>
<p class="profile-event-meta">${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}</p>
${addressMarkup}
</div>
${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();
}
});

View File

@ -1,69 +0,0 @@
// =============================================
// 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 `
<a
class="button-small auth-nav-button ${loginIsActive ? 'auth-nav-button--active' : 'auth-nav-button--default'}"
href="login.html"
aria-label="Login"
${loginIsActive ? 'aria-current="page"' : ''}
>
Login
</a>
<a
class="button-small auth-nav-button ${signupIsActive ? 'auth-nav-button--active' : 'auth-nav-button--default'}"
href="signup.html"
aria-label="Signup"
${signupIsActive ? 'aria-current="page"' : ''}
>
Signup
</a>
`;
}
// Baut die Navigation fuer eingeloggte Benutzer.
function buildLoggedInNavigation() {
return `
<a class="nav-tab" href="event_overview.html">Event finden</a>
<a class="nav-tab" href="event_create.html">Event erstellen</a>
<a class="button-small" href="my_profil.html" aria-label="Mein Profil">Mein Profil</a>
`;
}
const currentUser = getCurrentUser();
const nextMarkup = currentUser ? buildLoggedInNavigation() : buildLoggedOutNavigation();
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
navContainers.forEach(container => {
container.innerHTML = nextMarkup;
});
});

View File

@ -1,10 +1,3 @@
// =============================================
// 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');
@ -12,30 +5,6 @@ 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');
@ -48,7 +17,7 @@ function closeWelcomeModal() {
document.body.style.overflow = 'auto';
}
// Hauptfunktion fuer Formularvalidierung und Speicherung.
// Validierungsfunktion
function validateForm(event) {
event.preventDefault();
@ -109,38 +78,12 @@ function validateForm(event) {
passwortGroup.classList.remove('has-error');
}
// Wenn alles gueltig ist:
// 1) auf doppelte E-Mail pruefen
// 2) neuen Benutzer speichern
// 3) als aktuellen Benutzer einloggen
// Wenn alle Validierungen bestanden, Modal anzeigen
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 koennte spaeter ein echter API-Call zum Backend stehen.
// Hier würde später die Registrierung zum Backend gesendet
// Weiterleitung zur Event-Overview-Seite.
// Weiterleitung zur event overview Page
window.location.href = 'event_overview.html';
}
}

View File

@ -9,7 +9,6 @@
<link rel="stylesheet" href="css/login_signup.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
@ -18,11 +17,12 @@
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--active" href="login.html" aria-label="Login" aria-current="page">Login</a>
<a class="button-small auth-nav-button auth-nav-button--default" href="signup.html" aria-label="Signup">Signup</a>
<nav class="button-small-links" aria-label="Hauptnavigation">
<a class="button-small" href="event_overview.html">Event finden</a>
<a class="button-small" href="event_create.html">Event erstellen</a>
<a class="button-login" href="login.html" aria-label="Profil">Login</a>
</nav>
</div>
</header>

View File

@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Profil | Invité</title>
<!-- Stylesheet für diese Seite -->
<link rel="stylesheet" href="css/my_profil.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="container profile-page">
<section class="profile-hero" aria-label="Profilübersicht">
<div>
<p class="profile-kicker">Mein Bereich</p>
<h1 id="profile-headline">Mein Profil</h1>
<p id="profile-subline" class="profile-subline">Hier findest du deine Events, deine Anmeldungen und kannst deine Profildaten verwalten.</p>
</div>
<button id="logout-button" class="button-small profile-logout" type="button">Logout</button>
</section>
<section id="logged-out-state" class="profile-panel hidden" aria-live="polite">
<h2 class="panel-title">Du bist noch nicht eingeloggt</h2>
<p>Melde dich an, damit wir deine Events und Anmeldungen anzeigen können.</p>
<div class="profile-cta-row">
<a class="button" href="login.html">Zum Login</a>
<a class="button profile-button-secondary" href="signup.html">Konto erstellen</a>
</div>
</section>
<section id="logged-in-content" class="profile-grid">
<nav class="profile-tabs" aria-label="Profilbereiche">
<button type="button" class="profile-tab is-active" data-profile-tab="hosting">Hosting</button>
<button type="button" class="profile-tab" data-profile-tab="teilnehmen">Teilnehmen</button>
<button type="button" class="profile-tab" data-profile-tab="einstellungen">Einstellungen</button>
</nav>
<article class="profile-panel" data-profile-panel="hosting">
<div class="panel-head">
<h2 class="panel-title">Meine Events</h2>
<span id="my-events-count" class="panel-count">0</span>
</div>
<div id="my-events-list" class="profile-card-list"></div>
</article>
<article class="profile-panel hidden" data-profile-panel="teilnehmen">
<div class="panel-head">
<h2 class="panel-title">Meine Anmeldungen</h2>
<span id="my-registrations-count" class="panel-count">0</span>
</div>
<div id="my-registrations-list" class="profile-card-list"></div>
</article>
<article class="profile-panel profile-panel-form hidden" data-profile-panel="einstellungen">
<h2 class="panel-title">Profil verwalten</h2>
<form id="profile-form" novalidate>
<div class="form-grid">
<div class="form-group">
<label for="vorname">Vorname</label>
<input type="text" id="vorname" name="vorname" required>
<p class="input-error" id="vorname-error">Bitte gib deinen Vornamen ein.</p>
</div>
<div class="form-group">
<label for="nachname">Nachname</label>
<input type="text" id="nachname" name="nachname" required>
<p class="input-error" id="nachname-error">Bitte gib deinen Nachnamen ein.</p>
</div>
</div>
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
<p class="input-error" id="email-error">Bitte gib eine gültige E-Mail-Adresse ein.</p>
</div>
<div class="form-group">
<label for="passwort">Passwort</label>
<input type="password" id="passwort" name="passwort" minlength="6" placeholder="Mindestens 6 Zeichen">
<p class="input-hint">Nur ausfüllen, wenn du dein Passwort ändern möchtest.</p>
<p class="input-error" id="passwort-error">Das Passwort muss mindestens 6 Zeichen lang sein.</p>
</div>
<button class="button" type="submit">Profil speichern</button>
<p id="profile-feedback" class="profile-feedback" aria-live="polite"></p>
</form>
</article>
</section>
</main>
<script src="js/my_profil.js"></script>
</body>
</html>

View File

@ -9,19 +9,19 @@
<link rel="stylesheet" href="css/login_signup.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--default" href="login.html" aria-label="Login">Login</a>
<a class="button-small auth-nav-button auth-nav-button--active" href="signup.html" aria-label="Signup" aria-current="page">Signup</a>
<nav class="button-small-links" aria-label="Hauptnavigation">
<a class="button-small" href="event_overview.html">Event finden</a>
<a class="button-small" href="event_create.html">Event erstellen</a>
<a class="button-login" href="login.html" aria-label="Profil">Login</a>
</nav>
</div>
</header>