Compare commits

...

5 Commits

21 changed files with 768 additions and 289 deletions

View File

@ -14,16 +14,9 @@
box-sizing: border-box;
}
.event-create-page {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
padding: var(--space-5) 0 0;
}
.event-flow-header {
display: flex;
justify-content: flex-end;
margin-bottom: var(--space-4);
justify-content: flex-start;
}
.event-form {
@ -34,7 +27,6 @@
.step {
display: none;
padding: var(--space-4) 0 var(--space-4);
}
.submission-success {
@ -46,10 +38,11 @@
}
.step-layout {
width: min(100%, var(--content-width));
margin: 0 auto;
display: grid;
gap: var(--space-40);
gap: 80px;
}
.startseite {
display: flex;
}
.step-layout--intro {
@ -80,13 +73,13 @@ fieldset {
margin: 0;
padding: 0;
border: 0;
gap: var(--space-3);
gap: var(--space-16);
}
.step-text {
/* definiert Breite des Beschriebtexts der einzelnen Schritte*/
max-width: 600px;
max-width: 100%;
}
.intro-card,
@ -121,21 +114,6 @@ fieldset {
}
label {
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
}
legend {
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-3);
}
.field-hint {
margin: -0.25rem 0 0;
color: var(--color-muted);
@ -203,7 +181,7 @@ textarea:focus {
.option-grid {
display: grid;
gap: var(--space-3);
gap: var(--space-16);
}
.option-card {
@ -243,7 +221,7 @@ textarea:focus {
.counter {
display: inline-flex;
align-items: center;
gap: var(--space-3);
gap: var(--space-16);
}
.counter input {
@ -264,7 +242,7 @@ textarea:focus {
.review-card--success {
display: grid;
gap: var(--space-5);
padding: var(--space-3) 0 0;
padding: var(--space-16) 0 0;
border: 0;
border-radius: 0;
background: transparent;
@ -349,9 +327,9 @@ textarea:focus {
.progress-label {
flex-shrink: 0;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
font-size: 1rem;
font-weight: 400;
color: var(--black);
white-space: nowrap;
}
@ -377,9 +355,8 @@ textarea:focus {
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;
width: 100%;
}
.flow-actions-right {
@ -464,11 +441,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,
@ -484,7 +461,6 @@ textarea:focus-visible {
@media (min-width: 768px) {
.step-layout--intro {
width: min(100%, 56rem);
grid-template-columns: 1fr 1fr;
align-items: stretch;
gap: var(--space-8);

View File

@ -1,11 +1,7 @@
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
/* ---------------------------------------------------------
Shared Typography Tokens
Reuse common text styles across nav, controls and buttons
--------------------------------------------------------- */
.meta-filter select,
.meta-filter input[type="date"],
.detail-primary-btn {
@ -42,15 +38,19 @@
Overview Header + Filters
--------------------------------------------------------- */
.filter-label {
margin: 0 0 var(--space-1);
letter-spacing: var(--ls-la);
font-family: var(--font-main);
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-1);
}
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
gap: var(--space-16);
margin-bottom: var(--space-2);
}
@ -135,7 +135,7 @@
.event-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: var(--space-16);
}
.event-card {
@ -228,7 +228,7 @@
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
gap: var(--space-16);
padding-top: 36px;
flex-shrink: 0;
}
@ -250,15 +250,14 @@
.event-spots-full, .detail-spots-pill-full {
/* Sold-out visual state, intentionally high-contrast and always filled. */
border: 1.5px solid var(--tomato);
border: 1.5px solid var(--tomato-light);
padding: var(--space-01) var(--space-4);
border-radius: var(--radius-pill);
color: var(--butter-light);
background: var(--tomato);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
opacity: 0.6;
cursor: not-allowed;
}
@ -358,7 +357,7 @@
.detail-side-stack {
grid-area: side;
display: grid;
gap: var(--space-3);
gap: var(--space-16);
align-content: start;
}
@ -400,7 +399,7 @@
/* Editorial mosaic: first image spans two rows, side images stack vertically. */
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(2, minmax(220px, 1fr));
gap: var(--space-3);
gap: var(--space-16);
min-height: 520px;
}
@ -649,13 +648,16 @@
color: var(--olive);
}
/*
.detail-spots-pill-full {
border-color: var(--tomato);
color: var(--tomato);
opacity: 1;
font-weight: 600;
}*/
border: 1.5px solid var(--tomato-light);
color: var(--butter-light);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
cursor: not-allowed;
}
.detail-action-btn-wrap {
display: flex;
@ -717,11 +719,6 @@
box-shadow: 0 2px 6px rgba(102, 52, 13, 0.22);
}
.detail-primary-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.detail-primary-btn-own,
.detail-primary-btn-own:disabled {
border-color: var(--olive-light);

View File

@ -6,7 +6,6 @@
/* --- Navigation overrides (index-specific) --- */
.nav-link {
border: 2px solid var(--olive-light);
transition: background-color 0.2s ease, color 0.2s ease;
@ -36,10 +35,9 @@
/* --- Page layout --- */
.main-content {
width: min(100% - 4rem, 1120px);
.container {
width: min(100% - 4rem, 1200px);
margin: 0 auto;
padding: 0 20px;
}
@ -48,19 +46,10 @@
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 55px;
margin-bottom: 88px;
align-items: center;
padding: 44px 0;
gap: 80px;
margin-bottom: 100px;
}
/*
.hero__buttons {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
}*/
.hero__right {
display: flex;
align-items: center;
@ -103,7 +92,7 @@
/* --- "So funktioniert's" steps --- */
.how-it-works {
margin-bottom: 70px;
margin-bottom: 100px;
}
.how-it-works__header {
@ -254,6 +243,7 @@
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;
@ -391,4 +381,133 @@
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}
/* --- FAQ Section: Akkordion --- */
.faq-section {
padding: var(--space-8) var(--space-4);
margin: var(--space-8) 0 var(--space-5);
}
.faq-section h2 {
text-align: center;
margin-bottom: var(--space-5);
color: var(--brown);
}
.faq-accordion {
width: 100%;
max-width: 56rem;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.faq-item {
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--white);
transition: background-color 0.2s ease, box-shadow var(--shadow-interaction);
}
.faq-item:hover {
box-shadow: var(--shadow-interaction);
}
.faq-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: transparent;
border: none;
cursor: pointer;
font-size: 1.25rem;
font-weight: 400;
color: var(--olive);
text-align: left;
transition: background-color 0.2s ease;
font-family: var(--font-main);
}
.faq-trigger:hover {
background-color: var(--butter-light);
}
.faq-trigger:focus-visible {
outline: 2px solid var(--olive);
outline-offset: -2px;
}
.faq-title {
flex: 1;
font-weight: 600;
}
.faq-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
font-size: 1.5rem;
font-weight: 300;
color: var(--olive);
transition: transform 0.3s ease;
flex-shrink: 0;
}
.faq-trigger[aria-expanded="true"] .faq-icon {
transform: rotate(45deg);
}
.faq-content {
padding: 0 var(--space-4);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
font-size: 1.125rem;
line-height: 1.7;
color: var(--black);
font-family: var(--font-main);
}
.faq-content p {
margin: 0;
padding: var(--space-3) 0;
}
.faq-trigger[aria-expanded="true"] + .faq-content {
display: block;
max-height: 500px;
padding: var(--space-3) var(--space-4);
}
/* --- Responsive: FAQ Section --- */
@media (max-width: 768px) {
.faq-section {
padding: var(--space-40) var(--space-4);
margin: var(--space-40) 0 var(--space-5);
}
.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

@ -7,7 +7,7 @@
/* --- Page layout --- */
.main-content {
.container {
display: flex;
justify-content: center;
align-items: center;
@ -127,7 +127,7 @@ button[type="submit"]:active {
.info-box {
background-color: var(--butter-light);
border-left: 4px solid var(--olive);
padding: var(--space-3);
padding: var(--space-16);
margin-bottom: var(--space-40);
border-radius: 4px;
font-size: 0.9rem;
@ -232,7 +232,7 @@ button[type="submit"]:active {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: var(--space-3) var(--space-5);
padding: var(--space-16) var(--space-5);
border: none;
margin-top: 40px;
}
@ -284,7 +284,7 @@ button[type="submit"]:active {
transform: translateX(-50%) translateY(100px);
background: var(--olive);
color: var(--white);
padding: var(--space-3) var(--space-40);
padding: var(--space-16) var(--space-40);
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 600;

View File

@ -1,9 +1,4 @@
.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 {
@ -44,6 +39,22 @@
gap: var(--space-4);
}
.btn-count {
color: var(--black);
background: var(--tomato-light);
height: 32px;
width: 32px;
margin-right: -18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
}
.category-item.is-active .btn-count {
background: var(--butter-light);
}
/* Konsistentes Karten-Layout für alle Profilsektionen. */
.profile-panel {
@ -53,27 +64,10 @@
}
.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 {
@ -100,7 +94,7 @@
}
.profile-event-card-clickable:hover {
transform: translateY(-1px);
transform: translateY(-3px);
}
.profile-event-title h3{
@ -191,11 +185,11 @@
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
gap: var(--space-16);
}
.form-group {
margin-bottom: var(--space-3);
margin-bottom: var(--space-16);
}
.form-group label {
@ -250,7 +244,7 @@
.profile-cta-row {
display: flex;
gap: var(--space-2);
margin-top: var(--space-3);
margin-top: var(--space-16);
}
.profile-button-secondary {
@ -262,7 +256,7 @@
}
@media (max-width: 48rem) {
.profile-page {
.container {
padding-top: 5.5rem;
}

View File

@ -18,7 +18,8 @@
--tomato: #D44B24;
--tomato-dark: #D44B24;
--tomato-dark: #B53A18;
--tomato-light: #E5937C;
--olive: #6B6B05;
--olive-dark: #545404;
--olive-light: #C8CC7A;
@ -44,7 +45,7 @@
--space-01: 0.375rem; /* 6px */
--space-1: 0.5rem; /* 8px */
--space-2: 0.75rem; /* 12px */
--space-3: 1rem; /* 16px */
--space-16: 1rem; /* 16px */
--space-20: 1.25rem; /* 20px */
--space-4: 1.5rem; /* 24px */
--space-5: 2rem; /* 32px */
@ -124,6 +125,33 @@ p {
margin-bottom: 0rem;
}
.label-input-field {
font-family: var(--font-main);
font-size: 1.125rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-16);
}
label {
font-family: var(--font-main);
font-size: 1.125rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-0);
}
.option {
font-family: var(--font-main);
font-size: 1rem;
font-weight: 600;
letter-spacing: var(--ls-sm);
line-height: 1;
margin-bottom: var(--space-16);
}
.link-text a{
color: var(--blue);
margin-top: var(--space-4);
@ -167,7 +195,7 @@ p {
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
gap: var(--space-16);
padding-top: 36px;
flex-shrink: 0;
}
@ -188,16 +216,38 @@ p {
}
/* Layout */
.main-content {
margin-top: var(--space-8);
.layout-wide {
width: 75%;
max-width: 1200px;
margin: 82px auto 0 auto;
}
.layout-narrow {
width: 55%;
max-width: 900px;
margin: 82px auto 0 auto;
}
/*
.container {
width: 90%;
width: 80%;
max-width: 75rem;
margin: 0 auto;
}
*/
/* Media Queries (Responsive) */
@media (max-width: 48rem) {
.container {
width: 95%;
}
.nav {
flex-direction: column;
}
}
.icon {
width: 20px;
@ -436,7 +486,8 @@ p {
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.counter-button:hover, .counter-button:focus-visible {
.counter-button:hover,
.counter-button:focus-visible {
background-color: var(--olive-dark);
border-color: var(--olive-dark);
}
@ -456,28 +507,30 @@ p {
transition: background-color 0.2s ease, color 0.2s ease;
}
.category-item:hover, .category-item:focus-visible {
.category-item:hover,
.category-item:focus-visible,
.category-item.is-active,
.category-item.active {
background: var(--tomato);
color: var(--butter-light);
}
.category-item.is-active, .category-item.active {
background: var(--tomato);
color: var(--butter-light);
.category-items {
display: inline-flex;
flex-wrap: wrap;
gap: var(--space-1);
}
.category-item-profile {
font-size: 1.25rem;
font-weight: 400;
line-height: 1;
padding: var(--space-1) var(--space-4);
}
.category-items {
height: 44px;
padding: 0 var(--space-4);
display: inline-flex;
flex-wrap: wrap;
gap: var(--space-1);
align-items: center;
gap: var(--space-16);
}
.button-small {
@ -490,7 +543,7 @@ p {
font-weight: 400;
line-height: 1;
border-radius: var(--radius-pill);
padding: 10px var(--space-3);
padding: 10px var(--space-16);
text-decoration: none;
cursor: pointer;
}
@ -676,7 +729,7 @@ p {
transform: translateX(-50%) translateY(100px);
background: var(--olive);
color: var(--white);
padding: var(--space-3) var(--space-40);
padding: var(--space-16) var(--space-40);
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 600;
@ -754,7 +807,7 @@ p {
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-top: 120px;
padding: var(--space-3) var(--space-7);
padding: var(--space-16) var(--space-7);
}
.footer-links {
@ -779,13 +832,3 @@ p {
gap: var(--space-4);
}
/* Media Queries (Responsive) */
@media (max-width: 48rem) {
.container {
width: 95%;
}
.nav {
flex-direction: column;
}
}

View File

@ -4,11 +4,11 @@
"title": "Italienische Tavolata",
"location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern",
"date": "11. APR. 2026",
"date": "22. APR. 2026",
"time": "15:30 UHR",
"category": "Dinner",
"diet": "Vegetarisch",
"spots": 6,
"spots": 8,
"host": {
"name": "Ferdinando",
"initial": "F"
@ -45,7 +45,7 @@
"title": "Noche Peruana",
"location": "Chur",
"address": "Obere Gasse 41, 7000 Chur",
"date": "16. Juni 2026",
"date": "12. April 2026",
"time": "19:00 UHR",
"category": "Dinner",
"diet": "Omnivore",

View File

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

View File

@ -35,11 +35,11 @@
<form id="eventForm" class="event-form" novalidate>
<section
class="step step--active step--intro"
class="step step--active step--intro layout-wide"
data-step="0"
aria-labelledby="intro-title"
>
<div class="step-layout step-layout--intro">
<div class="step-layout hero startseite">
<div class="step-copy">
<p class="badge">Event erstellen</p>
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
@ -52,17 +52,17 @@
</button>
</div>
<aside class="intro-card intro-card--image" aria-label="Stimmungsbild zur Event-Erstellung">
<div class="intro-card intro-card--image hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable.jpg"
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
/>
</aside>
</div>
</div>
</section>
<section class="step" data-step="1" aria-labelledby="step1-title">
<section class="step layout-narrow" data-step="1" aria-labelledby="step1-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 1</p>
@ -75,25 +75,25 @@
<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">
<label class="option-card option">
<input type="radio" name="eventType" value="Brunch" required />
<span>Brunch</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="radio" name="eventType" value="Lunch" />
<span>Lunch</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="radio" name="eventType" value="Kaffee + Kuchen" />
<span>Kaffee + Kuchen</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="radio" name="eventType" value="Dinner" />
<span>Dinner</span>
</label>
@ -101,7 +101,7 @@
</fieldset>
<fieldset class="form-field">
<legend>Maximale Personenanzahl</legend>
<label>Maximale Personenanzahl</label>
<div class="counter" data-counter>
<button
@ -135,7 +135,7 @@
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<section class="step layout-narrow" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 2</p>
@ -147,25 +147,25 @@
<div class="step-fields">
<fieldset class="form-field">
<legend>Ernährungsform</legend>
<label>Ernährungsform</label>
<div class="option-grid option-grid--4">
<label class="option-card">
<label class="option-card option">
<input type="checkbox" name="dietType" value="Fleisch" />
<span>Fleisch</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="checkbox" name="dietType" value="Fisch" />
<span>Fisch</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="checkbox" name="dietType" value="Vegetarisch" />
<span>Vegetarisch</span>
</label>
<label class="option-card">
<label class="option-card option">
<input type="checkbox" name="dietType" value="Vegan" />
<span>Vegan</span>
</label>
@ -180,7 +180,7 @@
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<section class="step layout-narrow" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 3</p>
@ -192,23 +192,23 @@
<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">
<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>
@ -221,7 +221,7 @@
</div>
</section>
<section class="step" data-step="4" aria-labelledby="step4-title">
<section class="step layout-narrow" data-step="4" aria-labelledby="step4-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 4</p>
@ -247,7 +247,7 @@
</div>
</section>
<section class="step" data-step="5" aria-labelledby="step5-title">
<section class="step layout-narrow" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 5</p>
@ -271,7 +271,7 @@
</div>
</section>
<section class="step" data-step="6" aria-labelledby="step6-title">
<section class="step layout-narrow" data-step="6" aria-labelledby="step6-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 6</p>
@ -305,7 +305,7 @@
</div>
</section>
<section class="step" data-step="7" aria-labelledby="step7-title">
<section class="step layout-narrow" data-step="7" aria-labelledby="step7-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 7</p>
@ -378,7 +378,7 @@
</section>
<div class="flow-footer" id="flowFooter" hidden>
<div class="flow-footer layout-narrow" id="flowFooter" hidden>
<p id="errorMessage" class="error-message" role="alert" aria-live="assertive"></p>

View File

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

View File

@ -11,7 +11,6 @@
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
</head>
<body>
@ -28,13 +27,14 @@
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container">
<main class="container layout-wide">
<!-- Page headline -->
<h1>Events</h1>
<p class="badge margin-bottom-40">Event finden</p>
<h1>Was darf es sein?</h1>
<!-- Filter section: category chips + location/date filters -->
<section class="filter-section">
<p class="filter-label">Was darf es sein?</p>
<p class="filter-label">Art des Essens / Eventtyp</p>
<div class="filter-row">
<!-- Primary category filter buttons -->
<div class="category-group">
@ -63,6 +63,7 @@
</div>
</div>
<p class="filter-label">Ernährungsform</p>
<div class="filter-row">
<!-- Primary category filter buttons -->
<div class="category-group">
@ -73,12 +74,13 @@
</div>
</div>
<p class="filter-label">Allergene</p>
<div class="filter-row">
<!-- Primary category filter buttons -->
<!-- Allergen filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-cat="Fleisch">glutenfrei</button>
<button class="category-item" type="button" data-cat="Fisch">laktosefrei</button>
<button class="category-item" type="button" data-cat="Vegetarisch">ohne Nüsse</button>
<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>
@ -92,6 +94,19 @@
<!-- 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>
<!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div>

View File

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

View File

@ -29,14 +29,14 @@
</div>
</header>
<main class="main-content">
<main class="container layout-wide">
<!-- Hero: uses .hero, .btn, .image-card, and .hero-image for a polished first impression -->
<section class="hero">
<div class="hero__left">
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1>
<p>Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p>
<a class="button-primary" href="signup.html">Anmelden</a>
<a class="button-primary" href="signup.html">Registrieren</a>
</div>
<div class="hero__right">
@ -149,6 +149,77 @@
<script src="js/index-carousel.js"></script>
<!-- FAQ Section: Akkordion mit häufig gestellten Fragen -->
<section class="faq-section">
<div class="container">
<h2>Häufig gestellte Fragen</h2>
<div class="faq-accordion">
<div class="faq-item">
<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">
<p><strong>Schritt 1: Kostenloses Konto erstellen</strong><br>Gehe auf Invité, klicke auf "Jetzt beitreten" und fülle das Anmeldeformular aus. Du benötigst nur deine E-Mail und ein Passwort.</p>
<p><strong>Schritt 2: Dein Profil ausfüllen</strong><br>Lade ein Profilfoto hoch, schreib ein bisschen über dich und gib deine Allergien/Ernährungspräferenzen an. Das hilft anderen, dich besser kennenzulernen.</p>
<p><strong>Schritt 3: Erkunde Events</strong><br>Browsing durch unsere Events, filtere nach Diät oder Allergie-Einstellungen, und melde dich zu den Events an, die dich interessieren!</p>
<p><strong>Schritt 4: Erstelle dein eigenes Event</strong><br>Du kannst auch selbst ein Kochevent hosten! Klick auf "Event erstellen", beschreib dein Menü, und lade Gäste ein.</p>
</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>
</div>
</section>
<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>
@ -165,5 +236,30 @@
<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>
</body>
</html>

View File

@ -1,11 +1,12 @@
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 detailcontainer = document.getElementById('detail-view');
const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg';
@ -50,6 +51,57 @@
}
}
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();
return (firstName || `${firstName} ${lastName}`.trim() || 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));
}
@ -114,7 +166,7 @@
}
const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000;
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart <= twentyfourHoursInMs;
}
@ -138,7 +190,7 @@
return { daysLeft, isClosed: false };
}
// Adresse ist nur im 12h-Fenster VOR Eventstart sichtbar.
// Adresse ist nur im 24h-Fenster VOR Eventstart sichtbar.
function isAddressVisibleWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
@ -146,7 +198,7 @@
}
const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000;
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
}
@ -212,7 +264,7 @@
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);
@ -283,16 +335,26 @@
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);
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 galleryImages = Array.isArray(event.gallery)
? event.gallery.filter(Boolean)
: [];
const galleryMarkup = galleryImages.length > 0
? `
<div class="detail-gallery detail-gallery-large">
${galleryImages.slice(0, 9).map((img, index) => `
<button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image">
</button>
`).join('')}
</div>
`
: '';
const visibleParticipants = participants.slice(0, 6);
const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length);
const 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);
@ -331,7 +393,12 @@
<p>${event.address}</p>
</article>
`
: '';
: `
<article class="detail-panel">
<h2 class="detail-section-title">Adresse</h2>
<p>Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.</p>
</article>
`;
const detailChips = [
`<span class="event-tag">${eventCategory}</span>`,
...event.diet.split(', ').filter(d => d.trim() && d !== 'Keine Angabe').map(d => `<span class="event-tag">${getDietLabel(d.trim())}</span>`),
@ -340,7 +407,7 @@
// Render complete detail page layout including:
// hero metadata, host card, menu, participants, gallery and sticky action bar.
detailContainer.innerHTML = `
detailcontainer.innerHTML = `
<section class="detail-hero">
<div class="detail-top-row">
@ -400,13 +467,7 @@
${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">
@ -430,25 +491,34 @@
<strong>${event.title}</strong>
</div>
<div class="detail-action-buttons">
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze frei`}
</span>
<div class="detail-action-btn-wrap">
<button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
</button>
${isRegistered && deregInfo.daysLeft !== null ? `
<small class="detail-dereg-hint${deregInfo.isClosed ? ' detail-dereg-hint--closed' : ''}">
${deregInfo.isClosed
? 'Abmeldefrist abgelaufen'
: deregInfo.daysLeft === 1
? 'Noch 1 Tag zur Abmeldung'
: `Noch ${deregInfo.daysLeft} Tage zur Abmeldung`}
</small>
` : ''}
</div>
</div>
<div class="detail-action-btn-wrap">
${isFull ? `
<button
class="detail-primary-btn detail-spots-pill-full"
type="button"
disabled>
Ausgebucht
</button>
` : `
<button
class="detail-primary-btn${actionButtonVariantClass}"
type="button"
data-register-button
${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
</button>
`}
${isRegistered && deregInfo.daysLeft !== null ? `
<small class="detail-dereg-hint${deregInfo.isClosed ? ' detail-dereg-hint--closed' : ''}">
${deregInfo.isClosed
? 'Abmeldefrist abgelaufen'
: deregInfo.daysLeft === 1
? 'Noch 1 Tag zur Abmeldung'
: `Noch ${deregInfo.daysLeft} Tage zur Abmeldung`}
</small>
` : ''}
</div>
</section>
<div class="detail-lightbox" aria-hidden="true">
@ -465,11 +535,11 @@
// 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]');
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.
if (registerButton && isOwnEvent) {
@ -530,9 +600,9 @@
}
// "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]');
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', () => {
@ -586,5 +656,12 @@
}
});
}
// Back button navigation: returns to event overview page.
if (backButton) {
backButton.addEventListener('click', () => {
window.location.href = 'event_overview.html';
});
}
}
});

View File

@ -1,7 +1,9 @@
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.
// -------------------------------------------------------------
@ -15,10 +17,13 @@
// -------------------------------------------------------------
// 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() {
@ -69,6 +74,57 @@
}
}
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();
return (firstName || `${firstName} ${lastName}`.trim() || 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));
}
@ -91,8 +147,13 @@
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';
}
@ -283,26 +344,54 @@
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 => {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
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 => {
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);
@ -310,6 +399,8 @@
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:
@ -353,10 +444,9 @@
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;
@ -475,10 +565,32 @@
});
}
// Category filter interactions.
// 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
if (activeDiets.has(dietValue)) {
activeDiets.delete(dietValue);
} else {
activeDiets.add(dietValue);
}
} else if (allergieValue !== null) {
// Allergie filter: toggle selection
if (activeAllergies.has(allergieValue)) {
activeAllergies.delete(allergieValue);
} else {
activeAllergies.add(allergieValue);
}
}
applyFilters();
});
});
@ -501,6 +613,40 @@
}
}
// 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 hasShownInfoModal = localStorage.getItem(INFO_MODAL_SHOWN_KEY);
if (!hasShownInfoModal) {
infoModal.classList.add('show');
localStorage.setItem(INFO_MODAL_SHOWN_KEY, 'true');
}
}
// Kick off initial load/render cycle.
fetchEvents();
});

