Compare commits

...

67 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
viiivo
e3ac1a11f0 Update event dates and participant handling; adjust visibility window to 24 hours 2026-04-21 19:51:13 +02:00
viiivo
fa8a7f1fc2 Add FAQ section, back button, and info modal to event pages; update styles and scripts 2026-04-21 19:39:15 +02:00
24dc61a887 Icon Event-Beschrieb hinzugefügt 2026-04-21 15:04:39 +02:00
e17a41509c Icon Gast 2026-04-21 14:13:01 +02:00
add5c3eb05 Anpassungen Startseite, Footer, diversers Styling auf allen Seiten 2026-04-21 13:48:48 +02:00
540754d7d0 Logout-Button in Navigation 2026-04-20 22:18:14 +02:00
28e1647749 Anpassungen Details 2026-04-18 00:09:58 +02:00
536df27e57 Anpassungen Login Signup 2026-04-17 23:55:12 +02:00
01e30bfb64 Anpassung Event Detail II 2026-04-17 23:54:19 +02:00
db939c6660 Anpassungen Event Detail 2026-04-17 23:50:58 +02:00
433743069c Anpassungen Event Uebersicht II 2026-04-17 16:09:35 +02:00
23f3e85543 Anpassungen Event Übersicht 2026-04-17 15:15:50 +02:00
2015454f6c Anpassung Progress Bar Event erstellen 2026-04-17 09:57:41 +02:00
9a2c66e026 Anpassung primary-button, badge und Event erstellen 2026-04-15 10:35:35 +02:00
viiivo
9d763b3680 Funktioniert doch nicht also gelöscht 2026-04-12 17:22:54 +02:00
1c6dcea906 Netlify Deploy Code
Some checks failed
Deploy to Netlify / deploy (push) Has been cancelled
Hosting auf Netlify für einfachere User Test
2026-04-12 17:19:52 +02:00
Estelle Köhler
027f722d69 feat: Abmeldefrist-Hinweis auf Event-Detailseite, .hidden fix 2026-04-12 17:16:34 +02:00
Estelle Köhler
e91cd8572e fix: proper umlauts across all files, simplify gallery upload, fix Menu spelling, show local events in overview 2026-04-12 15:37:31 +02:00
Estelle Köhler
e805abbf12 feat: lightbox gallery, event create UX improvements, cancel modal, footer consistency, Swiss German fixes 2026-04-12 15:18:40 +02:00
Estelle Köhler
4b54c48311 Add snackbar feedback, logout modal, profile pill nav, calendar styling, fix image paths and signup flow 2026-04-12 14:19:09 +02:00
Estelle Köhler
221aa90649 Refactor hero section, update how-it-works cards, add Instagram invite + footer to all pages 2026-04-12 13:43:36 +02:00
Estelle Köhler
5ee62ff604 Resolve merge conflicts and fix styling issues
- Resolve all merge conflicts from main branch pull
- Fix CSS load order: global stylesheet before page-specific on all pages
- Add navigation.js to impressum.html and datenschutz.html
- Unify nav classes across all pages (nav-tab-links + button-small)
- Add footer styles to global stylesheet
- Remove duplicate landingpage.css (redundant with index.css)
- Fix duplicate head/body tags in event_detail.html
- Merge login.js: keep user auth logic + snackbar support
- Merge index-carousel.js: keep dots + responsive resize handling
2026-04-12 10:49:17 +02:00
Estelle Köhler
642f0d4b80 fix: correct stylesheet references, shorten login snackbar, add datenschutz page 2026-04-03 17:25:34 +02:00
Estelle Köhler
6d3f699ea4 Clean up CSS to use global stylesheet, fix carousel, add impressum page and footer to all pages, update login styles 2026-04-03 16:54:16 +02:00
Estelle Köhler
a456caa24a Rename landingpage.css to index.css and rename asset files 2026-04-03 14:26:04 +02:00
58 changed files with 5263 additions and 2561 deletions

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"chat.tools.terminal.autoApprove": {
"git remote": true,
"git push": true,
"ssh": true,
"git add": true
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

BIN
assets/Icon_instagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

12
assets/icon_calendar.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M9,1v2h6V1h2v2h4c.55,0,1,.45,1,1v16c0,.55-.45,1-1,1H3c-.55,0-1-.45-1-1V4c0-.55.45-1,1-1h4V1h2ZM20,11H4v8h16v-8ZM7,5h-3v4h16v-4h-3v2h-2v-2h-6v2h-2v-2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

12
assets/icon_gast.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 36 36">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M6,33c0-6.63,5.37-12,12-12s12,5.37,12,12h-3c0-4.97-4.03-9-9-9s-9,4.03-9,9h-3ZM18,19.5c-4.97,0-9-4.03-9-9S13.03,1.5,18,1.5s9,4.03,9,9-4.03,9-9,9ZM18,16.5c3.32,0,6-2.68,6-6s-2.68-6-6-6-6,2.68-6,6,2.68,6,6,6Z"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

12
assets/icon_location.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M12,20.9l4.95-4.95c2.73-2.73,2.73-7.17,0-9.9-2.73-2.73-7.17-2.73-9.9,0-2.73,2.73-2.73,7.17,0,9.9l4.95,4.95ZM12,23.73l-6.36-6.36c-3.51-3.51-3.51-9.21,0-12.73,3.51-3.51,9.21-3.51,12.73,0,3.51,3.51,3.51,9.21,0,12.73l-6.36,6.36ZM12,13c1.1,0,2-.9,2-2s-.9-2-2-2-2,.9-2,2,.9,2,2,2ZM12,15c-2.21,0-4-1.79-4-4s1.79-4,4-4,4,1.79,4,4-1.79,4-4,4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 282 KiB

View File

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 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.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,6 +0,0 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 23 27.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path d="M22 11.5C22 19.6667 11.5 26.6667 11.5 26.6667C11.5 26.6667 1 19.6667 1 11.5C1 8.71523 2.10625 6.04451 4.07538 4.07538C6.04451 2.10625 8.71523 1 11.5 1C14.2848 1 16.9555 2.10625 18.9246 4.07538C20.8938 6.04451 22 8.71523 22 11.5Z" stroke="var(--stroke-0, #D44B24)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" stroke="var(--stroke-0, #D44B24)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 762 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -6,25 +6,6 @@
--control-min-height: 3rem;
--input-min-height: 3.5rem;
--card-min-height: 6rem;
--color-bg: var(--butter);
--color-surface: var(--white);
--color-surface-soft: var(--butter-light);
--color-text: var(--black);
--color-text-secondary: rgba(34, 33, 26, 0.8);
--color-muted: rgba(34, 33, 26, 0.68);
--color-border: rgba(102, 52, 13, 0.16);
--color-border-strong: var(--brown);
--color-divider: rgba(102, 52, 13, 0.14);
--color-primary: var(--olive);
--color-primary-hover: var(--olive-dark);
--color-progress-bg: rgba(212, 75, 36, 0.18);
--color-focus: var(--blue);
--color-error: var(--error);
--shadow-soft: 0 12px 30px rgba(102, 52, 13, 0.1);
--input-border-soft: rgba(102, 52, 13, 0.2);
--input-border-focus: rgba(107, 107, 5, 0.45);
--input-shadow-focus: 0 0 0 4px rgba(107, 107, 5, 0.12);
}
*,
@ -33,87 +14,9 @@
box-sizing: border-box;
}
html {
font-size: 100%;
}
body {
margin: 0;
font-family: var(--font-main);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.5;
}
a,
button,
input,
textarea {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
.site-header {
background: var(--color-bg);
border-top: 2px solid #232323;
border-bottom: 1px solid var(--color-border);
}
.site-nav {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
min-height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.site-logo {
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.02em;
}
.site-nav-links {
display: flex;
align-items: center;
gap: var(--space-5);
margin: 0;
padding: 0;
list-style: none;
}
.site-nav-links a {
font-weight: 500;
}
.site-nav-links li:last-child a {
width: 2.25rem;
height: 2.25rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #231f20;
color: #ffffff;
font-weight: 700;
}
.event-create-page {
width: min(100% - 2rem, var(--max-width));
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 {
@ -124,11 +27,45 @@ a {
.step {
display: none;
padding: var(--space-4) 0 var(--space-7);
}
/*
.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 {
@ -136,17 +73,20 @@ a {
}
.step-layout {
width: min(100%, var(--content-width));
margin: 0 auto;
gap: 80px;
}
.startseite {
display: grid;
gap: var(--space-6);
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,
@ -154,15 +94,15 @@ a {
.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,
@ -170,73 +110,39 @@ fieldset {
margin: 0;
padding: 0;
border: 0;
gap: var(--space-3);
}
.step-kicker {
margin: 0;
font-weight: 700;
color: var(--color-muted);
letter-spacing: 0.02em;
}
h1,
h2 {
margin: 0;
font-family: "Bagel Fat One", cursive;
font-size: clamp(2rem, 4vw, 4rem);
line-height: 1.03;
letter-spacing: -0.03em;
}
.step-text {
margin: 0;
max-width: 42rem;
color: var(--color-text-secondary);
font-size: clamp(1rem, 1.4vw, 1.2rem);
/* definiert Breite des Beschriebtexts der einzelnen Schritte*/
max-width: 100%;
}
.intro-card,
.review-card {
border: 1px solid var(--color-border);
background: var(--color-surface);
box-shadow: var(--shadow-soft);
box-shadow: var(--shadow-interaction);
}
.intro-card {
max-width: 24rem;
padding: var(--space-6);
padding: var(--space-40);
border-radius: 1.75rem;
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
}
.intro-card--image {
width: 100%;
padding: 0;
border: 0;
overflow: hidden;
background: transparent;
box-shadow: none;
}
.intro-image {
width: 100%;
aspect-ratio: 16 / 10;
height: 100%;
display: block;
object-fit: cover;
border-radius: 1.875rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
label,
legend {
font-weight: 700;
border-radius: var(--radius-lg);
}
.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"],
@ -244,15 +150,27 @@ input[type="date"],
input[type="time"],
input[type="number"],
textarea {
width: 100%;
min-height: var(--input-min-height);
padding: 1rem 1.1rem;
border: 1px solid var(--input-border-soft);
border-radius: 1.125rem;
font-family: var(--font-main);
font-weight: 400;
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);
color: var(--color-text);
box-shadow: 0 1px 2px rgba(102, 52, 13, 0.04);
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
transition: border-color 0.2s ease;
}
/* Blendet die Standard-Buttons für number inputs aus */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
textarea {
@ -265,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,
@ -273,47 +191,78 @@ input[type="date"]:focus,
input[type="time"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: var(--input-border-focus);
box-shadow: var(--input-shadow-focus);
background: var(--butter-light);
outline: none;
border: 2px solid var(--olive);
}
.field-invalid {
border-color: var(--tomato) !important;
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
border-color: var(--error) !important;
box-shadow: var(--shadow-error);
}
.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 {
position: relative;
display: grid;
gap: 0.15rem;
min-height: var(--card-min-height);
padding: 1rem 1rem 1rem 1.05rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
transition: box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
.option-card small {
color: var(--color-muted);
.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);
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
box-shadow: var(--shadow-interaction);
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 {
@ -324,62 +273,47 @@ textarea:focus {
}
.option-card:has(input:checked) {
border: 1px solid var(--color-primary);
background: var(--color-primary);
color: var(--white);
}
.option-card:has(input:checked) small {
color: rgba(247, 246, 230, 0.88);
border: 1.5px solid var(--olive-light);
background: var(--olive-light);
}
.option-card--invalid {
border-color: var(--tomato) !important;
box-shadow: 0 0 0 2px rgba(212, 75, 36, 0.14);
border-color: var(--error) !important;
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;
}
.counter-button,
.button {
min-height: var(--control-min-height);
}
.counter-button {
width: var(--control-min-height);
height: var(--control-min-height);
border: 1px solid var(--color-primary);
border-radius: 50%;
background: var(--color-primary);
color: var(--white);
font-size: 1.5rem;
line-height: 1;
box-shadow: 0 6px 16px rgba(107, 107, 5, 0.18);
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.counter-button:hover {
background: var(--color-primary-hover);
transform: translateY(-1px);
}
.counter-button:focus-visible {
outline: 3px solid rgba(107, 107, 5, 0.22);
outline-offset: 3px;
}
.review-card {
display: grid;
gap: var(--space-4);
gap: var(--space-24);
padding: 0;
border: 0;
border-radius: 0;
@ -389,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;
@ -399,187 +333,214 @@ 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;
}
.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);
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
}
.review-item:hover,
.review-item:focus-visible {
outline: 3px solid rgba(107, 107, 5, 0.2);
outline-offset: 3px;
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 {
position: sticky;
bottom: 0;
z-index: 5;
margin-top: auto;
background: var(--color-bg);
padding-top: var(--space-80);
backdrop-filter: none;
padding-top: var(--space-4);
padding-bottom: env(safe-area-inset-bottom);
}
.flow-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-wrap {
flex: 1;
position: relative;
width: min(100%, var(--content-width));
margin: 0 auto;
padding-top: 4.35rem;
display: flex;
align-items: center;
align-self: center;
min-height: 2.75rem;
}
.progress-label {
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;
width: 100%;
height: 0.375rem;
background: var(--color-progress-bg);
height: 0.45rem;
background: var(--olive-light);
border-radius: var(--radius-sm);
overflow: hidden;
}
.progress-bar {
display: block;
width: 0;
height: 100%;
background: var(--tomato);
transition: width 0.25s ease;
}
.progress-marker {
position: absolute;
top: 0;
transform: translateX(-50%);
display: grid;
justify-items: center;
gap: 0.2rem;
pointer-events: none;
}
.progress-marker::after {
content: "";
width: 0.125rem;
height: 1rem;
background: var(--tomato);
background: var(--olive);
border-radius: 999px;
}
.progress-marker__circle {
width: 2.9rem;
height: 2.9rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--tomato);
color: var(--butter-light);
font-size: 1.35rem;
font-weight: 600;
line-height: 1;
box-shadow: 0 10px 24px rgba(212, 75, 36, 0.18);
transition: width 0.25s ease;
}
.flow-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
width: min(100%, var(--content-width));
margin: 0 auto;
padding: var(--space-4) 0;
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 {
min-height: 1.5rem;
margin: 0;
color: var(--color-error);
font-size: 0.95rem;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.9rem 1.35rem;
border-radius: var(--radius-pill);
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text);
cursor: pointer;
.error-message--callout {
position: absolute;
right: 0;
bottom: calc(100% + 1.25rem);
}
.button--ghost:hover {
background: rgba(0, 0, 0, 0.03);
}
.button--text {
border: 0;
padding-left: 0;
}
.button--primary {
min-width: 10rem;
border-color: var(--color-primary);
background: var(--color-primary);
color: #ffffff;
font-weight: 700;
}
.button--primary:hover {
background: var(--color-primary-hover);
}
.button--primary:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.button--intro {
justify-self: start;
margin-top: var(--space-2);
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: 7rem;
height: 7rem;
border-radius: var(--radius-sm, 0.5rem);
overflow: hidden;
flex-shrink: 0;
}
.gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-thumb-remove {
position: absolute;
top: 0.2rem;
right: 0.2rem;
width: 1.4rem;
height: 1.4rem;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 0.85rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.site-footer {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
padding: var(--space-5) 0 var(--space-6);
padding: var(--space-32) 0 var(--space-40);
color: var(--color-muted);
text-align: center;
}
@ -595,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,
@ -608,12 +569,24 @@ textarea:focus-visible {
align-items: stretch;
}
.button--text {
justify-content: flex-start;
.error-message--callout {
position: static;
width: 100%;
max-width: 100%;
margin-bottom: var(--space-12);
}
.button--primary {
width: 100%;
.progress-wrap {
min-height: auto;
}
.progress-label {
position: static;
transform: none;
}
.error-message--callout::after {
display: none;
}
.event-flow-header {
@ -623,9 +596,8 @@ textarea:focus-visible {
@media (min-width: 768px) {
.step-layout--intro {
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: center;
align-items: stretch;
gap: var(--space-8);
}

File diff suppressed because it is too large Load Diff

553
css/index.css Normal file
View File

@ -0,0 +1,553 @@
/* ===========================================
INDEX.CSS Styles specific to index.html
Global styles (reset, variables, body, nav,
brand, typography) are in stylesheet_global.css
=========================================== */
/* --- Navigation overrides (index-specific) --- */
.nav-link {
border: 2px solid var(--olive-light);
transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-link:hover,
.nav-link.active,
.nav-link:focus-visible {
background: var(--olive);
color: var(--white);
border-color: var(--olive);
}
.nav-link--login {
background: var(--olive);
color: var(--white);
border-color: var(--olive);
}
.nav-link--login:hover,
.nav-link--login:focus-visible {
background: var(--white);
color: var(--olive);
border-color: var(--olive);
}
/* --- Page layout --- */
.container {
width: min(100% - 4rem, 1200px);
margin: 0 auto;
}
/* --- Hero section --- */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
margin-bottom: 100px;
}
.hero__right {
display: flex;
align-items: center;
justify-content: center;
}
.image-card {
width: 100%;
max-width: 436px;
overflow: hidden;
border-radius: var(--radius-lg);
}
.hero-image {
width: 100%;
max-width: 436px;
max-height: 510px;
height: auto;
border-radius: var(--radius-lg);
object-fit: cover;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
@media (max-width: 900px) {
.hero {
grid-template-columns: 1fr;
padding: 30px 0;
}
.hero__right {
order: -1;
}
.hero-image {
min-height: 320px;
}
}
/* --- "So funktioniert's" steps --- */
.how-it-works {
margin-bottom: 100px;
}
.how-it-works__header {
text-align: center;
margin-bottom: 32px;
}
.how-it-works__header h2 {
font-size: 2rem;
margin: 0;
color: var(--black);
}
.how-it-works__steps {
display: grid;
grid-template-columns: repeat(3, minmax(180px, 1fr));
gap: 20px;
}
.how-step {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-40);
background: var(--butter-light);
border-radius: var(--radius-lg);
}
.how-step-number-numbered {
position: relative;
}
.how-step_corner-number {
position: absolute;
color: var(--butter-light);
background: var(--tomato);
border-radius: var(--radius-lg);
top: var(--space-20);
left: var(--space-20);
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Bagel Fat One';
font-size: 1.5rem;
font-weight: 400;
color: var(--butter-light);
}
.how-step_corner-number--brown {
color: var(--brown);
}
.how-step_icon {
font-size: 3.5rem;
color: var(--brown);
margin: var(--space-24) 0;
}
.how-step__png {
width: 192px;
height: 192px;
object-fit: contain;
}
.how-step__png--brown {
filter: brightness(0) saturate(100%) invert(18%) sepia(56%) saturate(2800%) hue-rotate(16deg) brightness(92%) contrast(95%);
}
.how-step_text {
margin-bottom: var(--space-24);
text-align: center;
}
.text-left{
text-align: left;
}
.how-step__footer-pill {
margin-bottom: 4px;
}
.how-step__footer-badges {
display: flex;
gap: var(--space-16);
justify-content: center;
}
.how-step__footer-banner {
width: 100%;
padding: 10px 16px;
background: var(--butter);
border-radius: var(--radius-sm);
text-align: center;
font-family: 'Bagel Fat One', sans-serif;
font-size: 0.85rem;
letter-spacing: 0.12rem;
color: var(--brown);
}
@media (max-width: 900px) {
.how-it-works__steps {
grid-template-columns: 1fr;
}
}
/* --- Carousel gallery --- */
.gallery {
margin-bottom: 100px;
}
.gallery__carousel {
position: relative;
overflow: hidden;
}
.gallery__track {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.gallery__item {
flex: 0 0 calc((100% - 40px) / 3);
min-width: calc((100% - 40px) / 3);
border-radius: 24px;
overflow: hidden;
background: var(--white);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
aspect-ratio: 2 / 3;
cursor: pointer;
}
.gallery__item img {
width: 100%;
height: 100%;
display: block;
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;
top: 45%;
width: 44px;
height: 44px;
border: 1.5px solid var(--butter-light);
background: var(--butter-light);
border-radius: var(--radius-lg);
font-family: var(--font-main);
display: none;
font-weight: 400;
font-size: 1.25rem;
place-items: center;
z-index: 2;
color: var(--olive);
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.gallery__arrow:hover,
.gallery__arrow:focus-visible {
border: 1.5px solid var(--butter);
background: var(--butter);
}
.gallery__arrow--prev {
left: var(--space-24);
}
.gallery__arrow--next {
right: var(--space-24);
}
/* --- Carousel dot indicators --- */
.gallery_dots {
display: flex;
justify-content: center;
gap: 10px;
padding: 12px 0 8px;
}
.gallery_dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--olive);
background: transparent;
cursor: pointer;
padding: 0;
transition: background 0.25s ease, transform 0.2s ease;
}
.gallery_dot:hover,
.gallery_dot--active {
background: var(--olive);;
transform: scale(1.2);
}
.gallery_dot:focus-visible {
outline: 2px solid var(--olive);
outline-offset: 3px;
}
@media (max-width: 900px) {
.gallery__track {
gap: 16px;
}
.gallery__item {
flex: 0 0 100%;
min-width: 100%;
}
}
/* --- Gallery info (Instagram link) --- */
.gallery__info {
display: flex;
align-items: center;
gap: 10px;
}
.gallery__icon--instagram {
height: 32px;
width: 32px;
object-fit: contain;
border-radius: 8px;
background: none;
filter: brightness(0) saturate(100%) invert(27%) sepia(81%) saturate(749%) hue-rotate(24deg) brightness(90%) contrast(90%);
}
.gallery__icon--invite {
height: 56px;
width: 56px;
object-fit: contain;
margin-left: 0;
transform: translate(-4%, -1%);
position: relative;
}
/* --- CTA button --- */
.btn {
border: none;
background: var(--olive);
color: var(--white);
padding: 12px 22px;
font-weight: 700;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
font-size: 0.95rem;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn:hover {
background-color: var(--olive-dark);
transform: translateY(-1px);
}
/* --- Footer --- */
.footer {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
padding: 16px 24px;
border: none;
margin-top: 40px;
}
.footer-link {
color: var(--black);
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}
/* --- FAQ Section: Akkordion --- */
.faq-section {
margin-bottom: 0px;
}
.faq-accordion {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.faq-item {
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--butter-light);
padding: var(--space-12) var(--space-24) ;
transition: background-color 0.2s ease;
}
.faq-item:hover {
background: var(--olive-light);
}
.faq-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background: transparent;
border: none;
cursor: pointer;
font-family: var(--font-main);
font-size: 1.25rem;
font-weight: 400;;
text-align: left;
transition: background-color 0.2s ease;
}
.faq-title {
flex: 1;
font-weight: 400;
font-size: 1.25rem;
}
.faq-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 1.5rem;
font-weight: 400;
color: var(--black);
transition: transform 0.3s ease;
flex-shrink: 0;
}
.faq-trigger[aria-expanded="true"] .faq-icon {
transform: rotate(45deg);
}
.faq-content {
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.faq-content p {
margin: 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-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)
}
/* --- Responsive: FAQ Section --- */
@media (max-width: 768px) {
.faq-section {
padding: var(--space-40) var(--space-24);
margin: var(--space-40) 0 var(--space-32);
}
.faq-trigger {
padding: var(--space-2) var(--space-3);
font-size: 1.125rem;
}
.faq-content {
padding: 0 var(--space-3);
font-size: 1rem;
}
.faq-content p {
padding: var(--space-2) 0;
}
}

View File

@ -1,541 +0,0 @@
/* Instagram and Invite logo in gallery info area */
.gallery__icon--instagram {
height: 32px;
width: 32px;
object-fit: contain;
border-radius: 8px;
background: none;
/* Olive green filter for PNG: #6b6b05 */
filter: brightness(0) saturate(100%) invert(27%) sepia(81%) saturate(749%) hue-rotate(24deg) brightness(90%) contrast(90%);
}
.gallery__icon--invite {
height: 56px;
width: 56px;
object-fit: contain;
margin-left: 0;
transform: translate(-4%, -1%);
position: relative;
}
* { box-sizing: border-box; }
:root {
--black: #22211A;
--white: #ffffff;
--button-green: var(--olive);
--button-green-dark: var(--olive-dark);
}
body {
margin: 0;
font-family: var(--font-main);
background: #FFFDE3; /* butter background color from stylesheet */
}
.page-wrapper {
max-width: 1440px;
margin: 0 auto;
background: var(--white);
padding: 40px;
border: 1px solid var(--black);
}
/* --- 1. Basic Layout Logic --- */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border: 1px solid var(--black);
margin-bottom: 40px; /* Using your 40px margin from the first block */
}
.header__brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 1.2rem;
}
.brand__logo {
height: 34px;
width: auto;
object-fit: contain;
}
/* Grouping container for 'Event finden' and 'Login' */
.header__actions {
display: flex;
align-items: center;
gap: 6px;
}
.top-nav-wrap {
background: #FFFDE3;
padding: 18px 0;
}
.top-nav {
background: rgba(255, 255, 255, 0.95);
border-radius: 30px;
box-shadow: 0 3px 12px rgba(102, 52, 13, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
min-height: 58px;
padding: 12px 24px;
width: 100%;
max-width: none;
box-sizing: border-box;
margin: 0 0 40px;
}
.brand {
display: inline-flex;
align-items: center;
height: 50px;
text-decoration: none;
}
.brand img {
width: auto;
height: 100%;
max-width: 104px;
display: block;
}
.top-nav-links {
display: flex;
align-items: center;
gap: 16px;
}
.nav-link {
color: #221c1a;
line-height: 1.3;
text-decoration: none;
padding: 8px 20px;
border: 2px solid #e5e1b7;
border-radius: 20px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-link:hover,
.nav-link.active,
.nav-link:focus-visible {
background: #6b6b05;
color: #ffffff;
border-color: #6b6b05;
}
.nav-link--login {
background: #6b6b05;
color: #ffffff;
border-color: #6b6b05;
}
.nav-link--login:hover,
.nav-link--login:focus-visible {
background: #ffffff;
color: #6b6b05;
border-color: #6b6b05;
}
.main-content {
width: min(100% - 4rem, 1120px);
margin: 0 auto;
padding: 0 20px;
}
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 50px;
margin-bottom: 80px;
align-items: center;
padding: 40px 0;
}
.how-it-works {
margin-bottom: 70px;
}
.how-it-works__header {
text-align: center;
margin-bottom: 32px;
}
.how-it-works__header h2 {
font-size: 2rem;
margin: 0;
color: #221c1a;
}
.how-it-works__steps {
display: grid;
grid-template-columns: repeat(3, minmax(180px, 1fr));
gap: 20px;
}
.how-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 28px 20px;
background: var(--white) !important;
border: 2px solid var(--tomato) !important;
border-radius: 28px;
box-shadow: 0 12px 30px rgba(212, 75, 36, 0.08);
}
.how-step__icon {
width: 64px;
height: 64px;
display: grid;
place-items: center;
background: var(--button-green);
color: var(--white);
border-radius: 50%;
font-size: 1.4rem;
}
.how-step__number {
width: 36px;
height: 36px;
display: grid;
place-items: center;
background: #f4efd7;
color: #221c1a;
border-radius: 50%;
font-weight: 700;
}
.how-step__label {
margin: 0;
font-size: 1rem;
line-height: 1.6;
font-weight: 600;
color: #221c1a;
text-align: center;
}
.how-step__icon--brown {
background: #66340d;
}
.how-step__label--brown {
color: var(--tomato);
}
.how-step__label--big {
font-size: 1.25rem;
font-weight: 700;
}
.how-step--numbered {
position: relative;
}
.how-step__corner-number {
position: absolute;
top: 12px;
left: 16px;
font-size: 2.2rem;
font-weight: 700;
}
.how-step__corner-number--brown {
color: var(--tomato);
}
.how-step__png {
width: 192px;
height: 192px;
object-fit: contain;
}
.how-step__png--brown {
filter: brightness(0) saturate(100%) invert(39%) sepia(84%) saturate(1682%) hue-rotate(349deg) brightness(93%) contrast(86%);
}
@media (max-width: 900px) {
.how-it-works__steps {
grid-template-columns: 1fr;
}
}
.hero__left {
max-width: 520px;
}
.hero__left p {
margin: 24px 0 32px;
line-height: 1.8;
}
.hero__right {
display: flex;
align-items: center;
justify-content: center;
}
.image-card {
width: 100%;
max-width: 396px;
overflow: hidden;
border-radius: 30px;
}
.hero-image {
width: 100%;
max-width: 396px;
max-height: 464px;
height: auto;
border-radius: 30px;
object-fit: cover;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
@media (max-width: 900px) {
.hero {
grid-template-columns: 1fr;
padding: 30px 0;
}
.hero__right {
order: -1;
}
.hero-image {
min-height: 320px;
}
}
/* Carousel gallery */
.gallery__carousel {
position: relative;
}
.gallery__track {
display: flex;
gap: 20px;
overflow: hidden;
margin-bottom: 30px; /* Space between photos and dots */
scroll-behavior: smooth;
}
.gallery__item {
flex: 0 0 calc((100% - 40px) / 3);
min-width: calc((100% - 40px) / 3);
border-radius: 24px;
overflow: hidden;
background: #fff;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
aspect-ratio: 2 / 3;
}
.gallery__item img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.gallery__arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
display: grid;
place-items: center;
border-radius: 0;
background: none;
backdrop-filter: none;
border: none;
color: #ffffff;
cursor: pointer;
font-size: 1.6rem;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
transition: transform 0.2s ease, color 0.2s ease;
z-index: 2;
}
.gallery__arrow:hover {
transform: translateY(-50%) scale(1.15);
color: #e5e1b7;
}
.gallery__arrow--prev {
left: 12px;
}
.gallery__arrow--next {
right: 12px;
}
/* Center arrow removed using side arrows only */
@media (max-width: 900px) {
.gallery__track {
gap: 16px;
}
.gallery__item {
flex: 0 0 100%;
min-width: 100%;
}
}
/* --- 2. Button & Link Styling --- */
.btn {
border: none;
background: var(--button-green);
color: var(--white);
padding: 12px 22px;
font-weight: 700;
cursor: pointer;
text-decoration: none; /* Keeps the link from having an underline */
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
font-size: 0.95rem;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.btn:hover,
.btn:focus-visible {
background-color: var(--button-green-dark);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(107, 107, 5, 0.25);
}
.nav__link {
color: var(--black); /* Fixes the purple link issue */
}
/* --- 3. The "X" Box Logic --- */
.image-card__placeholder, .placeholder {
width: 100%;
height: 100%;
min-height: 250px;
position: relative;
border: 1px solid var(--black);
background: var(--white);
}
.image-card__placeholder::before, .image-card__placeholder::after,
.placeholder::before, .placeholder::after {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
}
/* Drawing the diagonal lines for the wireframe look */
.image-card__placeholder::before, .placeholder::before {
background: linear-gradient(to top left, transparent 49.5%, var(--black) 49.5%, var(--black) 50.5%, transparent 50.5%);
}
.image-card__placeholder::after, .placeholder::after {
background: linear-gradient(to top right, transparent 49.5%, var(--black) 49.5%, var(--black) 50.5%, transparent 50.5%);
}
/* The "X" Box Logic - IMPORTANT */
.image-card__placeholder, .placeholder {
width: 100%;
height: 100%;
min-height: 250px; /* Ensures the box has height */
position: relative; /* REQUIRED for the X lines to stay inside */
border: 1px solid var(--black);
background: var(--white);
}
/* Creating the diagonal lines */
.image-card__placeholder::before, .image-card__placeholder::after,
.placeholder::before, .placeholder::after {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
}
.image-card__placeholder::before, .placeholder::before {
background: linear-gradient(to top left, transparent 49.5%, var(--black) 49.5%, var(--black) 50.5%, transparent 50.5%);
}
.image-card__placeholder::after, .placeholder::after {
background: linear-gradient(to top right, transparent 49.5%, var(--black) 49.5%, var(--black) 50.5%, transparent 50.5%);
}
/* Badges and Buttons */
.social-badge {
display: flex;
justify-content: center; /* This centers the dots */
gap: 10px;
margin-top: 20px; /* This creates space between the photos and the dots */
/* REMOVE any line that says "position: absolute" or "bottom: 16px" */
}
.social-badge__dot { width: 12px; height: 12px; background: var(--black); border-radius: 50%; }
.profile-badge {
width: 60px; height: 60px;
border-radius: 50%;
border: 1px solid var(--black);
margin-top: 20px;
}
.gallery__info {
display: flex;
align-items: center;
gap: 10px;
}
.gallery__handle {
display: flex;
align-items: center;
font-size: 1.1rem;
font-weight: 700;
color: var(--black);
gap: 6px;
}
.gallery__icon {
font-size: 1.15rem;
color: #DD541A;
}
.gallery__at {
font-size: 1.1rem;
color: #222;
}
.gallery__brand {
font-family: var(--font-main);
color: #DD541A;
}
/* Footer */
.footer {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 24px;
border: none;
margin-top: 40px;
}
.footer__link {
color: var(--black);
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}

View File

@ -1,116 +1,102 @@
.container {
margin-top: 5rem;
background-color: var(--white);
/* ===========================================
LOGIN_SIGNUP.CSS Styles for login & signup
Global styles (reset, variables, body, nav,
typography) are in stylesheet_global.css
=========================================== */
.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-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);
max-width: 1000px;
width: 100%;
display: flex;
gap: 20px;
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(--white);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 500px;
}
.image-section img {
width: 100%;
height: 100%;
object-fit: cover;
}
.form-section {
flex: 1;
padding: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Formularelemente */
.info-box {
background-color: var(--olive-light);
padding: 15px;
margin-bottom: 30px;
border-radius: 8px;
font-size: 16px;
color: var(--black);
line-height: 1.4;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 6px;
color: var(--black);
font-weight: 500;
font-size: 14px;
}
.form-group.has-error {
margin-bottom: 0;
}
input[type="text"],
input[type="email"],
input[type="password"] {
font-size: 1.125rem;
font-family: var(--font-main);
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
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;
}
input:focus {
outline: none;
border-color: var(--olive-dark);
border: 2px solid var(--olive);
}
/* Hilfstexte & Fehler */
.signup-hint, .login-hint {
text-align: left;
margin-top: 20px;
color: var(--black);
/* --- Info box --- */
.info-box {
background-color: var(--olive-light);
padding: var(--space-16);
margin-bottom: var(--space-40);
border-radius: var(--radius-md);
font-size: 1rem;
line-height: 130%;
color: var(--black);
line-height: 1.4;
}
.signup-hint a, .login-hint a {
color: var(--blue);
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.signup-hint a:hover, .login-hint a:hover {
text-decoration: underline;
text-underline-offset: 2px;
color: var(--blue);
}
/* --- Hints & errors --- */
.error-message {
color: var(--error);
font-size: 13px;
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: var(--shadow-error);
}
.form-group.has-error .error-message {
.form-group.has-error .error-message--field-callout {
display: block;
}
*/
/* --- Modal / Popup --- */
/* Modal / Popup Styles */
.modal {
display: none;
position: fixed;
@ -134,38 +120,19 @@ input:focus {
align-items: center;
}
.modal-content {
background-color: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
position: relative;
margin-bottom: 20px;
}
.modal-header h2 {
color: #0084ff;
font-size: 24px;
}
.close-btn {
position: absolute;
right: 0;
top: 0;
font-size: 28px;
color: #999;
color: var(--black);
background: none;
border: none;
cursor: pointer;
@ -176,20 +143,62 @@ input:focus {
justify-content: center;
}
.modal-body {
color: #333;
font-size: 16px;
line-height: 1.6;
margin-bottom: 30px;
.btn-primary {
padding: var(--space-2) var(--space-32);
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;
}
.modal-footer {
.btn-primary:hover {
background-color: var(--olive-dark);
}
/* --- Footer --- */
.footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: var(--space-16) var(--space-32);
border: none;
margin-top: 40px;
}
.footer_link {
color: var(--black);
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}
/* Left aligned */
.footer-left {
justify-self: start;
}
/* Center aligned */
.footer-center {
justify-self: center;
}
/* Right aligned */
.footer-right {
justify-self: end;
display: flex;
gap: 10px;
justify-content: center;
gap: var(--space-24);
}
/* Responsive Anpassungen */
/* --- Responsive --- */
@media (max-width: 768px) {
.container {
flex-direction: column;
@ -199,18 +208,13 @@ input:focus {
min-height: 300px;
}
.header {
flex-direction: column;
gap: 15px;
.error-message--field-callout {
margin-top: var(--space-1);
max-width: 100%;
white-space: normal;
}
.header-buttons {
width: 100%;
flex-direction: column;
}
.header-btn {
width: 100%;
text-align: center;
.error-message--field-callout::after {
display: none;
}
}

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-label);
letter-spacing: var(--ls-lg);
}
#profile-headline {
margin: 0.4rem 0;
#headline {
color: var(--brown);
font-size: clamp(2rem, 4.4vw, 2.8rem);
}
.profile-subline {
@ -41,136 +34,92 @@
.profile-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
gap: var(--space-24);
}
.profile-tabs {
display: inline-flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.profile-tab {
border: 2px solid var(--olive);
border-radius: var(--radius-md);
background: var(--butter);
.btn-count {
color: var(--black);
padding: 0.45rem 1rem;
min-height: 2.5rem;
font-family: "Jost", sans-serif;
font-size: 1rem;
font-weight: 500;
letter-spacing: var(--ls-ui);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
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);
}
.profile-tab:hover,
.profile-tab:focus-visible {
background: #faf8e8;
}
.profile-tab.is-active {
border-color: transparent;
background: var(--olive);
color: var(--white);
}
/* Konsistentes Karten-Layout fuer alle Profilsektionen. */
/* Konsistentes Karten-Layout für alle Profilsektionen. */
.profile-panel {
background: rgba(255, 255, 255, 0.88);
background: var(--butter-light);
border-radius: var(--radius-lg);
box-shadow: 0 3px 12px rgba(102, 52, 13, 0.1);
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 fuer "Meine Events" und "Meine Anmeldungen". */
/* Einzelne Eventkarte für "Meine Events" und "Meine Anmeldungen". */
.profile-event-card {
border: 1px solid rgba(107, 107, 5, 0.25);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-interaction);
padding: var(--space-32) var(--space-40);
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
}
.profile-event-card-clickable {
gap: var(--space-40);
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.profile-event-card-clickable {
cursor: pointer;
}
.profile-event-card-clickable:hover {
box-shadow: 0 6px 16px rgba(102, 52, 13, 0.14);
transform: translateY(-1px);
transform: translateY(-3px);
}
.profile-event-title {
.profile-event-title h3{
margin: 0;
color: var(--black);
font-family: "Jost", sans-serif;
font-size: 1.25rem;
font-weight: 600;
}
/*
.profile-event-meta {
margin: 0.3rem 0 0;
font-size: 0.95rem;
color: var(--olive);
}
}*/
.profile-event-address-block {
margin-top: 0.55rem;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-sm);
border-left: 4px solid var(--tomato);
background: rgba(232, 237, 209, 0.65);
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-label);
text-transform: uppercase;
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 {
@ -189,109 +138,22 @@
.profile-event-actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.profile-unregister-btn {
border: none;
border-radius: var(--radius-md);
background: var(--tomato);
color: var(--butter-light);
padding: 0.45rem 0.95rem;
font-family: "Jost", sans-serif;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.profile-cancel-btn {
border: none;
border-radius: var(--radius-md);
background: var(--tomato);
color: var(--butter-light);
padding: 0.45rem 0.95rem;
font-family: "Jost", sans-serif;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.profile-cancel-btn:hover,
.profile-cancel-btn:focus-visible {
background: var(--tomato-dark);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28);
}
.profile-cancel-btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25);
}
.profile-unregister-btn:hover,
.profile-unregister-btn:focus-visible {
background: var(--tomato-dark);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28);
}
.profile-unregister-btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25);
}
.profile-empty {
margin: 0;
color: var(--black);
}
.profile-empty-state {
text-align: center;
padding: 2.4rem 1.3rem;
border: 2px solid var(--olive-light);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 3px 12px rgba(102, 52, 13, 0.08);
}
.profile-empty-kicker {
margin: 0 0 0.5rem;
color: var(--olive);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: var(--ls-label);
text-transform: uppercase;
}
.profile-empty-state h3 {
margin: 0;
font-size: 1.5rem;
color: var(--brown);
}
.profile-empty-state p {
margin: 0.65rem auto 1rem;
max-width: 36rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
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%;
@ -326,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;
@ -337,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 {
@ -350,7 +221,7 @@
}
@media (max-width: 48rem) {
.profile-page {
.container {
padding-top: 5.5rem;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,22 @@
[
[
{
"id": 1,
"title": "Italienische Tavolata",
"location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern",
"date": "11. APR. 2026",
"time": "3:30 UHR",
"category": "DINNER",
"diet": "VEGGIE",
"spots": 6,
"date": "17. Mai. 2026",
"time": "15:30 UHR",
"category": "Dinner",
"diet": "Vegetarisch",
"spots": 8,
"host": {
"name": "Ferdinando",
"initial": "F"
},
"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 dafuer 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": "11. APR. 2026",
"date": "8. Mai 2026",
"time": "19:00 UHR",
"category": "DINNER",
"diet": "Omnivore",
"category": "Dinner",
"diet": "Fleisch, Fisch",
"spots": 4,
"host": {
"name": "Camila",
@ -57,7 +58,7 @@
"hostMessage": [
"¡Hola a todos! Ich lade euch ein auf eine kulinarische Reise nach Peru.",
"Ich koche für euch ein authentisches peruanisches Sharing-Menü, das vor Lebensfreude nur so sprüht. Freut euch auf eine Explosion aus leuchtenden Farben, fein abgestimmter Schärfe und der unverwechselbaren Frische verschiedenster Kräuter.",
"Wir genießen den Abend gemeinsam in mehreren kleinen Gängen, ganz nach dem Sharing-Prinzip. Dabei entdecken wir die klassischen Aromen meiner Heimatstadt Lima von traditionell bis modern interpretiert.",
"Wir geniessen den Abend gemeinsam in mehreren kleinen Gängen, ganz nach dem Sharing-Prinzip. Dabei entdecken wir die klassischen Aromen meiner Heimatstadt Lima von traditionell bis modern interpretiert.",
"Es wird gesellig, aromatisch und ein echtes Erlebnis für alle Sinne. ¡Buen provecho!"
],
"menu": [
@ -85,12 +86,12 @@
{
"id": 3,
"title": "Japanese Delight",
"location": "ZÜRICH",
"location": "Zürich",
"address": "Limmatquai 92, 8001 Zürich",
"date": "02. MAI. 2026",
"date": "12. Mai 2026",
"time": "12:30 UHR",
"category": "LUNCH",
"diet": "Pescetarisch",
"category": "Lunch",
"diet": "Fisch",
"spots": 8,
"host": {
"name": "Akiko",
@ -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"
]
}
]

107
datenschutz.html Normal file
View File

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Datenschutz</title>
<link rel="stylesheet" href="css/stylesheet_global.css" />
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="layout-wide">
<h1>Datenschutzerklärung</h1>
<h3>1. Verantwortliche Stelle</h3>
<p>
Invité GmbH<br>
Musterstrasse 12<br>
7000 Chur<br>
Schweiz<br>
E-Mail: datenschutz@invite-cooking.ch
</p>
<h3>2. Erhebung und Verarbeitung personenbezogener Daten</h3>
<p>
Beim Besuch unserer Website werden automatisch Informationen allgemeiner Natur erfasst.
Diese Informationen (Server-Logfiles) beinhalten die Art des Webbrowsers, das verwendete
Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und
Ähnliches. Sie werden ausschliesslich zur technischen Bereitstellung und Verbesserung
unserer Website verwendet.
</p>
<h3>3. Registrierung und Nutzerkonto</h3>
<p>
Bei der Erstellung eines Nutzerkontos erheben wir folgende Daten: Name, E-Mail-Adresse
und Passwort. Diese Daten werden ausschliesslich zur Bereitstellung unserer Dienste
verwendet und nicht an Dritte weitergegeben.
</p>
<h3>4. Cookies</h3>
<p>
Unsere Website verwendet Cookies, um die Nutzererfahrung zu verbessern. Cookies sind
kleine Textdateien, die auf Ihrem Endgerät gespeichert werden. Sie können die Verwendung
von Cookies in Ihren Browsereinstellungen deaktivieren. Bitte beachten Sie, dass dadurch
die Funktionalität der Website eingeschränkt sein kann.
</p>
<h3>5. Datenweitergabe an Dritte</h3>
<p>
Eine Übermittlung Ihrer persönlichen Daten an Dritte findet nicht statt, es sei denn,
wir sind gesetzlich dazu verpflichtet oder Sie haben Ihre ausdrückliche Einwilligung
erteilt.
</p>
<h3>6. Datensicherheit</h3>
<p>
Wir setzen technische und organisatorische Sicherheitsmassnahmen ein, um Ihre Daten
gegen zufällige oder vorsätzliche Manipulation, Verlust, Zerstörung oder den Zugriff
unberechtigter Personen zu schützen. Unsere Sicherheitsmassnahmen werden entsprechend
der technologischen Entwicklung fortlaufend verbessert.
</p>
<h3>7. Ihre Rechte</h3>
<p>
Sie haben jederzeit das Recht auf Auskunft über die bei uns gespeicherten
personenbezogenen Daten. Ebenso haben Sie das Recht auf Berichtigung, Löschung
oder Einschränkung der Verarbeitung Ihrer Daten. Bitte wenden Sie sich dazu an:
datenschutz@invite-cooking.ch
</p>
<h3>8. Änderungen dieser Datenschutzerklärung</h3>
<p>
Wir behalten uns vor, diese Datenschutzerklärung gelegentlich anzupassen, damit sie
stets den aktuellen rechtlichen Anforderungen entspricht oder um Änderungen unserer
Leistungen umzusetzen. Für Ihren erneuten Besuch gilt dann die neue Datenschutzerklärung.
</p>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -3,16 +3,22 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event erstellen | Invité</title>
<title>Invité | Event erstellen</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_create.css" />
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
@ -26,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>
@ -36,69 +42,73 @@
data-step="0"
aria-labelledby="intro-title"
>
<div class="step-layout step-layout--intro">
<div class="step-copy">
<p class="step-kicker">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">
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 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 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>
<section class="step" data-step="1" aria-labelledby="step1-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 1</p>
<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 entspannter Abend mit gutem Essen?
ein Dinner mit Wow-Effekt oder einfach ein entspanntes Mittagessen mit gutem Essen?
</p>
</div>
<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
@ -109,15 +119,18 @@
>
</button>
<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="4"
value="0"
required
/>
</div>
<button
type="button"
class="counter-button"
@ -135,31 +148,39 @@
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 2</p>
<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--3">
<label class="option-card">
<input type="radio" name="dietType" value="Omnivor" required />
<span>Omnivor</span>
<small>Fleisch und/oder Fisch</small>
<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">
<input type="radio" name="dietType" value="Vegetarisch" />
<label class="option-card option option-card--with-icon">
<input type="checkbox" name="dietType" value="Fisch" />
<i class="fa-solid fa-fish option-card__icon" aria-hidden="true"></i>
<span>Fisch</span>
</label>
<label class="option-card option 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">
<input type="radio" name="dietType" value="Vegan" />
<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>
@ -176,38 +197,39 @@
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 3</p>
<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>
@ -217,9 +239,9 @@
<section class="step" data-step="4" aria-labelledby="step4-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 4</p>
<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>
@ -243,9 +265,9 @@
<section class="step" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 5</p>
<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>
@ -267,9 +289,9 @@
<section class="step" data-step="6" aria-labelledby="step6-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 6</p>
<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>
@ -277,14 +299,27 @@
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heißen?</label>
<label for="eventTitle">Wie soll dein Event heissen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Event-Abends</label>
<label for="eventDescription">Beschreibung des Events</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
<div class="form-field">
<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="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>
</div>
</div>
</section>
@ -292,9 +327,9 @@
<section class="step" data-step="7" aria-labelledby="step7-title">
<div class="step-layout">
<div class="step-copy">
<p class="step-kicker">Schritt 7</p>
<h2 id="step7-title">Dein Event auf einen Blick.</h2>
<p class="step-text">
<p class="badge">Schritt 7</p>
<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>
@ -356,30 +391,49 @@
<dt>Event-Abend</dt>
<dd data-review="eventDescription"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="galleryAddBtn" role="button" tabindex="0" aria-label="Fotos bearbeiten">
<dt>Fotos</dt>
<dd>
<div class="review-gallery" data-review-gallery>
<span>Keine Fotos hinzugefügt</span>
</div>
</dd>
</div>
</dl>
</div>
</div>
</section>
<div class="flow-footer" id="flowFooter" hidden>
<div class="progress-wrap" aria-hidden="true">
<div class="progress-marker" id="progressMarker">
<span class="progress-marker__circle" id="progressMarkerLabel">1</span>
</div>
<div class="flow-footer" id="flowFooter" hidden>
<div class="flow-actions">
<button type="button" id="backButton" class="button-secondary">
Zurück
</button>
<div class="progress-wrap">
<span class="progress-label" id="progressMarkerLabel">
Schritt 1 von 7
</span>
<div class="progress">
<span id="progressBar" class="progress-bar"></span>
</div>
</div>
<div class="flow-actions">
<button type="button" id="backButton" class="button button--text">Zurück</button>
<div class="flow-actions-right">
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>
<button type="button" id="nextButton" class="button button--primary">Weiter</button>
</div>
<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>
</div>
<section
id="submissionSuccess"
@ -388,29 +442,51 @@
aria-live="polite"
hidden
>
<div class="step-layout">
<div class="step-layout hero startseite">
<div class="step-copy">
<p class="step-kicker">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 button--primary" href="event_overview.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>
</form>
</main>
<footer class="site-footer">
<p>&copy; Social Cooking</p>
</footer>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<script src="js/event_create.js"></script>
</body>

View File

@ -3,13 +3,15 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event-Detail</title>
<title>Invité | Event-Detail</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
@ -26,7 +28,7 @@
</header>
<!-- Main content: detail page gets fully injected by JavaScript -->
<main class="container">
<main class="container layout-wide">
<!-- Render target: loading, error state or full detail layout -->
<div id="detail-view">
<p>Lädt Event-Details...</p>
@ -36,5 +38,58 @@
<!-- 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>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -3,13 +3,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event-Overview</title>
<title>Invité | Event-Übersicht</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
@ -26,45 +27,140 @@
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container">
<main class="container layout-wide">
<!-- Page headline -->
<h1 class="overview-title">Events</h1>
<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" type="button" data-cat="BRUNCH">BRUNCH</button>
<button class="category-item" type="button" data-cat="LUNCH">LUNCH</button>
<button class="category-item" type="button" data-cat="DINNER">DINNER</button>
<button class="category-item" type="button" data-cat="COFFEE">COFFEE</button>
<button class="category-item active" type="button" data-cat="ALLE">ALLE</button>
<button class="category-item active" type="button" data-cat="ALLE">Alle</button>
<button class="category-item" type="button" data-cat="Brunch">Brunch</button>
<button class="category-item" type="button" data-cat="Lunch">Lunch</button>
<button class="category-item" type="button" data-cat="Kaffee + Kuchen">Kaffee + Kuchen</button>
<button class="category-item" type="button" data-cat="Dinner">Dinner</button>
</div>
<!-- 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">
</label>
</div>
</div>
</section>
</div>
</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>
<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>
</details>
<!-- Render target: event cards or empty state -->
<section id="event-grid" class="event-list"></section>
</main>
<!-- Page logic: data loading, filtering and card rendering -->
<!-- Seitenlogik: Daten laden, filtern und Event-Karten rendern -->
<script src="js/event_overview.js"></script>
<!-- Info Modal: Kostenlose Events Info -->
<div id="info-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Warum Invité kostenlos ist</h2>
<button type="button" class="modal-close" aria-label="Popup schließen">&times;</button>
</div>
<div class="modal-body">
<p>Alle Events bei uns sind komplett kostenlos. Invité basiert rein auf Freiwilligkeit und der Freude am Teilen. Kein Geldfluss, keine versteckten Kosten nur die pure Absicht, die Community zu stärken und den sozialen Zusammenhalt in unserer Nachbarschaft zu fördern. Egal ob du den Kochlöffel schwingst oder dich als Gast dazu gesellst: Bei uns zählt nur die menschliche Begegnung.</p>
</div>
</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>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

80
impressum.html Normal file
View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Impressum</title>
<link rel="stylesheet" href="css/stylesheet_global.css" />
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="layout-wide">
<h1>Impressum</h1>
<h3>Angaben gemäss § 5 TMG</h3>
<p>
Invité GmbH<br>
Musterstrasse 12<br>
7000 Chur<br>
Schweiz
</p>
<h3>Kontakt</h3>
<p>
Telefon: +41 81 123 45 67<br>
E-Mail: info@invite-cooking.ch
</p>
<h3>Vertretungsberechtigte Person</h3>
<p>Max Mustermann, Geschäftsführer</p>
<h3>Handelsregistereintrag</h3>
<p>
Eingetragen im Handelsregister des Kantons Graubünden<br>
Firmennummer: CHE-123.456.789
</p>
<h3>Haftungsausschluss</h3>
<p>
Die Inhalte dieser Website wurden mit grösster Sorgfalt erstellt. Für die Richtigkeit,
Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.
</p>
<h3>Urheberrecht</h3>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen
dem schweizerischen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede
Art der Verwertung ausserhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen
Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -3,16 +3,17 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Events entdecken</title>
<title>Invité</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/landingpage.css?v=2" />
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css?v=2">
<script src="js/navigation.js" defer></script>
<link rel="stylesheet" href="css/index.css">
<!-- Font Awesome
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-dU7ZrF1pFq5kVnPzlV9+04YhARzNjCX5Q5P1shgMpuN4s5I8mI8QD4981h7kYtV7sSgNldR0z5pZW5bS2ZpY3Q==" crossorigin="anonymous" referrerpolicy="no-referrer" /> -->
<!-- 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">
<script src="js/navigation.js" defer></script>
</head>
<body>
@ -20,95 +21,197 @@
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--default" href="login.html" aria-label="Login">Login</a>
<a class="button-small auth-nav-button auth-nav-button--default" href="signup.html" aria-label="Signup">Signup</a>
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</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">
<h1>Dein Platz am Tisch wartet schon.</h1>
<p>Egal, ob du leidenschaftlich gerne den Kochlöffel schwingst oder dich einfach auf ein hausgemachtes Essen in guter Gesellschaft freust: Bei Invité bringst du Menschen zusammen. Finde Events, die zu deinem Geschmack passen, und geniesse unkomplizierte Begegnungen ohne Networking-Zwang.</p>
<a class="btn" href="login.html">Anmelden</a>
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1>
<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/Startpage ingredients.jpg" alt="Startpage Ingredients" />
<img class="hero-image" src="assets/index_round table friends.jpeg" alt="Round table friends" />
</div>
</div>
</section>
<section class="how-it-works">
<div class="how-it-works__header">
<h2>So funktioniert's</h2>
</div>
<div class="how-it-works__steps">
<article class="how-step how-step--numbered">
<span class="how-step__corner-number how-step__corner-number--brown">1</span>
<img src="assets/register icon.png" alt="Register" class="how-step__png how-step__png--brown" />
<p class="how-step__label how-step__label--brown how-step__label--big">Anmelden</p>
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">1</span>
<i class="fa-solid fa-id-card how-step_icon"></i>
<h3 class="how-step_text">Anmelden und Dabeisein</h3>
<p class="how-step_text text-left">Erstelle kurz dein Profil und zeig uns deinen Geschmack. Bei uns zählt der Mensch am Tisch, nicht der Lebenslauf.</p>
<div class="badge margin-bottom-24">
<span>Quick Setup in 2 Min</span>
</div>
</article>
<article class="how-step how-step--numbered">
<span class="how-step__corner-number how-step__corner-number--brown">2</span>
<img src="assets/add-event icon.png" alt="Event erstellen" class="how-step__png how-step__png--brown" />
<p class="how-step__label how-step__label--brown how-step__label--big">Event erstellen</p>
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">2</span>
<i class="fa-solid fa-magnifying-glass-location how-step_icon"></i>
<h3 class="how-step_text">Tisch finden oder decken</h3>
<p class="how-step_text text-left">Entdecke spontane Events in deiner Nähe oder öffne deine eigene Küche. Du entscheidest, ob du Gast oder Gastgeber:in bist.</p>
<div class="badge margin-bottom-24">
<span>Gast Gastgeber:in</span>
</div>
</article>
<article class="how-step how-step--numbered">
<span class="how-step__corner-number how-step__corner-number--brown">3</span>
<img src="assets/Plate icon.png" alt="Gemeinsam essen" class="how-step__png how-step__png--brown" />
<p class="how-step__label how-step__label--brown how-step__label--big">Gemeinsam essen</p>
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">3</span>
<i class="fa-solid fa-utensils how-step_icon"></i>
<h3 class="how-step_text">Teile den Tisch</h3>
<p class="how-step_text text-left">Triff neue Leute in entspannter Atmosphäre. Geniesse gutes Essen in Gesellschaft und mach aus einer Mahlzeit eine echte Begegnung.</p>
<div class="badge margin-bottom-24">
<span>Gemeinsam geniessen</span>
</div>
</article>
</div>
</section>
<!-- Main Content: uses .gallery, .gallery__carousel, .gallery__track, .gallery__item, and .gallery__info to present event carousel content -->
<section class="gallery">
<section class="gallery" aria-label="Bildergalerie" aria-roledescription="Karussell">
</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">
<i class="fas fa-chevron-left"></i>
</button>
<div class="gallery__track">
<article class="gallery__item">
<img src="assets/Red checkered social eating.jpg" alt="Red checkered social eating">
<div class="gallery__track" aria-live="polite">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 1 von 12">
<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">
<img src="assets/Pasta and many forks.jpg" alt="Pasta and many forks">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 2 von 12">
<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">
<img src="assets/Zoomed in asian eating.jpg" alt="Zoomed in asian eating">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 3 von 12">
<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">
<img src="assets/Burger eating together.jpg" alt="Burger eating together">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 4 von 12">
<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">
<img src="assets/Cake cutting figs.jpg" alt="Cake cutting figs">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 5 von 12">
<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">
<img src="assets/Cooking woman at home.jpg" alt="Cooking woman at home">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 6 von 12">
<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">
<img src="assets/Eating and laughing girls.jpg" alt="Eating and laughing girls">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 7 von 12">
<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">
<img src="assets/Pasta in cheese.jpg" alt="Pasta in cheese">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 8 von 12">
<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">
<img src="assets/Salad roommates.jpg" alt="Salad roommates">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 9 von 12">
<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">
<img src="assets/Sharing food table.jpg" alt="Sharing food table">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 10 von 12">
<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">
<img src="assets/Spicy food zoomed.jpg" alt="Spicy food zoomed">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 11 von 12">
<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">
<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">
@ -116,19 +219,140 @@
</button>
</div>
<div class="gallery_dots" role="tablist" aria-label="Seite auswählen"></div>
</section>
<div class="gallery__info">
<div class="gallery__handle" style="display: flex; align-items: center; gap: 16px;">
<img src="assets/instagram.png" alt="Instagram" class="gallery__icon--instagram" />
<img src="assets/logo_invite.svg" alt="Invité Logo" class="gallery__icon--invite" />
<!-- Lightbox: Bildansicht vergrössert -->
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
<div class="lightbox__backdrop" data-close-lightbox></div>
<figure class="lightbox__content" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="lightbox__close" type="button" aria-label="Schliessen">&times;</button>
<img class="lightbox__image" src="" alt="Grossansicht">
</figure>
</div>
<script src="js/index-carousel.js"></script>
<!-- FAQ Section: Akkordion mit häufig gestellten Fragen -->
<section class="faq-section">
<h2>Häufig gestellte Fragen</h2>
<div class="faq-accordion">
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie kann ich bei Invité anfangen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<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>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Fallen bei Invité Kosten an?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Nein, Invité ist komplett kostenlos. Alle Events basieren auf Freiwilligkeit und der Freude am Teilen. Es gibt keine versteckten Kosten nur die pure Absicht, die Community zu stärken.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Kann ich ein eigenes Event erstellen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ja, absolut! Du kannst dein eigenes Kochevent erstellen und Gäste einladen. Beschreibe dein Menü, die Teilnehmerzahl und weitere Details. Es ist deine Küche, dein Event, deine Regeln.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie funktioniert die An-/Abmeldung?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Bei jedem Event sehen dich die verfügbaren Plätze. Du kannst dich mit einem Klick anmelden. Eine Abmeldung ist bis 24 Stunden vor dem Event möglich so respektieren wir den Aufwand des Gastgebers.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Was ist mit Allergien und Diäten?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ich kann Informationen zu Allergien und Ernährungseinstellungen in der Event-Beschreibung hinzufügen oder beim Anmelden angeben. So können Gastgeber und Gäste besser zusammenkommen und Überraschungen vermeiden.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Ist Invité sicher und vertrauenswürdig?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<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>
</section>
</main>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<!-- FAQ Akkordion Toggle Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const faqTriggers = document.querySelectorAll('.faq-trigger');
faqTriggers.forEach((trigger) => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const isExpanded = this.getAttribute('aria-expanded') === 'true';
// Close all other items (optional: comment out to allow multiple open)
faqTriggers.forEach((otherTrigger) => {
if (otherTrigger !== trigger) {
otherTrigger.setAttribute('aria-expanded', 'false');
}
});
// Toggle current item
this.setAttribute('aria-expanded', !isExpanded);
});
});
});
</script>
<script src="js/index-carousel.js"></script>
<footer class="footer">
<a href="#" class="footer__link">Impressum</a>
</footer>
</body>
</html>

View File

@ -1,4 +1,4 @@
// =============================
// =============================
// SETUP: Wichtige HTML-Elemente holen
// Diese Konstanten verbinden unser JavaScript mit dem HTML.
// So können wir später Buttons, Formularfelder und Bereiche steuern.
@ -8,7 +8,6 @@ const steps = Array.from(document.querySelectorAll(".step"));
const backButton = document.getElementById("backButton");
const nextButton = document.getElementById("nextButton");
const progressBar = document.getElementById("progressBar");
const progressMarker = document.getElementById("progressMarker");
const progressMarkerLabel = document.getElementById("progressMarkerLabel");
const errorMessage = document.getElementById("errorMessage");
const usernameElement = document.getElementById("username");
@ -24,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 = {
@ -121,7 +121,7 @@ function markRadioGroupInvalid(group) {
* Zeigt den gewünschten Schritt an.
* Dabei werden auch Buttons, Progress Bar und Review aktualisiert.
*/
function showStep(index) {
function showStep(index, pushHistory = true) {
currentStep = index;
submissionSuccess.hidden = true;
clearStepInvalidState(index);
@ -136,6 +136,11 @@ function showStep(index) {
updateProgressBar(index, lastStep);
setErrorMessage("");
// Browser-History aktualisieren, damit Zurück-Taste funktioniert
if (pushHistory) {
history.pushState({ step: index }, "");
}
// Für bessere UX: bei jedem Schritt wieder nach oben scrollen
window.scrollTo({ top: 0, behavior: "smooth" });
}
@ -147,10 +152,14 @@ function showStep(index) {
*/
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];
}
@ -168,22 +177,17 @@ function updateFlowVisibility(stepIndex) {
* - letzter Schritt = 100%
*/
function updateProgressBar(stepIndex, totalStepIndex) {
const totalFormSteps = totalStepIndex;
let progress = 0;
let markerPosition = 0;
let markerStep = 1;
let markerTransform = "translateX(-50%)";
if (stepIndex > 0) {
progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
markerPosition = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
progress = ((stepIndex) / totalFormSteps) * 100;
markerStep = stepIndex;
}
progressBar.style.width = `${progress}%`;
progressMarker.style.left = `${markerPosition}%`;
progressMarker.style.transform = markerTransform;
progressMarker.hidden = stepIndex === 0;
progressMarkerLabel.textContent = String(markerStep);
progressMarkerLabel.textContent = `Schritt ${markerStep} von ${totalFormSteps}`;
}
@ -266,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.
@ -293,6 +320,8 @@ function updateReview() {
Object.entries(reviewValues).forEach(([key, value]) => {
updateReviewField(key, value);
});
updateReviewGallery();
}
/**
@ -305,7 +334,7 @@ function getReviewValues() {
menuDescription: getFieldValue("menuDescription"),
eventDescription: getFieldValue("eventDescription"),
maxGuests: getFieldValue("maxGuests"),
dietType: getFieldValue("dietType"),
dietType: getCheckboxValues("dietType"),
allergies: buildAllergiesReviewValue(),
eventDate: formatDate(getFieldValue("eventDate")),
eventTime: getFieldValue("eventTime"),
@ -387,26 +416,64 @@ function buildMenuItems(value) {
*/
function mapEventTypeToCategory(value) {
const categoryMap = {
Brunch: "BRUNCH",
Lunch: "LUNCH",
Dinner: "DINNER",
"Kaffee + Kuchen": "COFFEE"
Brunch: "Brunch",
Lunch: "Lunch",
Dinner: "Dinner",
"Kaffee + Kuchen": "Kaffee + Kuchen"
};
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.
*/
function buildStoredEvent() {
const eventType = getFieldValue("eventType");
const dietType = getFieldValue("dietType");
const dietType = getCheckboxValues("dietType");
const menuDescription = form.elements.menuDescription.value.trim();
const eventDescription = form.elements.eventDescription.value.trim();
const eventDate = form.elements.eventDate.value;
const eventTime = form.elements.eventTime.value;
const eventCity = form.elements.eventCity.value.trim();
const fallbackGallery = [getPlaceholderImageByEventType(eventType)];
const resolvedGallery = galleryImages.length > 0 ? [...galleryImages] : fallbackGallery;
return {
id: Date.now(),
@ -415,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),
@ -429,9 +497,9 @@ function buildStoredEvent() {
? []
: getCheckboxValues("allergies").split(", ").filter(Boolean),
allergiesNote: form.elements.allergiesOther.value.trim(),
// Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt.
// Host wird separat geführt und nicht als angemeldeter Gast gezählt.
participants: [],
gallery: [],
gallery: resolvedGallery,
createdAt: new Date().toISOString(),
source: "local"
};
@ -473,6 +541,15 @@ function validateCurrentStep() {
return false;
}
// Speziell für Schritt 2: Ernährungsform prüfen
if (currentStep === 2) {
const dietCheck = validateDietType();
if (!dietCheck.isValid) {
setErrorMessage(dietCheck.message);
return false;
}
}
// Danach normale Pflichtfelder prüfen
const requiredCheck = validateRequiredFields(fields);
if (!requiredCheck.isValid) {
@ -510,6 +587,68 @@ function validateRadioGroups(fields) {
return { isValid: true };
}
/**
* Prüft die Ernährungsform-Checkboxen (mindestens eine muss ausgewählt sein).
*/
function validateDietType() {
const checked = form.querySelectorAll('input[name="dietType"]:checked');
if (checked.length === 0) {
// Mark all as invalid
form.querySelectorAll('input[name="dietType"]').forEach(input => {
const card = input.closest('.option-card');
if (card) card.classList.add('option-card--invalid');
});
return { isValid: false, message: "Bitte wähle mindestens eine Ernährungsform aus." };
}
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:
@ -543,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 {
@ -558,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 {
@ -609,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);
}
@ -677,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);
};
@ -724,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;
@ -738,6 +895,64 @@ function focusFieldByName(fieldName) {
}
// =============================
// STEP 8b: Galerie-Upload (Fotos hinzufügen)
// =============================
const galleryImages = [];
function registerGalleryHandlers() {
const addBtn = document.getElementById("galleryAddBtn");
const fileInput = document.getElementById("galleryFileInput");
const preview = document.getElementById("galleryPreview");
if (!addBtn) return;
// Klick auf + öffnet direkt den Datei-Dialog
addBtn.addEventListener("click", () => {
fileInput.click();
});
// Datei(en) hochladen
fileInput.addEventListener("change", () => {
Array.from(fileInput.files).forEach(file => {
if (!file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.onload = (e) => {
addGalleryImage(e.target.result);
};
reader.readAsDataURL(file);
});
fileInput.value = "";
});
function addGalleryImage(src) {
galleryImages.push(src);
renderGalleryPreview();
}
function removeGalleryImage(index) {
galleryImages.splice(index, 1);
renderGalleryPreview();
}
function renderGalleryPreview() {
preview.innerHTML = "";
galleryImages.forEach((src, i) => {
const thumb = document.createElement("div");
thumb.className = "gallery-thumb";
thumb.innerHTML = `
<img src="${src}" alt="Foto ${i + 1}">
<button type="button" class="gallery-thumb-remove" aria-label="Foto entfernen">&times;</button>
`;
thumb.querySelector(".gallery-thumb-remove").addEventListener("click", () => {
removeGalleryImage(i);
});
preview.appendChild(thumb);
});
}
}
// =============================
// STEP 9: Alles starten
// Hier werden alle Event Listener registriert
@ -763,13 +978,27 @@ function initEventCreationFlow() {
// Counter aktivieren
registerCounterHandlers();
registerDietConflictHandlers();
registerMenuBulletHandler();
registerValidationFeedbackHandlers();
registerReviewEditHandlers();
registerGalleryHandlers();
// Browser-Zurück-Taste: vorherigen Schritt wiederherstellen
window.addEventListener("popstate", (e) => {
if (e.state && typeof e.state.step === "number") {
showStep(e.state.step, false);
} else {
showStep(0, false);
}
});
// Startzustand: Intro anzeigen
submissionSuccess.hidden = true;
showStep(0);
// Initialen History-Eintrag ersetzen, damit Step 0 im Verlauf ist
history.replaceState({ step: 0 }, "");
}
// Startpunkt des Skripts

View File

@ -1,15 +1,15 @@
document.addEventListener('DOMContentLoaded', async () => {
document.addEventListener('DOMContentLoaded', async () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// -------------------------------------------------------------
// DOM entry point and shared asset path.
// -------------------------------------------------------------
const detailContainer = document.getElementById('detail-view');
const locationIconPath = 'assets/location-pin.svg';
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'));
@ -48,20 +48,81 @@ document.addEventListener('DOMContentLoaded', async () => {
}
}
function getStoredUsers() {
try {
const stored = localStorage.getItem(USERS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
function getUserDisplayName(user) {
if (!user) return '';
const firstName = String(user.vorname || '').trim();
const lastName = String(user.nachname || '').trim();
const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || String(user.email || '').trim()).trim();
}
function getResolvedParticipants(event, registrationMap) {
const baseParticipants = Array.isArray(event.participants)
? event.participants.map(name => String(name || '').trim()).filter(Boolean)
: [];
const usersByEmail = new Map(
getStoredUsers().map(user => [String(user.email || '').trim().toLowerCase(), user])
);
const participantLookup = new Set(baseParticipants.map(name => name.toLowerCase()));
Object.entries(registrationMap || {}).forEach(([email, ids]) => {
const isRegisteredForEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(event.id));
if (!isRegisteredForEvent) return;
const user = usersByEmail.get(String(email || '').trim().toLowerCase());
const displayName = getUserDisplayName(user) || String(email || '').trim();
const normalizedName = displayName.toLowerCase();
if (displayName && !participantLookup.has(normalizedName)) {
baseParticipants.push(displayName);
participantLookup.add(normalizedName);
}
});
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]);
@ -69,118 +130,158 @@ document.addEventListener('DOMContentLoaded', async () => {
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 twelveHoursInMs = 12 * 60 * 60 * 1000;
return msUntilStart <= twelveHoursInMs;
return msUntilStart <= 24 * 60 * 60 * 1000;
}
function getDeregistrationInfo(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return { daysLeft: null, isClosed: false };
const oneDayMs = 24 * 60 * 60 * 1000;
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 12h-Fenster VOR Eventstart sichtbar.
function isAddressVisibleWindow(event) {
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 twelveHoursInMs = 12 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twelveHoursInMs;
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;
}
function countRegistrationsForEvent(registrationMap, eventId) {
return Object.values(registrationMap).reduce((count, ids) => {
const hasEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(eventId));
return hasEvent ? count + 1 : count;
}, 0);
}
// Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde.
function isEventOwnedByCurrentUser(event, user) {
if (!event || !user) {
return false;
}
if (!event || !user) return false;
const userEmail = String(user.email || '').trim().toLowerCase();
const hostEmail = String(event.hostEmail || '').trim().toLowerCase();
if (userEmail && hostEmail) {
return userEmail === hostEmail;
}
// Fallback fuer aeltere Datensaetze ohne hostEmail.
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);
}
// Prueft, ob der aktuelle Benutzer bereits in der Teilnehmerliste des Events steht.
function isUserListedInEventParticipants(event, user) {
if (!event || !user || !Array.isArray(event.participants)) {
return false;
}
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');
@ -191,212 +292,184 @@ document.addEventListener('DOMContentLoaded', async () => {
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 = {
VEGGIE: 'Vegetarisch',
VEGAN: 'Vegan',
FLEISCH: 'Fleisch',
FISCH: 'Fisch'
};
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.'];
const menuItems = Array.isArray(event.menu) && event.menu.length > 0
? event.menu
: ['Menü wird in Kuerze bekannt gegeben.'];
: ['Menü wird in Kürze bekannt gegeben.'];
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
? event.specifications
: [];
const participants = Array.isArray(event.participants) ? event.participants : [];
const galleryImages = Array.isArray(event.gallery) && event.gallery.length > 0
? event.gallery
: [event.image, event.image, event.image];
const visibleParticipants = participants.slice(0, 6);
? event.specifications : [];
const registrationMap = getRegistrationMap();
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
const remainingParticipants = Math.max(0, participants.length + extraRegistrations - visibleParticipants.length);
const participants = getResolvedParticipants(event, registrationMap);
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 + extraRegistrations;
const confirmedGuests = participants.length;
const freePlaces = Math.max(0, totalGuests - confirmedGuests);
const isFull = freePlaces === 0;
const isRegistrationClosed = isRegistrationClosedForEvent(event);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? registrationMap[currentUser.email].map(id => Number(id))
: [];
const deregInfo = getDeregistrationInfo(event);
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
? 'Abmelden'
: isRegistrationClosed
? 'Anmeldung geschlossen'
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 = isOwnEvent || (!isRegistered && (isFull || isRegistrationClosed));
const actionButtonVariantClass = isOwnEvent
? ' detail-primary-btn-own'
: isRegistered
? ' detail-primary-btn-danger'
: isRegistrationClosed
? ' detail-primary-btn-danger'
: ' detail-primary-btn-register';
const actionButtonDisabled = isCanceled
|| isOwnEvent
|| (!isRegistered && (isFull || isRegistrationClosed))
|| (isRegistered && deregInfo.isClosed);
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 detail-panel-compact">
<h2 class="detail-section-title">Adresse</h2>
<p>${event.address}</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>`,
`<span class="event-tag">${dietLabel}</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 = `
<div class="detail-page">
<a class="detail-back" href="event_overview.html">
<span aria-hidden="true">&lsaquo;</span>
Alle Events
</a>
detailcontainer.innerHTML = `
<section class="detail-hero">
<div class="detail-top-row">
<span class="event-location">
<img src="${locationIconPath}" alt="">
${event.location}
</span>
<p class="event-date-time">${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</p>
<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>
<div class="event-meta-row detail-chip-row">${detailChips}</div>
</section>
<section class="detail-content-grid">
<div class="detail-side-stack">
<article class="host-card detail-panel">
<article class="detail-panel">
<header class="host-header">
<span class="host-avatar">${hostInitial}</span>
<span class="host-name">${hostName}</span>
<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 detail-panel-compact">
<h2 class="detail-section-title">Menue</h2>
<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 detail-panel-compact">
<article class="detail-panel">
<div class="detail-participants-head">
<h2 class="detail-section-title">Teilnehmer</h2>
<a href="#" class="detail-participants-link">Alle ansehen</a>
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
</div>
<div class="detail-avatar-row">
<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>
<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>
${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="detail-action-meta-text">| ${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</span>
<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>
` : ''}
<div class="detail-action-buttons">
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
</span>
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${(!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>
@ -407,84 +480,127 @@ document.addEventListener('DOMContentLoaded', async () => {
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
</figure>
</div>
</div>
`;
// ---------------------------------------------------------
// Lightbox behavior for gallery images:
// open on image click, close via backdrop, close button or ESC.
// ---------------------------------------------------------
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));
} else if (!isFull && !isRegistrationClosed) {
registrationSet.add(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);
}
nextRegistrationMap[currentUser.email] = Array.from(registrationSet);
setRegistrationMap(nextRegistrationMap);
// Re-Render aktualisiert Buttonzustand und CTA ohne Seitenreload.
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(); });
} else if (!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'), 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(); });
}
});
}
// Central close helper to keep all close paths consistent.
function closeLightbox() {
if (!lightbox) {
return;
// "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', () => {
const isExpanded = !fullList.classList.contains('hidden');
fullList.classList.toggle('hidden');
avatarRow.classList.toggle('hidden');
showAllBtn.textContent = isExpanded ? 'Alle ansehen' : 'Weniger anzeigen';
});
}
// Lightbox
function closeLightbox() {
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')) {
@ -492,14 +608,10 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
// Close via dedicated icon/button.
lightboxClose?.addEventListener('click', closeLightbox);
// Close with keyboard for accessibility.
document.addEventListener('keydown', event => {
if (event.key === 'Escape') {
closeLightbox();
}
if (event.key === 'Escape') closeLightbox();
});
}
}

View File

@ -1,7 +1,9 @@
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const INFO_MODAL_SHOWN_KEY = 'infoModalShownOnFirstLogin';
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
@ -9,13 +11,19 @@ document.addEventListener('DOMContentLoaded', () => {
const filterButtons = document.querySelectorAll('.category-item');
const locationFilter = document.getElementById('location-filter');
const dateFilter = document.getElementById('date-filter');
const locationIconPath = 'assets/location-pin.svg';
const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg';
// -------------------------------------------------------------
// In-memory state for fetched events and currently active category.
// In-memory state for fetched events and currently active filters.
// Separate state for category, diet, and allergie selections.
// -------------------------------------------------------------
let allEvents = [];
let activeCategory = 'ALLE';
let activeDiets = new Set();
let activeAllergies = new Set();
const currentUser = getCurrentUser();
function getCurrentUser() {
@ -28,7 +36,12 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
// Prueft, ob ein Event dem aktuellen Benutzer gehoert.
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) {
return false;
@ -66,6 +79,58 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function getStoredUsers() {
try {
const stored = localStorage.getItem(USERS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
function getUserDisplayName(user) {
if (!user) {
return '';
}
const firstName = String(user.vorname || '').trim();
const lastName = String(user.nachname || '').trim();
const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || String(user.email || '').trim()).trim();
}
function getResolvedParticipants(event, registrationMap) {
const baseParticipants = Array.isArray(event.participants)
? event.participants.map(name => String(name || '').trim()).filter(Boolean)
: [];
const usersByEmail = new Map(
getStoredUsers().map(user => [String(user.email || '').trim().toLowerCase(), user])
);
const participantLookup = new Set(baseParticipants.map(name => name.toLowerCase()));
Object.entries(registrationMap || {}).forEach(([email, ids]) => {
const isRegisteredForEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(event.id));
if (!isRegisteredForEvent) {
return;
}
const user = usersByEmail.get(String(email || '').trim().toLowerCase());
const displayName = getUserDisplayName(user) || String(email || '').trim();
const normalizedName = displayName.toLowerCase();
if (displayName && !participantLookup.has(normalizedName)) {
baseParticipants.push(displayName);
participantLookup.add(normalizedName);
}
});
return baseParticipants;
}
function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
@ -88,8 +153,13 @@ document.addEventListener('DOMContentLoaded', () => {
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE';
const savedDate = sessionStorage.getItem('activeDate') || '';
const savedDiets = sessionStorage.getItem('activeDiets') || '';
const savedAllergies = sessionStorage.getItem('activeAllergies') || '';
activeCategory = savedCategory;
activeDiets = new Set(savedDiets ? savedDiets.split(',') : []);
activeAllergies = new Set(savedAllergies ? savedAllergies.split(',') : []);
if (locationFilter) {
locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE';
}
@ -97,6 +167,7 @@ document.addEventListener('DOMContentLoaded', () => {
dateFilter.value = savedDate;
}
updateDietAvailability();
applyFilters();
} catch (error) {
console.error('Fehler:', error);
@ -198,7 +269,7 @@ document.addEventListener('DOMContentLoaded', () => {
: `${timeString} Uhr`;
}
// Baut aus Eventdatum/-zeit ein Date-Objekt fuer Fristlogik und Vergleiche.
// Baut aus Eventdatum/-zeit ein Date-Objekt für Fristlogik und Vergleiche.
function parseEventDateTime(event) {
if (!event?.date) {
return null;
@ -216,28 +287,42 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
@ -252,7 +337,7 @@ document.addEventListener('DOMContentLoaded', () => {
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
// Zaehlt eindeutige Registrierungen eines Events ueber alle Benutzer.
// Zählt eindeutige Registrierungen eines Events über alle Benutzer.
function countRegistrationsForEvent(registrationMap, eventId) {
return Object.values(registrationMap).reduce((count, ids) => {
const hasEvent = Array.isArray(ids)
@ -262,7 +347,7 @@ document.addEventListener('DOMContentLoaded', () => {
}, 0);
}
// Schliesst neue Anmeldungen ab 12h vor Start (inkl. bereits gestarteter Events).
// Schliesst neue Anmeldungen ab 24h vor Start (inkl. bereits gestarteter Events).
function isRegistrationClosedForEvent(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
@ -270,9 +355,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
const msUntilStart = eventDateTime.getTime() - Date.now();
const twelveHoursInMs = 12 * 60 * 60 * 1000;
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart <= twelveHoursInMs;
return msUntilStart <= twentyfourHoursInMs;
}
// Safely verify whether a value exists in the given select element.
@ -280,38 +365,64 @@ document.addEventListener('DOMContentLoaded', () => {
return Array.from(selectElement.options).some(option => option.value === value);
}
// Apply all filters together (category, location, date), update button state, render and persist.
// Apply all filters together (category, diet, allergie, location, date), update button state, render and persist.
function applyFilters() {
const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE';
const selectedDate = dateFilter ? dateFilter.value : '';
// Update active states for all filter types
filterButtons.forEach(btn => {
const isCategoryButton = btn.getAttribute('data-cat') !== null;
const isDietButton = btn.getAttribute('data-diet') !== null;
const isAllergieButton = btn.getAttribute('data-allergie') !== null;
if (isCategoryButton) {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isDietButton) {
if (activeDiets.has(btn.getAttribute('data-diet'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isAllergieButton) {
if (activeAllergies.has(btn.getAttribute('data-allergie'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
});
const filtered = allEvents.filter(event => {
// Lokal erstellte Events werden nicht in der allgemeinen Event-Uebersicht angezeigt.
if (event.source === 'local') {
return false;
}
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 ||
(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 ||
(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 && locationMatch && dateMatch;
return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch;
});
renderEvents(filtered);
sessionStorage.setItem('activeFilter', activeCategory);
sessionStorage.setItem('activeLocation', selectedLocation);
sessionStorage.setItem('activeDate', selectedDate);
sessionStorage.setItem('activeDiets', Array.from(activeDiets).join(','));
sessionStorage.setItem('activeAllergies', Array.from(activeAllergies).join(','));
}
// Render either:
@ -329,9 +440,9 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="empty-state">
<p class="empty-state-kicker">Keine Treffer</p>
<h3>Schade, aktuell gibt es hier keine Events.</h3>
<p>Starte dein eigenes Dinner und bringe die Community an deinen Tisch.</p>
<p>Starte dein eigenes Event und bringe die Community an deinen Tisch.</p>
<a class="empty-state-link" href="event_create.html">
<button class="empty-state-btn" type="button">Event erstellen</button>
<button class="button-primary" type="button">Event erstellen</button>
</a>
</div>
`;
@ -355,53 +466,78 @@ document.addEventListener('DOMContentLoaded', () => {
const displayTime = formatEventTime(event.time);
// Capacity logic:
// spots = total capacity, participants.length = booked seats.
const baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0;
const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
const bookedSeats = baseParticipants + extraRegistrations;
// spots = total capacity, resolved participants = booked seats.
const resolvedParticipants = getResolvedParticipants(event, registrationMap);
const bookedSeats = resolvedParticipants.length;
const totalCapacity = event.spots;
const freePlaces = Math.max(0, totalCapacity - bookedSeats);
const isFull = freePlaces === 0;
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const isRegistered = userRegistrationSet.has(Number(event.id));
const isRegistrationClosed = isRegistrationClosedForEvent(event);
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
? event.specifications.map(spec => `<span class="event-tag">${spec}</span>`).join('')
: '';
const actionMarkup = isOwnEvent
? '<button class="btn-primary btn-primary-own" type="button" data-registration-action="own" disabled>Dein Event!</button>'
: isRegistered
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="unregister">Abmelden</button>'
: isRegistrationClosed
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
: isFull
? ''
: !currentUser
? '<button class="btn-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
: '<button class="btn-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
// Build diet tags: split by comma and create individual tags
const dietTags = event.diet && event.diet !== 'Keine Angabe' && event.diet !== ''
? event.diet.split(', ').map(d => `<span class="event-tag">${d.trim()}</span>`).join('')
: '';
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">
<div class="event-top-row">
<span class="event-location">
<img src="${locationIconPath}" alt="">
<img src="${locationIconPath}" class="icon" alt="">
${event.location}
</span>
<p class="event-date-time">${displayDate} | ${displayTime} | ${bookedSeats}/${totalCapacity} Gaeste</p>
<span class="event-date-time"> <img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}
</span>
<span class="event-gast"> <img src="${gastIconPath}" class="icon" alt="Gaeste Icon">${bookedSeats}/${totalCapacity} </span>
</div>
<h2 class="event-title">${event.title}</h2>
<h2>${event.title}</h2>
<div class="event-meta-row">
<span class="event-tag">${event.category}</span>
<span class="event-tag">${event.diet}</span>
${dietTags}
${specsChips}
</div>
</div>
<div class="event-side${isFull ? ' event-side-full' : ''}">
<span class="event-spots${isFull ? ' event-spots-full' : ''}">${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`}</span>
${actionMarkup}
${sideInfoMarkup}
</div>
`;
@ -435,12 +571,55 @@ document.addEventListener('DOMContentLoaded', () => {
: [];
const idSet = new Set(currentIds);
if (action === 'unregister') {
idSet.delete(Number(event.id));
// 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;
}
if (action === 'register' && !isFull && !isRegistrationClosed) {
idSet.add(Number(event.id));
// 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);
@ -453,10 +632,107 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Category filter interactions.
// 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', () => {
activeCategory = button.getAttribute('data-cat');
const categoryValue = button.getAttribute('data-cat');
const dietValue = button.getAttribute('data-diet');
const allergieValue = button.getAttribute('data-allergie');
if (categoryValue !== null) {
// Category filter: exclusive selection
activeCategory = categoryValue;
} else if (dietValue !== null) {
// 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 {
// Removing a diet
activeDiets.delete(dietValue);
}
updateDietAvailability();
} else if (allergieValue !== null) {
// Allergie filter: toggle selection
if (activeAllergies.has(allergieValue)) {
activeAllergies.delete(allergieValue);
} else {
activeAllergies.add(allergieValue);
}
}
applyFilters();
});
});
@ -468,8 +744,87 @@ document.addEventListener('DOMContentLoaded', () => {
if (dateFilter) {
dateFilter.addEventListener('change', applyFilters);
// Make calendar icon clickable to focus the date input
const calendarIcon = document.querySelector('.calendar-icon');
if (calendarIcon) {
calendarIcon.addEventListener('click', () => {
dateFilter.focus();
dateFilter.click();
});
}
}
// 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');
if (infoButton && infoModal) {
infoButton.addEventListener('click', () => {
infoModal.classList.add('show');
});
}
if (modalClose && infoModal) {
modalClose.addEventListener('click', () => {
infoModal.classList.remove('show');
});
}
if (infoModal) {
infoModal.addEventListener('click', (event) => {
if (event.target === infoModal) {
infoModal.classList.remove('show');
}
});
}
// Auto-open info modal on first login
if (currentUser && infoModal) {
const userInfoModalKey = getInfoModalShownKeyForUser(currentUser);
const hasShownInfoModal = localStorage.getItem(userInfoModalKey);
if (!hasShownInfoModal) {
infoModal.classList.add('show');
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

@ -1,4 +1,4 @@
// =============================================
// =============================================
// Galerie-Karussell (Startseite)
// Diese Datei steuert die Foto-Galerie mit Pfeilen.
// =============================================
@ -7,8 +7,9 @@
const carouselTrack = document.querySelector('.gallery__track');
const prevArrow = document.querySelector('.gallery__arrow--prev');
const nextArrow = document.querySelector('.gallery__arrow--next');
const dotscontainer = document.querySelector('.gallery_dots');
// Nur ausfuehren, wenn die Galerie auf der Seite vorhanden ist.
// Nur ausführen, wenn die Galerie auf der Seite vorhanden ist.
if (carouselTrack) {
// Alle einzelnen Karten/Bilder im Track sammeln.
const items = Array.from(carouselTrack.querySelectorAll('.gallery__item'));
@ -16,55 +17,127 @@ if (carouselTrack) {
// Auf Mobile zeigen wir 1 Bild, auf Desktop 3 Bilder pro "Seite".
const getItemsPerPage = () => (window.matchMedia('(max-width: 900px)').matches ? 1 : 3);
let itemsPerPage = getItemsPerPage();
const pageCount = Math.ceil(items.length / itemsPerPage);
let pageCount = Math.ceil(items.length / itemsPerPage);
let activePage = 0;
var dots = [];
// Scrollt den Track auf eine bestimmte Seite.
function scrollToPage(page) {
activePage = page;
const pageWidth = carouselTrack.clientWidth;
carouselTrack.scrollTo({ left: pageWidth * page, behavior: 'smooth' });
function buildDots() {
if (!dotscontainer) return;
dotscontainer.innerHTML = '';
dots = [];
for (var i = 0; i < pageCount; i++) {
var dot = document.createElement('button');
dot.type = 'button';
dot.className = 'gallery_dot' + (i === activePage ? ' gallery_dot--active' : '');
dot.setAttribute('role', 'tab');
dot.setAttribute('aria-selected', i === activePage ? 'true' : 'false');
dot.setAttribute('aria-label', 'Seite ' + (i + 1) + ' von ' + pageCount);
dot.dataset.page = i;
dot.addEventListener('click', function() {
goToPage(parseInt(this.dataset.page));
});
dotscontainer.appendChild(dot);
dots.push(dot);
}
}
// Geht zur naechsten Seite (mit Wrap-around am Ende).
function updateDots() {
dots.forEach(function(dot, i) {
dot.classList.toggle('gallery_dot--active', i === activePage);
dot.setAttribute('aria-selected', i === activePage ? 'true' : 'false');
});
}
function updateTrack() {
var gap = parseFloat(getComputedStyle(carouselTrack).gap) || 20;
var itemWidth = items[0].getBoundingClientRect().width;
var offset = activePage * (itemWidth + gap) * itemsPerPage;
carouselTrack.style.transform = 'translateX(-' + offset + 'px)';
carouselTrack.style.transition = 'transform 0.4s ease';
updateDots();
}
function goToPage(page) {
activePage = page;
updateTrack();
}
// Geht zur nächsten Seite (mit Wrap-around am Ende).
function showNext() {
activePage = (activePage + 1) % pageCount;
scrollToPage(activePage);
updateTrack();
}
// Geht zur vorherigen Seite (mit Wrap-around zum Ende).
function showPrev() {
activePage = (activePage - 1 + pageCount) % pageCount;
scrollToPage(activePage);
updateTrack();
}
// Wenn sich bei Resize die Karten-Anzahl pro Seite aendert,
// laden wir die Seite neu, damit Layout und Seitenzahl wieder stimmen.
function refreshCarousel() {
const responsiveItemsPerPage = getItemsPerPage();
if (responsiveItemsPerPage !== itemsPerPage) {
itemsPerPage = responsiveItemsPerPage;
window.location.reload();
}
}
buildDots();
// Klick-Steuerung der Pfeile.
if (nextArrow) nextArrow.addEventListener('click', showNext);
if (prevArrow) prevArrow.addEventListener('click', showPrev);
// Tastatur-Support fuer Barrierefreiheit.
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
showNext();
// Tastatur-Support für Barrierefreiheit.
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowRight') showNext();
if (event.key === 'ArrowLeft') showPrev();
});
// Reagiert auf Bildschirmgrössen-Änderungen.
window.addEventListener('resize', function() {
var newPerPage = getItemsPerPage();
if (newPerPage !== itemsPerPage) {
itemsPerPage = newPerPage;
pageCount = Math.ceil(items.length / itemsPerPage);
activePage = 0;
}
if (event.key === 'ArrowLeft') {
showPrev();
buildDots();
updateTrack();
});
// =============================================
// Lightbox: Bild vergrössern bei Klick
// =============================================
const lightbox = document.getElementById('gallery-lightbox');
const lightboxImage = lightbox ? lightbox.querySelector('.lightbox__image') : null;
function openLightbox(src, alt) {
if (!lightbox || !lightboxImage) return;
lightboxImage.src = src;
lightboxImage.alt = alt || 'Grossansicht';
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
}
function closeLightbox() {
if (!lightbox) return;
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
lightboxImage.src = '';
}
// Klick auf Galerie-Bild öffnet die Lightbox.
items.forEach(function(item) {
var img = item.querySelector('img');
if (img) {
item.addEventListener('click', function() {
openLightbox(img.src, img.alt);
});
}
});
// Reagiert auf Bildschirmgroessen-Aenderungen.
window.addEventListener('resize', () => {
refreshCarousel();
scrollToPage(activePage);
// Lightbox schliessen: Backdrop, Close-Button oder ESC-Taste.
if (lightbox) {
lightbox.querySelector('.lightbox__close').addEventListener('click', closeLightbox);
lightbox.querySelector('[data-close-lightbox]').addEventListener('click', closeLightbox);
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && lightbox.classList.contains('is-open')) {
closeLightbox();
}
});
}
}

View File

@ -1,4 +1,4 @@
// =============================================
// =============================================
// Mini-Galerie auf der Landingpage
// Diese Datei hebt immer ein Bild hervor und
// erlaubt Navigation mit Pfeilen/Tastatur.
@ -38,7 +38,7 @@ function showPrev() {
if (nextBtn) nextBtn.addEventListener('click', showNext);
if (prevBtn) prevBtn.addEventListener('click', showPrev);
// Tastatursteuerung fuer bessere Bedienbarkeit.
// Tastatursteuerung für bessere Bedienbarkeit.
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
showNext();

View File

@ -1,4 +1,4 @@
// =============================================
// =============================================
// Login-Logik
// Diese Datei validiert die Eingaben, sucht den
// Benutzer im localStorage und legt die Session an.
@ -25,12 +25,12 @@ function getStoredUsers() {
}
}
// Speichert den aktiven Benutzer fuer nachfolgende Seiten.
// Speichert den aktiven Benutzer für nachfolgende Seiten.
function setCurrentUser(user) {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user));
}
// Erstellt einen Demo-Benutzer, falls fuer die E-Mail noch kein Account existiert.
// Erstellt einen Demo-Benutzer, falls für die E-Mail noch kein Account existiert.
function createFallbackUser(email, passwort) {
const localPart = email.split('@')[0] || 'Gast';
const normalized = localPart.replace(/[._-]/g, ' ').trim();
@ -51,59 +51,72 @@ function createFallbackUser(email, passwort) {
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) {
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, pruefen wir:
if (passwortValue.length < 6) {
passwortGroup.classList.add('has-error');
passwortError.textContent = 'Dein Passwort ist zu kurz. Bitte überprüfe dein Passwort.';
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());
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);
// Nach erfolgreichem Login geht es zur Event-Uebersicht.
// 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';
}
}

View File

@ -1,20 +1,22 @@
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// Zentrale DOM-Referenzen fuer klare, testbare Funktionen.
// Zentrale DOM-Referenzen für klare, testbare Funktionen.
const loggedOutState = document.getElementById('logged-out-state');
const loggedInContent = document.getElementById('logged-in-content');
const profileHeadline = document.getElementById('profile-headline');
const profileHeadline = document.getElementById('headline');
const profileSubline = document.getElementById('profile-subline');
const logoutButton = document.getElementById('logout-button');
const profileTabButtons = Array.from(document.querySelectorAll('[data-profile-tab]'));
const profileTabButtons = Array.from(document.querySelectorAll('[data-category-item]'));
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');
@ -109,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
profileSubline.textContent = 'Bitte logge dich ein, um deinen Bereich zu sehen.';
}
// Fuellt Ueberschriften und Formular mit den aktuellen Benutzerdaten.
// Füllt Überschriften und Formular mit den aktuellen Benutzerdaten.
function renderLoggedInState(user) {
loggedOutState.classList.add('hidden');
loggedInContent.classList.remove('hidden');
@ -131,7 +133,7 @@ document.addEventListener('DOMContentLoaded', () => {
profileTabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabName = button.getAttribute('data-profile-tab');
const tabName = button.getAttribute('data-category-item');
if (!tabName) {
return;
}
@ -148,11 +150,24 @@ document.addEventListener('DOMContentLoaded', () => {
});
logoutButton.addEventListener('click', () => {
localStorage.removeItem(CURRENT_USER_KEY);
window.location.href = 'login.html';
const logoutModal = document.getElementById('logoutModal');
logoutModal.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// Globale Funktionen für das Logout-Modal.
window.closeLogoutModal = function() {
const logoutModal = document.getElementById('logoutModal');
logoutModal.classList.remove('show');
document.body.style.overflow = 'auto';
};
window.confirmLogout = function() {
localStorage.removeItem(CURRENT_USER_KEY);
window.location.href = 'index.html';
};
// Reagiert auf Aktionen in der Liste "Meine Events" per Event Delegation.
function handleHostedListClick(event) {
const target = event.target;
@ -164,7 +179,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (cancelButton && currentUser?.email) {
const eventId = Number(cancelButton.getAttribute('data-cancel-event-id'));
if (Number.isFinite(eventId)) {
cancelHostedEvent(eventId, currentUser.email);
openCancelEventModal(eventId);
}
return;
}
@ -189,7 +204,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Schaltet den sichtbaren Profilbereich per Tabname um.
function activateProfileTab(tabName) {
profileTabButtons.forEach(button => {
const isActive = button.getAttribute('data-profile-tab') === tabName;
const isActive = button.getAttribute('data-category-item') === tabName;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
@ -198,6 +213,11 @@ document.addEventListener('DOMContentLoaded', () => {
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.
@ -209,18 +229,34 @@ document.addEventListener('DOMContentLoaded', () => {
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)) {
if (!Number.isFinite(eventId)) return;
const modal = document.getElementById('unregister-confirm-modal');
if (modal) modal.classList.add('show');
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;
}
unregisterFromEvent(eventId, currentUser.email);
return;
}
if (target.closest('a, button')) {
return;
@ -241,6 +277,43 @@ document.addEventListener('DOMContentLoaded', () => {
// Sagt ein gehostetes Event ab (aus eigener Profilansicht entfernen).
let pendingCancelEventId = null;
function openCancelEventModal(eventId) {
pendingCancelEventId = eventId;
const modal = document.getElementById('cancelEventModal');
modal.classList.add('show');
}
window.closeCancelEventModal = function() {
pendingCancelEventId = null;
const modal = document.getElementById('cancelEventModal');
modal.classList.remove('show');
};
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.
document.getElementById('cancelEventModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCancelEventModal();
}
});
function cancelHostedEvent(eventId, userEmail) {
// Lokal erstellte, eigene Events werden direkt aus dem Storage geloescht.
const storedEvents = getStoredEvents();
@ -253,7 +326,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
setStoredEvents(nextStoredEvents);
// Event-ID fuer alle Benutzer aus den Anmeldungen entfernen.
// Event-ID für alle Benutzer aus den Anmeldungen entfernen.
const registrationMap = getRegistrationMap();
Object.keys(registrationMap).forEach(email => {
const ids = Array.isArray(registrationMap[email])
@ -268,7 +341,7 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
@ -282,12 +355,10 @@ document.addEventListener('DOMContentLoaded', () => {
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@]+$/;
@ -314,12 +385,12 @@ document.addEventListener('DOMContentLoaded', () => {
return isValid;
}
// Speichert Profilaenderungen lokal und synchronisiert auch den Benutzerkatalog.
// Speichert Profiländerungen lokal und synchronisiert auch den Benutzerkatalog.
function handleProfileSubmit(event) {
event.preventDefault();
if (!validateProfileForm()) {
profileFeedback.textContent = 'Bitte pruefe die markierten Felder.';
profileFeedback.textContent = 'Bitte prüfe die markierten Felder.';
return;
}
@ -337,7 +408,7 @@ document.addEventListener('DOMContentLoaded', () => {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(nextUser));
syncUserInUserStore(previousEmail, nextUser);
// Falls sich die E-Mail geaendert hat, verschieben wir bestehende Anmeldungen auf die neue E-Mail.
// Falls sich die E-Mail geändert hat, verschieben wir bestehende Anmeldungen auf die neue E-Mail.
migrateRegistrationEmail(previousEmail, nextUser.email);
passwortInput.value = '';
@ -398,19 +469,37 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Ermittelt angemeldete Events ueber 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. Zaehler.
// Rendert angemeldete Events inkl. Zähler.
function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user);
myEventsCount.textContent = String(hostedEvents.length);
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.',
@ -419,30 +508,99 @@ document.addEventListener('DOMContentLoaded', () => {
}, 'hosting');
}
// Rendert angemeldete Events inkl. Zaehler.
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);
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');
}, '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);
}
}
// Baut die Eventkarten fuer beide Listen in einheitlichem Markup.
function renderEventCards(container, events, emptyStateConfig, mode) {
// 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, 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" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
<a class="empty-state-link button-primary" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
`;
container.appendChild(emptyElement);
return;
@ -452,26 +610,81 @@ document.addEventListener('DOMContentLoaded', () => {
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)
? `
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>
`
: '';
const actionMarkup = mode === 'registrations'
? `
<div class="profile-event-actions">
<button class="profile-unregister-btn" type="button" data-unregister-id="${event.id}">Abmelden</button>
</div>
`
: `
<div class="profile-event-actions">
<button class="profile-cancel-btn" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
`;
} 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>
@ -486,20 +699,27 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Gibt true zurueck, wenn ein Event innerhalb der naechsten 12 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 twelveHoursInMs = 12 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twelveHoursInMs;
// 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 fuer ISO- und lokalisierte Datumsformate aus den Eventdaten.
// Parse für ISO- und lokalisierte Datumsformate aus den Eventdaten.
function parseEventDateTime(event) {
if (!event?.date) {
return null;
@ -517,28 +737,42 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
@ -553,22 +787,51 @@ document.addEventListener('DOMContentLoaded', () => {
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
// Formatiert ein Eventdatum konsistent fuer die Profilkarten.
// Formatiert ein Eventdatum konsistent für die Profilkarten.
function formatEventDate(dateString) {
if (!dateString) {
return 'Kein Datum';
}
// ISO Format: 2026-02-12
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-');
const monthLabel = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1];
const monthLabel = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'][Number(month) - 1];
return `${Number(day)}. ${monthLabel} ${year}`;
}
return dateString;
// Format: 12. FEB. 2026
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (match) {
const day = match[1];
const month = match[2];
const shortMonthMap = {
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'
};
return `${day}. ${shortMonthMap[month] || month} ${match[3]}`;
}
// Vereinheitlicht die Zeitanzeige fuer die Profilseite.
return dateString;
}
// Vereinheitlicht die Zeitanzeige für die Profilseite.
function formatEventTime(timeString) {
if (!timeString) {
return 'Keine Uhrzeit';
@ -577,7 +840,7 @@ document.addEventListener('DOMContentLoaded', () => {
return timeString.includes('UHR') ? timeString.replace('UHR', 'Uhr').trim() : timeString;
}
// Normalisiert Vergleichswerte fuer robuste String-Matches.
// Normalisiert Vergleichswerte für robuste String-Matches.
function normalizeText(value) {
return String(value || '').trim().toLowerCase();
}

View File

@ -1,16 +1,18 @@
// =============================================
// =============================================
// Dynamische Navigation
// Je nach Login-Status wird die Kopfzeile fuer
// Je nach Login-Status wird die Kopfzeile für
// alle Seiten mit passendem Markup aufgebaut.
// =============================================
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 frueh, falls auf einer Seite keine Hauptnavigation vorhanden ist.
if (!navContainers.length) {
// Beendet früh, falls auf einer Seite keine Hauptnavigation vorhanden ist.
if (!navcontainers.length) {
return;
}
@ -25,10 +27,118 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
// Baut die Navigation fuer ausgeloggte Besucher.
// Logout-Funktion
window.logout = function() {
localStorage.removeItem(CURRENT_USER_KEY);
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';
const signupIsActive = currentPage === 'signup.html';
const isIndex = currentPage === 'index.html' || currentPage === '';
// Auf der Startseite, Login und Signup nur Login anzeigen.
if (isIndex || loginIsActive || signupIsActive) {
return `
<a
class="button-small"
href="login.html"
aria-label="Login"
>
Login
</a>
`;
}
return `
<a
@ -50,20 +160,63 @@ document.addEventListener('DOMContentLoaded', () => {
`;
}
// Baut die Navigation fuer eingeloggte Benutzer.
function buildLoggedInNavigation() {
// Baut die Navigation für eingeloggte Benutzer.
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 class="nav-tab" href="event_overview.html">Event finden</a>
<a class="nav-tab" href="event_create.html">Event erstellen</a>
<a class="button-small" href="my_profil.html" aria-label="Mein Profil">Mein Profil</a>
<a
class="nav-tab${isEventCreate ? ' nav-tab--active' : ''}"
href="event_create.html"
${isEventCreate ? 'aria-current="page"' : ''}
>
Event erstellen
</a>
<a
class="nav-tab${isEventOverview ? ' nav-tab--active' : ''}"
href="event_overview.html"
${isEventOverview ? 'aria-current="page"' : ''}
>
Event finden
</a>
<button
class="button-small logout-button"
onclick="logout()"
aria-label="Logout"
>
Logout
</button>
<a
class="profile-pill"
href="my_profil.html"
aria-label="Mein Profil"
title="${user.vorname || 'Profil'}"
>
${initial}
${notificationMarkup}
</a>
`;
}
async function initNavigation() {
const currentUser = getCurrentUser();
const nextMarkup = currentUser ? buildLoggedInNavigation() : buildLoggedOutNavigation();
let 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 => {
navcontainers.forEach(container => {
container.innerHTML = nextMarkup;
});
}
initNavigation();
});

View File

@ -1,4 +1,4 @@
// =============================================
// =============================================
// Signup-Logik
// Diese Datei validiert das Formular, speichert
// neue Benutzer lokal und startet direkt die Session.
@ -31,7 +31,7 @@ function setStoredUsers(users) {
localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users));
}
// Speichert den aktiven Benutzer fuer nachfolgende Seiten.
// Speichert den aktiven Benutzer für nachfolgende Seiten.
function setCurrentUser(user) {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user));
}
@ -42,78 +42,84 @@ function openWelcomeModal() {
document.body.style.overflow = 'hidden';
}
// Funktion zum Schließen des Welcome Modals
// Funktion zum Schliessen des Welcome Modals
function closeWelcomeModal() {
welcomeModal.classList.remove('show');
document.body.style.overflow = 'auto';
window.location.href = 'event_overview.html';
}
// Hauptfunktion fuer Formularvalidierung und Speicherung.
// 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) {
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 gueltig ist:
// 1) auf doppelte E-Mail pruefen
if (passwortValue.length < 8) {
passwortGroup.classList.add('has-error');
document.getElementById('passwortError').textContent = 'Dein Passwort muss mindestens 8 Zeichen lang sein.';
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);
@ -121,6 +127,7 @@ function validateForm(event) {
if (emailAlreadyUsed) {
emailGroup.classList.add('has-error');
document.getElementById('emailError').textContent = 'Diese E-Mail ist bereits registriert. Bitte nutze den Login.';
emailInput.focus();
return;
}
@ -138,11 +145,7 @@ function validateForm(event) {
setCurrentUser(newUser);
openWelcomeModal();
// Hier koennte spaeter ein echter API-Call zum Backend stehen.
// Weiterleitung zur Event-Overview-Seite.
window.location.href = 'event_overview.html';
}
// Weiterleitung erfolgt beim Klick auf "Weiter zu den Events".
}
// Fehlerbehandlung bei Input-Änderung (entfernt Fehler wenn Benutzer korrigiert)
@ -174,7 +177,7 @@ passwortInput.addEventListener('input', function() {
}
});
// Modal schließen wenn außerhalb geklickt wird
// Modal schliessen wenn ausserhalb geklickt wird
welcomeModal.addEventListener('click', function(event) {
if (event.target === welcomeModal) {
closeWelcomeModal();

View File

@ -3,62 +3,76 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Social Cooking</title>
<title>Invité | Login</title>
<!-- Stylesheet für diese Seite-->
<!-- 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>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--active" href="login.html" aria-label="Login" aria-current="page">Login</a>
<a class="button-small auth-nav-button auth-nav-button--default" href="signup.html" aria-label="Signup">Signup</a>
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<div class="container">
<div class="image-section">
<img src="assets/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">Login</button>
<button class="button-primary margin-bottom-24">Login</button>
<div class="signup-hint">
Du hast noch keinen Account? <a href="signup.html">Hier geht es zur Anmeldung.</a>
<div class="link-text">
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">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -19,50 +19,65 @@
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
<button id="logout-button" class="button-small profile-logout" type="button">
Logout
</button>
<a class="button-small" href="login.html" aria-label="Login">
Login
</a>
</nav>
</div>
</header>
<main class="container profile-page">
<main class="layout-wide">
<section class="profile-hero" aria-label="Profilübersicht">
<div>
<p class="profile-kicker">Mein Bereich</p>
<h1 id="profile-headline">Mein Profil</h1>
<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>
<button id="logout-button" class="button-small profile-logout" type="button">Logout</button>
</section>
<section id="logged-out-state" class="profile-panel hidden" aria-live="polite">
<h2 class="panel-title">Du bist noch nicht eingeloggt</h2>
<p>Melde dich an, damit wir deine Events und Anmeldungen anzeigen können.</p>
<div class="profile-cta-row">
<a class="button" href="login.html">Zum Login</a>
<a class="button profile-button-secondary" href="signup.html">Konto erstellen</a>
<a class="button-primary" href="login.html">Zum Login</a>
<a class="button-primary profile-button-secondary" href="signup.html">Konto erstellen</a>
</div>
</section>
<section id="logged-in-content" class="profile-grid">
<nav class="profile-tabs" aria-label="Profilbereiche">
<button type="button" class="profile-tab is-active" data-profile-tab="hosting">Hosting</button>
<button type="button" class="profile-tab" data-profile-tab="teilnehmen">Teilnehmen</button>
<button type="button" class="profile-tab" data-profile-tab="einstellungen">Einstellungen</button>
<nav class="category-items" aria-label="Profilbereiche">
<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>
<article class="profile-panel" data-profile-panel="hosting">
<article data-profile-panel="hosting">
<div class="panel-head">
<h2 class="panel-title">Meine Events</h2>
<span id="my-events-count" class="panel-count">0</span>
</div>
<div id="my-events-list" class="profile-card-list"></div>
</article>
<article class="profile-panel hidden" data-profile-panel="teilnehmen">
<article data-profile-panel="teilnehmen">
<div class="panel-head">
<h2 class="panel-title">Meine Anmeldungen</h2>
<span id="my-registrations-count" class="panel-count">0</span>
</div>
<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>
@ -70,39 +85,108 @@
<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>
</div>
<button class="button" type="submit">Profil speichern</button>
<button class="button-primary" type="submit">Profil speichern</button>
<p id="profile-feedback" class="profile-feedback" aria-live="polite"></p>
</form>
</article>
</section>
</main>
<!-- Logout Confirmation Modal -->
<div id="logoutModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<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>
<button class="button-primary" type="button" onclick="confirmLogout()">Abmelden</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
<button type="button" class="modal-close" id="unregister-modal-close" aria-label="Popup schließen">&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>
<!-- Event-Absage Confirmation Modal -->
<div id="cancelEventModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<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>
<button class="button-primary-abmelden" type="button" id="confirmCancelEventBtn";>Event absagen</button>
</div>
</div>
</div>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<script src="js/my_profil.js"></script>
</body>
</html>

View File

@ -3,12 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kontaktseite - Invité</title>
<title>Invité | Registration</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/login_signup.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/login_signup.css">
<script src="js/navigation.js" defer></script>
</head>
@ -20,19 +20,14 @@
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small auth-nav-button auth-nav-button--default" href="login.html" aria-label="Login">Login</a>
<a class="button-small auth-nav-button auth-nav-button--active" href="signup.html" aria-label="Signup" aria-current="page">Signup</a>
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<div class="container">
<div class="image-section">
<img src="assets/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>
@ -40,57 +35,80 @@
<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">Konto erstellen</button>
<button type="submit" class="button-primary margin-bottom-24">Konto erstellen</button>
<div class="login-hint">
<div class="link-text">
Du hast bereits einen Account? <a href="login.html">Hier geht es zum Login.</a>
</div>
</form>
</div>
</div>
</div> <!-- Schließt main-content -->
<div class="image-section">
<img src="assets/index_cooking.jpg" alt="Social Cooking">
</div>
</div>
</div> <!-- Schliesst container -->
<!-- Welcome Modal -->
<div id="welcomeModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Konto erfolgreich erstellt!</h2>
<button class="close-btn" onclick="closeWelcomeModal()">&times;</button>
<h2>🎉 Willkommen bei Invité!</h2>
</div>
<div class="modal-body">
Hier findest du die Übersicht zu den aktuellsten Events.
Willkommen bei Invité! Dein Account wurde erfolgreich erstellt. Entdecke jetzt die neuesten Events in deiner Nähe.
</div>
<div class="modal-footer">
<button class="btn-primary" onclick="closeWelcomeModal()">Weiter zu den Events</button>
<button class="button-primary" onclick="closeWelcomeModal()">Weiter zu den Events</button>
</div>
</div>
</div>
<script src="js/signup.js"></script>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>