Adresse
+${event.address}
+From c3bea2817c2368cffc9487d69409ec9568e46ec4 Mon Sep 17 00:00:00 2001
From: viiivo <«vivien.vonburg@outlook.com»>
Date: Fri, 10 Apr 2026 18:05:46 +0200
Subject: [PATCH] feat: Enhance profile page with tab navigation and event
management features
Fix Event anmeldung und abmeldung Button.
12h vor Event beginn wird Adresse angezeigt.
---
css/event_overview.css | 73 +++++++++++++++--
css/my_profil.css | 129 +++++++++++++++++++++++++++++
data/events.json | 7 +-
js/event_create.js | 3 +-
js/event_detail.js | 148 +++++++++++++++++++++++++++++++--
js/event_overview.js | 173 +++++++++++++++++++++++++++++++++++++--
js/my_profil.js | 182 +++++++++++++++++++++++++++++++++++++----
my_profil.html | 12 ++-
8 files changed, 686 insertions(+), 41 deletions(-)
diff --git a/css/event_overview.css b/css/event_overview.css
index 0c3e203..5f99e94 100644
--- a/css/event_overview.css
+++ b/css/event_overview.css
@@ -266,7 +266,6 @@
}
.btn-primary {
- background: var(--olive);
color: #fffde8;
border: none;
border-radius: var(--radius-pill);
@@ -275,10 +274,44 @@
line-height: 1.3;
cursor: pointer;
white-space: nowrap;
+ transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
-.btn-primary:hover {
- filter: brightness(0.95);
+.btn-primary-register {
+ background: var(--olive);
+}
+
+.btn-primary-register:hover,
+.btn-primary-register:focus-visible {
+ background: #575704;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28);
+}
+
+.btn-primary-register:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 6px rgba(107, 107, 5, 0.25);
+}
+
+.btn-primary-danger {
+ background: var(--tomato);
+}
+
+.btn-primary-danger:hover,
+.btn-primary-danger:focus-visible {
+ background: var(--tomato-dark);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28);
+}
+
+.btn-primary-danger:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 6px rgba(188, 74, 52, 0.25);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
}
.btn-primary-own,
@@ -688,12 +721,42 @@
}
.detail-primary-btn {
- border: 2px solid var(--tomato);
border-radius: var(--radius-pill);
- background: var(--tomato);
color: var(--white);
padding: 10px 22px;
cursor: pointer;
+ transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.detail-primary-btn-register {
+ border: 2px solid var(--olive);
+ background: var(--olive);
+}
+
+.detail-primary-btn-register:not(:disabled):hover,
+.detail-primary-btn-register:not(:disabled):focus-visible {
+ background: #575704;
+ border-color: #575704;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28);
+}
+
+.detail-primary-btn-danger {
+ border: 2px solid var(--tomato);
+ background: var(--tomato);
+}
+
+.detail-primary-btn-danger:not(:disabled):hover,
+.detail-primary-btn-danger:not(:disabled):focus-visible {
+ background: var(--tomato-dark);
+ border-color: var(--tomato-dark);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28);
+}
+
+.detail-primary-btn:not(:disabled):active {
+ transform: translateY(0);
+ box-shadow: 0 2px 6px rgba(102, 52, 13, 0.22);
}
.detail-primary-btn:disabled {
diff --git a/css/my_profil.css b/css/my_profil.css
index 825504a..28c9e15 100644
--- a/css/my_profil.css
+++ b/css/my_profil.css
@@ -44,6 +44,38 @@
gap: var(--space-4);
}
+.profile-tabs {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+}
+
+.profile-tab {
+ border: 2px solid var(--olive);
+ border-radius: var(--radius-md);
+ background: var(--butter);
+ color: var(--black);
+ padding: 0.45rem 1rem;
+ min-height: 2.5rem;
+ font-family: "Jost", sans-serif;
+ font-size: 1rem;
+ font-weight: 500;
+ letter-spacing: var(--ls-ui);
+ cursor: pointer;
+ transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
+}
+
+.profile-tab:hover,
+.profile-tab:focus-visible {
+ background: #faf8e8;
+}
+
+.profile-tab.is-active {
+ border-color: transparent;
+ background: var(--olive);
+ color: var(--white);
+}
+
/* Konsistentes Karten-Layout fuer alle Profilsektionen. */
.profile-panel {
background: rgba(255, 255, 255, 0.88);
@@ -92,6 +124,16 @@
gap: var(--space-3);
}
+.profile-event-card-clickable {
+ cursor: pointer;
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
+}
+
+.profile-event-card-clickable:hover {
+ box-shadow: 0 6px 16px rgba(102, 52, 13, 0.14);
+ transform: translateY(-1px);
+}
+
.profile-event-title {
margin: 0;
color: var(--black);
@@ -106,6 +148,31 @@
color: var(--olive);
}
+.profile-event-address-block {
+ margin-top: 0.55rem;
+ padding: 0.6rem 0.75rem;
+ border-radius: var(--radius-sm);
+ border-left: 4px solid var(--tomato);
+ background: rgba(232, 237, 209, 0.65);
+}
+
+.profile-event-address-label {
+ margin: 0;
+ color: var(--olive);
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: var(--ls-label);
+ text-transform: uppercase;
+}
+
+.profile-event-address {
+ margin: 0.2rem 0 0;
+ font-size: 0.95rem;
+ color: var(--black);
+ font-weight: 600;
+ line-height: 1.35;
+}
+
.profile-event-link {
flex-shrink: 0;
color: var(--blue);
@@ -135,11 +202,44 @@
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 {
@@ -147,6 +247,35 @@
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;
diff --git a/data/events.json b/data/events.json
index c5d1502..7f84219 100644
--- a/data/events.json
+++ b/data/events.json
@@ -3,8 +3,9 @@
"id": 1,
"title": "Italienische Tavolata",
"location": "Luzern",
- "date": "19. MÄR. 2026",
- "time": "18:30 UHR",
+ "address": "Pilatusstrasse 18, 6003 Luzern",
+ "date": "11. APR. 2026",
+ "time": "3:30 UHR",
"category": "DINNER",
"diet": "VEGGIE",
"spots": 6,
@@ -43,6 +44,7 @@
"id": 2,
"title": "Noche Peruana",
"location": "Chur",
+ "address": "Obere Gasse 41, 7000 Chur",
"date": "11. APR. 2026",
"time": "19:00 UHR",
"category": "DINNER",
@@ -84,6 +86,7 @@
"id": 3,
"title": "Japanese Delight",
"location": "ZÜRICH",
+ "address": "Limmatquai 92, 8001 Zürich",
"date": "02. MAI. 2026",
"time": "12:30 UHR",
"category": "LUNCH",
diff --git a/js/event_create.js b/js/event_create.js
index e72d82d..d3cef97 100644
--- a/js/event_create.js
+++ b/js/event_create.js
@@ -429,7 +429,8 @@ function buildStoredEvent() {
? []
: getCheckboxValues("allergies").split(", ").filter(Boolean),
allergiesNote: form.elements.allergiesOther.value.trim(),
- participants: [usernameElement.textContent.trim() || "Host"],
+ // Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt.
+ participants: [],
gallery: [],
createdAt: new Date().toISOString(),
source: "local"
diff --git a/js/event_detail.js b/js/event_detail.js
index 53bb7e2..2ca06ac 100644
--- a/js/event_detail.js
+++ b/js/event_detail.js
@@ -52,6 +52,80 @@ document.addEventListener('DOMContentLoaded', async () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
+ function parseEventDateTime(event) {
+ if (!event?.date) {
+ return null;
+ }
+
+ const dateValue = String(event.date).trim();
+ const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
+ let year;
+ let month;
+ let day;
+
+ if (isoDateMatch) {
+ year = Number(isoDateMatch[1]);
+ month = Number(isoDateMatch[2]);
+ day = Number(isoDateMatch[3]);
+ } else {
+ const monthMap = {
+ JAN: 1,
+ FEB: 2,
+ 'MÄR': 3,
+ MRZ: 3,
+ APR: 4,
+ MAI: 5,
+ JUN: 6,
+ JUL: 7,
+ AUG: 8,
+ SEP: 9,
+ OKT: 10,
+ NOV: 11,
+ DEZ: 12
+ };
+ const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
+
+ if (!localizedMatch) {
+ return null;
+ }
+
+ day = Number(localizedMatch[1]);
+ month = monthMap[localizedMatch[2]];
+ year = Number(localizedMatch[3]);
+
+ if (!month) {
+ return null;
+ }
+ }
+
+ const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
+ const hours = timeMatch ? Number(timeMatch[1]) : 0;
+ const minutes = timeMatch ? Number(timeMatch[2]) : 0;
+
+ return new Date(year, month - 1, day, hours, minutes, 0, 0);
+ }
+
+ function isRegistrationClosedForEvent(event) {
+ const eventDateTime = parseEventDateTime(event);
+ if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
+ return false;
+ }
+
+ const msUntilStart = eventDateTime.getTime() - Date.now();
+ const twelveHoursInMs = 12 * 60 * 60 * 1000;
+
+ return msUntilStart <= twelveHoursInMs;
+ }
+
+ 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) {
@@ -71,6 +145,29 @@ document.addEventListener('DOMContentLoaded', async () => {
return Boolean(userFirstName && hostName && userFirstName === hostName);
}
+ // Prueft, ob der aktuelle Benutzer bereits in der Teilnehmerliste des Events steht.
+ function isUserListedInEventParticipants(event, user) {
+ if (!event || !user || !Array.isArray(event.participants)) {
+ return false;
+ }
+
+ const participantSet = new Set(
+ event.participants
+ .map(name => String(name || '').trim().toLowerCase())
+ .filter(Boolean)
+ );
+
+ const userFirstName = String(user.vorname || '').trim().toLowerCase();
+ const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`
+ .trim()
+ .toLowerCase();
+
+ return Boolean(
+ (userFirstName && participantSet.has(userFirstName))
+ || (userFullName && participantSet.has(userFullName))
+ );
+ }
+
// Fetch data source and resolve the matching event record.
try {
const response = await fetch('data/events.json');
@@ -157,19 +254,47 @@ document.addEventListener('DOMContentLoaded', async () => {
? event.gallery
: [event.image, event.image, event.image];
const visibleParticipants = participants.slice(0, 6);
- const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length);
+ const registrationMap = getRegistrationMap();
+ const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
+ const remainingParticipants = Math.max(0, participants.length + extraRegistrations - visibleParticipants.length);
const totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
- const confirmedGuests = participants.length;
+ const confirmedGuests = participants.length + extraRegistrations;
const freePlaces = Math.max(0, totalGuests - confirmedGuests);
const isFull = freePlaces === 0;
+ const isRegistrationClosed = isRegistrationClosedForEvent(event);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
- const registrationMap = getRegistrationMap();
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? registrationMap[currentUser.email].map(id => Number(id))
: [];
const isRegistered = userRegistrations.includes(Number(event.id));
- const actionButtonLabel = isOwnEvent ? 'Dein Event' : !currentUser ? 'Einloggen' : isRegistered ? 'Abmelden' : 'Anmelden';
- const actionButtonDisabled = isOwnEvent || (!isRegistered && isFull);
+ const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
+ const hasAddressAccess = isRegistered || isListedParticipant;
+ const actionButtonLabel = isOwnEvent
+ ? 'Dein Event!'
+ : !currentUser
+ ? 'Einloggen'
+ : isRegistered
+ ? 'Abmelden'
+ : isRegistrationClosed
+ ? 'Anmeldung geschlossen'
+ : 'Anmelden';
+ const actionButtonDisabled = isOwnEvent || (!isRegistered && (isFull || isRegistrationClosed));
+ const actionButtonVariantClass = isOwnEvent
+ ? ' detail-primary-btn-own'
+ : isRegistered
+ ? ' detail-primary-btn-danger'
+ : isRegistrationClosed
+ ? ' detail-primary-btn-danger'
+ : ' detail-primary-btn-register';
+ const shouldRevealAddress = Boolean(event.address) && isRegistrationClosed && hasAddressAccess;
+ const addressPanelMarkup = shouldRevealAddress
+ ? `
+ ${event.address}Adresse
+
Keine Treffer
+${emptyStateConfig.text}
+ ${emptyStateConfig.buttonLabel} + `; container.appendChild(emptyElement); return; } events.forEach(event => { const card = document.createElement('article'); - card.className = 'profile-event-card'; + card.className = 'profile-event-card profile-event-card-clickable'; + card.setAttribute('data-event-id', String(event.id)); + const addressMarkup = mode === 'registrations' && event.address + ? ` +Adresse
+${event.address}
+