View File

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

View File

@ -14,7 +14,9 @@
const profileTabPanels = Array.from(document.querySelectorAll('[data-profile-panel]'));
const myEventsCount = document.getElementById('my-events-count');
const myEventsBtnCount = document.getElementById('btn-my-events-count');
const myRegistrationsCount = document.getElementById('my-registrations-count');
const myRegistrationsBtnCount = document.getElementById('btn-my-registrations-count');
const myEventsList = document.getElementById('my-events-list');
const myRegistrationsList = document.getElementById('my-registrations-list');
@ -459,28 +461,37 @@
return events.filter(event => idSet.has(Number(event.id)));
}
// Rendert gehostete Events inkl. Zähler.
// Rendert angemeldete Events inkl. Zähler.
function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user);
myEventsCount.textContent = String(hostedEvents.length);
renderEventCards(myEventsList, hostedEvents, {
title: 'Noch kein eigenes Event',
text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.',
buttonLabel: 'Event erstellen',
href: 'event_create.html'
}, 'hosting');
const hostedEvents = getMyHostedEvents(events, user);
const count = hostedEvents.length;
myEventsCount.textContent = String(count);
if (myEventsBtnCount) myEventsBtnCount.textContent = String(count);
renderEventCards(myEventsList, hostedEvents, {
title: 'Noch kein eigenes Event',
text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.',
buttonLabel: 'Event erstellen',
href: 'event_create.html'
}, 'hosting');
}
// Rendert angemeldete Events inkl. Zähler.
function renderMyRegistrations(events, user) {
const registeredEvents = getMyRegisteredEvents(events, user);
myRegistrationsCount.textContent = String(registeredEvents.length);
renderEventCards(myRegistrationsList, registeredEvents, {
title: 'Noch keine Anmeldungen',
text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.',
buttonLabel: 'Events entdecken',
href: 'event_overview.html'
}, 'registrations');
const registeredEvents = getMyRegisteredEvents(events, user);
const count = registeredEvents.length;
myRegistrationsCount.textContent = String(count);
if (myRegistrationsBtnCount) myRegistrationsBtnCount.textContent = String(count);
renderEventCards(myRegistrationsList, registeredEvents, {
title: 'Noch keine Anmeldungen',
text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.',
buttonLabel: 'Events entdecken',
href: 'event_overview.html'
}, 'registrations');
}
// Baut die Eventkarten für beide Listen in einheitlichem Markup.
@ -538,7 +549,7 @@
});
}
// Gibt true zurück, wenn ein Event innerhalb der nächsten 12 Stunden startet.
// Gibt true zurück, wenn ein Event innerhalb der nächsten 24 Stunden startet.
function isAddressVisibleWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
@ -546,7 +557,7 @@
}
const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 12 * 60 * 60 * 1000;
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart >= 0 && msUntilStart <= twentyfourHoursInMs;
}

