Compare commits

...

42 Commits

Author SHA1 Message Date
viiivo
5a8d958fa8 Improve event detail layout and update event dates in JSON
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 20:29:39 +02:00
Estelle Köhler
eb15ddb707 Neues Titelbild 2026-04-26 11:10:03 +02:00
Estelle Köhler
97badadfc5 Event absagen snackbar hinzugefügt 2026-04-26 10:49:53 +02:00
Estelle Köhler
9b63c632c7 Fixing Profile bugs 2026-04-26 10:28:22 +02:00
Estelle Köhler
a167e3277e Abmeldung bei my profile pop up hinzugefügt. 2026-04-26 10:12:53 +02:00
5439aeeacb Assets ergänzt 2026-04-26 09:55:50 +02:00
f3ddb5b3a3 Assets aktualisiert 2026-04-26 09:46:23 +02:00
a540f0348d Filterrestriktionen Ernährungsform, Button um Filter zu löschen 2026-04-25 23:25:16 +02:00
895c173847 Styling Prodil 2026-04-25 22:45:46 +02:00
3b7f41e33d Styling Übersicht Events 2026-04-25 22:44:07 +02:00
e3f7233581 Styling Filter Übersicht Events 2026-04-25 20:36:53 +02:00
ffee29d57e Accordion Filter (ohne Styling) 2026-04-25 17:38:51 +02:00
848242b10b Anpassung Farbe Detail dereg-hint 2026-04-25 17:04:43 +02:00
2119bd7e6c Schriftgrösse Detail dereg-hint 2026-04-25 17:02:10 +02:00
56bf28c439 Detail Styling Lightbox 2026-04-25 16:57:50 +02:00
03c5a0e900 Styling Bestätigung Event veröffentlicht 2026-04-25 16:52:21 +02:00
2bf3609662 Styling Event erstellen 2026-04-25 16:33:28 +02:00
e53d4c8031 Details FAQ angepasst 2026-04-25 14:55:49 +02:00
46eeecdc3a Layout Startseite/FAQ 2026-04-25 14:47:47 +02:00
0041ab8d7a Layout und Styling Login/Registration 2026-04-25 13:32:21 +02:00
viiivo
3b65e392fe feat: add support for canceled events in event management and UI updatesfor consistancy in buttons 2026-04-23 22:04:28 +02:00
viiivo
d46b65aa73 feat: enhance profile notifications and address visibility logic 2026-04-23 21:15:44 +02:00
viiivo
f4463bbd9a Anpassungen zu der Darstellung von Messages und Adresse im Profil. Disable Button wenn keine Abmeldung mehr möglich ist. 2026-04-23 21:04:39 +02:00
viiivo
ae631cd463 fix: update event date and enhance address visibility logic
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 18:46:24 +02:00
viiivo
4ff703b4ff feat: platzhalter-bilder finalisiert und gemerget 2026-04-23 18:24:53 +02:00
Ysabelle Moser
fdbb70a4e5 Success Seite Eventerstellung Fotoplatzierung anpassen 2026-04-23 16:33:34 +02:00
Ysabelle Moser
3b438626dd Merge branch 'main' of https://gitea.fhgr.ch/koehleestell/Social_Cooking
~                                                                      	Merge branch 'main' of https://gitea.fhgr.ch/koehleestell/Social_Cooking
:wq
2026-04-23 16:25:50 +02:00
Ysabelle Moser
4bd4f8046d Einfügen Icons + Anpassung Styling Radio-Buttons und Foto-Buttons bei Eventerstellung 2026-04-23 16:25:23 +02:00
Estelle Köhler
e277c7f7d4 Merge branch 'main' of https://gitea.fhgr.ch/koehleestell/Social_Cooking 2026-04-23 16:02:36 +02:00
Estelle Köhler
cbe3c0983f Texte angepasst 2026-04-23 16:02:30 +02:00
Ysabelle Moser
ee1c78f5de Bild austauschen Intro-Screen Eventerstellung 2026-04-23 12:12:22 +02:00
Ysabelle Moser
61d84022cc Merge branch 'main' of https://gitea.fhgr.ch/koehleestell/Social_Cooking 2026-04-23 11:50:27 +02:00
Ysabelle Moser
b9a5a348a8 Layoutanpassungen vom Success-Screen der Event-Erstellung 2026-04-23 11:47:39 +02:00
Estelle Köhler
60b8a5fd3a Pop-up auch bei Detailseite hinzufügen. Text angepasst. 2026-04-23 11:30:50 +02:00
Estelle Köhler
244f3b74b1 Pop-Up für ab und anmeldung. 2026-04-23 11:00:33 +02:00
Estelle Köhler
4fbabab800 Instagram Herze und Kommentare overlay auf den Fotos. 2026-04-23 07:55:30 +02:00
Ysabelle Moser
485254d3e7 Anpassungen event-create + Fehlermeldungen in login und signup 2026-04-23 00:56:09 +02:00
viiivo
7ecc2cf91a Enhance event overview layout and styling; update event date and improve participant name handling. Pop-Up that Events are free. Explenation for Event Adresse. 2026-04-22 10:19:13 +02:00
21d96597cc Konflikt lösen Button eigener Event Detailansicht 2026-04-21 22:30:36 +02:00
eb537e7d52 Konflikt lösen Übersicht HEAD / Übersicht Startseit 2026-04-21 22:12:13 +02:00
17c26b1cb5 Anpassung Layout 2026-04-21 22:00:11 +02:00
51a07b6347 Anpassung Layou 2026-04-21 21:57:57 +02:00
35 changed files with 2676 additions and 1589 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 298.748 298.748" xml:space="preserve">
<g id="XMLID_1429_">
<g>
<g>
<path d="M89.486,99.966l5.118,2.999l10.451-6.427c1.463-0.9,2.999-1.573,4.57-2.038l-12.606-7.387
c-3.552-2.081-8.113-0.889-10.193,2.661C84.746,93.323,85.937,97.887,89.486,99.966z"/>
<path d="M194.008,187.239c5.392,0,9.764-4.372,9.764-9.764c0-5.393-4.372-9.764-9.764-9.764H99.344
c-5.393,0-9.764,4.372-9.764,9.764c0,5.393,4.372,9.764,9.764,9.764h39.885v46.522h-7.948c-4.113,0-7.447,3.334-7.447,7.447
c0,4.113,3.334,7.447,7.447,7.447h30.791c4.113,0,7.447-3.334,7.447-7.447c0-4.113-3.334-7.447-7.447-7.447h-7.949v-46.522
H194.008z"/>
<path d="M80.72,177.226c-1.169-3.279-3.799-5.825-7.112-6.888l-10.951-3.51l7.194-25.11l-18.93-16.393l30.517,14.102
c2.778,1.283,6.078,1.142,8.782-0.521l29.67-18.248c4.379-2.694,5.746-8.427,3.052-12.806c-2.694-4.379-8.427-5.746-12.806-3.052
l-25.418,15.634l-25.987-12.009l13.749,2.106c-1.581-1.776-3.658-3.147-6.112-3.851l-21.263-6.092
c-3.485-0.998-7.227-0.497-10.326,1.384c-3.099,1.881-5.271,4.97-5.993,8.522l-12.821,63.063
c-0.664,3.266,0.174,6.657,2.283,9.237c0.327,0.4,0.686,0.764,1.06,1.11H7.621c-0.541,0-1.067,0.061-1.576,0.17
C2.603,184.73,0,187.751,0,191.384v49.815c0,4.113,3.334,7.447,7.447,7.447s7.447-3.334,7.447-7.447v-42.401H49.29v42.401
c0,4.113,3.334,7.447,7.447,7.447s7.447-3.334,7.447-7.447v-43.818l15.626,43.848c2.072,5.815,8.466,8.843,14.272,6.772
c5.812-2.071,8.844-8.461,6.773-14.273L80.72,177.226z"/>
<circle cx="66.103" cy="76.449" r="19.243"/>
<circle cx="252.446" cy="69.335" r="19.243"/>
<path d="M291.129,183.905h-14.077c0.415-0.256,0.819-0.532,1.201-0.844c2.579-2.111,4.073-5.268,4.07-8.6l-0.054-64.353
c-0.006-7.573-6.471-13.548-14.029-12.947l-22.049,1.752c-7.156,0.569-12.498,6.831-11.929,13.988l0.922,11.603l1.781-0.025
l16.473-14.381l-15.502,24.067l-29.838,0.426c-5.14,0.073-9.248,4.3-9.175,9.441c0.073,5.095,4.226,9.176,9.305,9.176
c0.045,0,0.091,0,0.136-0.001l34.828-0.498c3.117-0.044,6.005-1.646,7.693-4.267l18.205-28.263l-10.052,33.515
c-1.559,5.197-6.287,8.576-11.439,8.655c-5.26,0.071-3.13,0.042-9.398,0.131l0.362,4.555l-12.661,3.092
c-3.652,0.892-6.611,3.561-7.872,7.102l-20.136,56.505c-2.072,5.812,0.961,12.201,6.773,14.273
c5.812,2.072,12.202-0.963,14.272-6.772l15.626-43.85v43.82c0,4.113,3.335,7.447,7.447,7.447c4.113,0,7.447-3.334,7.447-7.447
v-42.401h34.395v42.401c0,4.113,3.335,7.447,7.447,7.447s7.447-3.334,7.447-7.447v-49.815
C298.748,187.265,295.406,183.905,291.129,183.905z"/>
<path d="M104.088,144.815c-0.057,0.25-0.09,0.502-0.09,0.757c0,4.239,6.881,7.896,16.812,9.586v1.397
c0,2.202,1.786,3.988,3.988,3.988h16.795c2.203,0,3.988-1.786,3.988-3.988v-1.396c9.931-1.69,16.812-5.346,16.812-9.586
c0-0.255-0.032-0.506-0.088-0.755c-0.11-0.487-0.933-0.854-1.915-0.854h-54.387C105.024,143.963,104.2,144.33,104.088,144.815z"
/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

View File

