Compare commits
19 Commits
21d96597cc
...
0041ab8d7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 0041ab8d7a | |||
|
|
3b65e392fe | ||
|
|
d46b65aa73 | ||
|
|
f4463bbd9a | ||
|
|
ae631cd463 | ||
|
|
4ff703b4ff | ||
|
|
fdbb70a4e5 | ||
|
|
3b438626dd | ||
|
|
4bd4f8046d | ||
|
|
e277c7f7d4 | ||
|
|
cbe3c0983f | ||
|
|
ee1c78f5de | ||
|
|
61d84022cc | ||
|
|
b9a5a348a8 | ||
|
|
60b8a5fd3a | ||
|
|
244f3b74b1 | ||
|
|
4fbabab800 | ||
|
|
485254d3e7 | ||
|
|
7ecc2cf91a |
BIN
assets/eventcreate_foodtable with friends.jpg
Normal file
BIN
assets/eventcreate_foodtable with friends.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
BIN
assets/eventcreate_foodtable-new.jpg
Normal file
BIN
assets/eventcreate_foodtable-new.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 493 KiB |
BIN
assets/platzhalter_brunch.jpeg
Normal file
BIN
assets/platzhalter_brunch.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 566 KiB |
BIN
assets/platzhalter_dinner.jpeg
Normal file
BIN
assets/platzhalter_dinner.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 689 KiB |
BIN
assets/platzhalter_kaffee.jpeg
Normal file
BIN
assets/platzhalter_kaffee.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 666 KiB |
BIN
assets/platzhalter_lunch.jpeg
Normal file
BIN
assets/platzhalter_lunch.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 604 KiB |
@ -30,7 +30,41 @@
|
||||
}
|
||||
|
||||
.submission-success {
|
||||
padding: var(--space-4) 0 var(--space-7);
|
||||
padding: var(--space-24) 0 var(--space-7);
|
||||
}
|
||||
|
||||
.submission-success-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submission-success-title-row h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.submission-success-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--brown);
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
transform: translateY(0.4rem);
|
||||
}
|
||||
|
||||
.submission-success-image {
|
||||
object-position: 42% center;
|
||||
}
|
||||
|
||||
.submission-success .intro-card--image {
|
||||
width: min(100%, 40rem);
|
||||
aspect-ratio: 4 / 5;
|
||||
align-self: flex-start;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.step--active {
|
||||
@ -42,7 +76,9 @@
|
||||
}
|
||||
|
||||
.startseite {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.step-layout--intro {
|
||||
@ -57,11 +93,12 @@
|
||||
.form-field,
|
||||
fieldset {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.step-copy {
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.step-fields {
|
||||
@ -95,25 +132,14 @@ fieldset {
|
||||
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%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
|
||||
.field-hint {
|
||||
margin: -0.25rem 0 0;
|
||||
color: var(--color-muted);
|
||||
@ -127,8 +153,8 @@ input[type="number"],
|
||||
textarea {
|
||||
font-family: var(--font-main);
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;;
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
padding: var(--space-16) var(--space-20);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--butter-light);
|
||||
@ -176,7 +202,7 @@ textarea:focus {
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.option-grid {
|
||||
@ -194,6 +220,24 @@ textarea:focus {
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.option-card--with-icon {
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
gap: var(--space-12);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.option-card__icon {
|
||||
color: var(--brown);
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.option-card--with-icon span {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.option-card:hover {
|
||||
background: var(--olive-light);
|
||||
@ -201,6 +245,41 @@ textarea:focus {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.option-grid--tomato-choices .option-card:hover,
|
||||
.option-grid--tomato-choices .option-card:has(input:focus-visible) {
|
||||
border-color: var(--tomato);
|
||||
background: var(--tomato);
|
||||
color: var(--butter-light);
|
||||
}
|
||||
|
||||
.option-grid--tomato-choices .option-card:has(input:checked) {
|
||||
border-color: var(--tomato);
|
||||
background: var(--tomato);
|
||||
color: var(--butter-light);
|
||||
}
|
||||
|
||||
.option-grid--icon-choices .option-card--with-icon:hover .option-card__icon,
|
||||
.option-grid--icon-choices .option-card--with-icon:has(input:focus-visible) .option-card__icon,
|
||||
.option-grid--icon-choices .option-card--with-icon:has(input:checked) .option-card__icon {
|
||||
color: var(--butter-light);
|
||||
}
|
||||
|
||||
.option-grid--icon-choices .option-card--with-icon:has(input:disabled) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover {
|
||||
border-color: var(--olive-light);
|
||||
background: var(--butter-light);
|
||||
color: var(--brown);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover .option-card__icon {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.option-card input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@ -216,6 +295,20 @@ textarea:focus {
|
||||
.option-card--invalid {
|
||||
border-color: var(--error) !important;
|
||||
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
|
||||
}
|
||||
|
||||
.guest-count-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--brown);
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.guest-count-icon::before {
|
||||
display: block;
|
||||
transform: translateX(1.1rem);
|
||||
}
|
||||
|
||||
.counter {
|
||||
@ -224,14 +317,23 @@ textarea:focus {
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.counter input {
|
||||
.counter-value-group {
|
||||
display: grid;
|
||||
justify-items: stretch;
|
||||
row-gap: var(--space-8);
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.counter input {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding-block: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.review-card {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@ -251,13 +353,13 @@ textarea:focus {
|
||||
|
||||
.review-list {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.review-item {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
gap: var(--space-12);
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px solid var(--input-border-soft);
|
||||
border-radius: 1.125rem;
|
||||
@ -292,15 +394,30 @@ textarea:focus {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.review-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.review-gallery__thumb {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: cover;
|
||||
box-shadow: var(--shadow-interaction);
|
||||
}
|
||||
|
||||
.submission-success-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flow-footer {
|
||||
bottom: 0;
|
||||
backdrop-filter: none;
|
||||
padding-top: var(--space-4);
|
||||
padding-top: var(--space-24);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
@ -310,32 +427,31 @@ textarea:focus {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
width: min(100%, var(--content-width));
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
align-self: center;
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
top: -1.1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--black);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex: 1;
|
||||
height: 0.3rem;
|
||||
width: 100%;
|
||||
height: 0.45rem;
|
||||
background: var(--olive-light);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
@ -354,15 +470,22 @@ textarea:focus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) 0;
|
||||
gap: var(--space-24);
|
||||
padding: var(--space-24) 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flow-actions-right {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.error-message--callout {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: calc(100% + 1.25rem);
|
||||
}
|
||||
|
||||
.button--ghost:hover {
|
||||
@ -372,25 +495,61 @@ textarea:focus {
|
||||
|
||||
.button--intro {
|
||||
justify-self: start;
|
||||
margin-top: var(--space-2);
|
||||
margin-top: var(--space-12);
|
||||
}
|
||||
|
||||
/* Gallery Upload */
|
||||
.gallery-upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.gallery-preview {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.gallery-add-button {
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-8);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--butter-light);
|
||||
color: var(--olive);
|
||||
font-family: var(--font-main);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.gallery-add-button__icon {
|
||||
color: var(--brown);
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gallery-add-button__text {
|
||||
color: var(--black);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gallery-add-button:hover {
|
||||
background: var(--olive-light);
|
||||
box-shadow: var(--shadow-interaction);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.gallery-thumb {
|
||||
position: relative;
|
||||
width: 5.5rem;
|
||||
height: 5.5rem;
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
border-radius: var(--radius-sm, 0.5rem);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
@ -454,6 +613,26 @@ textarea:focus-visible {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.error-message--callout {
|
||||
position: static;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.error-message--callout::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.event-flow-header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@ -477,4 +656,4 @@ textarea:focus-visible {
|
||||
.option-grid--4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
/* Heading hierarchy: page title > detail title > card title > section title */
|
||||
.overview-title {
|
||||
margin: 0 0 var(--space-4);
|
||||
margin: 0 0 var(--space-24);
|
||||
color: var(--olive);
|
||||
font-family: "Bagel Fat One", cursive;
|
||||
font-size: clamp(38px, 5vw, 44px);
|
||||
@ -21,8 +21,19 @@
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.overview-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.overview-title-row .overview-title {
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
@ -43,7 +54,7 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--ls-sm);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--space-1);
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
@ -56,7 +67,7 @@
|
||||
|
||||
.category-group {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-8);
|
||||
margin-bottom: 0;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
@ -70,7 +81,7 @@
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: var(--space-1) var(--space-20);
|
||||
padding: var(--space-8) var(--space-20);
|
||||
}
|
||||
|
||||
.meta-filter-group {
|
||||
@ -94,7 +105,7 @@
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--butter-light);
|
||||
height: 37px;
|
||||
padding: 0 var(--space-4);
|
||||
padding: 0 var(--space-24);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -122,11 +133,12 @@
|
||||
|
||||
.meta-filter select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='currentColor' stroke-width='1.5' fill='none' stroke-linecap='butt' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
color: var(--black);
|
||||
background-position: right var(--space-4) center;
|
||||
background-position: right var(--space-24) center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
@ -171,7 +183,7 @@
|
||||
/* Primary metadata line: location + date/time/guest counters. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
margin-bottom: var(--space-0);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@ -191,7 +203,7 @@
|
||||
.event-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-8);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@ -203,7 +215,7 @@
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: var(--space-1) var(--space-20);
|
||||
padding: var(--space-8) var(--space-20);
|
||||
}
|
||||
|
||||
.event-spec-chip {
|
||||
@ -235,7 +247,7 @@
|
||||
|
||||
.button-plaetze {
|
||||
border: none;
|
||||
padding: var(--space-01) var(--space-4);
|
||||
padding: var(--space-8) var(--space-24);
|
||||
font-family: var(--font-main);
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
@ -251,7 +263,7 @@
|
||||
.event-spots-full, .detail-spots-pill-full {
|
||||
/* Sold-out visual state, intentionally high-contrast and always filled. */
|
||||
border: 1.5px solid var(--tomato-light);
|
||||
padding: var(--space-01) var(--space-4);
|
||||
padding: var(--space-8) var(--space-24);
|
||||
border-radius: var(--radius-pill);
|
||||
color: var(--butter-light);
|
||||
background: var(--tomato-light);
|
||||
@ -387,7 +399,7 @@
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
letter-spacing: var(--ls-la);
|
||||
padding: var(--space-1) var(--space-20);
|
||||
padding: var(--space-8) var(--space-20);
|
||||
}*/
|
||||
|
||||
.detail-gallery {
|
||||
@ -433,6 +445,30 @@
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.detail-gallery-large--single {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.detail-gallery-large--single .detail-gallery-item {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: auto;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.detail-gallery-large--single .detail-gallery-image {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
object-fit: contain;
|
||||
object-position: top center;
|
||||
background: var(--butter-light);
|
||||
}
|
||||
|
||||
.detail-gallery-large--single img:first-child {
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.detail-lightbox {
|
||||
/* Full-screen overlay for enlarged gallery image view. */
|
||||
position: fixed;
|
||||
@ -488,7 +524,7 @@
|
||||
.detail-panel {
|
||||
background: var(--butter-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
padding: var(--space-24) var(--space-5);
|
||||
}
|
||||
|
||||
.detail-panel-compact {
|
||||
@ -587,7 +623,7 @@
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-interaction);
|
||||
|
||||
padding: var(--space-4) var(--space-5);
|
||||
padding: var(--space-24) var(--space-5);
|
||||
margin-top: var(--space-40);
|
||||
position: sticky;
|
||||
bottom: var(--space-40);
|
||||
@ -635,13 +671,13 @@
|
||||
.detail-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
|
||||
.detail-spots-pill {
|
||||
border: none;
|
||||
padding: var(--space-01) var(--space-4);
|
||||
padding: var(--space-8) var(--space-24);
|
||||
font-family: var(--font-main);
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
@ -752,6 +788,13 @@
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.overview-info-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex-basis: 44px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.top-nav-links {
|
||||
gap: 8px;
|
||||
}
|
||||
@ -828,6 +871,11 @@
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.detail-gallery-large--single {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.detail-lightbox {
|
||||
padding: 12px;
|
||||
}
|
||||
@ -878,6 +926,19 @@
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.overview-title-row {
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overview-info-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-basis: 40px;
|
||||
font-size: 1.35rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.top-nav-links {
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
|
||||
@ -152,7 +152,7 @@
|
||||
.how-step_icon {
|
||||
font-size: 3.5rem;
|
||||
color: var(--brown);
|
||||
margin: var(--space-4) 0;
|
||||
margin: var(--space-24) 0;
|
||||
}
|
||||
|
||||
.how-step__png {
|
||||
@ -166,7 +166,7 @@
|
||||
}
|
||||
|
||||
.how-step_text {
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-24);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -233,6 +233,52 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
NEW INSTAGRAM HOVER STYLES START HERE
|
||||
========================================= */
|
||||
|
||||
.ig-post-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%; /* Ensures it fills the existing gallery__item */
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ig-post-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ig-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.ig-post-wrapper:hover .ig-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ig-overlay span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gallery__arrow {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
@ -262,11 +308,11 @@
|
||||
}
|
||||
|
||||
.gallery__arrow--prev {
|
||||
left: var(--space-4);
|
||||
left: var(--space-24);
|
||||
}
|
||||
|
||||
.gallery__arrow--next {
|
||||
right: var(--space-4);
|
||||
right: var(--space-24);
|
||||
}
|
||||
|
||||
|
||||
@ -387,7 +433,7 @@
|
||||
/* --- FAQ Section: Akkordion --- */
|
||||
|
||||
.faq-section {
|
||||
padding: var(--space-8) var(--space-4);
|
||||
padding: var(--space-8) var(--space-24);
|
||||
margin: var(--space-8) 0 var(--space-5);
|
||||
}
|
||||
|
||||
@ -423,7 +469,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--space-3) var(--space-24);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@ -467,7 +513,7 @@
|
||||
}
|
||||
|
||||
.faq-content {
|
||||
padding: 0 var(--space-4);
|
||||
padding: 0 var(--space-24);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
@ -485,7 +531,7 @@
|
||||
.faq-trigger[aria-expanded="true"] + .faq-content {
|
||||
display: block;
|
||||
max-height: 500px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--space-3) var(--space-24);
|
||||
}
|
||||
|
||||
|
||||
@ -493,7 +539,7 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.faq-section {
|
||||
padding: var(--space-40) var(--space-4);
|
||||
padding: var(--space-40) var(--space-24);
|
||||
margin: var(--space-40) 0 var(--space-5);
|
||||
}
|
||||
|
||||
|
||||
@ -4,154 +4,95 @@
|
||||
typography) are in stylesheet_global.css
|
||||
=========================================== */
|
||||
|
||||
|
||||
/* --- Page layout --- */
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: var(--space-4);
|
||||
.container-login {
|
||||
background-color: var(--butter-light);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-40) var(--space-80) var(--space-80) var(--space-80);
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--white);
|
||||
.container-registration {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-64);
|
||||
|
||||
background-color: var(--butter-light);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-section {
|
||||
padding: var(--space-40) var(--space-80) var(--space-80) var(--space-80);
|
||||
}
|
||||
|
||||
|
||||
/* --- Image section --- */
|
||||
|
||||
.image-section {
|
||||
flex: 1;
|
||||
background-color: var(--butter-light);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.image-section img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* --- Form section --- */
|
||||
|
||||
.form-section {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--black);
|
||||
margin-bottom: var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* --- Form elements --- */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: var(--space-1);
|
||||
color: var(--black);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
.form-group.has-error {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: var(--space-2);
|
||||
border: 2px solid var(--olive-light);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9rem;
|
||||
font-size: 1.125rem;
|
||||
font-family: var(--font-main);
|
||||
width: 100%;
|
||||
padding: var(--space-8) var(--space-16);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--olive);
|
||||
box-shadow: 0 0 5px rgba(107, 107, 5, 0.25);
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
width: 100%;
|
||||
padding: var(--space-2);
|
||||
background-color: var(--olive);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-main);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: var(--olive-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button[type="submit"]:active {
|
||||
background-color: var(--olive-dark);
|
||||
transform: translateY(0);
|
||||
border: 2px solid var(--olive);
|
||||
}
|
||||
|
||||
|
||||
/* --- Info box --- */
|
||||
|
||||
.info-box {
|
||||
background-color: var(--butter-light);
|
||||
border-left: 4px solid var(--olive);
|
||||
background-color: var(--olive-light);
|
||||
padding: var(--space-16);
|
||||
margin-bottom: var(--space-40);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
color: var(--black);
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
/* --- Hints & errors --- */
|
||||
|
||||
.error-message {
|
||||
margin-top: 5px;
|
||||
.error-message--field-callout {
|
||||
display: none;
|
||||
margin-top: 0.65rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.form-group.has-error input {
|
||||
border-color: var(--error);
|
||||
box-shadow: 0 0 5px rgba(212, 75, 36, 0.3);
|
||||
}
|
||||
|
||||
.form-group.has-error .error-message {
|
||||
.form-group.has-error .error-message--field-callout {
|
||||
display: block;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/* --- Modal / Popup --- */
|
||||
|
||||
@ -227,7 +168,6 @@ button[type="submit"]:active {
|
||||
|
||||
/* --- Footer --- */
|
||||
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
@ -258,7 +198,7 @@ button[type="submit"]:active {
|
||||
.footer-right {
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
|
||||
@ -272,6 +212,16 @@ button[type="submit"]:active {
|
||||
.image-section {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.error-message--field-callout {
|
||||
margin-top: var(--space-1);
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.error-message--field-callout::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -299,4 +249,4 @@ button[type="submit"]:active {
|
||||
.snackbar--visible {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.btn-count {
|
||||
@ -72,7 +72,7 @@
|
||||
|
||||
.profile-card-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
gap: var();
|
||||
}
|
||||
|
||||
/* Einzelne Eventkarte für "Meine Events" und "Meine Anmeldungen". */
|
||||
@ -146,7 +146,7 @@
|
||||
.profile-event-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
gap: var();
|
||||
}
|
||||
|
||||
.profile-empty {
|
||||
@ -188,16 +188,14 @@
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
/*
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}*/
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
@ -243,7 +241,7 @@
|
||||
|
||||
.profile-cta-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
gap: var();
|
||||
margin-top: var(--space-16);
|
||||
}
|
||||
|
||||
|
||||
@ -42,16 +42,17 @@
|
||||
|
||||
/* Spacing Scale > 1rem = 16px*/
|
||||
--space-0: 0.25rem; /* 4px */
|
||||
--space-01: 0.375rem; /* 6px */
|
||||
--space-1: 0.5rem; /* 8px */
|
||||
--space-2: 0.75rem; /* 12px */
|
||||
--space-6: 0.375rem; /* 6px */
|
||||
--space-8: 0.5rem; /* 8px */
|
||||
--space-12: 0.75rem; /* 12px */
|
||||
--space-16: 1rem; /* 16px */
|
||||
--space-20: 1.25rem; /* 20px */
|
||||
--space-4: 1.5rem; /* 24px */
|
||||
--space-24: 1.5rem; /* 24px */
|
||||
--space-5: 2rem; /* 32px */
|
||||
--space-40: 2.5rem; /* 40px */
|
||||
--space-7: 3rem; /* 48px */
|
||||
--space-8: 4rem; /* 64px */
|
||||
--space-64: 4rem; /* 64px */
|
||||
--space-80: 5rem; /* 80px */
|
||||
|
||||
|
||||
/* Radius Scale */
|
||||
@ -81,32 +82,25 @@ img {
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Bagel Fat One';
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--ls-sm);
|
||||
line-height: 120%;
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.25rem, 5vw, 3rem);
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
letter-spacing: var(--ls-sm);
|
||||
color: var(--brown);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
letter-spacing: var(--ls-sm);
|
||||
color: var(--brown);
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
letter-spacing: var(--ls-sm);
|
||||
color: var(--brown);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@ -121,19 +115,20 @@ p {
|
||||
.p-small {
|
||||
font-family: var(--font-main);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
.label-input-field {
|
||||
font-family: var(--font-main);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--ls-sm);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
margin-bottom: var(--space-16);
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
/*
|
||||
label {
|
||||
font-family: var(--font-main);
|
||||
font-size: 1.125rem;
|
||||
@ -141,7 +136,7 @@ label {
|
||||
letter-spacing: var(--ls-sm);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--space-0);
|
||||
}
|
||||
}*/
|
||||
|
||||
.option {
|
||||
font-family: var(--font-main);
|
||||
@ -154,7 +149,7 @@ label {
|
||||
|
||||
.link-text a{
|
||||
color: var(--blue);
|
||||
margin-top: var(--space-4);
|
||||
margin-top: var(--space-24);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
@ -175,14 +170,83 @@ label {
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-text);
|
||||
font-family: var(--font-main);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error-message--inline {
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.error-message--field-callout {
|
||||
display: none;
|
||||
width: fit-content;
|
||||
max-width: min(100%, 20rem);
|
||||
padding: var(--space-6) var(--space-16);
|
||||
/*margin-bottom: -32px;*/
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
background: var(--error);
|
||||
color: var(--butter-light);
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-message--field-callout::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -0.35rem;
|
||||
right: 1.6rem;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
background: inherit;
|
||||
border-top-left-radius: 0.2rem;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.error-message--callout {
|
||||
display: none;
|
||||
width: max-content;
|
||||
max-width: 15rem;
|
||||
padding: 0.7rem 1.15rem;
|
||||
border-radius: 1rem;
|
||||
background: var(--error);
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.25;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
box-shadow: var(--shadow-interaction);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.error-message--callout:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-message--callout::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2.1rem;
|
||||
bottom: -0.55rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
background: inherit;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Margins */
|
||||
|
||||
.margin-bottom-16 {
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.margin-bottom-24 {
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.margin-bottom-40 {
|
||||
@ -362,7 +426,7 @@ label {
|
||||
/* Buttons */
|
||||
.button-primary {
|
||||
display: inline-block;
|
||||
padding: var(--space-01) var(--space-4);
|
||||
padding: var(--space-6) var(--space-24);
|
||||
background-color: var(--olive);
|
||||
border: 1.5px solid var(--olive);
|
||||
border-radius: var(--radius-lg);
|
||||
@ -385,7 +449,7 @@ label {
|
||||
background: transparent;
|
||||
color: var(--olive-dark);
|
||||
opacity: 0.6;
|
||||
padding: var(--space-01) 0 var(--space-01) var(--space-4);
|
||||
padding: var() 0 var(--space-6) var(--space-24);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@ -405,12 +469,20 @@ label {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.button-primary-abmelden:hover,
|
||||
.button-primary-abmelden:focus-visible {
|
||||
.button-primary-abmelden:hover:not(:disabled),
|
||||
.button-primary-abmelden:focus-visible:not(:disabled) {
|
||||
background: var(--blue-dark);
|
||||
border-color: var(--blue-dark);
|
||||
}
|
||||
|
||||
.button-primary-abmelden:disabled {
|
||||
background-color: var(--blue);
|
||||
border-color: var(--blue);
|
||||
color: var(--butter-light);
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-primary-eigener-event{
|
||||
display: inline-block;
|
||||
padding: 0.375rem 1.5rem;
|
||||
@ -523,11 +595,12 @@ label {
|
||||
}
|
||||
|
||||
.category-item-profile {
|
||||
position: relative;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
height: 44px;
|
||||
padding: 0 var(--space-4);
|
||||
padding: 0 var(--space-24);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-16);
|
||||
@ -577,6 +650,7 @@ label {
|
||||
}
|
||||
|
||||
.profile-pill {
|
||||
position: relative;
|
||||
width: 2.375rem;
|
||||
height: 2.375rem;
|
||||
border-radius: 1.1875rem;
|
||||
@ -592,6 +666,18 @@ label {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notification-dot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: var(--error);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--butter-light);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
@ -682,23 +768,82 @@ label {
|
||||
animation: modalSlideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
border: 1.5px solid var(--olive);
|
||||
border-radius: 999px;
|
||||
background: var(--butter-light);
|
||||
color: var(--olive);
|
||||
font-family: "Bagel Fat One", cursive;
|
||||
font-size: 1.7rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-interaction);
|
||||
transition: background-color 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-info:hover,
|
||||
.btn-info:focus-visible {
|
||||
background: var(--butter);
|
||||
border-color: var(--olive-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-info:focus-visible {
|
||||
outline: 2px solid var(--olive-dark);
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from { transform: translateY(-50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
padding: var(--space-20)var(--space-20) 0 var(--space-20);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex: 0 0 32px;
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--olive);
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover,
|
||||
.modal-close:focus-visible {
|
||||
color: var(--olive-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-close:focus-visible {
|
||||
outline: 2px solid var(--olive-dark);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
font-size: 28px;
|
||||
color: var(--black);
|
||||
background: none;
|
||||
@ -712,12 +857,14 @@ label {
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--space-20) var(--space-20) var(--space-4) var(--space-20);
|
||||
padding: var(--space-24) 0 0;
|
||||
text-align: left;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
gap: var();
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@ -800,6 +947,27 @@ label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Formulare */
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
font-size: 1.125rem;
|
||||
font-family: var(--font-main);
|
||||
width: 100%;
|
||||
padding: var(--space-12) var(--space-16);
|
||||
border: 1.5px solid var(--olive-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--olive);
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
|
||||
.footer {
|
||||
@ -812,7 +980,7 @@ label {
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
/* Left aligned */
|
||||
@ -829,6 +997,6 @@ label {
|
||||
.footer-right {
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"title": "Italienische Tavolata",
|
||||
"location": "Luzern",
|
||||
"address": "Pilatusstrasse 18, 6003 Luzern",
|
||||
"date": "22. APR. 2026",
|
||||
"date": "23. APR. 2026",
|
||||
"time": "15:30 UHR",
|
||||
"category": "Dinner",
|
||||
"diet": "Vegetarisch",
|
||||
@ -87,7 +87,7 @@
|
||||
"title": "Japanese Delight",
|
||||
"location": "Zürich",
|
||||
"address": "Limmatquai 92, 8001 Zürich",
|
||||
"date": "02. MAI. 2026",
|
||||
"date": "24. April. 2026",
|
||||
"time": "12:30 UHR",
|
||||
"category": "Lunch",
|
||||
"diet": "Fisch",
|
||||
@ -121,5 +121,39 @@
|
||||
"https://i.pinimg.com/1200x/b1/fb/3a/b1fb3a7809f4046843904ac8800daacc.jpg",
|
||||
"https://i.pinimg.com/1200x/c6/93/42/c69342ec621333e853c35bda891d8bc6.jpg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Mexican Fiesta",
|
||||
"location": "Basel",
|
||||
"address": "Münsterplatz 10, 4051 Basel",
|
||||
"date": "28. April. 2026",
|
||||
"time": "18:00 UHR",
|
||||
"category": "Dinner",
|
||||
"diet": "Omnivore",
|
||||
"spots": 6,
|
||||
"status": "canceled",
|
||||
"host": {
|
||||
"name": "Carlos",
|
||||
"initial": "C"
|
||||
},
|
||||
"hostMessage": [
|
||||
"Hallo zusammen! Leider muss ich dieses Event absagen, da mir etwas Wichtiges dazwischengekommen ist.",
|
||||
"Ich hoffe, wir können das bald nachholen!"
|
||||
],
|
||||
"menu": [
|
||||
"Guacamole & Nachos",
|
||||
"Tacos al Pastor",
|
||||
"Churros"
|
||||
],
|
||||
"specifications": [],
|
||||
"participants": [
|
||||
"Carlos",
|
||||
"Vivien",
|
||||
"Test"
|
||||
],
|
||||
"gallery": [
|
||||
"https://i.pinimg.com/1200x/e2/6a/f5/e26af5c24b805081a3f304d240818302.jpg"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -9,9 +9,12 @@
|
||||
<link rel="stylesheet" href="css/event_create.css" />
|
||||
<script src="js/navigation.js" defer></script>
|
||||
|
||||
<!-- Globales Stylesheet -->
|
||||
<!-- Globales Stylesheet -->
|
||||
<link rel="stylesheet" href="css/stylesheet_global.css">
|
||||
|
||||
<!-- Font Awesome Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -29,13 +32,13 @@
|
||||
</header>
|
||||
|
||||
|
||||
<main class="event-create-page">
|
||||
<main class="event-create-page layout-narrow">
|
||||
<section class="event-flow-header" aria-label="Event erstellen Aktionen">
|
||||
</section>
|
||||
|
||||
<form id="eventForm" class="event-form" novalidate>
|
||||
<section
|
||||
class="step step--active step--intro layout-wide"
|
||||
class="step step--active step--intro"
|
||||
data-step="0"
|
||||
aria-labelledby="intro-title"
|
||||
>
|
||||
@ -52,17 +55,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="intro-card intro-card--image hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
|
||||
<div class="hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
|
||||
<img
|
||||
class="intro-image"
|
||||
src="assets/eventcreate_foodtable.jpg"
|
||||
src="assets/eventcreate_foodtable-new.jpg"
|
||||
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="1" aria-labelledby="step1-title">
|
||||
<section class="step" data-step="1" aria-labelledby="step1-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 1</p>
|
||||
@ -77,24 +80,28 @@
|
||||
<fieldset class="form-field">
|
||||
<label>Art des Essens / Eventtyp</label>
|
||||
|
||||
<div class="option-grid option-grid--4">
|
||||
<label class="option-card option">
|
||||
<div class="option-grid option-grid--4 option-grid--event-type option-grid--icon-choices option-grid--tomato-choices">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="radio" name="eventType" value="Brunch" required />
|
||||
<i class="fa-solid fa-bread-slice option-card__icon" aria-hidden="true"></i>
|
||||
<span>Brunch</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="radio" name="eventType" value="Lunch" />
|
||||
<i class="fa-solid fa-pizza-slice option-card__icon" aria-hidden="true"></i>
|
||||
<span>Lunch</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="radio" name="eventType" value="Kaffee + Kuchen" />
|
||||
<i class="fa-solid fa-mug-hot option-card__icon" aria-hidden="true"></i>
|
||||
<span>Kaffee + Kuchen</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="radio" name="eventType" value="Dinner" />
|
||||
<i class="fa-solid fa-martini-glass option-card__icon" aria-hidden="true"></i>
|
||||
<span>Dinner</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -112,15 +119,18 @@
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
id="maxGuests"
|
||||
name="maxGuests"
|
||||
min="1"
|
||||
step="1"
|
||||
value="4"
|
||||
required
|
||||
/>
|
||||
<div class="counter-value-group">
|
||||
<i class="fa-solid fa-user-group guest-count-icon" aria-hidden="true"></i>
|
||||
<input
|
||||
type="number"
|
||||
id="maxGuests"
|
||||
name="maxGuests"
|
||||
min="1"
|
||||
step="1"
|
||||
value="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="counter-button"
|
||||
@ -135,7 +145,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="2" aria-labelledby="step2-title">
|
||||
<section class="step" data-step="2" aria-labelledby="step2-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 2</p>
|
||||
@ -149,24 +159,28 @@
|
||||
<fieldset class="form-field">
|
||||
<label>Ernährungsform</label>
|
||||
|
||||
<div class="option-grid option-grid--4">
|
||||
<label class="option-card option">
|
||||
<div class="option-grid option-grid--4 option-grid--icon-choices option-grid--tomato-choices">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="checkbox" name="dietType" value="Fleisch" />
|
||||
<i class="fa-solid fa-drumstick-bite option-card__icon" aria-hidden="true"></i>
|
||||
<span>Fleisch</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="checkbox" name="dietType" value="Fisch" />
|
||||
<i class="fa-solid fa-fish option-card__icon" aria-hidden="true"></i>
|
||||
<span>Fisch</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="checkbox" name="dietType" value="Vegetarisch" />
|
||||
<i class="fa-solid fa-seedling option-card__icon" aria-hidden="true"></i>
|
||||
<span>Vegetarisch</span>
|
||||
</label>
|
||||
|
||||
<label class="option-card option">
|
||||
<label class="option-card option option-card--with-icon">
|
||||
<input type="checkbox" name="dietType" value="Vegan" />
|
||||
<i class="fa-solid fa-leaf option-card__icon" aria-hidden="true"></i>
|
||||
<span>Vegan</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -180,7 +194,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="3" aria-labelledby="step3-title">
|
||||
<section class="step" data-step="3" aria-labelledby="step3-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 3</p>
|
||||
@ -195,7 +209,7 @@
|
||||
<label>Allergene / Unverträglichkeiten</label>
|
||||
<p class="field-hint">Optional – nur auswählen, wenn es für dein Event relevant ist.</p>
|
||||
|
||||
<div class="option-grid option-grid--3">
|
||||
<div class="option-grid option-grid--3 option-grid--tomato-choices">
|
||||
<label class="option-card option">
|
||||
<input type="checkbox" name="allergies" value="glutenfrei" />
|
||||
<span>Glutenfrei</span>
|
||||
@ -221,7 +235,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="4" aria-labelledby="step4-title">
|
||||
<section class="step" data-step="4" aria-labelledby="step4-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 4</p>
|
||||
@ -247,7 +261,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="5" aria-labelledby="step5-title">
|
||||
<section class="step" data-step="5" aria-labelledby="step5-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 5</p>
|
||||
@ -271,7 +285,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="6" aria-labelledby="step6-title">
|
||||
<section class="step" data-step="6" aria-labelledby="step6-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 6</p>
|
||||
@ -294,10 +308,13 @@
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Fotos hinzufügen <span class="field-hint-inline">(optional)</span></label>
|
||||
<label>Wie wird dein Event aussehen? <span class="field-hint-inline">(optional)</span></label>
|
||||
<div class="gallery-upload">
|
||||
<div class="gallery-preview" id="galleryPreview"></div>
|
||||
<button type="button" class="button-primary" id="galleryAddBtn" aria-label="Foto hinzufügen">Foto hinzufügen</button>
|
||||
<button type="button" class="gallery-add-button" id="galleryAddBtn" aria-label="Foto hinzufügen">
|
||||
<i class="fa-solid fa-plus gallery-add-button__icon" aria-hidden="true"></i>
|
||||
<span class="gallery-add-button__text">Foto<br>hinzufügen</span>
|
||||
</button>
|
||||
<input type="file" id="galleryFileInput" accept="image/*" multiple hidden />
|
||||
</div>
|
||||
</div>
|
||||
@ -305,7 +322,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step layout-narrow" data-step="7" aria-labelledby="step7-title">
|
||||
<section class="step" data-step="7" aria-labelledby="step7-title">
|
||||
<div class="step-layout">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Schritt 7</p>
|
||||
@ -372,16 +389,22 @@
|
||||
<dt>Event-Abend</dt>
|
||||
<dd data-review="eventDescription">–</dd>
|
||||
</div>
|
||||
|
||||
<div class="review-item" data-edit-step="6" data-edit-field="galleryAddBtn" role="button" tabindex="0" aria-label="Fotos bearbeiten">
|
||||
<dt>Fotos</dt>
|
||||
<dd>
|
||||
<div class="review-gallery" data-review-gallery>
|
||||
<span>Keine Fotos hinzugefügt</span>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="flow-footer layout-narrow" id="flowFooter" hidden>
|
||||
|
||||
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>
|
||||
|
||||
<div class="flow-footer" id="flowFooter" hidden>
|
||||
<div class="flow-actions">
|
||||
<button type="button" id="backButton" class="button-secondary">
|
||||
Zurück
|
||||
@ -396,38 +419,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-actions-right">
|
||||
<p
|
||||
id="errorMessage"
|
||||
class="error-message error-message--callout"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
></p>
|
||||
<button type="button" id="nextButton" class="button-primary">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flow-actions-right">
|
||||
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
id="submissionSuccess"
|
||||
class="submission-success"
|
||||
class="submission-success layout-wide"
|
||||
aria-labelledby="success-title"
|
||||
aria-live="polite"
|
||||
hidden
|
||||
>
|
||||
<div class="step-layout">
|
||||
<div class="step-layout step-layout--intro hero startseite">
|
||||
<div class="step-copy">
|
||||
<p class="badge">Event erstellt</p>
|
||||
<h2 id="success-title">Dein Event ist ready.</h2>
|
||||
<div class="submission-success-title-row">
|
||||
<h2 id="success-title">Dein Event ist ready</h2>
|
||||
<span class="submission-success-icon" aria-hidden="true">
|
||||
<i class="fa-solid fa-champagne-glasses"></i>
|
||||
</span>
|
||||
</div>
|
||||
<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 class="submission-success-actions">
|
||||
<a class="button-primary button--intro" href="my_profil.html">Weiter zu deinem Profil</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="review-card review-card--success">
|
||||
<div class="submission-success-actions">
|
||||
<a class="button-primary" href="my_profil.html">Weiter zu deinem Profil</a>
|
||||
</div>
|
||||
<div class="intro-card intro-card--image hero__right" aria-label="Stimmungsbild zum erstellten Event">
|
||||
<img
|
||||
class="intro-image submission-success-image"
|
||||
src="assets/eventcreate_foodtable with friends.jpg"
|
||||
alt="Gemeinsames Essen an einem gedeckten Tisch"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -38,6 +38,38 @@
|
||||
<!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox -->
|
||||
<script src="js/event_detail.js"></script>
|
||||
|
||||
<div id="register-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Ein Platz für dich am Tisch!</h2>
|
||||
<button type="button" class="modal-close" id="register-modal-close" aria-label="Popup schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
|
||||
<button class="button-primary button--outline" type="button" id="register-modal-cancel">Abbrechen</button>
|
||||
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="unregister-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Pläne haben sich geändert?</h2>
|
||||
<button type="button" class="modal-close" id="unregister-modal-close" aria-label="Popup schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
|
||||
<button class="button-primary button--outline" type="button" id="unregister-modal-cancel">Abbrechen</button>
|
||||
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Ja, abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar: Feedback bei An-/Abmeldung -->
|
||||
<div class="snackbar" id="snackbar"></div>
|
||||
|
||||
|
||||
@ -30,7 +30,10 @@
|
||||
<main class="container layout-wide">
|
||||
<!-- Page headline -->
|
||||
<p class="badge margin-bottom-40">Event finden</p>
|
||||
<h1>Was darf es sein?</h1>
|
||||
<div class="overview-title-row">
|
||||
<h1 class="overview-title">Was darf es sein?</h1>
|
||||
<button type="button" id="info-button" class="btn-info" aria-label="Informationen zu kostenlosen Events">?</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter section: category chips + location/date filters -->
|
||||
<section class="filter-section">
|
||||
@ -107,6 +110,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="register-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Ein Platz für dich am Tisch!</h2>
|
||||
<button type="button" class="modal-close" onclick="closeRegisterModal()" aria-label="Popup schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
|
||||
<button class="button-primary button--outline" type="button" onclick="closeRegisterModal()">Abbrechen</button>
|
||||
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="unregister-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Pläne haben sich geändert?</h2>
|
||||
<button type="button" class="modal-close" onclick="closeUnregisterModal()" aria-label="Popup schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
|
||||
<button class="button-primary button--outline" type="button" onclick="closeUnregisterModal()">Abbrechen</button>
|
||||
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Ja, abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar: Feedback bei An-/Abmeldung -->
|
||||
<div class="snackbar" id="snackbar"></div>
|
||||
|
||||
|
||||
113
index.html
113
index.html
@ -82,7 +82,7 @@
|
||||
|
||||
<!-- Main Content: uses .gallery, .gallery__carousel, .gallery__track, .gallery__item, and .gallery__info to present event carousel content -->
|
||||
<section class="gallery" aria-label="Bildergalerie" aria-roledescription="Karussell">
|
||||
<h2>Einblick in Cooking-Erlebnisse</h2>
|
||||
<div class="gallery__header"> </div><h2>Einblick in Cooking-Erlebnisse</h2>
|
||||
<p>#gemeinsam_invité auf Instagram</p>
|
||||
</div>
|
||||
<div class="gallery__carousel">
|
||||
@ -91,44 +91,129 @@
|
||||
</button>
|
||||
|
||||
<div class="gallery__track" aria-live="polite">
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 1 von 12">
|
||||
<img src="assets/index_Red checkered social eating.jpg" alt="Red checkered social eating">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Red checkered social eating.jpg" alt="Red checkered social eating">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 142</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 18</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 2 von 12">
|
||||
<img src="assets/index_Pasta and many forks.jpg" alt="Pasta and many forks">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Pasta and many forks.jpg" alt="Pasta and many forks">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 89</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 5</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 3 von 12">
|
||||
<img src="assets/index_Zoomed in asian eating.jpg" alt="Zoomed in asian eating">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Zoomed in asian eating.jpg" alt="Zoomed in asian eating">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 215</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 32</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 4 von 12">
|
||||
<img src="assets/index_Burger eating together.jpg" alt="Burger eating together">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Burger eating together.jpg" alt="Burger eating together">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 304</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 41</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 5 von 12">
|
||||
<img src="assets/index_Cake cutting figs.jpg" alt="Cake cutting figs">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Cake cutting figs.jpg" alt="Cake cutting figs">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 178</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 12</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 6 von 12">
|
||||
<img src="assets/index_Cooking woman at home.jpg" alt="Cooking woman at home">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Cooking woman at home.jpg" alt="Cooking woman at home">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 95</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 8</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 7 von 12">
|
||||
<img src="assets/index_Eating and laughing girls.jpg" alt="Eating and laughing girls">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Eating and laughing girls.jpg" alt="Eating and laughing girls">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 420</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 55</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 8 von 12">
|
||||
<img src="assets/index_Pasta in cheese.jpg" alt="Pasta in cheese">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Pasta in cheese.jpg" alt="Pasta in cheese">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 267</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 29</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 9 von 12">
|
||||
<img src="assets/index_Salad roommates.jpg" alt="Salad roommates">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Salad roommates.jpg" alt="Salad roommates">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 112</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 4</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 10 von 12">
|
||||
<img src="assets/index_Sharing food table.jpg" alt="Sharing food table">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Sharing food table.jpg" alt="Sharing food table">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 389</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 47</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 11 von 12">
|
||||
<img src="assets/index_Spicy food zoomed.jpg" alt="Spicy food zoomed">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_Spicy food zoomed.jpg" alt="Spicy food zoomed">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 156</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 11</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 12 von 12">
|
||||
<img src="assets/index_cooking.jpg" alt="Cooking">
|
||||
<div class="ig-post-wrapper">
|
||||
<img src="assets/index_cooking.jpg" alt="Cooking">
|
||||
<div class="ig-overlay">
|
||||
<span><i class="fa-solid fa-heart"></i> 234</span>
|
||||
<span><i class="fa-solid fa-comment"></i> 21</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<button type="button" class="gallery__arrow gallery__arrow--next" aria-label="Nächstes Bild">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
@ -23,6 +23,7 @@ const CURRENT_USER_KEY = "socialCookingCurrentUser";
|
||||
// =============================
|
||||
let currentStep = 0;
|
||||
const lastStep = steps.length - 1;
|
||||
let reviewReturnStep = null;
|
||||
|
||||
// Text für den Weiter-Button je nach Schritt
|
||||
const nextLabels = {
|
||||
@ -151,10 +152,14 @@ function showStep(index, pushHistory = true) {
|
||||
*/
|
||||
function updateFlowVisibility(stepIndex) {
|
||||
const isIntroStep = stepIndex === 0;
|
||||
const isReviewReturnMode = reviewReturnStep === lastStep && stepIndex < lastStep;
|
||||
|
||||
flowFooter.hidden = isIntroStep;
|
||||
backButton.hidden = isIntroStep;
|
||||
nextButton.textContent = nextLabels[stepIndex];
|
||||
backButton.textContent = "Zurück";
|
||||
nextButton.textContent = isReviewReturnMode
|
||||
? "Zurück zur Übersicht"
|
||||
: nextLabels[stepIndex];
|
||||
}
|
||||
|
||||
|
||||
@ -265,6 +270,29 @@ function updateReviewField(fieldName, value) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt hochgeladene Fotos als kleine Vorschaubilder in der Review.
|
||||
*/
|
||||
function updateReviewGallery() {
|
||||
const target = document.querySelector("[data-review-gallery]");
|
||||
if (!target) return;
|
||||
|
||||
target.innerHTML = "";
|
||||
|
||||
if (galleryImages.length === 0) {
|
||||
target.textContent = "Keine Fotos hinzugefügt";
|
||||
return;
|
||||
}
|
||||
|
||||
galleryImages.forEach((src, index) => {
|
||||
const image = document.createElement("img");
|
||||
image.className = "review-gallery__thumb";
|
||||
image.src = src;
|
||||
image.alt = `Foto ${index + 1}`;
|
||||
target.appendChild(image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Text für Allergien / Hinweise zusammen.
|
||||
* Dabei werden Checkboxen und zusätzliches Freitextfeld kombiniert.
|
||||
@ -292,6 +320,8 @@ function updateReview() {
|
||||
Object.entries(reviewValues).forEach(([key, value]) => {
|
||||
updateReviewField(key, value);
|
||||
});
|
||||
|
||||
updateReviewGallery();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -395,6 +425,42 @@ function mapEventTypeToCategory(value) {
|
||||
return categoryMap[value] || value.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert je Eventtyp ein passendes Platzhalterbild.
|
||||
*/
|
||||
function getPlaceholderImageByEventType(eventType) {
|
||||
const normalizedType = String(eventType || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[+&/_-]/g, " ")
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
if (normalizedType.includes("brunch")) {
|
||||
return "assets/platzhalter_brunch.jpeg";
|
||||
}
|
||||
|
||||
if (normalizedType.includes("lunch")) {
|
||||
return "assets/platzhalter_lunch.jpeg";
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedType.includes("kaffee")
|
||||
|| normalizedType.includes("coffee")
|
||||
|| normalizedType.includes("cafe")
|
||||
|| normalizedType.includes("kuchen")
|
||||
) {
|
||||
return "assets/platzhalter_kaffee.jpeg";
|
||||
}
|
||||
|
||||
if (normalizedType.includes("dinner")) {
|
||||
return "assets/platzhalter_dinner.jpeg";
|
||||
}
|
||||
|
||||
return "assets/platzhalter_dinner.jpeg";
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus den Formulardaten ein lokal speicherbares Event-Objekt.
|
||||
*/
|
||||
@ -406,6 +472,8 @@ function buildStoredEvent() {
|
||||
const eventDate = form.elements.eventDate.value;
|
||||
const eventTime = form.elements.eventTime.value;
|
||||
const eventCity = form.elements.eventCity.value.trim();
|
||||
const fallbackGallery = [getPlaceholderImageByEventType(eventType)];
|
||||
const resolvedGallery = galleryImages.length > 0 ? [...galleryImages] : fallbackGallery;
|
||||
|
||||
return {
|
||||
id: Date.now(),
|
||||
@ -414,6 +482,7 @@ function buildStoredEvent() {
|
||||
address: form.elements.eventAddress.value.trim(),
|
||||
date: formatDateForStorage(eventDate),
|
||||
time: formatTimeForStorage(eventTime),
|
||||
eventType,
|
||||
category: mapEventTypeToCategory(eventType),
|
||||
diet: dietType,
|
||||
spots: Number(form.elements.maxGuests.value),
|
||||
@ -430,7 +499,7 @@ function buildStoredEvent() {
|
||||
allergiesNote: form.elements.allergiesOther.value.trim(),
|
||||
// Host wird separat geführt und nicht als angemeldeter Gast gezählt.
|
||||
participants: [],
|
||||
gallery: [...galleryImages],
|
||||
gallery: resolvedGallery,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: "local"
|
||||
};
|
||||
@ -534,6 +603,52 @@ function validateDietType() {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verhindert widersprüchliche Ernährungsformen:
|
||||
* Fleisch/Fisch dürfen zusammen, schliessen aber vegetarisch/vegan aus.
|
||||
*/
|
||||
function registerDietConflictHandlers() {
|
||||
const dietOptions = Array.from(form.querySelectorAll('input[name="dietType"]'));
|
||||
const meatFishOptions = dietOptions.filter(input => ["Fleisch", "Fisch"].includes(input.value));
|
||||
const plantOptions = dietOptions.filter(input => ["Vegetarisch", "Vegan"].includes(input.value));
|
||||
|
||||
const updateDietAvailability = changedInput => {
|
||||
if (changedInput?.checked && changedInput.value === "Vegetarisch") {
|
||||
dietOptions.forEach(input => {
|
||||
if (input.value === "Vegan") input.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (changedInput?.checked && changedInput.value === "Vegan") {
|
||||
dietOptions.forEach(input => {
|
||||
if (input.value === "Vegetarisch") input.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
const hasMeatOrFish = meatFishOptions.some(input => input.checked);
|
||||
const selectedPlantDiet = plantOptions.find(input => input.checked);
|
||||
|
||||
meatFishOptions.forEach(input => {
|
||||
input.disabled = Boolean(selectedPlantDiet);
|
||||
});
|
||||
|
||||
plantOptions.forEach(input => {
|
||||
input.disabled = hasMeatOrFish || Boolean(selectedPlantDiet && input !== selectedPlantDiet);
|
||||
});
|
||||
};
|
||||
|
||||
dietOptions.forEach(input => {
|
||||
input.addEventListener("change", () => {
|
||||
dietOptions.forEach(option => {
|
||||
option.closest(".option-card")?.classList.remove("option-card--invalid");
|
||||
});
|
||||
updateDietAvailability(input);
|
||||
});
|
||||
});
|
||||
|
||||
updateDietAvailability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft alle Pflichtfelder ausser Radios und Checkboxen.
|
||||
* Rückgabe:
|
||||
@ -567,6 +682,10 @@ function validateRequiredFields(fields) {
|
||||
* Wenn der User im ersten Formularschritt ist, geht es zurück zum Intro.
|
||||
*/
|
||||
function handleBackClick() {
|
||||
if (reviewReturnStep === lastStep && currentStep < lastStep) {
|
||||
reviewReturnStep = null;
|
||||
}
|
||||
|
||||
if (currentStep > 1) {
|
||||
showStep(currentStep - 1);
|
||||
} else {
|
||||
@ -582,6 +701,12 @@ function handleBackClick() {
|
||||
function handleNextClick() {
|
||||
if (!validateCurrentStep()) return;
|
||||
|
||||
if (reviewReturnStep === lastStep && currentStep < lastStep) {
|
||||
showStep(lastStep);
|
||||
reviewReturnStep = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep < lastStep) {
|
||||
showStep(currentStep + 1);
|
||||
} else {
|
||||
@ -633,11 +758,17 @@ function registerCounterHandlers() {
|
||||
|
||||
/**
|
||||
* Erhöht oder verringert den Wert eines Zahlenfelds.
|
||||
* Der Wert darf dabei nie kleiner als das definierte Minimum werden.
|
||||
* 0 bleibt als bewusster Startwert erlaubt; gültig ist erst eine Auswahl ab min.
|
||||
*/
|
||||
function updateCounterValue(input, change) {
|
||||
const min = Number(input.min || 1);
|
||||
const currentValue = Number(input.value || min);
|
||||
const currentValue = Number(input.value || 0);
|
||||
|
||||
if (currentValue < min && change < 0) {
|
||||
input.value = currentValue;
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = Math.max(min, currentValue + change);
|
||||
}
|
||||
|
||||
@ -701,6 +832,8 @@ function registerReviewEditHandlers() {
|
||||
const stepIndex = Number(item.dataset.editStep);
|
||||
const fieldName = item.dataset.editField;
|
||||
|
||||
// Öffnet einen Schritt aus der Übersicht und springt danach direkt zurück zu Schritt 7.
|
||||
reviewReturnStep = lastStep;
|
||||
showStep(stepIndex);
|
||||
focusFieldByName(fieldName);
|
||||
};
|
||||
@ -748,7 +881,7 @@ function registerValidationFeedbackHandlers() {
|
||||
* Setzt den Fokus auf ein bestimmtes Feld oder die erste Option einer Radio-Gruppe.
|
||||
*/
|
||||
function focusFieldByName(fieldName) {
|
||||
const field = form.elements[fieldName];
|
||||
const field = form.elements[fieldName] || document.getElementById(fieldName);
|
||||
|
||||
if (!field) return;
|
||||
|
||||
@ -845,6 +978,7 @@ function initEventCreationFlow() {
|
||||
|
||||
// Counter aktivieren
|
||||
registerCounterHandlers();
|
||||
registerDietConflictHandlers();
|
||||
registerMenuBulletHandler();
|
||||
registerValidationFeedbackHandlers();
|
||||
registerReviewEditHandlers();
|
||||
|
||||
@ -3,16 +3,13 @@
|
||||
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
|
||||
const USERS_STORAGE_KEY = 'socialCookingUsers';
|
||||
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
|
||||
// -------------------------------------------------------------
|
||||
// DOM entry point and shared asset path.
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const detailcontainer = document.getElementById('detail-view');
|
||||
const locationIconPath = 'assets/icon_location.svg';
|
||||
const calendarIconPath = 'assets/icon_calendar.svg';
|
||||
const gastIconPath = 'assets/icon_gast.svg';
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
// Read event id from query string (detail page deep-link support).
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const eventId = parseInt(params.get('id'));
|
||||
|
||||
@ -62,14 +59,11 @@
|
||||
}
|
||||
|
||||
function getUserDisplayName(user) {
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!user) return '';
|
||||
const firstName = String(user.vorname || '').trim();
|
||||
const lastName = String(user.nachname || '').trim();
|
||||
|
||||
return (firstName || `${firstName} ${lastName}`.trim() || String(user.email || '').trim()).trim();
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
return (fullName || firstName || String(user.email || '').trim()).trim();
|
||||
}
|
||||
|
||||
function getResolvedParticipants(event, registrationMap) {
|
||||
@ -84,10 +78,7 @@
|
||||
Object.entries(registrationMap || {}).forEach(([email, ids]) => {
|
||||
const isRegisteredForEvent = Array.isArray(ids)
|
||||
&& ids.map(id => Number(id)).includes(Number(event.id));
|
||||
|
||||
if (!isRegisteredForEvent) {
|
||||
return;
|
||||
}
|
||||
if (!isRegisteredForEvent) return;
|
||||
|
||||
const user = usersByEmail.get(String(email || '').trim().toLowerCase());
|
||||
const displayName = getUserDisplayName(user) || String(email || '').trim();
|
||||
@ -102,20 +93,36 @@
|
||||
return baseParticipants;
|
||||
}
|
||||
|
||||
function getParticipantNameForViewer(name, canSeeLastName) {
|
||||
const rawName = String(name || '').trim();
|
||||
if (!rawName) return '';
|
||||
if (canSeeLastName) return rawName;
|
||||
if (rawName.includes('@')) return rawName.split('@')[0].trim() || rawName;
|
||||
return rawName.split(/\s+/)[0];
|
||||
}
|
||||
|
||||
function setRegistrationMap(registrationMap) {
|
||||
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
|
||||
}
|
||||
|
||||
function parseEventDateTime(event) {
|
||||
if (!event?.date) {
|
||||
return null;
|
||||
}
|
||||
function getRegistrationIdsForUser(registrationMap, user) {
|
||||
const userEmail = String(user?.email || '').trim().toLowerCase();
|
||||
if (!userEmail) return [];
|
||||
|
||||
const matchingIds = Object.entries(registrationMap || {})
|
||||
.filter(([email]) => String(email || '').trim().toLowerCase() === userEmail)
|
||||
.flatMap(([, ids]) => (Array.isArray(ids) ? ids : []))
|
||||
.map(id => Number(id))
|
||||
.filter(id => Number.isFinite(id));
|
||||
|
||||
return Array.from(new Set(matchingIds));
|
||||
}
|
||||
|
||||
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;
|
||||
let year, month, day;
|
||||
|
||||
if (isoDateMatch) {
|
||||
year = Number(isoDateMatch[1]);
|
||||
@ -123,137 +130,158 @@
|
||||
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
|
||||
jan: 1,
|
||||
januar: 1,
|
||||
feb: 2,
|
||||
februar: 2,
|
||||
'mär': 3,
|
||||
mrz: 3,
|
||||
mar: 3,
|
||||
maerz: 3,
|
||||
märz: 3,
|
||||
apr: 4,
|
||||
april: 4,
|
||||
mai: 5,
|
||||
jun: 6,
|
||||
juni: 6,
|
||||
jul: 7,
|
||||
juli: 7,
|
||||
aug: 8,
|
||||
august: 8,
|
||||
sep: 9,
|
||||
sept: 9,
|
||||
september: 9,
|
||||
okt: 10,
|
||||
oktober: 10,
|
||||
nov: 11,
|
||||
november: 11,
|
||||
dez: 12,
|
||||
dezember: 12
|
||||
};
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
||||
|
||||
if (!localizedMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
|
||||
if (!localizedMatch) return null;
|
||||
day = Number(localizedMatch[1]);
|
||||
month = monthMap[localizedMatch[2]];
|
||||
month = monthMap[String(localizedMatch[2]).toLowerCase()];
|
||||
year = Number(localizedMatch[3]);
|
||||
|
||||
if (!month) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const msUntilStart = eventDateTime.getTime() - Date.now();
|
||||
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
return msUntilStart <= twentyfourHoursInMs;
|
||||
return msUntilStart <= 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
// Abmeldefrist: 1 Tag (24 h) vor Eventstart.
|
||||
function getDeregistrationInfo(event) {
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
|
||||
return { daysLeft: null, isClosed: false };
|
||||
}
|
||||
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return { daysLeft: null, isClosed: false };
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
const deadlineMs = eventDateTime.getTime() - oneDayMs;
|
||||
const msUntilDeadline = deadlineMs - Date.now();
|
||||
|
||||
if (msUntilDeadline <= 0) {
|
||||
return { daysLeft: 0, isClosed: true };
|
||||
}
|
||||
|
||||
const daysLeft = Math.ceil(msUntilDeadline / oneDayMs);
|
||||
return { daysLeft, isClosed: false };
|
||||
const msUntilDeadline = (eventDateTime.getTime() - oneDayMs) - Date.now();
|
||||
if (msUntilDeadline <= 0) return { daysLeft: 0, isClosed: true };
|
||||
return { daysLeft: Math.ceil(msUntilDeadline / oneDayMs), isClosed: false };
|
||||
}
|
||||
|
||||
// Adresse ist nur im 24h-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 twentyfourHoursInMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const now = Date.now();
|
||||
const start = eventDateTime.getTime();
|
||||
const revealStart = start - (24 * 60 * 60 * 1000);
|
||||
const revealEnd = start + (1 * 60 * 60 * 1000);
|
||||
return now >= revealStart && now <= revealEnd;
|
||||
}
|
||||
|
||||
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);
|
||||
function isEventPastAddressWindow(event) {
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const revealEnd = eventDateTime.getTime() + (1 * 60 * 60 * 1000);
|
||||
return Date.now() > revealEnd;
|
||||
}
|
||||
|
||||
// Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde.
|
||||
function isEventOwnedByCurrentUser(event, user) {
|
||||
if (!event || !user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 für ältere Datensätze ohne hostEmail.
|
||||
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);
|
||||
}
|
||||
|
||||
// Prüft, ob der aktuelle Benutzer bereits in der Teilnehmerliste des Events steht.
|
||||
function isUserListedInEventParticipants(event, user) {
|
||||
if (!event || !user || !Array.isArray(event.participants)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!event || !user || !Array.isArray(event.participants)) return false;
|
||||
const participantSet = new Set(
|
||||
event.participants
|
||||
.map(name => String(name || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
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();
|
||||
|
||||
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
|
||||
return Boolean(
|
||||
(userFirstName && participantSet.has(userFirstName))
|
||||
|| (userFullName && participantSet.has(userFullName))
|
||||
);
|
||||
}
|
||||
|
||||
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', 'MÄR':'März', MRZ:'März', APR:'April', MAI:'Mai', JUN:'Juni', JUL:'Juli', AUG:'August', SEP:'September', OKT:'Oktober', NOV:'November', DEZ:'Dezember' };
|
||||
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
||||
if (!match) return dateString;
|
||||
const monthLabel = labels[match[2]];
|
||||
return monthLabel ? `${Number(match[1])}. ${monthLabel} ${match[3]}` : dateString;
|
||||
}
|
||||
|
||||
function formatEventTime(timeString) {
|
||||
return timeString.replace('UHR', 'Uhr').trim();
|
||||
}
|
||||
|
||||
function getDietLabel(diet) {
|
||||
const labels = { FLEISCH:'Fleisch', FISCH:'Fisch', VEGGIE:'Vegetarisch', VEGAN:'Vegan' };
|
||||
return labels[diet] || diet;
|
||||
}
|
||||
|
||||
function getPlaceholderImageByEventType(event) {
|
||||
const rawType = String(event?.eventType || event?.category || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[+&/_-]/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
if (rawType.includes('brunch')) {
|
||||
return 'assets/platzhalter_brunch.jpeg';
|
||||
}
|
||||
|
||||
if (rawType.includes('lunch')) {
|
||||
return 'assets/platzhalter_lunch.jpeg';
|
||||
}
|
||||
|
||||
if (
|
||||
rawType.includes('kaffee')
|
||||
|| rawType.includes('coffee')
|
||||
|| rawType.includes('cafe')
|
||||
|| rawType.includes('kuchen')
|
||||
) {
|
||||
return 'assets/platzhalter_kaffee.jpeg';
|
||||
}
|
||||
|
||||
if (rawType.includes('dinner')) {
|
||||
return 'assets/platzhalter_dinner.jpeg';
|
||||
}
|
||||
|
||||
return 'assets/platzhalter_dinner.jpeg';
|
||||
}
|
||||
|
||||
// Fetch data source and resolve the matching event record.
|
||||
try {
|
||||
const response = await fetch('data/events.json');
|
||||
@ -270,62 +298,11 @@
|
||||
console.error("Fehler beim Laden der Details:", error);
|
||||
}
|
||||
|
||||
// Format localized date token into full readable date.
|
||||
function formatEventDate(dateString) {
|
||||
const labels = {
|
||||
JAN: 'Januar',
|
||||
FEB: 'Februar',
|
||||
'MÄR': 'März',
|
||||
MRZ: 'März',
|
||||
APR: 'April',
|
||||
MAI: 'Mai',
|
||||
JUN: 'Juni',
|
||||
JUL: 'Juli',
|
||||
AUG: 'August',
|
||||
SEP: 'September',
|
||||
OKT: 'Oktober',
|
||||
NOV: 'November',
|
||||
DEZ: 'Dezember'
|
||||
};
|
||||
|
||||
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
||||
if (!match) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const day = Number(match[1]);
|
||||
const monthLabel = labels[match[2]];
|
||||
const year = match[3];
|
||||
|
||||
return monthLabel ? `${day}. ${monthLabel} ${year}` : dateString;
|
||||
}
|
||||
|
||||
// Normalize time casing for UI consistency.
|
||||
function formatEventTime(timeString) {
|
||||
return timeString.replace('UHR', 'Uhr').trim();
|
||||
}
|
||||
|
||||
// Map diet keys to readable labels while keeping unknown values untouched.
|
||||
function getDietLabel(diet) {
|
||||
const labels = {
|
||||
FLEISCH: 'Fleisch',
|
||||
FISCH: 'Fisch',
|
||||
VEGGIE: 'Vegetarisch',
|
||||
VEGAN: 'Vegan'
|
||||
};
|
||||
|
||||
return labels[diet] || diet;
|
||||
}
|
||||
|
||||
// Compose and inject the full detail UI for a single event.
|
||||
function renderDetailPage(event) {
|
||||
// Core display values and resilient fallbacks for optional data fields.
|
||||
const displayDate = formatEventDate(event.date);
|
||||
const displayTime = formatEventTime(event.time);
|
||||
const dietLabel = getDietLabel(event.diet);
|
||||
const eventCategory = event.category || 'EVENT';
|
||||
const hostName = event.host?.name || 'Host';
|
||||
const hostInitial = (event.host?.initial || hostName.charAt(0) || 'H').charAt(0).toUpperCase();
|
||||
const hostMessage = Array.isArray(event.hostMessage) && event.hostMessage.length > 0
|
||||
? event.hostMessage
|
||||
: ['Der Host hat für dieses Event noch keine Nachricht hinterlegt.'];
|
||||
@ -333,273 +310,255 @@
|
||||
? event.menu
|
||||
: ['Menü wird in Kürze bekannt gegeben.'];
|
||||
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
|
||||
? event.specifications
|
||||
: [];
|
||||
? event.specifications : [];
|
||||
const registrationMap = getRegistrationMap();
|
||||
const participants = getResolvedParticipants(event, registrationMap);
|
||||
const galleryImages = Array.isArray(event.gallery)
|
||||
? event.gallery.filter(Boolean)
|
||||
: [];
|
||||
const galleryMarkup = galleryImages.length > 0
|
||||
? `
|
||||
<div class="detail-gallery detail-gallery-large">
|
||||
${galleryImages.slice(0, 9).map((img, index) => `
|
||||
<button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
|
||||
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image">
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
const visibleParticipants = participants.slice(0, 6);
|
||||
const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length);
|
||||
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
|
||||
const participantNamesForView = participants
|
||||
.map(name => getParticipantNameForViewer(name, isOwnEvent))
|
||||
.filter(Boolean);
|
||||
const galleryImages = Array.isArray(event.gallery) ? event.gallery.filter(Boolean) : [];
|
||||
const resolvedGalleryImages = galleryImages.length > 0
|
||||
? galleryImages
|
||||
: [getPlaceholderImageByEventType(event)];
|
||||
const galleryLayoutClass = resolvedGalleryImages.length === 1
|
||||
? 'detail-gallery detail-gallery-large detail-gallery-large--single'
|
||||
: 'detail-gallery detail-gallery-large';
|
||||
const galleryMarkup = resolvedGalleryImages.length > 0
|
||||
? `<div class="${galleryLayoutClass}">
|
||||
${resolvedGalleryImages.slice(0, 9).map((img, index) => `
|
||||
<button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
|
||||
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image">
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>` : '';
|
||||
|
||||
const visibleParticipants = participantNamesForView.slice(0, 6);
|
||||
const remainingParticipants = Math.max(0, participantNamesForView.length - visibleParticipants.length);
|
||||
const totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
|
||||
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 deregInfo = getDeregistrationInfo(event);
|
||||
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
|
||||
? registrationMap[currentUser.email].map(id => Number(id))
|
||||
: [];
|
||||
const userRegistrations = getRegistrationIdsForUser(registrationMap, currentUser);
|
||||
const isRegistered = userRegistrations.includes(Number(event.id));
|
||||
const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
|
||||
const hasAddressAccess = isRegistered || isListedParticipant;
|
||||
const actionButtonLabel = isOwnEvent
|
||||
? 'Dein Event!'
|
||||
: !currentUser
|
||||
? 'Einloggen'
|
||||
: isRegistered
|
||||
? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden')
|
||||
: isRegistrationClosed
|
||||
? 'Anmeldung geschlossen'
|
||||
: 'Anmelden';
|
||||
const actionButtonDisabled = isOwnEvent
|
||||
const hasAddressAccess = isRegistered || isListedParticipant || isOwnEvent;
|
||||
const isCanceled = event.status === 'canceled';
|
||||
|
||||
const actionButtonLabel = isCanceled ? 'Abgesagt'
|
||||
: isOwnEvent ? 'Dein Event!'
|
||||
: !currentUser ? 'Einloggen'
|
||||
: isRegistered ? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden')
|
||||
: isRegistrationClosed ? 'Anmeldung geschlossen'
|
||||
: 'Anmelden';
|
||||
const actionButtonDisabled = isCanceled
|
||||
|| isOwnEvent
|
||||
|| (!isRegistered && (isFull || isRegistrationClosed))
|
||||
|| (isRegistered && deregInfo.isClosed);
|
||||
const actionButtonVariantClass = isOwnEvent
|
||||
? ' button-primary-eigener-event'
|
||||
: isRegistered
|
||||
? ' button-primary-abmelden '
|
||||
: isRegistrationClosed
|
||||
? ' button-primary '
|
||||
: ' button-primary ';
|
||||
const actionButtonVariantClass = isOwnEvent ? ' button-primary-eigener-event'
|
||||
: (isRegistered || isRegistrationClosed) ? ' button-primary-abmelden '
|
||||
: ' button-primary ';
|
||||
|
||||
const shouldRevealAddress = Boolean(event.address) && isAddressVisibleWindow(event) && hasAddressAccess;
|
||||
let addressMessage = 'Wenn du dich anmeldest, wird die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
|
||||
|
||||
if (isCanceled) {
|
||||
addressMessage = 'Dieses Event wurde leider vom Gastgeber abgesagt.';
|
||||
} else if (isOwnEvent) {
|
||||
addressMessage = 'Deine Adresse für diesen Event wird 24 Stunden vorher genau hier für alle Teilnehmer sichtbar sein';
|
||||
} else if (hasAddressAccess) {
|
||||
addressMessage = 'Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
|
||||
}
|
||||
|
||||
if (!isCanceled && isEventPastAddressWindow(event)) {
|
||||
addressMessage = 'Vielen Dank, dass du an diesem Event teilgenommen hast.';
|
||||
}
|
||||
|
||||
const addressPanelMarkup = shouldRevealAddress
|
||||
? `
|
||||
<article class="detail-panel">
|
||||
<h2 class="detail-section-title">Adresse</h2>
|
||||
<p>${event.address}</p>
|
||||
</article>
|
||||
`
|
||||
: `
|
||||
<article class="detail-panel">
|
||||
<h2 class="detail-section-title">Adresse</h2>
|
||||
<p>Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.</p>
|
||||
</article>
|
||||
`;
|
||||
? `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${event.address}</p></article>`
|
||||
: `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${addressMessage}</p></article>`;
|
||||
|
||||
const detailChips = [
|
||||
`<span class="event-tag">${eventCategory}</span>`,
|
||||
...event.diet.split(', ').filter(d => d.trim() && d !== 'Keine Angabe').map(d => `<span class="event-tag">${getDietLabel(d.trim())}</span>`),
|
||||
...specifications.map(item => `<span class="event-tag">${item}</span>`)
|
||||
].join('');
|
||||
|
||||
// Render complete detail page layout including:
|
||||
// hero metadata, host card, menu, participants, gallery and sticky action bar.
|
||||
detailcontainer.innerHTML = `
|
||||
|
||||
<section class="detail-hero">
|
||||
<div class="detail-top-row">
|
||||
<span class="event-location">
|
||||
<img src="${locationIconPath}" class="icon" alt="">${event.location}
|
||||
</span>
|
||||
|
||||
<span class="event-date-time">
|
||||
<img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}
|
||||
</span>
|
||||
|
||||
<span class="event-date-time">
|
||||
<img src="${gastIconPath}" class="icon" alt="">${confirmedGuests}/${totalGuests}
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="detail-title">${event.title}</h1>
|
||||
<div class="event-meta-row detail-chip-row">
|
||||
${detailChips}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-content-grid">
|
||||
<div class="detail-side-stack">
|
||||
<article class="detail-panel">
|
||||
<header class="host-header">
|
||||
<span class="host-role">Host</span>
|
||||
<span class="host-name">${hostName}</span>
|
||||
</header>
|
||||
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
|
||||
</article>
|
||||
|
||||
<article class="detail-panel">
|
||||
<h2 class="detail-section-title">Menu</h2>
|
||||
<ul class="detail-menu-list">
|
||||
${menuItems.map(item => `<li>${item}</li>`).join('')}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="detail-panel">
|
||||
<div class="detail-participants-head">
|
||||
<h2 class="detail-section-title">Teilnehmer</h2>
|
||||
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
|
||||
</div>
|
||||
<div class="detail-avatar-row" data-participants-row>
|
||||
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
|
||||
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
|
||||
</div>
|
||||
<div class="detail-participants-full hidden" data-participants-full>
|
||||
${participants.map(name => `
|
||||
<div class="detail-participant-item">
|
||||
<span class="participant-name">${name}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
${addressPanelMarkup}
|
||||
</div>
|
||||
|
||||
${galleryMarkup}
|
||||
</section>
|
||||
|
||||
<section class="detail-action-bar">
|
||||
<div class="detail-action-summary">
|
||||
<small class="detail-action-meta">
|
||||
<span class="event-location detail-action-location">
|
||||
<img src="${locationIconPath}" alt="">
|
||||
${event.location}
|
||||
</span>
|
||||
|
||||
<span class="event-date-time detail-action-location">
|
||||
<img src="${calendarIconPath}" alt=""> ${displayDate} | ${displayTime}
|
||||
</span>
|
||||
|
||||
<span class="event-gast detail-action-location">
|
||||
<img src="${gastIconPath}" alt="">
|
||||
${confirmedGuests}/${totalGuests}
|
||||
</span>
|
||||
|
||||
</small>
|
||||
<strong>${event.title}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-action-btn-wrap">
|
||||
${isFull ? `
|
||||
<button
|
||||
class="detail-primary-btn detail-spots-pill-full"
|
||||
type="button"
|
||||
disabled>
|
||||
Ausgebucht
|
||||
</button>
|
||||
` : `
|
||||
<button
|
||||
class="detail-primary-btn${actionButtonVariantClass}"
|
||||
type="button"
|
||||
data-register-button
|
||||
${actionButtonDisabled ? 'disabled' : ''}>
|
||||
${actionButtonLabel}
|
||||
</button>
|
||||
`}
|
||||
|
||||
${isRegistered && deregInfo.daysLeft !== null ? `
|
||||
<small class="detail-dereg-hint${deregInfo.isClosed ? ' detail-dereg-hint--closed' : ''}">
|
||||
${deregInfo.isClosed
|
||||
? 'Abmeldefrist abgelaufen'
|
||||
: deregInfo.daysLeft === 1
|
||||
? 'Noch 1 Tag zur Abmeldung'
|
||||
: `Noch ${deregInfo.daysLeft} Tage zur Abmeldung`}
|
||||
</small>
|
||||
` : ''}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="detail-lightbox" aria-hidden="true">
|
||||
<div class="detail-lightbox-backdrop" data-close-lightbox="true"></div>
|
||||
<figure class="detail-lightbox-content" role="dialog" aria-modal="true" aria-label="Bildansicht">
|
||||
<button class="detail-lightbox-close" type="button" aria-label="Schliessen">×</button>
|
||||
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
|
||||
</figure>
|
||||
<section class="detail-hero">
|
||||
<div class="detail-top-row">
|
||||
<span class="event-location"><img src="${locationIconPath}" class="icon" alt="">${event.location}</span>
|
||||
<span class="event-date-time"><img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}</span>
|
||||
<span class="event-date-time"><img src="${gastIconPath}" class="icon" alt="">${confirmedGuests}/${totalGuests}</span>
|
||||
</div>
|
||||
<h1 class="detail-title">${event.title}</h1>
|
||||
<div class="event-meta-row detail-chip-row">${detailChips}</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-content-grid">
|
||||
<div class="detail-side-stack">
|
||||
<article class="detail-panel">
|
||||
<header class="host-header">
|
||||
<span class="host-role">Host</span>
|
||||
<span class="host-name">${hostName}</span>
|
||||
</header>
|
||||
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
|
||||
</article>
|
||||
|
||||
<article class="detail-panel">
|
||||
<h2 class="detail-section-title">Menu</h2>
|
||||
<ul class="detail-menu-list">
|
||||
${menuItems.map(item => `<li>${item}</li>`).join('')}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="detail-panel">
|
||||
<div class="detail-participants-head">
|
||||
<h2 class="detail-section-title">Teilnehmer</h2>
|
||||
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
|
||||
</div>
|
||||
<div class="detail-avatar-row" data-participants-row>
|
||||
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
|
||||
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
|
||||
</div>
|
||||
<div class="detail-participants-full hidden" data-participants-full>
|
||||
${participantNamesForView.map(name => `
|
||||
<div class="detail-participant-item">
|
||||
<span class="participant-name">${name}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
${addressPanelMarkup}
|
||||
</div>
|
||||
${galleryMarkup}
|
||||
</section>
|
||||
|
||||
<section class="detail-action-bar">
|
||||
<div class="detail-action-summary">
|
||||
<small class="detail-action-meta">
|
||||
<span class="event-location detail-action-location"><img src="${locationIconPath}" alt="">${event.location}</span>
|
||||
<span class="event-date-time detail-action-location"><img src="${calendarIconPath}" alt=""> ${displayDate} | ${displayTime}</span>
|
||||
<span class="event-gast detail-action-location"><img src="${gastIconPath}" alt="">${confirmedGuests}/${totalGuests}</span>
|
||||
</small>
|
||||
<strong>${event.title}</strong>
|
||||
</div>
|
||||
<div class="detail-action-btn-wrap">
|
||||
${isFull ? `
|
||||
<button class="detail-primary-btn detail-spots-pill-full" type="button" disabled>Ausgebucht</button>
|
||||
` : `
|
||||
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
|
||||
${actionButtonLabel}
|
||||
</button>
|
||||
`}
|
||||
${isRegistered && deregInfo.daysLeft !== null ? `
|
||||
<small class="detail-dereg-hint${deregInfo.isClosed ? ' detail-dereg-hint--closed' : ''}">
|
||||
${deregInfo.isClosed ? 'Abmeldefrist abgelaufen'
|
||||
: deregInfo.daysLeft === 1 ? 'Noch 1 Tag zur Abmeldung'
|
||||
: `Noch ${deregInfo.daysLeft} Tage zur Abmeldung`}
|
||||
</small>
|
||||
` : ''}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="detail-lightbox" aria-hidden="true">
|
||||
<div class="detail-lightbox-backdrop" data-close-lightbox="true"></div>
|
||||
<figure class="detail-lightbox-content" role="dialog" aria-modal="true" aria-label="Bildansicht">
|
||||
<button class="detail-lightbox-close" type="button" aria-label="Schliessen">×</button>
|
||||
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
|
||||
</figure>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Lightbox behavior for gallery images:
|
||||
// open on image click, close via backdrop, close button or ESC.
|
||||
// ---------------------------------------------------------
|
||||
// DOM references after render
|
||||
const lightbox = detailcontainer.querySelector('.detail-lightbox');
|
||||
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.
|
||||
// Eigene Events 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.
|
||||
// Anmeldung / Abmeldung mit Bestätigungs-Modal
|
||||
if (registerButton) {
|
||||
registerButton.addEventListener('click', () => {
|
||||
if (isOwnEvent) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
const alreadyRegistered = (() => {
|
||||
const map = getRegistrationMap();
|
||||
const ids = Array.isArray(map[currentUser.email])
|
||||
? map[currentUser.email].map(id => Number(id)) : [];
|
||||
return ids.includes(Number(event.id));
|
||||
})();
|
||||
|
||||
if (registrationSet.has(Number(event.id))) {
|
||||
registrationSet.delete(Number(event.id));
|
||||
if (alreadyRegistered) {
|
||||
const modal = document.getElementById('unregister-confirm-modal');
|
||||
if (modal) modal.classList.add('show');
|
||||
|
||||
document.getElementById('confirm-unregister-btn').onclick = () => {
|
||||
modal.classList.remove('show');
|
||||
const map = getRegistrationMap();
|
||||
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
|
||||
ids.delete(Number(event.id));
|
||||
map[currentUser.email] = Array.from(ids);
|
||||
setRegistrationMap(map);
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
|
||||
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
|
||||
setTimeout(() => {
|
||||
snackbar.classList.remove('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
|
||||
}, 3000);
|
||||
}
|
||||
renderDetailPage(event);
|
||||
};
|
||||
|
||||
document.getElementById('unregister-modal-close').onclick = () => modal.classList.remove('show');
|
||||
document.getElementById('unregister-modal-cancel').onclick = () => modal.classList.remove('show');
|
||||
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('show'); });
|
||||
|
||||
// Snackbar: Feedback bei Abmeldung.
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
|
||||
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
|
||||
setTimeout(() => {
|
||||
snackbar.classList.remove('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
|
||||
}, 3000);
|
||||
}
|
||||
} else if (!isFull && !isRegistrationClosed) {
|
||||
registrationSet.add(Number(event.id));
|
||||
const modal = document.getElementById('register-confirm-modal');
|
||||
if (modal) modal.classList.add('show');
|
||||
|
||||
// Snackbar: Feedback bei Anmeldung.
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000);
|
||||
}
|
||||
document.getElementById('confirm-register-btn').onclick = () => {
|
||||
modal.classList.remove('show');
|
||||
const map = getRegistrationMap();
|
||||
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
|
||||
ids.add(Number(event.id));
|
||||
map[currentUser.email] = Array.from(ids);
|
||||
setRegistrationMap(map);
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000);
|
||||
}
|
||||
renderDetailPage(event);
|
||||
};
|
||||
|
||||
document.getElementById('register-modal-close').onclick = () => modal.classList.remove('show');
|
||||
document.getElementById('register-modal-cancel').onclick = () => modal.classList.remove('show');
|
||||
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('show'); });
|
||||
}
|
||||
|
||||
nextRegistrationMap[currentUser.email] = Array.from(registrationSet);
|
||||
setRegistrationMap(nextRegistrationMap);
|
||||
|
||||
// Re-Render aktualisiert Buttonzustand und CTA ohne Seitenreload.
|
||||
renderDetailPage(event);
|
||||
});
|
||||
}
|
||||
|
||||
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen.
|
||||
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen
|
||||
const showAllBtn = detailcontainer.querySelector('[data-show-all-participants]');
|
||||
const avatarRow = detailcontainer.querySelector('[data-participants-row]');
|
||||
const fullList = detailcontainer.querySelector('[data-participants-full]');
|
||||
@ -613,32 +572,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Central close helper to keep all close paths consistent.
|
||||
// Lightbox
|
||||
function closeLightbox() {
|
||||
if (!lightbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lightbox) return;
|
||||
lightbox.classList.remove('is-open');
|
||||
lightbox.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
if (lightbox && lightboxImage) {
|
||||
// Open with selected image source.
|
||||
galleryButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const imageSrc = button.getAttribute('data-fullsrc');
|
||||
if (!imageSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imageSrc) return;
|
||||
lightboxImage.src = imageSrc;
|
||||
lightbox.classList.add('is-open');
|
||||
lightbox.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
// Close when user clicks on backdrop.
|
||||
lightbox.addEventListener('click', event => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
|
||||
@ -646,22 +597,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Close via dedicated icon/button.
|
||||
lightboxClose?.addEventListener('click', closeLightbox);
|
||||
|
||||
// Close with keyboard for accessibility.
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Back button navigation: returns to event overview page.
|
||||
if (backButton) {
|
||||
backButton.addEventListener('click', () => {
|
||||
window.location.href = 'event_overview.html';
|
||||
if (event.key === 'Escape') closeLightbox();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -36,6 +36,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getInfoModalShownKeyForUser(user) {
|
||||
const email = String(user?.email || '').trim().toLowerCase();
|
||||
return email ? `${INFO_MODAL_SHOWN_KEY}:${email}` : INFO_MODAL_SHOWN_KEY;
|
||||
}
|
||||
|
||||
// Prüft, ob ein Event dem aktuellen Benutzer gehört.
|
||||
function isEventOwnedByCurrentUser(event, user) {
|
||||
if (!event || !user) {
|
||||
@ -91,8 +96,9 @@
|
||||
|
||||
const firstName = String(user.vorname || '').trim();
|
||||
const lastName = String(user.nachname || '').trim();
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
|
||||
return (firstName || `${firstName} ${lastName}`.trim() || String(user.email || '').trim()).trim();
|
||||
return (fullName || firstName || String(user.email || '').trim()).trim();
|
||||
}
|
||||
|
||||
function getResolvedParticipants(event, registrationMap) {
|
||||
@ -280,28 +286,42 @@
|
||||
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
|
||||
jan: 1,
|
||||
januar: 1,
|
||||
feb: 2,
|
||||
februar: 2,
|
||||
'mär': 3,
|
||||
mrz: 3,
|
||||
mar: 3,
|
||||
maerz: 3,
|
||||
märz: 3,
|
||||
apr: 4,
|
||||
april: 4,
|
||||
mai: 5,
|
||||
jun: 6,
|
||||
juni: 6,
|
||||
jul: 7,
|
||||
juli: 7,
|
||||
aug: 8,
|
||||
august: 8,
|
||||
sep: 9,
|
||||
sept: 9,
|
||||
september: 9,
|
||||
okt: 10,
|
||||
oktober: 10,
|
||||
nov: 11,
|
||||
november: 11,
|
||||
dez: 12,
|
||||
dezember: 12
|
||||
};
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
|
||||
|
||||
if (!localizedMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
day = Number(localizedMatch[1]);
|
||||
month = monthMap[localizedMatch[2]];
|
||||
month = monthMap[String(localizedMatch[2]).toLowerCase()];
|
||||
year = Number(localizedMatch[3]);
|
||||
|
||||
if (!month) {
|
||||
@ -377,23 +397,24 @@
|
||||
});
|
||||
|
||||
const filtered = allEvents.filter(event => {
|
||||
if (event.status === 'canceled') return false;
|
||||
|
||||
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
|
||||
|
||||
|
||||
// Diet filter: if no diets selected, show all. Otherwise, event MUST have at least one selected diet.
|
||||
const dietMatch = activeDiets.size === 0 ||
|
||||
const dietMatch = activeDiets.size === 0 ||
|
||||
(event.diet && event.diet.split(', ').some(d => activeDiets.has(d.trim())));
|
||||
|
||||
|
||||
// Allergie filter: if no allergies selected, show all. Otherwise, event MUST have at least one selected allergie.
|
||||
const allergieMatch = activeAllergies.size === 0 ||
|
||||
const allergieMatch = activeAllergies.size === 0 ||
|
||||
(event.specifications && event.specifications.some(spec => activeAllergies.has(spec)));
|
||||
|
||||
|
||||
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
|
||||
const eventDateIso = parseEventDateToIso(event.date);
|
||||
const dateMatch = !selectedDate || eventDateIso === selectedDate;
|
||||
|
||||
return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch;
|
||||
});
|
||||
|
||||
renderEvents(filtered);
|
||||
|
||||
sessionStorage.setItem('activeFilter', activeCategory);
|
||||
@ -453,6 +474,11 @@
|
||||
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
|
||||
const isRegistered = userRegistrationSet.has(Number(event.id));
|
||||
const isRegistrationClosed = isRegistrationClosedForEvent(event);
|
||||
const isCanceled = event.status === 'canceled';
|
||||
|
||||
if (isCanceled) {
|
||||
card.style.opacity = '0.6';
|
||||
}
|
||||
|
||||
// Build optional specification chips only when data exists.
|
||||
const specsChips = event.specifications && event.specifications.length > 0
|
||||
@ -464,17 +490,31 @@
|
||||
? event.diet.split(', ').map(d => `<span class="event-tag">${d.trim()}</span>`).join('')
|
||||
: '';
|
||||
|
||||
const actionMarkup = isOwnEvent
|
||||
? '<button class="button-primary-eigener-event" type="button" data-registration-action="own" disabled>Dein Event!</button>'
|
||||
: isRegistered
|
||||
? '<button class="button-primary button-primary-abmelden" type="button" data-registration-action="unregister">Abmelden</button>'
|
||||
: isRegistrationClosed
|
||||
? '<button class="button-primary" button-plaetze" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
|
||||
: isFull
|
||||
? ''
|
||||
: !currentUser
|
||||
? '<button class="button-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
|
||||
: '<button class="button-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
|
||||
let actionMarkup = '';
|
||||
if (isCanceled) {
|
||||
actionMarkup = '<button class="button-primary" type="button" disabled>Abgesagt</button>';
|
||||
} else if (isOwnEvent) {
|
||||
actionMarkup = '<button class="button-primary-eigener-event" type="button" data-registration-action="own" disabled>Dein Event!</button>';
|
||||
} else if (isRegistered) {
|
||||
actionMarkup = isRegistrationClosed
|
||||
? '<button class="button-primary-abmelden" type="button" disabled>Abmeldung geschlossen</button>'
|
||||
: '<button class="button-primary-abmelden" type="button" data-registration-action="unregister">Abmelden</button>';
|
||||
} else if (isRegistrationClosed) {
|
||||
actionMarkup = '<button class="button-primary-abmelden" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>';
|
||||
} else if (!isFull) {
|
||||
if (!currentUser) {
|
||||
actionMarkup = '<button class="button-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>';
|
||||
} else {
|
||||
actionMarkup = '<button class="button-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
|
||||
}
|
||||
}
|
||||
|
||||
let sideInfoMarkup = '';
|
||||
if (isCanceled) {
|
||||
sideInfoMarkup = '<span class="button-plaetze">Event abgesagt</span>';
|
||||
} else if (!isRegistrationClosed) {
|
||||
sideInfoMarkup = `<span class="button-plaetze${isFull ? ' event-spots-full' : ''}">${isFull ? 'Ausgebucht' : `${freePlaces} Plätze frei`}</span>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="event-main">
|
||||
@ -495,7 +535,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-side${isFull ? ' event-side-full' : ''}">
|
||||
${isRegistrationClosed ? '' : `<span class="button-plaetze${isFull ? ' event-spots-full' : ''}">${isFull ? 'Ausgebucht' : `${freePlaces} Plätze frei`}</span>`}
|
||||
${sideInfoMarkup}
|
||||
${actionMarkup}
|
||||
</div>
|
||||
`;
|
||||
@ -530,29 +570,55 @@
|
||||
: [];
|
||||
const idSet = new Set(currentIds);
|
||||
|
||||
// Abmeldung: Benutzer vom Event entfernen und Snackbar anzeigen.
|
||||
if (action === 'unregister') {
|
||||
idSet.delete(Number(event.id));
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
|
||||
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
|
||||
setTimeout(() => {
|
||||
snackbar.classList.remove('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
|
||||
}, 3000);
|
||||
// Anmelde-Modal öffnen
|
||||
if (action === 'register' && !isFull && !isRegistrationClosed) {
|
||||
const modal = document.getElementById('register-confirm-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('show');
|
||||
document.getElementById('confirm-register-btn').onclick = () => {
|
||||
modal.classList.remove('show');
|
||||
const map = getRegistrationMap();
|
||||
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
|
||||
ids.add(Number(event.id));
|
||||
map[currentUser.email] = Array.from(ids);
|
||||
setRegistrationMap(map);
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000);
|
||||
}
|
||||
applyFilters();
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Anmeldung: Benutzer zum Event hinzufügen und Snackbar anzeigen.
|
||||
if (action === 'register' && !isFull && !isRegistrationClosed) {
|
||||
idSet.add(Number(event.id));
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000);
|
||||
// Abmelde-Modal öffnen
|
||||
if (action === 'unregister') {
|
||||
const modal = document.getElementById('unregister-confirm-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('show');
|
||||
document.getElementById('confirm-unregister-btn').onclick = () => {
|
||||
modal.classList.remove('show');
|
||||
const map = getRegistrationMap();
|
||||
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
|
||||
ids.delete(Number(event.id));
|
||||
map[currentUser.email] = Array.from(ids);
|
||||
setRegistrationMap(map);
|
||||
const snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
|
||||
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
|
||||
setTimeout(() => {
|
||||
snackbar.classList.remove('snackbar--visible');
|
||||
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
|
||||
}, 3000);
|
||||
}
|
||||
applyFilters();
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nextRegistrationMap[currentUser.email] = Array.from(idSet);
|
||||
@ -640,10 +706,11 @@
|
||||
|
||||
// Auto-open info modal on first login
|
||||
if (currentUser && infoModal) {
|
||||
const hasShownInfoModal = localStorage.getItem(INFO_MODAL_SHOWN_KEY);
|
||||
const userInfoModalKey = getInfoModalShownKeyForUser(currentUser);
|
||||
const hasShownInfoModal = localStorage.getItem(userInfoModalKey);
|
||||
if (!hasShownInfoModal) {
|
||||
infoModal.classList.add('show');
|
||||
localStorage.setItem(INFO_MODAL_SHOWN_KEY, 'true');
|
||||
localStorage.setItem(userInfoModalKey, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
js/login.js
73
js/login.js
@ -50,69 +50,74 @@ function createFallbackUser(email, passwort) {
|
||||
// Validierungsfunktion
|
||||
function validateForm(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Email-Validierung
|
||||
// Wir zeigen bewusst immer nur den ersten Fehler im Formular an.
|
||||
// So bleibt der Ablauf ruhig und führt den Nutzer Feld für Feld.
|
||||
const emailValue = emailInput.value.trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const emailGroup = emailInput.parentElement;
|
||||
const passwortGroup = passwortInput.parentElement;
|
||||
|
||||
emailGroup.classList.remove('has-error');
|
||||
passwortGroup.classList.remove('has-error');
|
||||
|
||||
if (!emailValue) {
|
||||
emailGroup.classList.add('has-error');
|
||||
emailError.textContent = 'Bitte gib deine E-Mail Adresse ein.';
|
||||
isValid = false;
|
||||
} else if (!emailRegex.test(emailValue)) {
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emailRegex.test(emailValue)) {
|
||||
emailGroup.classList.add('has-error');
|
||||
emailError.textContent = 'Bitte gib eine gültige E-Mail Adresse ein.';
|
||||
isValid = false;
|
||||
} else {
|
||||
emailGroup.classList.remove('has-error');
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Passwort-Validierung
|
||||
const passwortValue = passwortInput.value;
|
||||
const passwortGroup = passwortInput.parentElement;
|
||||
|
||||
if (!passwortValue) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
passwortError.textContent = 'Bitte gib dein Passwort ein.';
|
||||
isValid = false;
|
||||
} else if (passwortValue.length < 6) {
|
||||
passwortInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwortValue.length < 6) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
passwortError.textContent = 'Dein Passwort ist zu kurz. Bitte überprüfe dein Passwort.';
|
||||
isValid = false;
|
||||
} else {
|
||||
passwortGroup.classList.remove('has-error');
|
||||
passwortInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn alle Validierungen bestanden, prüfen wir:
|
||||
// 1) gibt es den Benutzer schon?
|
||||
// 2) ist das Passwort korrekt?
|
||||
// Danach speichern wir die aktive Session.
|
||||
if (isValid) {
|
||||
const users = getStoredUsers();
|
||||
const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase());
|
||||
const users = getStoredUsers();
|
||||
const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase());
|
||||
|
||||
if (matchedUser && matchedUser.passwort !== passwortValue) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
passwortError.textContent = 'Das Passwort ist nicht korrekt.';
|
||||
return;
|
||||
}
|
||||
if (matchedUser && matchedUser.passwort !== passwortValue) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
passwortError.textContent = 'Das Passwort ist nicht korrekt.';
|
||||
passwortInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const userToLogin = matchedUser || createFallbackUser(emailValue, passwortValue);
|
||||
setCurrentUser(userToLogin);
|
||||
const userToLogin = matchedUser || createFallbackUser(emailValue, passwortValue);
|
||||
setCurrentUser(userToLogin);
|
||||
|
||||
// Snackbar anzeigen und dann zur Event-Übersicht weiterleiten.
|
||||
var snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(function() {
|
||||
window.location.href = 'event_overview.html';
|
||||
}, 2000);
|
||||
} else {
|
||||
// Snackbar anzeigen und dann zur Event-Übersicht weiterleiten.
|
||||
var snackbar = document.getElementById('snackbar');
|
||||
if (snackbar) {
|
||||
snackbar.classList.add('snackbar--visible');
|
||||
setTimeout(function() {
|
||||
window.location.href = 'event_overview.html';
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
window.location.href = 'event_overview.html';
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,4 +137,4 @@ passwortInput.addEventListener('input', function() {
|
||||
});
|
||||
|
||||
// Form Submit Event
|
||||
loginForm.addEventListener('submit', validateForm);
|
||||
loginForm.addEventListener('submit', validateForm);
|
||||
|
||||
239
js/my_profil.js
239
js/my_profil.js
@ -213,6 +213,11 @@
|
||||
const isActive = panel.getAttribute('data-profile-panel') === tabName;
|
||||
panel.classList.toggle('hidden', !isActive);
|
||||
});
|
||||
|
||||
if (tabName === 'teilnehmen') {
|
||||
const registeredEvents = getMyRegisteredEvents(allEvents, currentUser);
|
||||
markRegistrationsAsRead(registeredEvents);
|
||||
}
|
||||
}
|
||||
|
||||
// Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation.
|
||||
@ -452,13 +457,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Ermittelt angemeldete Events über die Registration-Map.
|
||||
// Ermittelt angemeldete Events über die Registration-Map und participants-Liste.
|
||||
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)));
|
||||
const userFirstName = String(user.vorname || '').trim().toLowerCase();
|
||||
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
|
||||
|
||||
return events.filter(event => {
|
||||
if (idSet.has(Number(event.id))) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(event.participants)) {
|
||||
const participantSet = new Set(event.participants.map(name => String(name || '').trim().toLowerCase()).filter(Boolean));
|
||||
if ((userFirstName && participantSet.has(userFirstName)) || (userFullName && participantSet.has(userFullName))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Rendert angemeldete Events inkl. Zähler.
|
||||
@ -477,6 +496,35 @@
|
||||
}, 'hosting');
|
||||
}
|
||||
|
||||
function getSeenAddresses() {
|
||||
try {
|
||||
const raw = localStorage.getItem('socialCookingSeenAddresses');
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function markRegistrationsAsRead(events) {
|
||||
const seen = getSeenAddresses();
|
||||
let changed = false;
|
||||
events.forEach(event => {
|
||||
if (isAddressVisibleWindow(event) && !seen.includes(Number(event.id))) {
|
||||
seen.push(Number(event.id));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
localStorage.setItem('socialCookingSeenAddresses', JSON.stringify(seen));
|
||||
// Remove dots from UI
|
||||
const tabDot = document.querySelector('[data-category-item="teilnehmen"] .notification-dot');
|
||||
if (tabDot) tabDot.remove();
|
||||
|
||||
const navDot = document.querySelector('.profile-pill .notification-dot');
|
||||
if (navDot) navDot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Rendert angemeldete Events inkl. Zähler.
|
||||
function renderMyRegistrations(events, user) {
|
||||
const registeredEvents = getMyRegisteredEvents(events, user);
|
||||
@ -486,16 +534,51 @@
|
||||
myRegistrationsCount.textContent = String(count);
|
||||
if (myRegistrationsBtnCount) myRegistrationsBtnCount.textContent = String(count);
|
||||
|
||||
const seenAddresses = getSeenAddresses();
|
||||
const unreadEvents = registeredEvents.filter(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id)));
|
||||
const hasNotifications = unreadEvents.length > 0;
|
||||
|
||||
const tabButton = document.querySelector('[data-category-item="teilnehmen"]');
|
||||
if (tabButton) {
|
||||
let dot = tabButton.querySelector('.notification-dot');
|
||||
if (hasNotifications) {
|
||||
if (!dot) {
|
||||
dot = document.createElement('span');
|
||||
dot.className = 'notification-dot';
|
||||
tabButton.appendChild(dot);
|
||||
}
|
||||
} else if (dot) {
|
||||
dot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}, 'registrations', seenAddresses);
|
||||
|
||||
// Falls wir bereits auf dem Tab sind, direkt als gelesen markieren
|
||||
const activeTab = document.querySelector('[data-category-item="teilnehmen"].is-active');
|
||||
if (activeTab && hasNotifications) {
|
||||
// Kurze Verzögerung, damit UI sich erst aufbaut
|
||||
setTimeout(() => markRegistrationsAsRead(registeredEvents), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Gibt true zurück, wenn die Abmeldung gesperrt ist (innerhalb von 24h oder in der Vergangenheit).
|
||||
function isDeregistrationClosed(event) {
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
|
||||
return false;
|
||||
}
|
||||
const msUntilStart = eventDateTime.getTime() - Date.now();
|
||||
return msUntilStart <= 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
// Baut die Eventkarten für beide Listen in einheitlichem Markup.
|
||||
function renderEventCards(container, events, emptyStateConfig, mode) {
|
||||
function renderEventCards(container, events, emptyStateConfig, mode, seenAddresses = []) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (events.length === 0) {
|
||||
@ -515,26 +598,81 @@
|
||||
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'
|
||||
? `
|
||||
const isCanceled = event.status === 'canceled';
|
||||
if (isCanceled) {
|
||||
card.style.opacity = '0.6';
|
||||
}
|
||||
|
||||
let addressMessage = 'Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
|
||||
if (isEventPastAddressWindow(event)) {
|
||||
addressMessage = 'Vielen Dank, dass du an diesem Event teilgenommen hast.';
|
||||
}
|
||||
|
||||
let addressMarkup = '';
|
||||
if (mode === 'registrations' && event.address) {
|
||||
if (isCanceled) {
|
||||
addressMarkup = `
|
||||
<div class="profile-event-address-block" aria-label="Hinweis zur Adresse">
|
||||
<p class="profile-event-address-label">Adresse</p>
|
||||
<p class="profile-event-address">Dieses Event wurde leider vom Gastgeber abgesagt.</p>
|
||||
</div>
|
||||
`;
|
||||
} else if (isAddressVisibleWindow(event)) {
|
||||
addressMarkup = `
|
||||
<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>
|
||||
`;
|
||||
} else {
|
||||
addressMarkup = `
|
||||
<div class="profile-event-address-block" aria-label="Hinweis zur Adresse">
|
||||
<p class="profile-event-address-label">Adresse</p>
|
||||
<p class="profile-event-address">${addressMessage}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isDeregClosed = isDeregistrationClosed(event);
|
||||
|
||||
let actionMarkup = '';
|
||||
if (mode === 'registrations') {
|
||||
if (isCanceled) {
|
||||
actionMarkup = `
|
||||
<div class="event-side">
|
||||
<button class="button-primary-abmelden" type="button" disabled>Abgesagt</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (isDeregClosed) {
|
||||
actionMarkup = `
|
||||
<div class="event-side">
|
||||
<button class="button-primary-abmelden" type="button" disabled>Abmeldung geschlossen</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
actionMarkup = `
|
||||
<div class="event-side">
|
||||
<button class="button-primary-abmelden" type="button" data-unregister-id="${event.id}">Abmelden</button>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
if (isCanceled) {
|
||||
actionMarkup = `
|
||||
<div class="event-side">
|
||||
<button class="button-primary-eigener-event" type="button" disabled>Abgesagt</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
actionMarkup = `
|
||||
<div class="event-side">
|
||||
<button class="button-primary-eigener-event" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div>
|
||||
@ -549,17 +687,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Gibt true zurück, wenn ein Event innerhalb der nächsten 24 Stunden startet.
|
||||
// Gibt true zurück, wenn die Adresse sichtbar sein soll (24h vor bis 1h nach Start).
|
||||
function isAddressVisibleWindow(event) {
|
||||
if (event.status === 'canceled') return false;
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
|
||||
return false;
|
||||
}
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const now = Date.now();
|
||||
const start = eventDateTime.getTime();
|
||||
const revealStart = start - (24 * 60 * 60 * 1000);
|
||||
const revealEnd = start + (1 * 60 * 60 * 1000);
|
||||
return now >= revealStart && now <= revealEnd;
|
||||
}
|
||||
|
||||
const msUntilStart = eventDateTime.getTime() - Date.now();
|
||||
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
|
||||
// Gibt true zurück, wenn ein Event bereits vorbei ist (1h nach Start).
|
||||
function isEventPastAddressWindow(event) {
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const revealEnd = eventDateTime.getTime() + (1 * 60 * 60 * 1000);
|
||||
return Date.now() > revealEnd;
|
||||
}
|
||||
|
||||
// Parse für ISO- und lokalisierte Datumsformate aus den Eventdaten.
|
||||
@ -580,28 +725,42 @@
|
||||
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
|
||||
jan: 1,
|
||||
januar: 1,
|
||||
feb: 2,
|
||||
februar: 2,
|
||||
'mär': 3,
|
||||
mrz: 3,
|
||||
mar: 3,
|
||||
maerz: 3,
|
||||
märz: 3,
|
||||
apr: 4,
|
||||
april: 4,
|
||||
mai: 5,
|
||||
jun: 6,
|
||||
juni: 6,
|
||||
jul: 7,
|
||||
juli: 7,
|
||||
aug: 8,
|
||||
august: 8,
|
||||
sep: 9,
|
||||
sept: 9,
|
||||
september: 9,
|
||||
okt: 10,
|
||||
oktober: 10,
|
||||
nov: 11,
|
||||
november: 11,
|
||||
dez: 12,
|
||||
dezember: 12
|
||||
};
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
|
||||
|
||||
if (!localizedMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
day = Number(localizedMatch[1]);
|
||||
month = monthMap[localizedMatch[2]];
|
||||
month = monthMap[String(localizedMatch[2]).toLowerCase()];
|
||||
year = Number(localizedMatch[3]);
|
||||
|
||||
if (!month) {
|
||||
|
||||
117
js/navigation.js
117
js/navigation.js
@ -6,6 +6,8 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
|
||||
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
|
||||
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
|
||||
const navcontainers = document.querySelectorAll('.nav-tab-links');
|
||||
const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase();
|
||||
|
||||
@ -31,6 +33,94 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.location.href = 'index.html';
|
||||
};
|
||||
|
||||
// Hilfsfunktionen für Datumsberechnungen
|
||||
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, month, day;
|
||||
|
||||
if (isoDateMatch) {
|
||||
year = Number(isoDateMatch[1]);
|
||||
month = Number(isoDateMatch[2]);
|
||||
day = Number(isoDateMatch[3]);
|
||||
} else {
|
||||
const monthMap = {
|
||||
jan: 1, januar: 1,
|
||||
feb: 2, februar: 2,
|
||||
'mär': 3, mrz: 3, mar: 3, maerz: 3, märz: 3,
|
||||
apr: 4, april: 4,
|
||||
mai: 5,
|
||||
jun: 6, juni: 6,
|
||||
jul: 7, juli: 7,
|
||||
aug: 8, august: 8,
|
||||
sep: 9, sept: 9, september: 9,
|
||||
okt: 10, oktober: 10,
|
||||
nov: 11, november: 11,
|
||||
dez: 12, dezember: 12
|
||||
};
|
||||
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
|
||||
if (!localizedMatch) return null;
|
||||
day = Number(localizedMatch[1]);
|
||||
month = monthMap[String(localizedMatch[2]).toLowerCase()];
|
||||
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 isAddressVisibleWindow(event) {
|
||||
if (event.status === 'canceled') return false;
|
||||
const eventDateTime = parseEventDateTime(event);
|
||||
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
|
||||
const now = Date.now();
|
||||
const start = eventDateTime.getTime();
|
||||
const revealStart = start - (24 * 60 * 60 * 1000);
|
||||
const revealEnd = start + (1 * 60 * 60 * 1000);
|
||||
return now >= revealStart && now <= revealEnd;
|
||||
}
|
||||
|
||||
async function hasUnreadNotifications(user) {
|
||||
if (!user || !user.email) return false;
|
||||
|
||||
let events = [];
|
||||
try {
|
||||
const rawStored = localStorage.getItem(EVENTS_STORAGE_KEY);
|
||||
const storedEvents = rawStored ? JSON.parse(rawStored) : [];
|
||||
|
||||
const response = await fetch('data/events.json');
|
||||
const apiEvents = await response.json();
|
||||
events = [...storedEvents, ...apiEvents];
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Events für Benachrichtigungen', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
let map = {};
|
||||
try {
|
||||
const rawReg = localStorage.getItem(REGISTRATION_STORAGE_KEY);
|
||||
map = rawReg ? JSON.parse(rawReg) : {};
|
||||
} catch (err) {}
|
||||
|
||||
let seenAddresses = [];
|
||||
try {
|
||||
const rawSeen = localStorage.getItem('socialCookingSeenAddresses');
|
||||
seenAddresses = rawSeen ? JSON.parse(rawSeen) : [];
|
||||
} catch (err) {}
|
||||
|
||||
const registeredIds = Array.isArray(map[user.email]) ? map[user.email] : [];
|
||||
const idSet = new Set(registeredIds.map(id => Number(id)));
|
||||
|
||||
const myRegisteredEvents = events.filter(e => idSet.has(Number(e.id)));
|
||||
|
||||
// Unread = address visible AND NOT marked as seen
|
||||
return myRegisteredEvents.some(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id)));
|
||||
}
|
||||
|
||||
// Baut die Navigation für ausgeloggte Besucher.
|
||||
function buildLoggedOutNavigation() {
|
||||
const loginIsActive = currentPage === 'login.html';
|
||||
@ -71,10 +161,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// Baut die Navigation für eingeloggte Benutzer.
|
||||
function buildLoggedInNavigation(user) {
|
||||
function buildLoggedInNavigation(user, hasNotifications) {
|
||||
const initial = (user.vorname || 'U').charAt(0).toUpperCase();
|
||||
const isEventOverview = currentPage === 'event_overview.html';
|
||||
const isEventCreate = currentPage === 'event_create.html';
|
||||
const notificationMarkup = hasNotifications ? '<span class="notification-dot"></span>' : '';
|
||||
|
||||
return `
|
||||
<a
|
||||
@ -105,15 +196,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
title="${user.vorname || 'Profil'}"
|
||||
>
|
||||
${initial}
|
||||
${notificationMarkup}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
const nextMarkup = currentUser ? buildLoggedInNavigation(currentUser) : buildLoggedOutNavigation();
|
||||
async function initNavigation() {
|
||||
const currentUser = getCurrentUser();
|
||||
let nextMarkup;
|
||||
|
||||
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
|
||||
navcontainers.forEach(container => {
|
||||
container.innerHTML = nextMarkup;
|
||||
});
|
||||
if (currentUser) {
|
||||
const hasNotifications = await hasUnreadNotifications(currentUser);
|
||||
nextMarkup = buildLoggedInNavigation(currentUser, hasNotifications);
|
||||
} else {
|
||||
nextMarkup = buildLoggedOutNavigation();
|
||||
}
|
||||
|
||||
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
|
||||
navcontainers.forEach(container => {
|
||||
container.innerHTML = nextMarkup;
|
||||
});
|
||||
}
|
||||
|
||||
initNavigation();
|
||||
});
|
||||
|
||||
101
js/signup.js
101
js/signup.js
@ -52,95 +52,100 @@ function closeWelcomeModal() {
|
||||
// Hauptfunktion für Formularvalidierung und Speicherung.
|
||||
function validateForm(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Vorname-Validierung
|
||||
// Wir zeigen pro Submit nur den ersten Fehler an.
|
||||
// So bleibt der Formularfluss klar und ruhig.
|
||||
const vornameValue = vornameInput.value.trim();
|
||||
const vornameGroup = vornameInput.parentElement;
|
||||
const nachnameGroup = nachnameInput.parentElement;
|
||||
const emailGroup = emailInput.parentElement;
|
||||
const passwortGroup = passwortInput.parentElement;
|
||||
|
||||
vornameGroup.classList.remove('has-error');
|
||||
nachnameGroup.classList.remove('has-error');
|
||||
emailGroup.classList.remove('has-error');
|
||||
passwortGroup.classList.remove('has-error');
|
||||
|
||||
if (!vornameValue) {
|
||||
vornameGroup.classList.add('has-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
vornameGroup.classList.remove('has-error');
|
||||
vornameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nachname-Validierung
|
||||
const nachnameValue = nachnameInput.value.trim();
|
||||
const nachnameGroup = nachnameInput.parentElement;
|
||||
|
||||
if (!nachnameValue) {
|
||||
nachnameGroup.classList.add('has-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
nachnameGroup.classList.remove('has-error');
|
||||
nachnameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Email-Validierung
|
||||
const emailValue = emailInput.value.trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const emailGroup = emailInput.parentElement;
|
||||
|
||||
if (!emailValue) {
|
||||
emailGroup.classList.add('has-error');
|
||||
document.getElementById('emailError').textContent = 'Bitte gib deine E-Mail Adresse ein.';
|
||||
isValid = false;
|
||||
} else if (!emailRegex.test(emailValue)) {
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emailRegex.test(emailValue)) {
|
||||
emailGroup.classList.add('has-error');
|
||||
document.getElementById('emailError').textContent = 'Bitte gib eine gültige E-Mail Adresse ein.';
|
||||
isValid = false;
|
||||
} else {
|
||||
emailGroup.classList.remove('has-error');
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Passwort-Validierung
|
||||
const passwortValue = passwortInput.value;
|
||||
const passwortGroup = passwortInput.parentElement;
|
||||
|
||||
if (!passwortValue) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
document.getElementById('passwortError').textContent = 'Bitte gib ein Passwort ein.';
|
||||
isValid = false;
|
||||
} else if (passwortValue.length < 8) {
|
||||
passwortInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwortValue.length < 8) {
|
||||
passwortGroup.classList.add('has-error');
|
||||
document.getElementById('passwortError').textContent = 'Dein Passwort muss mindestens 8 Zeichen lang sein.';
|
||||
isValid = false;
|
||||
} else {
|
||||
passwortGroup.classList.remove('has-error');
|
||||
passwortInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn alles gültig ist:
|
||||
// 1) auf doppelte E-Mail prüfen
|
||||
// 2) neuen Benutzer speichern
|
||||
// 3) als aktuellen Benutzer einloggen
|
||||
if (isValid) {
|
||||
const existingUsers = getStoredUsers();
|
||||
const emailLower = emailValue.toLowerCase();
|
||||
const emailAlreadyUsed = existingUsers.some(user => user.email?.toLowerCase() === emailLower);
|
||||
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();
|
||||
// Weiterleitung erfolgt beim Klick auf "Weiter zu den Events".
|
||||
if (emailAlreadyUsed) {
|
||||
emailGroup.classList.add('has-error');
|
||||
document.getElementById('emailError').textContent = 'Diese E-Mail ist bereits registriert. Bitte nutze den Login.';
|
||||
emailInput.focus();
|
||||
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();
|
||||
// Weiterleitung erfolgt beim Klick auf "Weiter zu den Events".
|
||||
}
|
||||
|
||||
// Fehlerbehandlung bei Input-Änderung (entfernt Fehler wenn Benutzer korrigiert)
|
||||
@ -180,4 +185,4 @@ welcomeModal.addEventListener('click', function(event) {
|
||||
});
|
||||
|
||||
// Form Submit Event
|
||||
signupForm.addEventListener('submit', validateForm);
|
||||
signupForm.addEventListener('submit', validateForm);
|
||||
|
||||
39
login.html
39
login.html
@ -5,10 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invité | Login</title>
|
||||
|
||||
<!-- Globales Stylesheet -->
|
||||
<link rel="stylesheet" href="css/stylesheet_global.css">
|
||||
<!-- Stylesheet für diese Seite -->
|
||||
<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>
|
||||
@ -26,36 +27,34 @@
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="image-section">
|
||||
<img src="assets/index_cooking.jpg" alt="Social Cooking">
|
||||
</div>
|
||||
<div class="container-login layout-narrow">
|
||||
|
||||
<div class="form-section">
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required placeholder="deine.email@example.com">
|
||||
<div class="error-message" id="emailError">Bitte gib eine gültige E-Mail Adresse ein.</div>
|
||||
<form id="loginForm" novalidate >
|
||||
<div class="form-group margin-bottom-16">
|
||||
<p class= "label-input-field" for="email">E-Mail</p>
|
||||
<input type="email" id="email" name="email" required placeholder="Deine E-mail-Adresse">
|
||||
<div class="error-message error-message--field-callout" id="emailError">Bitte gib eine gültige E-Mail-Adresse ein.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="passwort">Passwort</label>
|
||||
<input type="password" id="passwort" name="passwort" required placeholder="Gib dein Passwort ein">
|
||||
<div class="error-message" id="passwortError">Bitte gib dein Passwort ein.</div>
|
||||
<div class="form-group margin-bottom-40">
|
||||
<p class= "label-input-field" for="passwort">Passwort</p>
|
||||
<input type="password" id="passwort" name="passwort" required placeholder="Dein Passwort">
|
||||
<div class="error-message error-message--field-callout" id="passwortError">Bitte gib dein Passwort ein.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button-primary">Login</button>
|
||||
<button class="button-primary margin-bottom-24">Login</button>
|
||||
|
||||
<div class="link-text">
|
||||
Du hast noch keinen Account? <a href="signup.html">Hier geht es zur Anmeldung.</a>
|
||||
Du hast noch keinen Account? <a href="signup.html">Hier geht es zur Registration.</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- Schliesst container -->
|
||||
<div class="snackbar" id="snackbar">Willkommen zurück! Du wirst weitergeleitet...</div>
|
||||
<script src="js/login.js"></script>
|
||||
@ -76,4 +75,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -70,6 +70,14 @@
|
||||
<div class="panel-head">
|
||||
<span id="my-registrations-count" class="panel-count">0</span>
|
||||
</div>
|
||||
<p style="font-size: 0.9rem; color: var(--olive); margin-bottom: 16px; display: flex; align-items: flex-start; gap: 8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--olive)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 1px;">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="8"/>
|
||||
<line x1="12" y1="12" x2="12" y2="16"/>
|
||||
</svg>
|
||||
Eine Abmeldung ist bis 24 Stunden vor Eventbeginn möglich. Bitte respektiere den Host und melde dich rechtzeitig ab, wenn du nicht kommen kannst.
|
||||
</p>
|
||||
<div id="my-registrations-list" class="profile-card-list"></div>
|
||||
</article>
|
||||
|
||||
@ -77,26 +85,26 @@
|
||||
<h2 class="panel-title">Profil verwalten</h2>
|
||||
<form id="profile-form" novalidate>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<div class="margin-bottom-16">
|
||||
<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">
|
||||
<div class="margin-bottom-16">
|
||||
<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">
|
||||
<div class="margin-bottom-16">
|
||||
<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">
|
||||
<div class="margin-bottom-40">
|
||||
<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>
|
||||
|
||||
49
signup.html
49
signup.html
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kontaktseite - Invité</title>
|
||||
<title>Invité | Registration</title>
|
||||
|
||||
<!-- Globales Stylesheet -->
|
||||
<link rel="stylesheet" href="css/stylesheet_global.css">
|
||||
@ -26,12 +26,8 @@
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="image-section">
|
||||
<img src="assets/index_cooking.jpg" alt="Social Cooking">
|
||||
</div>
|
||||
|
||||
<div class="container-registration layout-wide">
|
||||
<div class="text-section">
|
||||
<div class="form-section">
|
||||
<h1>Erstelle deinen Account</h1>
|
||||
|
||||
@ -39,32 +35,32 @@
|
||||
<strong>Hinweis:</strong> Sichtbar auf der Plattform ist nur dein Vorname. Erst einer Anmeldung zum Event ist der Nachname für die Teilnehmenden sichtbar.
|
||||
</div>
|
||||
|
||||
<form id="signupForm">
|
||||
<div class="form-group">
|
||||
<label for="vorname">Vorname *</label>
|
||||
<form id="signupForm" novalidate>
|
||||
<div class="margin-bottom-16">
|
||||
<p class= "label-input-field" for="vorname">Vorname*</p>
|
||||
<input type="text" id="vorname" name="vorname" required placeholder="Dein Vorname">
|
||||
<div class="error-message" id="vornameError">Bitte gib deinen Vornamen ein.</div>
|
||||
<div class="error-message error-message--field-callout" id="vornameError">Bitte gib deinen Vornamen ein.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nachname">Nachname *</label>
|
||||
<div class="margin-bottom-16">
|
||||
<p class= "label-input-field" for="nachname">Nachname*</p>
|
||||
<input type="text" id="nachname" name="nachname" required placeholder="Dein Nachname">
|
||||
<div class="error-message" id="nachnameError">Bitte gib deinen Nachnamen ein.</div>
|
||||
<div class="error-message error-message--field-callout" id="nachnameError">Bitte gib deinen Nachnamen ein.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail *</label>
|
||||
<input type="email" id="email" name="email" required placeholder="deine.email@example.com">
|
||||
<div class="error-message" id="emailError">Bitte gib eine gültige E-Mail Adresse ein.</div>
|
||||
<div class="margin-bottom-16">
|
||||
<p class= "label-input-field" for="email">E-Mail*</p>
|
||||
<input type="email" id="email" name="email" required placeholder="Deine E-mail-Adresse">
|
||||
<div class="error-message error-message--field-callout" id="emailError">Bitte gib eine gültige E-Mail-Adresse ein.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="passwort">Passwort *</label>
|
||||
<div class="margin-bottom-40">
|
||||
<p class= "label-input-field" for="passwort">Passwort*</p>
|
||||
<input type="password" id="passwort" name="passwort" required placeholder="Mindestens 8 Zeichen">
|
||||
<div class="error-message" id="passwortError">Dein Passwort muss mindestens 8 Zeichen lang sein.</div>
|
||||
<div class="error-message error-message--field-callout" id="passwortError">Dein Passwort muss mindestens 8 Zeichen lang sein.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button-primary">Konto erstellen</button>
|
||||
<button type="submit" class="button-primary margin-bottom-24">Konto erstellen</button>
|
||||
|
||||
|
||||
<div class="link-text">
|
||||
@ -72,6 +68,13 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="image-section">
|
||||
<img src="assets/index_cooking.jpg" alt="Social Cooking">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div> <!-- Schliesst container -->
|
||||
|
||||
@ -108,4 +111,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user