View File

@ -6,11 +6,11 @@
document.addEventListener('DOMContentLoaded', () => {
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const navContainers = document.querySelectorAll('.nav-tab-links');
const navcontainers = document.querySelectorAll('.nav-tab-links');
const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase();
// Beendet früh, falls auf einer Seite keine Hauptnavigation vorhanden ist.
if (!navContainers.length) {
if (!navcontainers.length) {
return;
}
@ -113,7 +113,7 @@ document.addEventListener('DOMContentLoaded', () => {
const nextMarkup = currentUser ? buildLoggedInNavigation(currentUser) : buildLoggedOutNavigation();
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
navContainers.forEach(container => {
navcontainers.forEach(container => {
container.innerHTML = nextMarkup;
});
});

View File

@ -26,7 +26,7 @@
</header>
<!-- Main Content -->
<div class="main-content">
<div class="container">
<div class="container">
<div class="image-section">
<img src="assets/index_cooking.jpg" alt="Social Cooking">
@ -56,7 +56,7 @@
</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">

View File

@ -29,7 +29,7 @@
</div>
</header>
<main class="container profile-page">
<main class="layout-wide">
<section class="profile-hero" aria-label="Profilübersicht">
<div>
<p class="badge">Mein Bereich</p>
@ -49,8 +49,13 @@
<section id="logged-in-content" class="profile-grid">
<nav class="category-items" aria-label="Profilbereiche">
<button type="button" class="category-item is-active category-item-profile" data-category-item="hosting">Meine Events</button>
<button type="button" class="category-item category-item-profile" data-category-item="teilnehmen">Meine Anmeldungen</button>
<button type="button" class="category-item is-active category-item-profile" data-category-item="hosting">
Meine Events <span class="btn-count" id="btn-my-events-count">0</span>
</button>
<button type="button"
class="category-item category-item-profile" data-category-item="teilnehmen">
Meine Anmeldungen <span class="btn-count" id="btn-my-registrations-count">0</span>
</button>
<button type="button" class="category-item category-item-profile" data-category-item="einstellungen">Profil-Einstellungen</button>
</nav>

View File

@ -26,7 +26,7 @@
</header>
<!-- Main Content -->
<div class="main-content">
<div class="container">
<div class="container">
<div class="image-section">
<img src="assets/index_cooking.jpg" alt="Social Cooking">
@ -73,7 +73,7 @@
</form>
</div>
</div>
</div> <!-- Schliesst main-content -->
</div> <!-- Schliesst container -->
<!-- Welcome Modal -->
<div id="welcomeModal" class="modal">