Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a7fbc969d | ||
|
|
4e1b5a5495 |
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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 |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 geht’s!
|
||||
</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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
36
index.html
36
index.html
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
62
js/login.js
62
js/login.js
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
584
js/my_profil.js
584
js/my_profil.js
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
65
js/signup.js
65
js/signup.js
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
10
login.html
10
login.html
@ -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>
|
||||
|
||||
108
my_profil.html
108
my_profil.html
@ -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>
|
||||
12
signup.html
12
signup.html
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user