feat: Enhance profile page with tab navigation and event management features

Fix Event anmeldung und abmeldung Button.

12h vor Event beginn wird Adresse angezeigt.
This commit is contained in:
viiivo 2026-04-10 18:05:46 +02:00
parent 1efa4dcd39
commit c3bea2817c
8 changed files with 686 additions and 41 deletions

View File

@ -266,7 +266,6 @@
} }
.btn-primary { .btn-primary {
background: var(--olive);
color: #fffde8; color: #fffde8;
border: none; border: none;
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
@ -275,10 +274,44 @@
line-height: 1.3; line-height: 1.3;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
} }
.btn-primary:hover { .btn-primary-register {
filter: brightness(0.95); 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, .btn-primary-own,
@ -688,12 +721,42 @@
} }
.detail-primary-btn { .detail-primary-btn {
border: 2px solid var(--tomato);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
background: var(--tomato);
color: var(--white); color: var(--white);
padding: 10px 22px; padding: 10px 22px;
cursor: pointer; 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 { .detail-primary-btn:disabled {

View File

@ -44,6 +44,38 @@
gap: var(--space-4); 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. */ /* Konsistentes Karten-Layout fuer alle Profilsektionen. */
.profile-panel { .profile-panel {
background: rgba(255, 255, 255, 0.88); background: rgba(255, 255, 255, 0.88);
@ -92,6 +124,16 @@
gap: var(--space-3); 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 { .profile-event-title {
margin: 0; margin: 0;
color: var(--black); color: var(--black);
@ -106,6 +148,31 @@
color: var(--olive); 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 { .profile-event-link {
flex-shrink: 0; flex-shrink: 0;
color: var(--blue); color: var(--blue);
@ -135,11 +202,44 @@
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 500; font-weight: 500;
cursor: pointer; 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:hover,
.profile-unregister-btn:focus-visible { .profile-unregister-btn:focus-visible {
background: var(--tomato-dark); 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 { .profile-empty {
@ -147,6 +247,35 @@
color: var(--black); 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 { .form-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;

View File

@ -3,8 +3,9 @@
"id": 1, "id": 1,
"title": "Italienische Tavolata", "title": "Italienische Tavolata",
"location": "Luzern", "location": "Luzern",
"date": "19. MÄR. 2026", "address": "Pilatusstrasse 18, 6003 Luzern",
"time": "18:30 UHR", "date": "11. APR. 2026",
"time": "3:30 UHR",
"category": "DINNER", "category": "DINNER",
"diet": "VEGGIE", "diet": "VEGGIE",
"spots": 6, "spots": 6,
@ -43,6 +44,7 @@
"id": 2, "id": 2,
"title": "Noche Peruana", "title": "Noche Peruana",
"location": "Chur", "location": "Chur",
"address": "Obere Gasse 41, 7000 Chur",
"date": "11. APR. 2026", "date": "11. APR. 2026",
"time": "19:00 UHR", "time": "19:00 UHR",
"category": "DINNER", "category": "DINNER",
@ -84,6 +86,7 @@
"id": 3, "id": 3,
"title": "Japanese Delight", "title": "Japanese Delight",
"location": "ZÜRICH", "location": "ZÜRICH",
"address": "Limmatquai 92, 8001 Zürich",
"date": "02. MAI. 2026", "date": "02. MAI. 2026",
"time": "12:30 UHR", "time": "12:30 UHR",
"category": "LUNCH", "category": "LUNCH",

View File

@ -429,7 +429,8 @@ function buildStoredEvent() {
? [] ? []
: getCheckboxValues("allergies").split(", ").filter(Boolean), : getCheckboxValues("allergies").split(", ").filter(Boolean),
allergiesNote: form.elements.allergiesOther.value.trim(), allergiesNote: form.elements.allergiesOther.value.trim(),
participants: [usernameElement.textContent.trim() || "Host"], // Host wird separat gefuehrt und nicht als angemeldeter Gast gezaehlt.
participants: [],
gallery: [], gallery: [],
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
source: "local" source: "local"

View File

@ -52,6 +52,80 @@ document.addEventListener('DOMContentLoaded', async () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); 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. // Ermittelt, ob das Event vom aktuell eingeloggten Benutzer erstellt wurde.
function isEventOwnedByCurrentUser(event, user) { function isEventOwnedByCurrentUser(event, user) {
if (!event || !user) { if (!event || !user) {
@ -71,6 +145,29 @@ document.addEventListener('DOMContentLoaded', async () => {
return Boolean(userFirstName && hostName && userFirstName === hostName); 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. // Fetch data source and resolve the matching event record.
try { try {
const response = await fetch('data/events.json'); const response = await fetch('data/events.json');
@ -157,19 +254,47 @@ document.addEventListener('DOMContentLoaded', async () => {
? event.gallery ? event.gallery
: [event.image, event.image, event.image]; : [event.image, event.image, event.image];
const visibleParticipants = participants.slice(0, 6); 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 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 freePlaces = Math.max(0, totalGuests - confirmedGuests);
const isFull = freePlaces === 0; const isFull = freePlaces === 0;
const isRegistrationClosed = isRegistrationClosedForEvent(event);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const registrationMap = getRegistrationMap();
const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email]) const userRegistrations = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? registrationMap[currentUser.email].map(id => Number(id)) ? registrationMap[currentUser.email].map(id => Number(id))
: []; : [];
const isRegistered = userRegistrations.includes(Number(event.id)); const isRegistered = userRegistrations.includes(Number(event.id));
const actionButtonLabel = isOwnEvent ? 'Dein Event' : !currentUser ? 'Einloggen' : isRegistered ? 'Abmelden' : 'Anmelden'; const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
const actionButtonDisabled = isOwnEvent || (!isRegistered && isFull); 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
? `
<article class="detail-panel detail-panel-compact">
<h2 class="detail-section-title">Adresse</h2>
<p>${event.address}</p>
</article>
`
: '';
const detailChips = [ const detailChips = [
`<span class="event-tag">${eventCategory}</span>`, `<span class="event-tag">${eventCategory}</span>`,
`<span class="event-tag">${dietLabel}</span>`, `<span class="event-tag">${dietLabel}</span>`,
@ -227,6 +352,8 @@ document.addEventListener('DOMContentLoaded', async () => {
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''} ${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
</div> </div>
</article> </article>
${addressPanelMarkup}
</div> </div>
<div class="detail-gallery detail-gallery-large"> <div class="detail-gallery detail-gallery-large">
@ -254,7 +381,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}"> <span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`} ${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
</span> </span>
<button class="detail-primary-btn${isOwnEvent ? ' detail-primary-btn-own' : ''}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}> <button class="detail-primary-btn${actionButtonVariantClass}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel} ${actionButtonLabel}
</button> </button>
</div> </div>
@ -280,6 +407,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item'); const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item');
const registerButton = detailContainer.querySelector('[data-register-button]'); 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. // Anmeldung toggeln und im lokalen Registrierungs-Store persistieren.
if (registerButton) { if (registerButton) {
registerButton.addEventListener('click', () => { registerButton.addEventListener('click', () => {
@ -300,7 +434,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (registrationSet.has(Number(event.id))) { if (registrationSet.has(Number(event.id))) {
registrationSet.delete(Number(event.id)); registrationSet.delete(Number(event.id));
} else if (!isFull) { } else if (!isFull && !isRegistrationClosed) {
registrationSet.add(Number(event.id)); registrationSet.add(Number(event.id));
} }

View File

@ -1,6 +1,7 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// ------------------------------------------------------------- // -------------------------------------------------------------
// DOM references used throughout the page lifecycle. // 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: // Initial data bootstrap:
// 1) fetch JSON, // 1) fetch JSON,
@ -183,6 +198,83 @@ document.addEventListener('DOMContentLoaded', () => {
: `${timeString} Uhr`; : `${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. // Safely verify whether a value exists in the given select element.
function hasOption(selectElement, value) { function hasOption(selectElement, value) {
return Array.from(selectElement.options).some(option => option.value === value); return Array.from(selectElement.options).some(option => option.value === value);
@ -202,6 +294,11 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const filtered = allEvents.filter(event => { 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 categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation; const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date); const eventDateIso = parseEventDateToIso(event.date);
@ -222,6 +319,10 @@ document.addEventListener('DOMContentLoaded', () => {
// - or event cards with status and metadata. // - or event cards with status and metadata.
function renderEvents(events) { function renderEvents(events) {
eventGrid.innerHTML = ''; 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) { if (events.length === 0) {
eventGrid.innerHTML = ` eventGrid.innerHTML = `
@ -242,20 +343,28 @@ document.addEventListener('DOMContentLoaded', () => {
const card = document.createElement('article'); const card = document.createElement('article');
card.className = 'event-card'; card.className = 'event-card';
card.style.cursor = 'pointer'; 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}`; window.location.href = `event_detail.html?id=${event.id}`;
}; });
const displayDate = formatEventDate(event.date); const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time); const displayTime = formatEventTime(event.time);
// Capacity logic: // Capacity logic:
// spots = total capacity, participants.length = booked seats. // 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 totalCapacity = event.spots;
const freePlaces = Math.max(0, totalCapacity - bookedSeats); const freePlaces = Math.max(0, totalCapacity - bookedSeats);
const isFull = freePlaces === 0; const isFull = freePlaces === 0;
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser); const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const isRegistered = userRegistrationSet.has(Number(event.id));
const isRegistrationClosed = isRegistrationClosedForEvent(event);
// Build optional specification chips only when data exists. // Build optional specification chips only when data exists.
const specsChips = event.specifications && event.specifications.length > 0 const specsChips = event.specifications && event.specifications.length > 0
@ -263,10 +372,16 @@ document.addEventListener('DOMContentLoaded', () => {
: ''; : '';
const actionMarkup = isOwnEvent const actionMarkup = isOwnEvent
? '<button class="btn-primary btn-primary-own" type="button" disabled>Dein Event</button>' ? '<button class="btn-primary btn-primary-own" type="button" data-registration-action="own" disabled>Dein Event!</button>'
: isRegistered
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="unregister">Abmelden</button>'
: isRegistrationClosed
? '<button class="btn-primary btn-primary-danger" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>'
: isFull : isFull
? '' ? ''
: '<button class="btn-primary" type="button">Anmelden</button>'; : !currentUser
? '<button class="btn-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>'
: '<button class="btn-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
card.innerHTML = ` card.innerHTML = `
<div class="event-main"> <div class="event-main">
@ -290,6 +405,50 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
`; `;
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); eventGrid.appendChild(card);
}); });
} }

View File

@ -10,6 +10,8 @@ document.addEventListener('DOMContentLoaded', () => {
const profileHeadline = document.getElementById('profile-headline'); const profileHeadline = document.getElementById('profile-headline');
const profileSubline = document.getElementById('profile-subline'); const profileSubline = document.getElementById('profile-subline');
const logoutButton = document.getElementById('logout-button'); 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 myEventsCount = document.getElementById('my-events-count');
const myRegistrationsCount = document.getElementById('my-registrations-count'); const myRegistrationsCount = document.getElementById('my-registrations-count');
@ -36,6 +38,7 @@ document.addEventListener('DOMContentLoaded', () => {
renderLoggedInState(currentUser); renderLoggedInState(currentUser);
bindFormHandlers(); bindFormHandlers();
activateProfileTab('hosting');
allEvents = await loadAllEvents(); allEvents = await loadAllEvents();
renderMyEvents(allEvents, currentUser); renderMyEvents(allEvents, currentUser);
@ -80,6 +83,11 @@ document.addEventListener('DOMContentLoaded', () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap)); 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. // Fuehrt JSON-Daten und lokal erstellte Events in einer Liste zusammen.
async function loadAllEvents() { async function loadAllEvents() {
try { try {
@ -119,6 +127,18 @@ document.addEventListener('DOMContentLoaded', () => {
function bindFormHandlers() { function bindFormHandlers() {
profileForm.addEventListener('submit', handleProfileSubmit); profileForm.addEventListener('submit', handleProfileSubmit);
myRegistrationsList.addEventListener('click', handleRegistrationListClick); 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 => { [vornameInput, nachnameInput, emailInput, passwortInput].forEach(input => {
input.addEventListener('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. // Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation.
function handleRegistrationListClick(event) { function handleRegistrationListClick(event) {
const target = event.target; const target = event.target;
@ -141,7 +208,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
const unregisterButton = target.closest('[data-unregister-id]'); const unregisterButton = target.closest('[data-unregister-id]');
if (!unregisterButton || !currentUser?.email) { if (unregisterButton) {
if (!currentUser?.email) {
return; return;
} }
@ -151,8 +219,57 @@ document.addEventListener('DOMContentLoaded', () => {
} }
unregisterFromEvent(eventId, currentUser.email); unregisterFromEvent(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}`;
}
// 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. // Entfernt eine Event-ID aus der Benutzerliste und aktualisiert die UI sofort.
function unregisterFromEvent(eventId, userEmail) { function unregisterFromEvent(eventId, userEmail) {
const registrationMap = getRegistrationMap(); const registrationMap = getRegistrationMap();
@ -260,15 +377,20 @@ document.addEventListener('DOMContentLoaded', () => {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map)); 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) { function getMyHostedEvents(events, user) {
const userFirstName = normalizeText(user.vorname); const userFirstName = normalizeText(user.vorname || '');
const userEmail = normalizeText(user.email || '');
return events.filter(event => { return events.filter(event => {
if (event.source !== 'local') {
return false;
}
const hostEmail = normalizeText(event.hostEmail || ''); const hostEmail = normalizeText(event.hostEmail || '');
const hostName = normalizeText(event.host?.name || ''); const hostName = normalizeText(event.host?.name || '');
if (hostEmail && hostEmail === normalizeText(user.email)) { if (hostEmail && hostEmail === userEmail) {
return true; return true;
} }
@ -289,45 +411,73 @@ document.addEventListener('DOMContentLoaded', () => {
function renderMyEvents(events, user) { function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user); const hostedEvents = getMyHostedEvents(events, user);
myEventsCount.textContent = String(hostedEvents.length); 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. // Rendert angemeldete Events inkl. Zaehler.
function renderMyRegistrations(events, user) { function renderMyRegistrations(events, user) {
const registeredEvents = getMyRegisteredEvents(events, user); const registeredEvents = getMyRegisteredEvents(events, user);
myRegistrationsCount.textContent = String(registeredEvents.length); 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. // Baut die Eventkarten fuer beide Listen in einheitlichem Markup.
function renderEventCards(container, events, emptyText, withUnregisterButton) { function renderEventCards(container, events, emptyStateConfig, mode) {
container.innerHTML = ''; container.innerHTML = '';
if (events.length === 0) { if (events.length === 0) {
const emptyElement = document.createElement('p'); const emptyElement = document.createElement('div');
emptyElement.className = 'profile-empty'; emptyElement.className = 'profile-empty-state';
emptyElement.textContent = emptyText; emptyElement.innerHTML = `
<p class="profile-empty-kicker">Keine Treffer</p>
<h3>${emptyStateConfig.title}</h3>
<p>${emptyStateConfig.text}</p>
<a class="button" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
`;
container.appendChild(emptyElement); container.appendChild(emptyElement);
return; return;
} }
events.forEach(event => { events.forEach(event => {
const card = document.createElement('article'); 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
? `
<div class="profile-event-address-block" aria-label="Event Adresse">
<p class="profile-event-address-label">Adresse</p>
<p class="profile-event-address">${event.address}</p>
</div>
`
: '';
const actionMarkup = withUnregisterButton const actionMarkup = mode === 'registrations'
? ` ? `
<div class="profile-event-actions"> <div class="profile-event-actions">
<a class="profile-event-link" href="event_detail.html?id=${event.id}">Zum Event</a>
<button class="profile-unregister-btn" type="button" data-unregister-id="${event.id}">Abmelden</button> <button class="profile-unregister-btn" type="button" data-unregister-id="${event.id}">Abmelden</button>
</div> </div>
` `
: `<a class="profile-event-link" href="event_detail.html?id=${event.id}">Zum Event</a>`; : `
<div class="profile-event-actions">
<button class="profile-cancel-btn" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
</div>
`;
card.innerHTML = ` card.innerHTML = `
<div> <div>
<h3 class="profile-event-title">${event.title}</h3> <h3 class="profile-event-title">${event.title}</h3>
<p class="profile-event-meta">${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}</p> <p class="profile-event-meta">${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}</p>
${addressMarkup}
</div> </div>
${actionMarkup} ${actionMarkup}
`; `;

View File

@ -44,7 +44,13 @@
</section> </section>
<section id="logged-in-content" class="profile-grid"> <section id="logged-in-content" class="profile-grid">
<article class="profile-panel"> <nav class="profile-tabs" aria-label="Profilbereiche">
<button type="button" class="profile-tab is-active" data-profile-tab="hosting">Hosting</button>
<button type="button" class="profile-tab" data-profile-tab="teilnehmen">Teilnehmen</button>
<button type="button" class="profile-tab" data-profile-tab="einstellungen">Einstellungen</button>
</nav>
<article class="profile-panel" data-profile-panel="hosting">
<div class="panel-head"> <div class="panel-head">
<h2 class="panel-title">Meine Events</h2> <h2 class="panel-title">Meine Events</h2>
<span id="my-events-count" class="panel-count">0</span> <span id="my-events-count" class="panel-count">0</span>
@ -52,7 +58,7 @@
<div id="my-events-list" class="profile-card-list"></div> <div id="my-events-list" class="profile-card-list"></div>
</article> </article>
<article class="profile-panel"> <article class="profile-panel hidden" data-profile-panel="teilnehmen">
<div class="panel-head"> <div class="panel-head">
<h2 class="panel-title">Meine Anmeldungen</h2> <h2 class="panel-title">Meine Anmeldungen</h2>
<span id="my-registrations-count" class="panel-count">0</span> <span id="my-registrations-count" class="panel-count">0</span>
@ -60,7 +66,7 @@
<div id="my-registrations-list" class="profile-card-list"></div> <div id="my-registrations-list" class="profile-card-list"></div>
</article> </article>
<article class="profile-panel profile-panel-form"> <article class="profile-panel profile-panel-form hidden" data-profile-panel="einstellungen">
<h2 class="panel-title">Profil verwalten</h2> <h2 class="panel-title">Profil verwalten</h2>
<form id="profile-form" novalidate> <form id="profile-form" novalidate>
<div class="form-grid"> <div class="form-grid">