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
+ ? `
+
+ Adresse
+ ${event.address}
+
+ `
+ : '';
const detailChips = [
`${eventCategory}`,
`${dietLabel}`,
@@ -227,6 +352,8 @@ document.addEventListener('DOMContentLoaded', async () => {
${remainingParticipants > 0 ? `+${remainingParticipants}` : ''}
+
+ ${addressPanelMarkup}
@@ -254,7 +381,7 @@ document.addEventListener('DOMContentLoaded', async () => {
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
-
@@ -280,6 +407,13 @@ document.addEventListener('DOMContentLoaded', async () => {
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) {
+ registerButton.disabled = true;
+ registerButton.textContent = 'Dein Event!';
+ registerButton.setAttribute('aria-disabled', 'true');
+ }
+
// Anmeldung toggeln und im lokalen Registrierungs-Store persistieren.
if (registerButton) {
registerButton.addEventListener('click', () => {
@@ -300,7 +434,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (registrationSet.has(Number(event.id))) {
registrationSet.delete(Number(event.id));
- } else if (!isFull) {
+ } else if (!isFull && !isRegistrationClosed) {
registrationSet.add(Number(event.id));
}
diff --git a/js/event_overview.js b/js/event_overview.js
index 15bf312..723a753 100644
--- a/js/event_overview.js
+++ b/js/event_overview.js
@@ -1,6 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
+ const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
@@ -55,6 +56,20 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
+ function getRegistrationMap() {
+ try {
+ const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY);
+ return stored ? JSON.parse(stored) : {};
+ } catch (error) {
+ console.error('Anmeldedaten konnten nicht gelesen werden.', error);
+ return {};
+ }
+ }
+
+ function setRegistrationMap(registrationMap) {
+ localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
+ }
+
// -------------------------------------------------------------
// Initial data bootstrap:
// 1) fetch JSON,
@@ -183,6 +198,83 @@ document.addEventListener('DOMContentLoaded', () => {
: `${timeString} Uhr`;
}
+ // Baut aus Eventdatum/-zeit ein Date-Objekt fuer Fristlogik und Vergleiche.
+ 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);
+ }
+
+ // Zaehlt eindeutige Registrierungen eines Events ueber alle Benutzer.
+ 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);
+ }
+
+ // Schliesst neue Anmeldungen ab 12h vor Start (inkl. bereits gestarteter Events).
+ 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;
+ }
+
// Safely verify whether a value exists in the given select element.
function hasOption(selectElement, value) {
return Array.from(selectElement.options).some(option => option.value === value);
@@ -202,6 +294,11 @@ document.addEventListener('DOMContentLoaded', () => {
});
const filtered = allEvents.filter(event => {
+ // Lokal erstellte Events werden nicht in der allgemeinen Event-Uebersicht angezeigt.
+ if (event.source === 'local') {
+ return false;
+ }
+
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date);
@@ -222,6 +319,10 @@ document.addEventListener('DOMContentLoaded', () => {
// - or event cards with status and metadata.
function renderEvents(events) {
eventGrid.innerHTML = '';
+ const registrationMap = getRegistrationMap();
+ const userRegistrationSet = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
+ ? new Set(registrationMap[currentUser.email].map(id => Number(id)))
+ : new Set();
if (events.length === 0) {
eventGrid.innerHTML = `
@@ -242,20 +343,28 @@ document.addEventListener('DOMContentLoaded', () => {
const card = document.createElement('article');
card.className = 'event-card';
card.style.cursor = 'pointer';
- card.onclick = () => {
+ card.addEventListener('click', clickedEvent => {
+ if (clickedEvent.target instanceof HTMLElement && clickedEvent.target.closest('button')) {
+ return;
+ }
+
window.location.href = `event_detail.html?id=${event.id}`;
- };
+ });
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
// Capacity logic:
// spots = total capacity, participants.length = booked seats.
- const bookedSeats = event.participants ? event.participants.length : 0;
+ const baseParticipants = Array.isArray(event.participants) ? event.participants.length : 0;
+ const extraRegistrations = countRegistrationsForEvent(registrationMap, event.id);
+ const bookedSeats = baseParticipants + extraRegistrations;
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);
// Build optional specification chips only when data exists.
const specsChips = event.specifications && event.specifications.length > 0
@@ -263,10 +372,16 @@ document.addEventListener('DOMContentLoaded', () => {
: '';
const actionMarkup = isOwnEvent
- ? 'Dein Event'
- : isFull
- ? ''
- : 'Anmelden';
+ ? 'Dein Event!'
+ : isRegistered
+ ? 'Abmelden'
+ : isRegistrationClosed
+ ? 'Anmeldung geschlossen'
+ : isFull
+ ? ''
+ : !currentUser
+ ? 'Anmelden'
+ : 'Anmelden';
card.innerHTML = `
@@ -290,6 +405,50 @@ document.addEventListener('DOMContentLoaded', () => {
`;
+ const actionButton = card.querySelector('[data-registration-action]');
+ if (actionButton) {
+ actionButton.addEventListener('click', clickEvent => {
+ clickEvent.stopPropagation();
+
+ const action = actionButton.getAttribute('data-registration-action');
+ if (action === 'own') {
+ return;
+ }
+
+ if (action === 'closed') {
+ return;
+ }
+
+ if (action === 'login') {
+ window.location.href = 'login.html';
+ return;
+ }
+
+ if (!currentUser?.email) {
+ window.location.href = 'login.html';
+ return;
+ }
+
+ const nextRegistrationMap = getRegistrationMap();
+ const currentIds = Array.isArray(nextRegistrationMap[currentUser.email])
+ ? nextRegistrationMap[currentUser.email].map(id => Number(id))
+ : [];
+ const idSet = new Set(currentIds);
+
+ if (action === 'unregister') {
+ idSet.delete(Number(event.id));
+ }
+
+ if (action === 'register' && !isFull && !isRegistrationClosed) {
+ idSet.add(Number(event.id));
+ }
+
+ nextRegistrationMap[currentUser.email] = Array.from(idSet);
+ setRegistrationMap(nextRegistrationMap);
+ applyFilters();
+ });
+ }
+
eventGrid.appendChild(card);
});
}
diff --git a/js/my_profil.js b/js/my_profil.js
index 9d7ee5f..9d47a5b 100644
--- a/js/my_profil.js
+++ b/js/my_profil.js
@@ -10,6 +10,8 @@ document.addEventListener('DOMContentLoaded', () => {
const profileHeadline = document.getElementById('profile-headline');
const profileSubline = document.getElementById('profile-subline');
const logoutButton = document.getElementById('logout-button');
+ const profileTabButtons = Array.from(document.querySelectorAll('[data-profile-tab]'));
+ const profileTabPanels = Array.from(document.querySelectorAll('[data-profile-panel]'));
const myEventsCount = document.getElementById('my-events-count');
const myRegistrationsCount = document.getElementById('my-registrations-count');
@@ -36,6 +38,7 @@ document.addEventListener('DOMContentLoaded', () => {
renderLoggedInState(currentUser);
bindFormHandlers();
+ activateProfileTab('hosting');
allEvents = await loadAllEvents();
renderMyEvents(allEvents, currentUser);
@@ -80,6 +83,11 @@ document.addEventListener('DOMContentLoaded', () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
+ // Schreibt die lokal erstellten Events in den Storage.
+ function setStoredEvents(events) {
+ localStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(events));
+ }
+
// Fuehrt JSON-Daten und lokal erstellte Events in einer Liste zusammen.
async function loadAllEvents() {
try {
@@ -119,6 +127,18 @@ document.addEventListener('DOMContentLoaded', () => {
function bindFormHandlers() {
profileForm.addEventListener('submit', handleProfileSubmit);
myRegistrationsList.addEventListener('click', handleRegistrationListClick);
+ myEventsList.addEventListener('click', handleHostedListClick);
+
+ profileTabButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const tabName = button.getAttribute('data-profile-tab');
+ if (!tabName) {
+ return;
+ }
+
+ activateProfileTab(tabName);
+ });
+ });
[vornameInput, nachnameInput, emailInput, passwortInput].forEach(input => {
input.addEventListener('input', () => {
@@ -133,6 +153,53 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+ // Reagiert auf Aktionen in der Liste "Meine Events" per Event Delegation.
+ function handleHostedListClick(event) {
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ const cancelButton = target.closest('[data-cancel-event-id]');
+ if (cancelButton && currentUser?.email) {
+ const eventId = Number(cancelButton.getAttribute('data-cancel-event-id'));
+ if (Number.isFinite(eventId)) {
+ cancelHostedEvent(eventId, currentUser.email);
+ }
+ return;
+ }
+
+ if (target.closest('a, button')) {
+ return;
+ }
+
+ const card = target.closest('[data-event-id]');
+ if (!card) {
+ return;
+ }
+
+ const eventId = Number(card.getAttribute('data-event-id'));
+ if (!Number.isFinite(eventId)) {
+ return;
+ }
+
+ window.location.href = `event_detail.html?id=${eventId}`;
+ }
+
+ // Schaltet den sichtbaren Profilbereich per Tabname um.
+ function activateProfileTab(tabName) {
+ profileTabButtons.forEach(button => {
+ const isActive = button.getAttribute('data-profile-tab') === tabName;
+ button.classList.toggle('is-active', isActive);
+ button.setAttribute('aria-selected', isActive ? 'true' : 'false');
+ });
+
+ profileTabPanels.forEach(panel => {
+ const isActive = panel.getAttribute('data-profile-panel') === tabName;
+ panel.classList.toggle('hidden', !isActive);
+ });
+ }
+
// Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation.
function handleRegistrationListClick(event) {
const target = event.target;
@@ -141,18 +208,68 @@ document.addEventListener('DOMContentLoaded', () => {
}
const unregisterButton = target.closest('[data-unregister-id]');
- if (!unregisterButton || !currentUser?.email) {
+ if (unregisterButton) {
+ if (!currentUser?.email) {
+ return;
+ }
+
+ const eventId = Number(unregisterButton.getAttribute('data-unregister-id'));
+ if (!Number.isFinite(eventId)) {
+ return;
+ }
+
+ unregisterFromEvent(eventId, currentUser.email);
return;
}
- const eventId = Number(unregisterButton.getAttribute('data-unregister-id'));
+ if (target.closest('a, button')) {
+ return;
+ }
+
+ const card = target.closest('[data-event-id]');
+ if (!card) {
+ return;
+ }
+
+ const eventId = Number(card.getAttribute('data-event-id'));
if (!Number.isFinite(eventId)) {
return;
}
- unregisterFromEvent(eventId, currentUser.email);
+ window.location.href = `event_detail.html?id=${eventId}`;
}
+
+ // Sagt ein gehostetes Event ab (aus eigener Profilansicht entfernen).
+ function cancelHostedEvent(eventId, userEmail) {
+ // Lokal erstellte, eigene Events werden direkt aus dem Storage geloescht.
+ const storedEvents = getStoredEvents();
+ const nextStoredEvents = storedEvents.filter(event => {
+ const isTargetEvent = Number(event.id) === eventId;
+ const isOwnedByUser = normalizeText(event.hostEmail || '') === normalizeText(userEmail)
+ || normalizeText(event.host?.name || '') === normalizeText(currentUser?.vorname || '');
+
+ return !(isTargetEvent && isOwnedByUser);
+ });
+ setStoredEvents(nextStoredEvents);
+
+ // Event-ID fuer alle Benutzer aus den Anmeldungen entfernen.
+ const registrationMap = getRegistrationMap();
+ Object.keys(registrationMap).forEach(email => {
+ const ids = Array.isArray(registrationMap[email])
+ ? registrationMap[email].map(id => Number(id)).filter(Number.isFinite)
+ : [];
+
+ registrationMap[email] = ids.filter(id => id !== eventId);
+ });
+ setRegistrationMap(registrationMap);
+
+ allEvents = allEvents.filter(event => Number(event.id) !== eventId);
+
+ 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) {
const registrationMap = getRegistrationMap();
@@ -260,15 +377,20 @@ document.addEventListener('DOMContentLoaded', () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map));
}
- // Ermittelt gehostete Events anhand Host-E-Mail oder Host-Vorname.
+ // Ermittelt gehostete Events aus lokal erstellten Daten des aktuellen Benutzers.
function getMyHostedEvents(events, user) {
- const userFirstName = normalizeText(user.vorname);
+ const userFirstName = normalizeText(user.vorname || '');
+ const userEmail = normalizeText(user.email || '');
return events.filter(event => {
+ if (event.source !== 'local') {
+ return false;
+ }
+
const hostEmail = normalizeText(event.hostEmail || '');
const hostName = normalizeText(event.host?.name || '');
- if (hostEmail && hostEmail === normalizeText(user.email)) {
+ if (hostEmail && hostEmail === userEmail) {
return true;
}
@@ -289,45 +411,73 @@ document.addEventListener('DOMContentLoaded', () => {
function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user);
myEventsCount.textContent = String(hostedEvents.length);
- renderEventCards(myEventsList, hostedEvents, 'Du hast noch kein eigenes Event erstellt.', false);
+ 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. Zaehler.
function renderMyRegistrations(events, user) {
const registeredEvents = getMyRegisteredEvents(events, user);
myRegistrationsCount.textContent = String(registeredEvents.length);
- renderEventCards(myRegistrationsList, registeredEvents, 'Du bist aktuell bei keinem Event angemeldet.', true);
+ 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 fuer beide Listen in einheitlichem Markup.
- function renderEventCards(container, events, emptyText, withUnregisterButton) {
+ function renderEventCards(container, events, emptyStateConfig, mode) {
container.innerHTML = '';
if (events.length === 0) {
- const emptyElement = document.createElement('p');
- emptyElement.className = 'profile-empty';
- emptyElement.textContent = emptyText;
+ const emptyElement = document.createElement('div');
+ emptyElement.className = 'profile-empty-state';
+ emptyElement.innerHTML = `
+ Keine Treffer
+ ${emptyStateConfig.title}
+ ${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}
+
+ `
+ : '';
- const actionMarkup = withUnregisterButton
+ const actionMarkup = mode === 'registrations'
? `
`
- : `Zum Event`;
+ : `
+
+ Event absagen
+
+ `;
card.innerHTML = `
${event.title}
${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}
+ ${addressMarkup}
${actionMarkup}
`;
diff --git a/my_profil.html b/my_profil.html
index f5610a9..da5bb7f 100644
--- a/my_profil.html
+++ b/my_profil.html
@@ -44,7 +44,13 @@
-
+
+
+
Meine Events
0
@@ -52,7 +58,7 @@
-
+
Meine Anmeldungen
0
@@ -60,7 +66,7 @@
-
+
Profil verwalten