@ -14,16 +14,9 @@
box-sizing: border-box;
}
.event-create-page {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
padding: var(--space-5) 0 0;
}
.event-flow-header {
display: flex;
justify-content: flex-end;
margin-bottom: var(--space-4);
justify-content: flex-start;
}
.event-form {
@ -34,11 +27,45 @@
.step {
display: none;
padding: var(--space-4) 0 var(--space-4);
}
/*
.submission-success {
padding: var(--space-4) 0 var(--space-7);
padding: var(--space-24) 0 var(--space-48);
}*/
.submission-success-title-row {
display: flex;
align-items: center;
gap: var(--space-32);
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 {
@ -46,17 +73,20 @@
}
.step-layout {
width: min(100%, var(--content-width));
margin: 0 auto;
display: grid;
gap: var(--space-40);
gap: 80px;
}
.startseite {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
}
.step-layout--intro {
min-height: 60vh;
align-content: center;
grid-template-columns: 1fr;
gap: var(--space-7);
gap: var(--space-48);
}
.step-copy,
@ -64,15 +94,15 @@
.form-field,
fieldset {
display: grid;
gap: var(--space-4);
}
.step-copy {
gap: var(--space-4);
gap: var(--space-24);
align-content: start;
}
.step-fields {
gap: var(--space-5);
gap: var(--space-32);
}
.form-field,
@ -80,13 +110,12 @@ fieldset {
margin: 0;
padding: 0;
border: 0;
gap: var(--space-3);
}
.step-text {
/* definiert Breite des Beschriebtexts der einzelnen Schritte*/
max-width: 600px;
max-width: 100%;
}
.intro-card,
@ -102,44 +131,18 @@ 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);
}
label {
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
}
legend {
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-3);
}
.field-hint {
margin: -0.25rem 0 0;
color: var(--color-muted);
font-size: 0.95rem;
color: var(--olive);
font-size: 1rem;
margin-bottom: var(--space-8);
}
input[type="text"],
@ -149,8 +152,8 @@ input[type="number"],
textarea {
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;;
padding: 1rem 1.25rem;
font-size: 1.125rem;
padding: var(--space-16) var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
@ -180,7 +183,7 @@ input[type="date"]:hover,
input[type="time"]:hover,
input[type="number"]:hover,
textarea:hover {
border-color: rgba(102, 52, 13, 0.28);
border: 2px solid var(--olive);
}
input[type="text"]:focus,
@ -188,7 +191,7 @@ input[type="date"]:focus,
input[type="time"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: var(--olive-dark);
border: 2px solid var(--olive);
}
.field-invalid {
@ -198,12 +201,12 @@ textarea:focus {
.field-row {
display: grid;
gap: var(--space-4);
gap: var(--space-24);
}
.option-grid {
display: grid;
gap: var(--space-3);
gap: var(--space-16);
}
.option-card {
@ -216,6 +219,23 @@ 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(--black);
}
.option-card__icon {
color: var(--black);
font-size: 1.25rem;
pointer-events: none;
}
.option-card--with-icon span {
pointer-events: none;
}
.option-card:hover {
background: var(--olive-light);
@ -223,6 +243,28 @@ textarea:focus {
transform: translateY(-3px);
}
.option-grid--tomato-choices .option-card:hover,
.option-grid--tomato-choices .option-card:has(input:focus-visible),
.option-grid--tomato-choices .option-card:has(input:checked) {
background: var(--olive-light);
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled) {
opacity: 0.6;
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover {
border-color: var(--olive-light);
background: var(--butter-light);
color: var(--black);
box-shadow: none;
transform: none;
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover .option-card__icon {
color: var(--black);
}
.option-card input {
position: absolute;
inset: 0;
@ -237,23 +279,41 @@ textarea:focus {
.option-card--invalid {
border-color: var(--error) !important;
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
box-shadow: var(--shadow-error);
}
.guest-count-icon {
display: block;
text-align: center;
width: 100%;
color: var(--black);
font-size: 1.5rem;
line-height: 1;
}
.counter {
display: inline-flex;
align-items: center;
gap: var(--space-3);
gap: var(--space-16);
}
.counter-value-group {
display: grid;
justify-items: center;
row-gap: var(--space-8);
width: 6rem;
}
.counter input {
width: 6rem;
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;
@ -263,8 +323,8 @@ textarea:focus {
.review-card--success {
display: grid;
gap: var(--space-5);
padding: var(--space-3) 0 0;
gap: var(--space-32);
padding: var(--space-16) 0 0;
border: 0;
border-radius: 0;
background: transparent;
@ -273,56 +333,58 @@ textarea:focus {
.review-list {
display: grid;
gap: var(--space-4);
gap: var(--space-12);
margin: 0;
}
.review-item {
display: grid;
gap: var(--space-2);
padding: 1rem 1.1rem;
border: 1px solid var(--input-border-soft);
border: 1.5px solid var(--olive-light);
border-radius: 1.125rem;
background: var(--butter-light);
box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04);
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
}
.review-item:last-child {
border-bottom: 1px solid var(--input-border-soft);
}
.review-item:hover {
border-color: rgba(102, 52, 13, 0.28);
background: rgba(247, 246, 230, 0.92);
transform: translateY(-1px);
}
.review-item:focus-visible {
outline: 3px solid rgba(107, 107, 5, 0.2);
outline-offset: 3px;
.review-item:hover,
.review-item:focus-visible {
border: 2px solid var(--olive);
box-shadow: var(--shadow-interaction);
transform: translateY(-3px);
}
.review-item dt {
font-weight: 700;
font-weight: 600;
}
.review-item dd {
margin: 0;
white-space: pre-wrap;
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;
padding-top: var(--space-80);
backdrop-filter: none;
padding-top: var(--space-4);
padding-bottom: env(safe-area-inset-bottom);
}
@ -332,32 +394,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;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
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;
@ -376,16 +437,21 @@ textarea:focus {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
width: min(100%, var(--content-width));
margin: 0 auto;
padding: var(--space-4) 0;
gap: var(--space-24);
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 {
@ -395,25 +461,51 @@ textarea:focus {
.button--intro {
justify-self: start;
margin-top: var(--space-2);
}
/* Gallery Upload */
.gallery-upload {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: var(--space-12);
}
.gallery-preview {
display: contents;
}
.gallery-add-button {
display: grid;
place-items: center;
align-content: center;
gap: var(--space-8);
padding: var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
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(--black);
font-size: 1.5rem;
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;
@ -448,7 +540,7 @@ textarea:focus {
.site-footer {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
padding: var(--space-5) 0 var(--space-40);
padding: var(--space-32) 0 var(--space-40);
color: var(--color-muted);
text-align: center;
}
@ -464,11 +556,11 @@ textarea:focus-visible {
@media (max-width: 767px) {
.site-nav {
flex-wrap: wrap;
padding: var(--space-3) 0;
padding: var(--space-16) 0;
}
.site-nav-links {
gap: var(--space-3);
gap: var(--space-16);
}
.flow-actions,
@ -477,6 +569,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;
}
@ -484,7 +596,6 @@ textarea:focus-visible {
@media (min-width: 768px) {
.step-layout--intro {
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: stretch;
gap: var(--space-8);
@ -501,4 +612,4 @@ textarea:focus-visible {
.option-grid--4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
}

View File

@ -1,11 +1,7 @@
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
/* ---------------------------------------------------------
Shared Typography Tokens
Reuse common text styles across nav, controls and buttons
--------------------------------------------------------- */
.meta-filter select,
.meta-filter input[type="date"],
.detail-primary-btn {
@ -16,17 +12,19 @@
}
/* Heading hierarchy: page title > detail title > card title > section title */
.overview-title {
margin: 0 0 var(--space-4);
color: var(--olive);
font-family: "Bagel Fat One", cursive;
font-size: clamp(38px, 5vw, 44px);
font-weight: 400;
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 {
@ -41,22 +39,15 @@
/* ---------------------------------------------------------
Overview Header + Filters
--------------------------------------------------------- */
.filter-label {
margin: 0 0 var(--space-1);
letter-spacing: var(--ls-la);
}
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
margin-bottom: var(--space-2);
justify-content: flex-start;
}
.category-group {
display: flex;
gap: var(--space-1);
display: flex;
gap: var(--space-6);
margin-bottom: 0;
flex-wrap: wrap;
flex: 1;
@ -70,13 +61,13 @@
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 {
display: flex;
flex-wrap: nowrap;
gap: var(--space-2);
gap: var(--space-12);
margin-bottom: 0;
}
@ -94,7 +85,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;
}
@ -116,17 +107,18 @@
transition: background-color 0.2s ease;
}
.meta-filter input[type="date"]::-webkit-calendar-picker-indicator:hover {
filter: brightness(0.8);
.meta-filter input[type="date"]:hover::-webkit-calendar-picker-indicator {
filter: brightness(0.8);
}
.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;
}
/* ---------------------------------------------------------
@ -135,7 +127,7 @@
.event-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: var(--space-16);
}
.event-card {
@ -143,7 +135,7 @@
background: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
padding: var(--space-5) var(--space-40);
padding: var(--space-32) var(--space-40);
display: flex;
gap: var(--space-40);
cursor: pointer;
@ -171,7 +163,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 +183,7 @@
.event-meta-row {
display: flex;
align-items: center;
gap: var(--space-1);
gap: var(--space-8);
flex-wrap: wrap;
}
@ -203,7 +195,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 {
@ -228,14 +220,14 @@
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
gap: var(--space-16);
padding-top: 36px;
flex-shrink: 0;
}
.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;
@ -250,15 +242,14 @@
.event-spots-full, .detail-spots-pill-full {
/* Sold-out visual state, intentionally high-contrast and always filled. */
border: 1.5px solid var(--tomato);
padding: var(--space-01) var(--space-4);
border: 1.5px solid var(--tomato-light);
padding: var(--space-8) var(--space-24);
border-radius: var(--radius-pill);
color: var(--butter-light);
background: var(--tomato);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
opacity: 0.6;
cursor: not-allowed;
}
@ -274,47 +265,6 @@
cursor: not-allowed;
}
/* ---------------------------------------------------------
Overview Empty State
--------------------------------------------------------- */
.empty-state {
text-align: center;
padding: 42px var(--space-5);
border: 2px solid var(--olive-light);
border-radius: var(--radius-lg);
background: var(--butter-light);
margin-top: 8px;
}
.empty-state-kicker {
margin: 0 0 8px;
color: var(--olive);
font-size: 13px;
font-weight: 600;
letter-spacing: var(--ls-la);
}
.empty-state h3 {
margin: 0;
font-family: "Jost", sans-serif;
font-size: 28px;
font-weight: 600;
color: var(--brown);
line-height: 1.25;
}
.empty-state p {
margin: 12px 0 0;
font-size: 18px;
color: var(--black);
}
.empty-state-link {
display: inline-block;
margin-top: 20px;
text-decoration: none;
}
/* ---------------------------------------------------------
Detail Page
@ -332,7 +282,7 @@
.detail-top-row {
display: inline-flex;
align-items: center;
gap: var(--space-5);
gap: var(--space-32);
flex-wrap: wrap;
}
@ -358,7 +308,7 @@
.detail-side-stack {
grid-area: side;
display: grid;
gap: var(--space-3);
gap: var(--space-16);
align-content: start;
}
@ -387,8 +337,8 @@
font-weight: 400;
font-size: 1rem;
line-height: 1;
letter-spacing: var(--ls-la);
padding: var(--space-1) var(--space-20);
letter-spacing: var(--ls-lg);
padding: var(--space-8) var(--space-20);
}*/
.detail-gallery {
@ -400,7 +350,7 @@
/* Editorial mosaic: first image spans two rows, side images stack vertically. */
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(2, minmax(220px, 1fr));
gap: var(--space-3);
gap: var(--space-16);
min-height: 520px;
}
@ -434,6 +384,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;
@ -468,16 +442,16 @@
.detail-lightbox-image {
display: block;
width: 100%;
max-height: 90vh;
max-height: 80vh;
object-fit: contain;
border-radius: var(--radius-md);
background: #111;
background: var(--black);
}
.detail-lightbox-close {
position: absolute;
top: -42px;
right: 0;
top: -40px;
right: -40px;
border: 0;
background: transparent;
color: var(--butter-light);
@ -489,7 +463,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-32);
}
.detail-panel-compact {
@ -500,7 +474,7 @@
margin: 20px;
list-style: disc;
font-size: 1.125rem;
line-height: 1.5;
line-height: 1.45;
}
.detail-participants-head {
@ -510,14 +484,6 @@
gap: 20px;
}
.detail-participants-link {
font-size: 12px;
font-weight: 600;
color: var(--tomato);
letter-spacing: var(--ls-la);
text-decoration: none;
}
.detail-avatar-row {
display: flex;
align-items: center;
@ -545,27 +511,26 @@
.detail-participants-full {
display: flex;
flex-direction: column;
gap: 10px;
}
.detail-participant-item {
display: flex;
align-items: center;
gap: 10px;
}
.participant-name {
font-size: 0.95rem;
font-weight: 500;
font-size: 1.125rem;
font-weight: 400;
color: var(--black);
}
.detail-participants-link {
background: none;
border: none;
font-family: var(--font-main);
color: var(--olive);
font-size: 0.85rem;
font-weight: 600;
font-size: 1rem;
font-weight: 400;
cursor: pointer;
padding: 0;
text-decoration: underline;
@ -582,13 +547,13 @@
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
gap: var(--space-16);
background: var(--white);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
box-shadow: var(--shadow-interaction);
padding: var(--space-4) var(--space-5);
padding: var(--space-24) var(--space-32);
margin-top: var(--space-40);
position: sticky;
bottom: var(--space-40);
@ -636,46 +601,75 @@
.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;
color: var(--olive);
}
/*
.detail-spots-pill-full {
border-color: var(--tomato);
color: var(--tomato);
opacity: 1;
font-weight: 600;
}*/
border: 1.5px solid var(--tomato-light);
color: var(--butter-light);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
cursor: not-allowed;
}
.detail-action-btn-wrap {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
gap: 4px;
justify-content: flex-end;
gap: 12px;
}
.detail-action-row {
display: flex;
gap: 12px;
align-items: center;
}
.detail-dereg-column {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.detail-action-btn-wrap .button-plaetze,
.detail-action-btn-wrap .event-spots-full,
.detail-action-btn-wrap .button-primary,
.detail-action-btn-wrap .button-primary-abmelden,
.detail-action-btn-wrap .button-primary-eigener-event {
/* Force identical sizing and vertical alignment for action buttons in detail bar */
display: inline-flex;
align-items: center;
justify-content: center;
height: 52px;
padding: 0 22px;
font-size: 1.25rem;
border-radius: var(--radius-pill);
}
.detail-dereg-hint {
display: block;
font-size: 11px;
font-size: 14px;
font-weight: 400;
color: var(--olive);
opacity: 0.75;
}
.detail-dereg-hint--closed {
color: var(--tomato);
font-weight: 500;
opacity: 1;
.detail-dereg-hint--placeholder {
visibility: hidden;
}
.detail-primary-btn {
@ -717,11 +711,6 @@
box-shadow: 0 2px 6px rgba(102, 52, 13, 0.22);
}
.detail-primary-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.detail-primary-btn-own,
.detail-primary-btn-own:disabled {
border-color: var(--olive-light);
@ -755,6 +744,13 @@
font-size: 34px;
}
.overview-info-button {
width: 44px;
height: 44px;
flex-basis: 44px;
font-size: 1.5rem;
}
.top-nav-links {
gap: 8px;
}
@ -831,13 +827,13 @@
grid-row: auto;
}
.detail-lightbox {
padding: 12px;
.detail-gallery-large--single {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.detail-lightbox-close {
top: -36px;
font-size: 34px;
.detail-lightbox {
padding: 12px;
}
.detail-section-title {
@ -881,6 +877,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;
@ -909,11 +918,21 @@
.meta-filter-group {
width: auto;
flex-wrap: wrap;
gap: 10px;
}
.meta-filter {
flex: 1 1 160px;
min-width: 140px;
}
}
.filter-section {
display: flex;
flex-direction: column;
}
.filter-box summary {
cursor: pointer;
margin-bottom: var(--space-8);
}

View File

@ -6,7 +6,6 @@
/* --- Navigation overrides (index-specific) --- */
.nav-link {
border: 2px solid var(--olive-light);
transition: background-color 0.2s ease, color 0.2s ease;
@ -36,10 +35,9 @@
/* --- Page layout --- */
.main-content {
width: min(100% - 4rem, 1120px);
.container {
width: min(100% - 4rem, 1200px);
margin: 0 auto;
padding: 0 20px;
}
@ -48,19 +46,10 @@
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 55px;
margin-bottom: 88px;
align-items: center;
padding: 44px 0;
gap: 80px;
margin-bottom: 100px;
}
/*
.hero__buttons {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
}*/
.hero__right {
display: flex;
align-items: center;
@ -103,7 +92,7 @@
/* --- "So funktioniert's" steps --- */
.how-it-works {
margin-bottom: 70px;
margin-bottom: 100px;
}
.how-it-works__header {
@ -163,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 {
@ -177,7 +166,7 @@
}
.how-step_text {
margin-bottom: var(--space-4);
margin-bottom: var(--space-24);
text-align: center;
}
@ -190,7 +179,7 @@
.how-step__footer-badges {
display: flex;
gap: var(--space-2);
gap: var(--space-16);
justify-content: center;
}
@ -215,6 +204,10 @@
/* --- Carousel gallery --- */
.gallery {
margin-bottom: 100px;
}
.gallery__carousel {
position: relative;
overflow: hidden;
@ -244,6 +237,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;
@ -273,11 +312,11 @@
}
.gallery__arrow--prev {
left: var(--space-4);
left: var(--space-24);
}
.gallery__arrow--next {
right: var(--space-4);
right: var(--space-24);
}
@ -398,35 +437,27 @@
/* --- FAQ Section: Akkordion --- */
.faq-section {
padding: var(--space-8) var(--space-4);
margin: var(--space-8) 0 var(--space-5);
}
.faq-section h2 {
text-align: center;
margin-bottom: var(--space-5);
color: var(--brown);
margin-bottom: 0px;
}
.faq-accordion {
width: 100%;
max-width: 56rem;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-2);
gap: var(--space-8);
}
.faq-item {
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--white);
transition: background-color 0.2s ease, box-shadow var(--shadow-interaction);
background: var(--butter-light);
padding: var(--space-12) var(--space-24) ;
transition: background-color 0.2s ease;
}
.faq-item:hover {
box-shadow: var(--shadow-interaction);
background: var(--olive-light);
}
.faq-trigger {
@ -434,41 +465,32 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: transparent;
border: none;
cursor: pointer;
font-family: var(--font-main);
font-size: 1.25rem;
font-weight: 400;
color: var(--olive);
font-weight: 400;;
text-align: left;
transition: background-color 0.2s ease;
font-family: var(--font-main);
}
.faq-trigger:hover {
background-color: var(--butter-light);
}
.faq-trigger:focus-visible {
outline: 2px solid var(--olive);
outline-offset: -2px;
}
.faq-title {
flex: 1;
font-weight: 600;
font-weight: 400;
font-size: 1.25rem;
}
.faq-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
width: 24px;
height: 24px;
font-size: 1.5rem;
font-weight: 300;
color: var(--olive);
font-weight: 400;
color: var(--black);
transition: transform 0.3s ease;
flex-shrink: 0;
}
@ -478,25 +500,32 @@
}
.faq-content {
padding: 0 var(--space-4);
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
font-size: 1.125rem;
line-height: 1.7;
color: var(--black);
font-family: var(--font-main);
transition: max-height 0.3s ease;
}
.faq-content p {
margin: 0;
padding: var(--space-3) 0;
padding: var(--space-12) var(--space-40) var(--space-12) 0;
}
.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);
}
.faq-list {
padding-left: var(--space-24);
margin: var(--space-12) var(--space-24) var(--space-12) 0;
}
.faq-list li {
font-size: 1.125rem;
margin-bottom: var(--space-12)
}
@ -504,8 +533,8 @@
@media (max-width: 768px) {
.faq-section {
padding: var(--space-40) var(--space-4);
margin: var(--space-40) 0 var(--space-5);
padding: var(--space-40) var(--space-24);
margin: var(--space-40) 0 var(--space-32);
}
.faq-trigger {

View File

@ -4,154 +4,96 @@
typography) are in stylesheet_global.css
=========================================== */
/* --- Page layout --- */
.main-content {
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);
padding: var(--space-3);
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);
box-shadow: var(--shadow-error);
}
.form-group.has-error .error-message {
.form-group.has-error .error-message--field-callout {
display: block;
}
*/
/* --- Modal / Popup --- */
@ -201,14 +143,8 @@ button[type="submit"]:active {
justify-content: center;
}
.modal-footer {
display: flex;
gap: var(--space-2);
justify-content: center;
}
.btn-primary {
padding: var(--space-2) var(--space-5);
padding: var(--space-2) var(--space-32);
background-color: var(--olive);
color: var(--white);
border: none;
@ -227,12 +163,11 @@ button[type="submit"]:active {
/* --- Footer --- */
.footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: var(--space-3) var(--space-5);
padding: var(--space-16) var(--space-32);
border: none;
margin-top: 40px;
}
@ -258,7 +193,7 @@ button[type="submit"]:active {
.footer-right {
justify-self: end;
display: flex;
gap: var(--space-4);
gap: var(--space-24);
}
@ -272,31 +207,14 @@ 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;
}
}
/* --- Snackbar --- */
.snackbar {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--olive);
color: var(--white);
padding: var(--space-3) var(--space-40);
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 600;
font-family: var(--font-main);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: transform 0.4s ease, opacity 0.4s ease;
z-index: 9999;
pointer-events: none;
}
.snackbar--visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}

View File

@ -1,17 +1,12 @@
.profile-page {
/* Reserve a large safe zone below sticky nav so title/actions are never covered. */
margin-top: 0;
padding-top: 6.5rem;
margin-bottom: var(--space-8);
}

/* Kopfbereich mit Titel und Logout-Aktion. */
.profile-hero {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
margin-bottom: var(--space-5);
gap: var(--space-24);
margin-bottom: var(--space-32);
}
.profile-kicker {
@ -19,13 +14,11 @@
color: var(--olive);
font-size: 1rem;
font-weight: 500;
letter-spacing: var(--ls-la);
letter-spacing: var(--ls-lg);
}
#headline {
margin: 0.4rem 0;
color: var(--brown);
font-size: clamp(2rem, 4.4vw, 2.8rem);
}
.profile-subline {
@ -41,44 +34,38 @@
.profile-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
gap: var(--space-24);
}
.btn-count {
color: var(--black);
background: var(--tomato-light);
height: 32px;
width: 32px;
margin-right: -18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
}
/* Konsistentes Karten-Layout für alle Profilsektionen. */
.profile-panel {
background: var(--butter-light);
border-radius: var(--radius-lg);
padding: var(--space-5);
padding: var(--space-32);
}
.panel-head {
display: flex;
display: none;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
}
.panel-title {
margin: 0;
color: var(--brown);
font-size: 1.8rem;
}
.panel-count {
min-width: 2rem;
padding: 0.1rem 0.65rem;
border-radius: var(--radius-pill);
background: var(--olive-light);
color: var(--black);
font-size: 0.95rem;
font-weight: 600;
text-align: center;
margin-bottom: var(--space-16);
}
.profile-card-list {
display: grid;
gap: var(--space-2);
gap: var(--space-16);
}
/* Einzelne Eventkarte für "Meine Events" und "Meine Anmeldungen". */
@ -87,7 +74,7 @@
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-interaction);
padding: var(--space-5) var(--space-40);
padding: var(--space-32) var(--space-40);
display: flex;
justify-content: space-between;
gap: var(--space-40);
@ -100,7 +87,7 @@
}
.profile-event-card-clickable:hover {
transform: translateY(-1px);
transform: translateY(-3px);
}
.profile-event-title h3{
@ -113,27 +100,26 @@
}*/
.profile-event-address-block {
margin-top: 0.55rem;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-sm);
border-left: 4px solid var(--tomato);
background: rgba(232, 237, 209, 0.65);
margin-top: var(--space-24);
background-color: var(--olive-light);
padding: var(--space-16);
border-radius: var(--radius-md);
color: var(--black);
line-height: 1.4;
}
.profile-event-address-label {
margin: 0;
color: var(--olive);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: var(--ls-la);
font-size: 1rem;
font-weight: 400;
color: var(--olive-dark);
}
.profile-event-address {
margin: 0.2rem 0 0;
font-size: 0.95rem;
font-size: 1.125rem;
font-weight: 400;
line-height: 1.4;
color: var(--black);
font-weight: 600;
line-height: 1.35;
}
.profile-event-link {
@ -152,58 +138,22 @@
.profile-event-actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.profile-empty {
margin: 0;
color: var(--black);
}
.profile-empty-state {
text-align: center;
padding: 2.4rem 1.3rem;
border: 2px solid var(--olive-light);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 3px 12px rgba(102, 52, 13, 0.08);
}
.profile-empty-kicker {
margin: 0 0 0.5rem;
color: var(--olive);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: var(--ls-la);
}
.profile-empty-state h3 {
margin: 0;
font-size: 1.5rem;
color: var(--brown);
}
.profile-empty-state p {
margin: 0.65rem auto 1rem;
max-width: 36rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
gap: var(--space-16);
}
.form-group {
margin-bottom: var(--space-3);
}
/*
.form-group label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.95rem;
font-weight: 500;
}
}*/
.form-group input {
width: 100%;
@ -238,8 +188,17 @@
.form-group.has-error input {
border-color: var(--error);
box-shadow: var(--shadow-error);
}
.info-abmeldung {
font-size: 1rem;
color: var(--olive);
margin-bottom: 16px;
display: flex; align-items:
flex-start;
gap: 8px;
}
.profile-feedback {
margin: 0.75rem 0 0;
font-size: 0.95rem;
@ -249,8 +208,8 @@
.profile-cta-row {
display: flex;
gap: var(--space-2);
margin-top: var(--space-3);
gap: var();
margin-top: var(--space-16);
}
.profile-button-secondary {
@ -262,7 +221,7 @@
}
@media (max-width: 48rem) {
.profile-page {
.container {
padding-top: 5.5rem;
}

View File

@ -18,7 +18,8 @@
--tomato: #D44B24;
--tomato-dark: #D44B24;
--tomato-dark: #B53A18;
--tomato-light: #E5937C;
--olive: #6B6B05;
--olive-dark: #545404;
--olive-light: #C8CC7A;
@ -41,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-3: 1rem; /* 16px */
--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-5: 2rem; /* 32px */
--space-24: 1.5rem; /* 24px */
--space-32: 2rem; /* 32px */
--space-40: 2.5rem; /* 40px */
--space-7: 3rem; /* 48px */
--space-8: 4rem; /* 64px */
--space-48: 3rem; /* 48px */
--space-64: 4rem; /* 64px */
--space-80: 5rem; /* 80px */
/* Radius Scale */
@ -62,7 +64,7 @@
/* Letter Spacing */
--ls-none: 0;
--ls-sm: 2.5%;
--ls-la: 5%;
--ls-lg: 5%;
}
/* Base Styles */
@ -80,53 +82,73 @@ 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);
margin-bottom: var(--space-32);
}
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;
}
p {
font-family: var(--font-main);
font-size: 1.125rem;
line-height: 1.5;
line-height: 1.45;
color: var(--black);
margin-bottom: 1rem;
}
.p-small {
font-family: var(--font-main);
font-size: 1rem;
line-height: 1.5;
margin-bottom: 0rem;
font-weight: 400;
line-height: 1.4;
}
.label-input-field,
.filter-label {
font-family: var(--font-main);
font-size: 1rem;
font-weight: 400;
line-height: 1;
margin-bottom: var(--space-8);
}
label {
font-family: var(--font-main);
font-size: 1.125rem;
font-weight: 600;
letter-spacing: var(--ls-lg);
line-height: 1;
margin-bottom: var(--space-16);
}
.option {
font-family: var(--font-main);
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-8);
}
.link-text a{
color: var(--blue);
margin-top: var(--space-4);
margin-top: var(--space-24);
text-decoration: none;
transition: color 0.3s ease;
}
@ -147,14 +169,80 @@ p {
}
.error-message {
color: var(--error-text);
font-family: var(--font-main);
font-size: 1rem;
line-height: 1.4;
}
.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: var(--space-6) var(--space-16);
border-radius: var(--radius-md);
background: var(--error);
color: var(--butter-light);
text-align: center;
overflow-wrap: anywhere;
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 {
@ -167,7 +255,7 @@ p {
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
gap: var(--space-16);
padding-top: 36px;
flex-shrink: 0;
}
@ -188,28 +276,37 @@ p {
}
/* Layout */
.main-content {
margin-top: var(--space-8);
.layout-wide {
width: 75%;
max-width: 1200px;
margin: 82px auto 0 auto;
}
.layout-narrow {
width: 55%;
max-width: 900px;
margin: 82px auto 0 auto;
}
/*
.container {
width: 90%;
width: 80%;
max-width: 75rem;
margin: 0 auto;
}
*/
/*
Content pages with sticky nav require top padding to avoid overlap.
Used on event_overview, event_detail, and similar pages.
*/
.container.page-content-safe {
padding-top: 6.5rem;
}
/* Media Queries (Responsive) */
@media (max-width: 48rem) {
.container {
width: 95%;
}
/* Detail pages with back button need less top padding. */
.container.page-content-safe.detail-page {
padding-top: 3.5rem;
.nav {
flex-direction: column;
}
}
.icon {
@ -240,7 +337,7 @@ p {
justify-content: space-between;
align-items: center;
min-height: 3rem;
padding: 0.1875rem 0.75rem 0.1875rem var(--space-5);
padding: 0.1875rem 0.75rem 0.1875rem var(--space-32);
max-width: none;
width: 100%;
box-sizing: border-box;
@ -286,7 +383,7 @@ p {
.nav-tab-links {
display: flex;
align-items: center;
gap: var(--space-5);
gap: var(--space-32);
}
@ -300,7 +397,7 @@ p {
font-family: var(--font-main);
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-la);
letter-spacing: var(--ls-lg);
z-index: 0;
}
@ -325,7 +422,7 @@ p {
/* 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);
@ -348,7 +445,7 @@ p {
background: transparent;
color: var(--olive-dark);
opacity: 0.6;
padding: var(--space-01) 0 var(--space-01) var(--space-4);
padding: var(--space-6) 0 var(--space-6) var(--space-24);
cursor: not-allowed;
}
@ -368,12 +465,20 @@ p {
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;
@ -422,29 +527,6 @@ p {
border-color: var(--olive-dark);
}
/* Butter-colored back button for detail pages. */
.btn-back-to-overview {
display: inline-block;
padding: 0.5rem 1.5rem;
background-color: var(--butter);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.125rem;
color: var(--olive);
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
margin-bottom: var(--space-4);
}
.btn-back-to-overview:hover,
.btn-back-to-overview:focus-visible {
background-color: var(--butter-light);
border-color: var(--olive);
}
.button--outline {
background-color: transparent;
@ -454,7 +536,8 @@ p {
.button--outline:hover {
background-color: var(--olive-light);
color: var(--black);
color: var(--olive-dark);
border: 1.5px solid var(--olive-dark);
}
.counter-button {
@ -472,7 +555,8 @@ p {
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.counter-button:hover, .counter-button:focus-visible {
.counter-button:hover,
.counter-button:focus-visible {
background-color: var(--olive-dark);
border-color: var(--olive-dark);
}
@ -487,33 +571,56 @@ p {
font-weight: 400;
font-size: 1.125rem;
line-height: 1;
padding: var(--space-1) var(--space-20);
padding: var(--space-8) var(--space-20);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.category-item:hover, .category-item:focus-visible {
.category-item:hover,
.category-item:focus-visible,
.category-item.is-active,
.category-item.active {
background: var(--tomato);
color: var(--butter-light);
}
.category-item.is-active, .category-item.active {
background: var(--tomato);
color: var(--butter-light);
.category-item.disabled {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
.category-item-profile {
font-size: 1.25rem;
font-weight: 400;
line-height: 1;
padding: var(--space-1) var(--space-4);
}
.category-items {
display: inline-flex;
flex-wrap: wrap;
gap: var(--space-1);
gap: var(--space-8);
}
.category-item-profile {
position: relative;
font-size: 1.25rem;
font-weight: 400;
line-height: 1;
height: 44px;
padding: 0 var(--space-24);
display: inline-flex;
align-items: center;
gap: var(--space-16);
}
.filter-delete {
border: 1.5px solid var(--olive);
color: var(--olive);
background: transparent;
}
.filter-delete:hover {
border: 1.5px solid var(--olive-dark);
color: var(--olive-dark);
background: var(--olive-light);
}
.button-small {
@ -526,7 +633,7 @@ p {
font-weight: 400;
line-height: 1;
border-radius: var(--radius-pill);
padding: 10px var(--space-3);
padding: 10px var(--space-16);
text-decoration: none;
cursor: pointer;
}
@ -560,6 +667,7 @@ p {
}
.profile-pill {
position: relative;
width: 2.375rem;
height: 2.375rem;
border-radius: 1.1875rem;
@ -575,6 +683,18 @@ p {
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;
@ -599,7 +719,7 @@ p {
align-items: center;
justify-content: center;
height: 1em; /* keeps it aligned with text size */
margin-top: var(--space-7);
margin-top: var(--space-48);
}
.instagram-invite__link {
@ -614,7 +734,6 @@ p {
width: 32px;
object-fit: contain;
border-radius: 8px;
filter: brightness(0) saturate(100%) invert(27%) sepia(81%) saturate(749%) hue-rotate(24deg) brightness(90%) contrast(90%);
}
.footer-invite_logo {
@ -630,32 +749,6 @@ p {
gap: 8px;
}
/* Info button for event overview page */
.btn-info {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 1.5px solid var(--olive-light);
border-radius: 999px;
background-color: var(--butter);
color: var(--olive);
font-family: var(--font-main);
font-size: 1.5rem;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
flex-shrink: 0;
}
.btn-info:hover,
.btn-info:focus-visible {
background-color: var(--butter-light);
border-color: var(--olive);
}
/* Modal / Popup */
.modal {
display: none;
@ -682,13 +775,34 @@ p {
.modal-content {
background-color: var(--white);
padding: var(--space-20) var(--space-20) var(--space-40) var(--space-20);
padding: var(--space-40);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-interaction);
max-width: 500px;
max-width: 460px;
width: 90%;
text-align: center;
animation: modalSlideIn 0.3s ease;
animation: modalSlideIn 0.5s ease;
}
.btn-info {
width: 48px;
height: 48px;
flex: 0 0 48px;
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-pill);
color: var(--black);
background: var(--butter-light);
font-family: "Bagel Fat One";
font-size: 1.75rem;
font-weight: 500;
line-height: 1;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-info:hover,
.btn-info:focus-visible {
background: var(--olive-light);
}
@keyframes modalSlideIn {
@ -697,33 +811,43 @@ p {
}
.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);
}
.close-btn {
position: absolute;
right: 0;
top: 0;
font-size: 28px;
color: var(--black);
background: none;
border: none;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
text-align: left;
flex: 1;
}
.modal-close {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
flex: 0 0 32px;
margin-left: auto;
border: none;
background: transparent;
color: transparent;
font-size: 2rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: transform 0.2s ease;
}
.modal-close:hover,
.modal-close:focus-visible {
transform: translateY(-1px);
}
.close-btn {
font-size: 28px;
color: var(--black);
background: none;
@ -737,31 +861,36 @@ p {
}
.modal-body {
padding: var(--space-20) var(--space-20) var(--space-4) var(--space-20);
font-family: var(--font-main);
font-size: 1.125rem;
padding: var(--space-24) 0 var(--space-40) 0;
text-align: left;
line-height: 1.45;
}
.modal-footer {
display: flex;
gap: var(--space-2);
gap: var(--space-12);
justify-content: center;
}
/* Snackbar */
.snackbar {
.snackbar,
.snackbar--danger {
position: fixed;
bottom: 30px;
bottom: 40px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--olive);
color: var(--white);
padding: var(--space-3) var(--space-40);
color: var(--butter-light);
background: var(--tomato);
padding: var(--space-16) var(--space-40);
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 600;
font-size: 1.125rem;
font-weight: 400;
font-family: var(--font-main);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
box-shadow: var(--shadow-interaction);
opacity: 0;
transition: transform 0.4s ease, opacity 0.4s ease;
transition: transform 0.5s ease, opacity 0.4s ease;
z-index: 9999;
pointer-events: none;
}
@ -771,10 +900,6 @@ p {
opacity: 1;
}
.snackbar--danger {
background: var(--tomato);
}
/* Lightbox */
.lightbox {
position: fixed;
@ -815,16 +940,59 @@ p {
.lightbox__close {
position: absolute;
top: -42px;
right: 0;
top: -40px;
right: -40px;
border: 0;
background: transparent;
color: var(--white);
color: var(--butter-light);
font-size: 40px;
line-height: 1;
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: var(--butter-light);
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border: 2px solid var(--olive);
}
/* ---------------------------------------------------------
Overview Empty State
--------------------------------------------------------- */
.empty-state {
text-align: center;
padding: var(--space-32) var(--space-40);
border-radius: var(--radius-lg);
background: var(--butter-light);
}
.empty-state-kicker {
color: var(--olive);
font-size: 1rem;
font-weight: 400;
}
.empty-state-link {
display: inline-block;
margin-top: 20px;
text-decoration: none;
}
/* Footer */
.footer {
@ -832,12 +1000,12 @@ p {
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-top: 120px;
padding: var(--space-3) var(--space-7);
padding: var(--space-16) var(--space-48);
}
.footer-links {
display: flex;
gap: var(--space-4);
gap: var(--space-24);
}
/* Left aligned */
@ -854,16 +1022,6 @@ p {
.footer-right {
justify-self: end;
display: flex;
gap: var(--space-4);
gap: var(--space-24);
}
/* Media Queries (Responsive) */
@media (max-width: 48rem) {
.container {
width: 95%;
}
.nav {
flex-direction: column;
}
}

View File

@ -4,7 +4,7 @@
"title": "Italienische Tavolata",
"location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern",
"date": "22. APR. 2026",
"date": "17. Mai. 2026",
"time": "15:30 UHR",
"category": "Dinner",
"diet": "Vegetarisch",
@ -15,7 +15,8 @@
},
"hostMessage": [
"Ciao zusammen! Ich liebe die italienische Küche, nicht nur wegen des Essens, sondern wegen des Gefühls: Alle sitzen an einem langen Tisch, teilen sich grosse Platten und geniessen die Zeit.",
"Genau das möchte ich mit euch teilen. Ich bereite dafür eine klassische Tavolata vor, bei der verschiedene Gerichte in die Mitte des Tisches kommen und sich jeder bedient."
"Genau das möchte ich mit euch teilen. Ich bereite dafür eine klassische Tavolata vor, bei der verschiedene Gerichte in die Mitte des Tisches kommen und sich jeder bedient.",
"Wenn es das Wetter erlaubt sind wir draussen."
],
"menu": [
"Bruschetta-Variationen und Antipasti",
@ -45,10 +46,10 @@
"title": "Noche Peruana",
"location": "Chur",
"address": "Obere Gasse 41, 7000 Chur",
"date": "12. April 2026",
"date": "8. Mai 2026",
"time": "19:00 UHR",
"category": "Dinner",
"diet": "Omnivore",
"diet": "Fleisch, Fisch",
"spots": 4,
"host": {
"name": "Camila",
@ -87,7 +88,7 @@
"title": "Japanese Delight",
"location": "Zürich",
"address": "Limmatquai 92, 8001 Zürich",
"date": "02. MAI. 2026",
"date": "12. Mai 2026",
"time": "12:30 UHR",
"category": "Lunch",
"diet": "Fisch",
@ -121,5 +122,145 @@
"https://i.pinimg.com/1200x/b1/fb/3a/b1fb3a7809f4046843904ac8800daacc.jpg",
"https://i.pinimg.com/1200x/c6/93/42/c69342ec621333e853c35bda891d8bc6.jpg"
]
},
{
"id": 4,
"title": "Cucina Brasileira",
"location": "Basel",
"address": "Fredy Kübler Weg 5, 8134 Adliswil",
"date": "15. Mai. 2026",
"time": "19:00 UHR",
"category": "Dinner",
"diet": "Fleisch",
"spots": 8,
"host": {
"name": "Mia",
"initial": "M"
},
"hostMessage": [
"Ihr seit herzlich eingeladen zu meinem Brasilianischen Abendessen! Lasst euch überraschen."
],
"menu": [
"Feijoada Brasileira com Farofa",
"Arroz",
"Vinagrette",
"Salada de Couve",
"Salada de batata",
"Bolo de Mandioca"
],
"specifications": [],
"participants": [
"Carlos",
"Vivien",
"Estelle",
"Simona",
"Ysabelle"
],
"gallery": [
"https://i.pinimg.com/736x/62/39/4b/62394bb73b986dfb89f41e809e2c8dd4.jpg",
"https://i.pinimg.com/1200x/68/fe/bd/68febdd512a00f0a345e51ebed7ddd63.jpg",
"https://i.pinimg.com/1200x/0a/8d/67/0a8d674a7923c6e9bfe3665bc63522d0.jpg"
]
},
{
"id": 5,
"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,
"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",
"Vanessa",
"Christina",
"Julian"
],
"gallery": [
"https://i.pinimg.com/736x/7d/5c/29/7d5c29117ef6f974b1a6f77b22408ae7.jpg",
"https://i.pinimg.com/1200x/4e/4e/5d/4e4e5d57576d475316f25f84e5afb38f.jpg",
"https://i.pinimg.com/webp/1200x/d6/c2/4c/d6c24c1582d944229d271d8948b53dbb.webp",
"https://i.pinimg.com/webp/1200x/24/51/8e/24518e6e7bd9a68befcd9a98bba72a23.webp"
]
}, {
"id": 6,
"title": "Schwedentorte Schlemmern",
"location": "Zürich",
"address": "Münsterplatz 10, 8009 Zürich",
"date": "9. Mai 2026",
"time": "14:00 UHR",
"category": "Kaffee + Kuchen",
"diet": "Vegan",
"spots": 5,
"host": {
"name": "Annalea",
"initial": "A"
},
"hostMessage": [
"Hallo :) Ich suche Personen die Lust haben meine Vegane Schwedentorten Kreation zu probieren. Es ist eine Schwedentorte, die ich mit einer veganen Buttercreme und frischen Früchten zubereite. Es wird ein süsser Genuss, den ihr nicht verpassen solltet!"
],
"menu": [
"Schwedentorte",
"Diverse Teesorten"
],
"specifications": [],
"participants": [
"Annalea",
"Andi",
"Leah"
],
"gallery": [
"https://i.pinimg.com/736x/0e/44/78/0e4478e4e3389c77e3e859b2663e6d47.jpg"
]
}, {
"id": 7,
"title": "Mexican Fiesta",
"location": "Basel",
"address": "Münsterplatz 10, 4051 Basel",
"date": "29. Mai. 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"
]
}
]

View File

@ -19,7 +19,7 @@
</div>
</header>
<main class="main-content" style="padding: 40px 20px; max-width: 800px; margin: 0 auto;">
<main class="layout-wide">
<h1>Datenschutzerklärung</h1>
<h3>1. Verantwortliche Stelle</h3>

View File

@ -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,7 +32,7 @@
</header>
<main class="event-create-page">
<main class="event-create-page layout-narrow">
<section class="event-flow-header" aria-label="Event erstellen Aktionen">
</section>
@ -39,26 +42,26 @@
data-step="0"
aria-labelledby="intro-title"
>
<div class="step-layout step-layout--intro">
<div class="step-copy">
<p class="badge">Event erstellen</p>
<div class="step-layout hero startseite">
<div>
<p class="badge margin-bottom-40">Event erstellen</p>
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<p class="step-text">
<p class="step-text margin-bottom-40">
Erzähl uns von deiner Idee, vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
</p>
<button type="button" class="button-primary button--intro" data-start-flow>
<button type="button" class="button-primary" data-start-flow>
Los gehts!
</button>
</div>
<aside class="intro-card intro-card--image" 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"
/>
</aside>
</div>
</div>
</section>
@ -67,7 +70,7 @@
<div class="step-copy">
<p class="badge">Schritt 1</p>
<h2 id="step1-title">Was hast du vor?</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Erzähl uns, was für ein Event du planst. Ist es ein gemütlicher Brunch,
ein Dinner mit Wow-Effekt oder einfach ein entspanntes Mittagessen mit gutem Essen?
</p>
@ -75,33 +78,37 @@
<div class="step-fields">
<fieldset class="form-field">
<legend>Art des Essens / Eventtyp</legend>
<label>Art des Essens / Eventtyp</label>
<div class="option-grid option-grid--4">
<label class="option-card">
<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">
<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">
<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">
<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>
</fieldset>
<fieldset class="form-field">
<legend>Maximale Personenanzahl</legend>
<label>Maximale Personenanzahl</label>
<div class="counter" data-counter>
<button
@ -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"
@ -140,33 +150,37 @@
<div class="step-copy">
<p class="badge">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Mach uns neugierig. Was gibt es zu essen? Gibt es eine bestimmte Ernährungsform oder ein Motto? Je mehr du verrätst, desto besser können sich deine Gäste auf dein Event freuen.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Ernährungsform</legend>
<label>Ernährungsform</label>
<div class="option-grid option-grid--4">
<label class="option-card">
<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">
<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">
<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">
<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>
@ -185,36 +199,37 @@
<div class="step-copy">
<p class="badge">Schritt 3</p>
<h2 id="step3-title">Gibt es etwas zu beachten?</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Gibt es Allergien, Unverträglichkeiten oder andere Hinweise, die für dein Event wichtig sind? So wissen deine Gäste gleich, worauf sie sich einstellen können.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<legend>Allergene / Unverträglichkeiten</legend>
<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">
<label class="option-card option-card--checkbox">
<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>
<span>Glutenfrei</span>
</label>
<label class="option-card option-card--checkbox">
<label class="option-card option">
<input type="checkbox" name="allergies" value="laktosefrei" />
<span>laktosefrei</span>
<span>Laktosefrei</span>
</label>
<label class="option-card option-card--checkbox">
<label class="option-card option">
<input type="checkbox" name="allergies" value="ohne Nüsse" />
<span>ohne Nüsse</span>
<span>Ohne Nüsse</span>
</label>
</div>
</fieldset>
<div class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise (optional)</label>
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise</label>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
</div>
@ -226,7 +241,7 @@
<div class="step-copy">
<p class="badge">Schritt 4</p>
<h2 id="step4-title">Wann findet dein Event statt?</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Wähle Datum und Uhrzeit für dein Event. So können deine Gäste direkt einschätzen, ob der Termin für sie passt.
</p>
</div>
@ -252,7 +267,7 @@
<div class="step-copy">
<p class="badge">Schritt 5</p>
<h2 id="step5-title">Wo findet dein Event statt?</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Sag uns, wo dein Event stattfindet. Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p>
</div>
@ -276,7 +291,7 @@
<div class="step-copy">
<p class="badge">Schritt 6</p>
<h2 id="step6-title">Gib deinem Event den letzten Schliff.</h2>
<p class="step-text">
<p class="step-text margin-bottom-40">
Jetzt bekommt dein Event seinen Namen und die Atmosphäre, die Lust aufs Dabeisein macht.
Ein klarer Titel (z.B. "Italienische Tavolata") und ein guter Beschreibungstext (Ablauf etc.) machen den Unterschied.
</p>
@ -294,10 +309,14 @@
</div>
<div class="form-field">
<label>Fotos hinzufügen <span class="field-hint-inline">(optional)</span></label>
<div class="gallery-upload">
<label>Wie wird dein Event aussehen?</label>
<p class="field-hint">Optional füge Bilder zu deinem Event hinzu.</p>
<div class= "option-grid option-grid--4">
<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="option">Foto hinzufügen</span>
</button>
<input type="file" id="galleryFileInput" accept="image/*" multiple hidden />
</div>
</div>
@ -309,8 +328,8 @@
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 7</p>
<h2 id="step7-title">Dein Event auf einen Blick.</h2>
<p class="step-text">
<h2 id="step7-title">Dein Event auf einen Blick</h2>
<p class="step-text margin-bottom-40">
Schau dir alle Details nochmal in Ruhe an. Wenn alles passt,
kannst du dein Event jetzt veröffentlichen und Gäste einladen.
</p>
@ -372,6 +391,15 @@
<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>
@ -379,9 +407,6 @@
<div class="flow-footer" id="flowFooter" hidden>
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>
<div class="flow-actions">
<button type="button" id="backButton" class="button-secondary">
Zurück
@ -396,14 +421,17 @@
</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>
@ -414,20 +442,29 @@
aria-live="polite"
hidden
>
<div class="step-layout">
<div class="step-layout hero startseite">
<div class="step-copy">
<p class="badge">Event erstellt</p>
<h2 id="success-title">Dein Event ist ready.</h2>
<p class="step-text">
<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 margin-bottom-40">
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="hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable with friends.jpg"
alt="Gemeinsames Essen an einem gedeckten Tisch"
/>
</div>
</div>
</section>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invité | Event-Detail</title>
<title>Invité | Event-Detail</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
@ -28,7 +28,7 @@
</header>
<!-- Main content: detail page gets fully injected by JavaScript -->
<main class="container page-content-safe detail-page">
<main class="container layout-wide">
<!-- Render target: loading, error state or full detail layout -->
<div id="detail-view">
<p>Lädt Event-Details...</p>
@ -38,6 +38,39 @@
<!-- 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" aria-label="Popup schließen">&times;</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">
<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" aria-label="Popup schließen">&times;</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">
<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>

View File

@ -11,7 +11,6 @@
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
</head>
<body>
@ -28,17 +27,18 @@
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container page-content-safe">
<!-- Page headline with info button -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem;">
<h1 style="margin-bottom: 0;">Events</h1>
<button type="button" class="btn-info" id="info-button" aria-label="Information über kostenlose Events">?</button>
<main class="container layout-wide">
<!-- Page headline -->
<p class="badge margin-bottom-40">Event finden</p>
<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">
<p class="filter-label">Was darf es sein?</p>
<div class="filter-row">
<section class="filter-section margin-bottom-24">
<p class="filter-label">Art des Essens / Eventtyp</p>
<div class="filter-row margin-bottom-24">
<!-- Primary category filter buttons -->
<div class="category-group">
<button class="category-item active" type="button" data-cat="ALLE">Alle</button>
@ -50,43 +50,45 @@
<!-- Secondary filters populated/handled by JavaScript -->
<div class="meta-filter-group" aria-label="Weitere Filter">
<label class="meta-filter" for="location-filter">
<div class="meta-filter" for="location-filter">
<span>Ort</span>
<select id="location-filter">
<option value="ALLE_ORTE">Alle Orte</option>
</select>
</label>
</div>
<label class="meta-filter" for="date-filter">
<div class="meta-filter" for="date-filter">
<span>Datum</span>
<div class="date-input-wrapper">
<input id="date-filter" type="date">
</div>
</label>
</div>
</div>
</div>
<div class="filter-row">
<!-- Diet filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-diet="Fleisch">Fleisch</button>
<button class="category-item" type="button" data-diet="Fisch">Fisch</button>
<button class="category-item" type="button" data-diet="Vegetarisch">Vegetarisch</button>
<button class="category-item" type="button" data-diet="Vegan">Vegan</button>
</div>
<details class="filter-box">
<summary>Nach Ernährungform filtern</summary>
<div class="filter-row margin-bottom-16">
<div class="category-group">
<button class="category-item" type="button" data-diet="Fleisch">Fleisch</button>
<button class="category-item" type="button" data-diet="Fisch">Fisch</button>
<button class="category-item" type="button" data-diet="Vegetarisch">Vegetarisch</button>
<button class="category-item" type="button" data-diet="Vegan">Vegan</button>
<button class="category-item filter-delete" type="button" id="clear-all-filters">Alle Filter löschen</button>
</div>
</div>
</details>
<div class="filter-row">
<!-- Allergen filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-allergie="glutenfrei">glutenfrei</button>
<button class="category-item" type="button" data-allergie="laktosefrei">laktosefrei</button>
<button class="category-item" type="button" data-allergie="ohne Nüsse">ohne Nüsse</button>
</div>
<details class="filter-box margin-bottom-24">
<summary>Nach Allergenen filtern</summary>
<div class="filter-row margin-bottom-16">
<div class="category-group">
<button class="category-item" type="button" data-allergie="Glutenfrei">Glutenfrei</button>
<button class="category-item" type="button" data-allergie="Laktosefrei">Laktosefrei</button>
<button class="category-item" type="button" data-allergie="Ohne Nüsse">Ohne Nüsse</button>
</div>
</div>
</section>
</details>
<!-- Render target: event cards or empty state -->
<section id="event-grid" class="event-list"></section>
@ -108,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" aria-label="Popup schließen">&times;</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">
<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" aria-label="Popup schließen">&times;</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">
<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>

View File

@ -19,7 +19,7 @@
</div>
</header>
<main class="main-content" style="padding: 40px 20px; max-width: 800px; margin: 0 auto;">
<main class="layout-wide">
<h1>Impressum</h1>
<h3>Angaben gemäss § 5 TMG</h3>

View File

@ -29,19 +29,19 @@
</div>
</header>
<main class="main-content">
<main class="container layout-wide">
<!-- Hero: uses .hero, .btn, .image-card, and .hero-image for a polished first impression -->
<section class="hero">
<div class="hero__left">
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1>
<p>Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p>
<p class="margin-bottom-40">Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p>
<a class="button-primary" href="signup.html">Registrieren</a>
</div>
<div class="hero__right">
<div class="image-card">
<img class="hero-image" src="assets/index_ingredients.jpg" alt="Startpage Ingredients" />
<img class="hero-image" src="assets/index_round table friends.jpeg" alt="Round table friends" />
</div>
</div>
</section>
@ -82,8 +82,8 @@
<!-- 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>
<p>#gemeinsam_invité auf Instagram</p>
</div><h2>Einblick in Cooking-Erlebnisse</h2>
<p class="margin-bottom-16">#gemeinsam_invité auf Instagram</p>
</div>
<div class="gallery__carousel">
<button type="button" class="gallery__arrow gallery__arrow--prev" aria-label="Vorheriges Bild">
@ -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_Sharing food table.jpg" alt="Sharing food table">
<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_Pasta and many forks.jpg" alt="Pasta and many forks">
<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>
@ -136,7 +221,6 @@
<div class="gallery_dots" role="tablist" aria-label="Seite auswählen"></div>
</section>
</main>
<!-- Lightbox: Bildansicht vergrössert -->
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
@ -151,7 +235,6 @@
<!-- FAQ Section: Akkordion mit häufig gestellten Fragen -->
<section class="faq-section">
<div class="container">
<h2>Häufig gestellte Fragen</h2>
<div class="faq-accordion">
<div class="faq-item">
@ -160,10 +243,20 @@
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p><strong>Schritt 1: Kostenloses Konto erstellen</strong><br>Gehe auf Invité, klicke auf "Jetzt beitreten" und fülle das Anmeldeformular aus. Du benötigst nur deine E-Mail und ein Passwort.</p>
<p><strong>Schritt 2: Dein Profil ausfüllen</strong><br>Lade ein Profilfoto hoch, schreib ein bisschen über dich und gib deine Allergien/Ernährungspräferenzen an. Das hilft anderen, dich besser kennenzulernen.</p>
<p><strong>Schritt 3: Erkunde Events</strong><br>Browsing durch unsere Events, filtere nach Diät oder Allergie-Einstellungen, und melde dich zu den Events an, die dich interessieren!</p>
<p><strong>Schritt 4: Erstelle dein eigenes Event</strong><br>Du kannst auch selbst ein Kochevent hosten! Klick auf "Event erstellen", beschreib dein Menü, und lade Gäste ein.</p>
<ol class="faq-list">
<li>
<strong>Kostenloses Konto erstellen</strong><br>
Gehe auf Invité, klicke auf «Jetzt beitreten» und fülle das Anmeldeformular aus. Du benötigst nur deine E-Mail und ein Passwort.
</li>
<li>
<strong>Finde passende Events</strong><br>
Erkunde unsere Events, filtere nach Diät- oder Allergie-Einstellungen und melde dich zu den Events an, die dich interessieren!
</li>
<li>
<strong>Erstelle dein eigenes Event</strong><br>
Du kannst auch selbst ein Kochevent hosten! Klick auf «Event erstellen», beschreib dein Menü und lade Gäste ein.
</li>
</ol>
</div>
</div>
@ -216,9 +309,9 @@
<p>Ja, dein Profil hilft anderen, dich besser kennenzulernen. Wir ermutigen zu Offenheit und gegenseitigem Vertrauen. Allerdings bleibt es deine Entscheidung, wem du deine Adresse mitteilst die erfolgt nur 12 Stunden vor dem Event.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="footer">
<div class="footer-left">

View File

@ -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();

View File

@ -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 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');
@ -264,68 +292,17 @@
if (event) {
renderDetailPage(event);
} else {
detailContainer.innerHTML = "<h1>Event wurde nicht gefunden.</h1><a href='event_overview.html'>Zurück zur Übersicht</a>";
detailcontainer.innerHTML = "<h1>Event wurde nicht gefunden.</h1><a href='event_overview.html'>Zurück zur Übersicht</a>";
}
} catch (error) {
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,269 +310,269 @@
? 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'
: isRegistered
? ' button-primary-abmelden '
: isRegistrationClosed
? ' button-primary-abmelden '
: ' 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 = `
<button type="button" class="btn-back-to-overview" data-navigate-back>Zurück</button>
<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-buttons">
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze frei`}
</span>
<div class="detail-action-btn-wrap">
<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>
</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">&times;</button>
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
</figure>
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>
${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">
<div class="detail-action-row" style="margin-left:auto; display:flex; gap:12px; align-items:center;">
${isFull ? `
<div class="detail-dereg-column">
<button class="button-plaetze event-spots-full" type="button" disabled>Ausgebucht</button>
<small class="detail-dereg-hint detail-dereg-hint--placeholder">&nbsp;</small>
</div>
` : ''}
${(!isFull || isRegistered) ? `
<div class="detail-dereg-column">
<button class="${actionButtonVariantClass.trim()}" 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>
` : ''}
</div>
</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">&times;</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.
// ---------------------------------------------------------
const backButton = detailContainer.querySelector('[data-navigate-back]');
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]');
// 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);
}, 4000);
}
renderDetailPage(event);
};
const closeUnregister = () => modal.classList.remove('show');
document.getElementById('unregister-modal-close')?.addEventListener('click', closeUnregister);
document.getElementById('unregister-modal-cancel')?.addEventListener('click', closeUnregister);
modal.addEventListener('click', e => { if (e.target === modal) closeUnregister(); });
// 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'), 4000);
}
renderDetailPage(event);
};
const closeRegister = () => modal.classList.remove('show');
document.getElementById('register-modal-close')?.addEventListener('click', closeRegister);
document.getElementById('register-modal-cancel')?.addEventListener('click', closeRegister);
modal.addEventListener('click', e => { if (e.target === modal) closeRegister(); });
}
nextRegistrationMap[currentUser.email] = Array.from(registrationSet);
setRegistrationMap(nextRegistrationMap);
// Re-Render aktualisiert Buttonzustand und CTA ohne Seitenreload.
renderDetailPage(event);
});
}
// "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]');
// "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]');
if (showAllBtn && avatarRow && fullList) {
showAllBtn.addEventListener('click', () => {
@ -606,32 +583,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')) {
@ -639,22 +608,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();
});
}
}
});
});

View File

@ -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) {
@ -161,6 +167,7 @@
dateFilter.value = savedDate;
}
updateDietAvailability();
applyFilters();
} catch (error) {
console.error('Fehler:', error);
@ -280,28 +287,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 +398,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 +475,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 +491,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,8 +536,8 @@
</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>`}
${actionMarkup}
${sideInfoMarkup}
</div>
`;
@ -530,29 +571,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);
@ -565,6 +632,59 @@
});
}
// Verhindert widersprüchliche Ernährungsformen:
// Fleisch/Fisch schliessen Vegetarisch/Vegan aus.
// Vegan schliesst alles andere (Fleisch, Fisch, Vegetarisch) aus.
// Vegetarisch schliesst Fleisch/Fisch aus, aber nicht Vegan.
function updateDietAvailability() {
const dietButtons = Array.from(filterButtons).filter(btn => btn.getAttribute('data-diet') !== null);
const meatFishButtons = dietButtons.filter(btn => ['Fleisch', 'Fisch'].includes(btn.getAttribute('data-diet')));
const plantButtons = dietButtons.filter(btn => ['Vegetarisch', 'Vegan'].includes(btn.getAttribute('data-diet')));
const vegetarischBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegetarisch');
const veganBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegan');
const hasVegan = activeDiets.has('Vegan');
const hasVegetarisch = activeDiets.has('Vegetarisch');
const hasMeatOrFish = meatFishButtons.some(btn => activeDiets.has(btn.getAttribute('data-diet')));
// If Vegan is selected, disable everything else
if (hasVegan) {
meatFishButtons.forEach(btn => {
btn.classList.add('disabled');
});
if (vegetarischBtn) {
vegetarischBtn.classList.add('disabled');
}
}
// If Vegetarisch is selected, disable only Fleisch/Fisch
else if (hasVegetarisch) {
meatFishButtons.forEach(btn => {
btn.classList.add('disabled');
});
if (veganBtn) {
veganBtn.classList.remove('disabled');
}
}
// If Fleisch/Fisch is selected, disable both Vegetarisch and Vegan
else if (hasMeatOrFish) {
if (vegetarischBtn) {
vegetarischBtn.classList.add('disabled');
}
if (veganBtn) {
veganBtn.classList.add('disabled');
}
}
// No conflicts, enable everything
else {
plantButtons.forEach(btn => {
btn.classList.remove('disabled');
});
meatFishButtons.forEach(btn => {
btn.classList.remove('disabled');
});
}
}
// Category filter interactions: mutually exclusive (radio button behavior).
filterButtons.forEach(button => {
button.addEventListener('click', () => {
@ -576,12 +696,34 @@
// Category filter: exclusive selection
activeCategory = categoryValue;
} else if (dietValue !== null) {
// Diet filter: toggle selection
if (activeDiets.has(dietValue)) {
activeDiets.delete(dietValue);
// Diet filter: toggle selection with conflict handling
const isCurrentlySelected = activeDiets.has(dietValue);
if (!isCurrentlySelected) {
// Adding a diet - handle conflicts
if (dietValue === 'Vegetarisch') {
// Vegetarisch removes Fleisch/Fisch but not Vegan
activeDiets.delete('Fleisch');
activeDiets.delete('Fisch');
activeDiets.add('Vegetarisch');
} else if (dietValue === 'Vegan') {
// Vegan removes all other options
activeDiets.delete('Vegetarisch');
activeDiets.delete('Fleisch');
activeDiets.delete('Fisch');
activeDiets.add('Vegan');
} else if (dietValue === 'Fleisch' || dietValue === 'Fisch') {
// Fleisch/Fisch remove Vegetarisch/Vegan
activeDiets.delete('Vegetarisch');
activeDiets.delete('Vegan');
activeDiets.add(dietValue);
}
} else {
activeDiets.add(dietValue);
// Removing a diet
activeDiets.delete(dietValue);
}
updateDietAvailability();
} else if (allergieValue !== null) {
// Allergie filter: toggle selection
if (activeAllergies.has(allergieValue)) {
@ -613,10 +755,30 @@
}
}
// Clear all filters button
const clearAllFiltersBtn = document.getElementById('clear-all-filters');
if (clearAllFiltersBtn) {
clearAllFiltersBtn.addEventListener('click', () => {
activeCategory = 'ALLE';
activeDiets.clear();
activeAllergies.clear();
if (locationFilter) {
locationFilter.value = 'ALLE_ORTE';
}
if (dateFilter) {
dateFilter.value = '';
}
updateDietAvailability();
applyFilters();
});
}
// Info button modal behavior
const infoButton = document.getElementById('info-button');
const infoModal = document.getElementById('info-modal');
const modalClose = infoModal?.querySelector('.modal-close');
const modalClose = infoModal?.querySelector('modal-close');
if (infoButton && infoModal) {
infoButton.addEventListener('click', () => {
@ -640,13 +802,29 @@
// 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');
}
}
// Kick off initial load/render cycle.
fetchEvents();
});
// Modal closing helper functions
function closeRegisterModal() {
const modal = document.getElementById('register-confirm-modal');
if (modal) {
modal.classList.remove('show');
}
}
function closeUnregisterModal() {
const modal = document.getElementById('unregister-confirm-modal');
if (modal) {
modal.classList.remove('show');
}
}

View File

@ -7,7 +7,7 @@
const carouselTrack = document.querySelector('.gallery__track');
const prevArrow = document.querySelector('.gallery__arrow--prev');
const nextArrow = document.querySelector('.gallery__arrow--next');
const dotsContainer = document.querySelector('.gallery_dots');
const dotscontainer = document.querySelector('.gallery_dots');
// Nur ausführen, wenn die Galerie auf der Seite vorhanden ist.
if (carouselTrack) {
@ -22,8 +22,8 @@ if (carouselTrack) {
var dots = [];
function buildDots() {
if (!dotsContainer) return;
dotsContainer.innerHTML = '';
if (!dotscontainer) return;
dotscontainer.innerHTML = '';
dots = [];
for (var i = 0; i < pageCount; i++) {
var dot = document.createElement('button');
@ -36,7 +36,7 @@ if (carouselTrack) {
dot.addEventListener('click', function() {
goToPage(parseInt(this.dataset.page));
});
dotsContainer.appendChild(dot);
dotscontainer.appendChild(dot);
dots.push(dot);
}
}

View File

@ -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);

View File

@ -14,7 +14,9 @@
const profileTabPanels = Array.from(document.querySelectorAll('[data-profile-panel]'));
const myEventsCount = document.getElementById('my-events-count');
const myEventsBtnCount = document.getElementById('btn-my-events-count');
const myRegistrationsCount = document.getElementById('my-registrations-count');
const myRegistrationsBtnCount = document.getElementById('btn-my-registrations-count');
const myEventsList = document.getElementById('my-events-list');
const myRegistrationsList = document.getElementById('my-registrations-list');
@ -211,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.
@ -220,32 +227,37 @@
return;
}
const unregisterButton = target.closest('[data-unregister-id]');
const unregisterButton = target.closest('[data-unregister-id]');
if (unregisterButton) {
if (!currentUser?.email) {
return;
}
if (!currentUser?.email) return;
const eventId = Number(unregisterButton.getAttribute('data-unregister-id'));
if (!Number.isFinite(eventId)) {
return;
}
if (!Number.isFinite(eventId)) return;
unregisterFromEvent(eventId, currentUser.email);
const modal = document.getElementById('unregister-confirm-modal');
if (modal) modal.classList.add('show');
// Snackbar: Feedback bei erfolgreicher 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);
}
document.getElementById('confirm-unregister-btn').onclick = () => {
modal.classList.remove('show');
unregisterFromEvent(eventId, currentUser.email);
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);
}
};
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'); });
return;
}
if (target.closest('a, button')) {
return;
}
@ -279,11 +291,20 @@
modal.classList.remove('show');
};
document.getElementById('confirmCancelEventBtn').addEventListener('click', function() {
document.getElementById('confirmCancelEventBtn').addEventListener('click', function() {
if (pendingCancelEventId !== null && currentUser?.email) {
cancelHostedEvent(pendingCancelEventId, currentUser.email);
}
closeCancelEventModal();
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Dein Event wurde erfolgreich abgesagt.';
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
setTimeout(() => {
snackbar.classList.remove('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
}, 3000);
}
});
// Schliesst das Modal bei Klick ausserhalb des Inhalts.
@ -320,10 +341,10 @@
renderMyEvents(allEvents, currentUser);
renderMyRegistrations(allEvents, currentUser);
profileFeedback.textContent = 'Event wurde abgesagt und aus deinem Hosting entfernt.';
}
// Entfernt eine Event-ID aus der Benutzerliste und aktualisiert die UI sofort.
function unregisterFromEvent(eventId, userEmail) {
function unregisterFromEvent(eventId, userEmail) {
const registrationMap = getRegistrationMap();
const currentIds = Array.isArray(registrationMap[userEmail]) ? registrationMap[userEmail] : [];
const nextIds = currentIds
@ -334,12 +355,10 @@
setRegistrationMap(registrationMap);
renderMyRegistrations(allEvents, currentUser);
profileFeedback.textContent = 'Du wurdest von dem Event abgemeldet.';
}
// Validiert Profildaten konsistent und liefert true/false zur Submit-Steuerung.
function validateProfileForm() {
let isValid = true;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@ -450,51 +469,138 @@
});
}
// 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 gehostete Events inkl. Zähler.
// Rendert angemeldete Events inkl. Zähler.
function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user);
myEventsCount.textContent = String(hostedEvents.length);
renderEventCards(myEventsList, hostedEvents, {
title: 'Noch kein eigenes Event',
text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.',
buttonLabel: 'Event erstellen',
href: 'event_create.html'
}, 'hosting');
const hostedEvents = getMyHostedEvents(events, user);
const count = hostedEvents.length;
myEventsCount.textContent = String(count);
if (myEventsBtnCount) myEventsBtnCount.textContent = String(count);
renderEventCards(myEventsList, hostedEvents, {
title: 'Noch kein eigenes Event',
text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.',
buttonLabel: 'Event erstellen',
href: 'event_create.html'
}, 'hosting');
}
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);
myRegistrationsCount.textContent = String(registeredEvents.length);
renderEventCards(myRegistrationsList, registeredEvents, {
title: 'Noch keine Anmeldungen',
text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.',
buttonLabel: 'Events entdecken',
href: 'event_overview.html'
}, 'registrations');
const registeredEvents = getMyRegisteredEvents(events, user);
const count = registeredEvents.length;
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', 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) {
const emptyElement = document.createElement('div');
emptyElement.className = 'profile-empty-state';
emptyElement.className = 'empty-state';
emptyElement.innerHTML = `
<p class="profile-empty-kicker">Keine Treffer</p>
<div class="empty-state-kicker">Keine Treffer</div>
<h3>${emptyStateConfig.title}</h3>
<p>${emptyStateConfig.text}</p>
<a class="button-primary" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
<a class="empty-state-link button-primary" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
`;
container.appendChild(emptyElement);
return;
@ -504,26 +610,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>
@ -538,17 +699,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.
@ -569,28 +737,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) {

View File

@ -6,11 +6,13 @@
document.addEventListener('DOMContentLoaded', () => {
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const navContainers = document.querySelectorAll('.nav-tab-links');
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();
// Beendet früh, falls auf einer Seite keine Hauptnavigation vorhanden ist.
if (!navContainers.length) {
if (!navcontainers.length) {
return;
}
@ -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();
});

View File

@ -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);

View File

@ -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,37 +27,35 @@
</header>
<!-- Main Content -->
<div class="main-content">
<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 main-content -->
</div> <!-- Schliesst container -->
<div class="snackbar" id="snackbar">Willkommen zurück! Du wirst weitergeleitet...</div>
<script src="js/login.js"></script>
<div class="footer">
@ -76,4 +75,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@ -29,10 +29,10 @@
</div>
</header>
<main class="container profile-page">
<main class="layout-wide">
<section class="profile-hero" aria-label="Profilübersicht">
<div>
<p class="badge">Mein Bereich</p>
<p class="badge margin-bottom-40">Mein Bereich</p>
<h1 id="headline">Mein Profil</h1>
<p id="profile-subline" class="profile-subline">Hier findest du deine Events, deine Anmeldungen und kannst deine Profildaten verwalten.</p>
</div>
@ -49,8 +49,13 @@
<section id="logged-in-content" class="profile-grid">
<nav class="category-items" aria-label="Profilbereiche">
<button type="button" class="category-item is-active category-item-profile" data-category-item="hosting">Meine Events</button>
<button type="button" class="category-item category-item-profile" data-category-item="teilnehmen">Meine Anmeldungen</button>
<button type="button" class="category-item is-active category-item-profile" data-category-item="hosting">
Meine Events <span class="btn-count" id="btn-my-events-count">0</span>
</button>
<button type="button"
class="category-item category-item-profile" data-category-item="teilnehmen">
Meine Anmeldungen <span class="btn-count" id="btn-my-registrations-count">0</span>
</button>
<button type="button" class="category-item category-item-profile" data-category-item="einstellungen">Profil-Einstellungen</button>
</nav>
@ -65,6 +70,14 @@
<div class="panel-head">
<span id="my-registrations-count" class="panel-count">0</span>
</div>
<p class="info-abmeldung">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
@ -72,27 +85,27 @@
<h2 class="panel-title">Profil verwalten</h2>
<form id="profile-form" novalidate>
<div class="form-grid">
<div class="form-group">
<label for="vorname">Vorname</label>
<div class="margin-bottom-16">
<label class="label-input-field" for="vorname">Vorname</label>
<input type="text" id="vorname" name="vorname" required>
<p class="input-error" id="vorname-error">Bitte gib deinen Vornamen ein.</p>
</div>
<div class="form-group">
<label for="nachname">Nachname</label>
<div class="margin-bottom-16">
<label class="label-input-field" for="nachname">Nachname</label>
<input type="text" id="nachname" name="nachname" required>
<p class="input-error" id="nachname-error">Bitte gib deinen Nachnamen ein.</p>
</div>
</div>
<div class="form-group">
<label for="email">E-Mail</label>
<div class="margin-bottom-16">
<label class="label-input-field" for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
<p class="input-error" id="email-error">Bitte gib eine gültige E-Mail-Adresse ein.</p>
</div>
<div class="form-group">
<label for="passwort">Passwort</label>
<div class="margin-bottom-40">
<label class="label-input-field" for="passwort">Passwort</label>
<input type="password" id="passwort" name="passwort" minlength="6" placeholder="Mindestens 6 Zeichen">
<p class="input-hint">Nur ausfüllen, wenn du dein Passwort ändern möchtest.</p>
<p class="input-error" id="passwort-error">Das Passwort muss mindestens 6 Zeichen lang sein.</p>
@ -109,11 +122,11 @@
<div id="logoutModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<button class="close-btn" onclick="closeLogoutModal()">&times;</button>
<h2>Abmelden?</h2>
</div>
<p class="modal-body">
Bist du sicher, dass du dich abmelden möchtest?
<button type="button" class="modal-close" aria-label="Popup schließen">&times;</button>
</p>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeLogoutModal()">Abbrechen</button>
@ -122,6 +135,21 @@
</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">&times;</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 Abmeldung von Events -->
<div class="snackbar" id="snackbar"></div>
@ -129,11 +157,11 @@
<div id="cancelEventModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<button class="close-btn" onclick="closeCancelEventModal()">&times;</button>
<h2>Event absagen?</h2>
</div>
<p class="modal-body">
Bist du sicher, dass du dieses Event absagen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
<button type="button" class="modal-close" aria-label="Popup schließen">&times;</button>
</p>
<div class="modal-footer">
<button class="button-secondary" type="button" onclick="closeCancelEventModal()">Abbrechen</button>

View File

@ -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="main-content">
<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="form-group 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="form-group 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="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>
<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="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,15 +68,22 @@
</div>
</form>
</div>
</div>
<div class="image-section">
<img src="assets/index_cooking.jpg" alt="Social Cooking">
</div>
</div>
</div> <!-- Schliesst main-content -->
</div> <!-- Schliesst container -->
<!-- Welcome Modal -->
<div id="welcomeModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<button class="close-btn" onclick="closeWelcomeModal()">&times;</button>
<h2>Konto erfolgreich erstellt!</h2>
<button class="close-btn" onclick="closeWelcomeModal()">&times;</button>
</div>
<div class="modal-body">
Willkommen bei Invité! Dein Account wurde erfolgreich erstellt. Entdecke jetzt die neuesten Events in deiner Nähe.
@ -108,4 +111,4 @@
</div>
</div>
</body>
</html>
</html>