585 lines
22 KiB
JavaScript
585 lines
22 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
|
|
const USERS_STORAGE_KEY = 'socialCookingUsers';
|
|
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
|
|
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
|
|
|
|
// Zentrale DOM-Referenzen fuer klare, testbare Funktionen.
|
|
const loggedOutState = document.getElementById('logged-out-state');
|
|
const loggedInContent = document.getElementById('logged-in-content');
|
|
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');
|
|
const myEventsList = document.getElementById('my-events-list');
|
|
const myRegistrationsList = document.getElementById('my-registrations-list');
|
|
|
|
const profileForm = document.getElementById('profile-form');
|
|
const profileFeedback = document.getElementById('profile-feedback');
|
|
const vornameInput = document.getElementById('vorname');
|
|
const nachnameInput = document.getElementById('nachname');
|
|
const emailInput = document.getElementById('email');
|
|
const passwortInput = document.getElementById('passwort');
|
|
|
|
let currentUser = getCurrentUser();
|
|
let allEvents = [];
|
|
|
|
init();
|
|
|
|
async function init() {
|
|
if (!currentUser) {
|
|
renderLoggedOutState();
|
|
return;
|
|
}
|
|
|
|
renderLoggedInState(currentUser);
|
|
bindFormHandlers();
|
|
activateProfileTab('hosting');
|
|
|
|
allEvents = await loadAllEvents();
|
|
renderMyEvents(allEvents, currentUser);
|
|
renderMyRegistrations(allEvents, currentUser);
|
|
}
|
|
|
|
// Liest den aktuell eingeloggten Benutzer robust aus dem Storage.
|
|
function getCurrentUser() {
|
|
try {
|
|
const raw = localStorage.getItem(CURRENT_USER_KEY);
|
|
return raw ? JSON.parse(raw) : null;
|
|
} catch (error) {
|
|
console.error('Der aktuelle Benutzer konnte nicht geladen werden.', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Liest lokal erstellte Events aus dem Storage.
|
|
function getStoredEvents() {
|
|
try {
|
|
const raw = localStorage.getItem(EVENTS_STORAGE_KEY);
|
|
return raw ? JSON.parse(raw) : [];
|
|
} catch (error) {
|
|
console.error('Lokale Events konnten nicht gelesen werden.', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Liest den Anmeldestatus pro Benutzer-E-Mail.
|
|
function getRegistrationMap() {
|
|
try {
|
|
const raw = localStorage.getItem(REGISTRATION_STORAGE_KEY);
|
|
return raw ? JSON.parse(raw) : {};
|
|
} catch (error) {
|
|
console.error('Anmeldedaten konnten nicht gelesen werden.', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Schreibt den gesamten Registrierungszustand in localStorage.
|
|
function setRegistrationMap(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.
|
|
async function loadAllEvents() {
|
|
try {
|
|
const response = await fetch('data/events.json');
|
|
const apiEvents = await response.json();
|
|
return [...getStoredEvents(), ...apiEvents];
|
|
} catch (error) {
|
|
console.error('Events konnten nicht geladen werden.', error);
|
|
return getStoredEvents();
|
|
}
|
|
}
|
|
|
|
// Schaltet in den ausgeloggten Zustand und blendet geschuetzte Inhalte aus.
|
|
function renderLoggedOutState() {
|
|
loggedOutState.classList.remove('hidden');
|
|
loggedInContent.classList.add('hidden');
|
|
logoutButton.classList.add('hidden');
|
|
profileHeadline.textContent = 'Mein Profil';
|
|
profileSubline.textContent = 'Bitte logge dich ein, um deinen Bereich zu sehen.';
|
|
}
|
|
|
|
// Fuellt Ueberschriften und Formular mit den aktuellen Benutzerdaten.
|
|
function renderLoggedInState(user) {
|
|
loggedOutState.classList.add('hidden');
|
|
loggedInContent.classList.remove('hidden');
|
|
logoutButton.classList.remove('hidden');
|
|
|
|
profileHeadline.textContent = `Hallo ${user.vorname || 'Gast'}`;
|
|
profileSubline.textContent = 'Hier kannst du deine Events und Anmeldungen verwalten.';
|
|
|
|
vornameInput.value = user.vorname || '';
|
|
nachnameInput.value = user.nachname || '';
|
|
emailInput.value = user.email || '';
|
|
}
|
|
|
|
// Bindet Submit-, Input- und Logout-Verhalten an die Profilseite.
|
|
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', () => {
|
|
input.parentElement.classList.remove('has-error');
|
|
profileFeedback.textContent = '';
|
|
});
|
|
});
|
|
|
|
logoutButton.addEventListener('click', () => {
|
|
localStorage.removeItem(CURRENT_USER_KEY);
|
|
window.location.href = 'login.html';
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const unregisterButton = target.closest('[data-unregister-id]');
|
|
if (unregisterButton) {
|
|
if (!currentUser?.email) {
|
|
return;
|
|
}
|
|
|
|
const eventId = Number(unregisterButton.getAttribute('data-unregister-id'));
|
|
if (!Number.isFinite(eventId)) {
|
|
return;
|
|
}
|
|
|
|
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.
|
|
function unregisterFromEvent(eventId, userEmail) {
|
|
const registrationMap = getRegistrationMap();
|
|
const currentIds = Array.isArray(registrationMap[userEmail]) ? registrationMap[userEmail] : [];
|
|
const nextIds = currentIds
|
|
.map(id => Number(id))
|
|
.filter(id => Number.isFinite(id) && id !== eventId);
|
|
|
|
registrationMap[userEmail] = nextIds;
|
|
setRegistrationMap(registrationMap);
|
|
|
|
renderMyRegistrations(allEvents, currentUser);
|
|
profileFeedback.textContent = 'Du wurdest von dem Event abgemeldet.';
|
|
}
|
|
|
|
// Validiert Profildaten konsistent und liefert true/false zur Submit-Steuerung.
|
|
function validateProfileForm() {
|
|
let isValid = true;
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
if (!vornameInput.value.trim()) {
|
|
vornameInput.parentElement.classList.add('has-error');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!nachnameInput.value.trim()) {
|
|
nachnameInput.parentElement.classList.add('has-error');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!emailRegex.test(emailInput.value.trim())) {
|
|
emailInput.parentElement.classList.add('has-error');
|
|
isValid = false;
|
|
}
|
|
|
|
if (passwortInput.value && passwortInput.value.length < 6) {
|
|
passwortInput.parentElement.classList.add('has-error');
|
|
isValid = false;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
// Speichert Profilaenderungen lokal und synchronisiert auch den Benutzerkatalog.
|
|
function handleProfileSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
if (!validateProfileForm()) {
|
|
profileFeedback.textContent = 'Bitte pruefe die markierten Felder.';
|
|
return;
|
|
}
|
|
|
|
const previousEmail = currentUser.email;
|
|
const nextUser = {
|
|
...currentUser,
|
|
vorname: vornameInput.value.trim(),
|
|
nachname: nachnameInput.value.trim(),
|
|
email: emailInput.value.trim(),
|
|
passwort: passwortInput.value ? passwortInput.value : currentUser.passwort,
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
currentUser = nextUser;
|
|
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(nextUser));
|
|
syncUserInUserStore(previousEmail, nextUser);
|
|
|
|
// Falls sich die E-Mail geaendert hat, verschieben wir bestehende Anmeldungen auf die neue E-Mail.
|
|
migrateRegistrationEmail(previousEmail, nextUser.email);
|
|
|
|
passwortInput.value = '';
|
|
profileHeadline.textContent = `Hallo ${nextUser.vorname}`;
|
|
profileFeedback.textContent = 'Profil erfolgreich gespeichert.';
|
|
}
|
|
|
|
// Synchronisiert einen Benutzer im zentralen User-Array.
|
|
function syncUserInUserStore(previousEmail, nextUser) {
|
|
let users = [];
|
|
|
|
try {
|
|
const raw = localStorage.getItem(USERS_STORAGE_KEY);
|
|
users = raw ? JSON.parse(raw) : [];
|
|
} catch (error) {
|
|
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
|
|
}
|
|
|
|
const nextUsers = users.filter(user => user.email !== previousEmail && user.email !== nextUser.email);
|
|
nextUsers.unshift(nextUser);
|
|
localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(nextUsers));
|
|
}
|
|
|
|
// Migriert bestehende Registrierungen, falls die E-Mail aktualisiert wurde.
|
|
function migrateRegistrationEmail(previousEmail, nextEmail) {
|
|
if (!previousEmail || !nextEmail || previousEmail === nextEmail) {
|
|
return;
|
|
}
|
|
|
|
const map = getRegistrationMap();
|
|
const existingRegistrations = Array.isArray(map[previousEmail]) ? map[previousEmail] : [];
|
|
const alreadyPresent = Array.isArray(map[nextEmail]) ? map[nextEmail] : [];
|
|
|
|
map[nextEmail] = Array.from(new Set([...alreadyPresent, ...existingRegistrations]));
|
|
delete map[previousEmail];
|
|
|
|
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map));
|
|
}
|
|
|
|
// Ermittelt gehostete Events aus lokal erstellten Daten des aktuellen Benutzers.
|
|
function getMyHostedEvents(events, user) {
|
|
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 === userEmail) {
|
|
return true;
|
|
}
|
|
|
|
return userFirstName && hostName === userFirstName;
|
|
});
|
|
}
|
|
|
|
// Ermittelt angemeldete Events ueber die Registration-Map.
|
|
function getMyRegisteredEvents(events, user) {
|
|
const registrationMap = getRegistrationMap();
|
|
const registeredIds = Array.isArray(registrationMap[user.email]) ? registrationMap[user.email] : [];
|
|
const idSet = new Set(registeredIds.map(id => Number(id)));
|
|
|
|
return events.filter(event => idSet.has(Number(event.id)));
|
|
}
|
|
|
|
// Rendert gehostete Events inkl. Zaehler.
|
|
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');
|
|
}
|
|
|
|
// Rendert angemeldete Events inkl. Zaehler.
|
|
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');
|
|
}
|
|
|
|
// Baut die Eventkarten fuer beide Listen in einheitlichem Markup.
|
|
function renderEventCards(container, events, emptyStateConfig, mode) {
|
|
container.innerHTML = '';
|
|
|
|
if (events.length === 0) {
|
|
const emptyElement = document.createElement('div');
|
|
emptyElement.className = 'profile-empty-state';
|
|
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);
|
|
return;
|
|
}
|
|
|
|
events.forEach(event => {
|
|
const card = document.createElement('article');
|
|
card.className = 'profile-event-card profile-event-card-clickable';
|
|
card.setAttribute('data-event-id', String(event.id));
|
|
const addressMarkup = mode === 'registrations' && event.address && isAddressVisibleWindow(event)
|
|
? `
|
|
<div class="profile-event-address-block" aria-label="Event Adresse">
|
|
<p class="profile-event-address-label">Adresse</p>
|
|
<p class="profile-event-address">${event.address}</p>
|
|
</div>
|
|
`
|
|
: '';
|
|
|
|
const actionMarkup = mode === 'registrations'
|
|
? `
|
|
<div class="profile-event-actions">
|
|
<button class="profile-unregister-btn" type="button" data-unregister-id="${event.id}">Abmelden</button>
|
|
</div>
|
|
`
|
|
: `
|
|
<div class="profile-event-actions">
|
|
<button class="profile-cancel-btn" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
|
|
</div>
|
|
`;
|
|
|
|
card.innerHTML = `
|
|
<div>
|
|
<h3 class="profile-event-title">${event.title}</h3>
|
|
<p class="profile-event-meta">${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}</p>
|
|
${addressMarkup}
|
|
</div>
|
|
${actionMarkup}
|
|
`;
|
|
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
// Gibt true zurueck, wenn ein Event innerhalb der naechsten 12 Stunden startet.
|
|
function isAddressVisibleWindow(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 >= 0 && msUntilStart <= twelveHoursInMs;
|
|
}
|
|
|
|
// Parse fuer ISO- und lokalisierte Datumsformate aus den Eventdaten.
|
|
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);
|
|
}
|
|
|
|
// Formatiert ein Eventdatum konsistent fuer die Profilkarten.
|
|
function formatEventDate(dateString) {
|
|
if (!dateString) {
|
|
return 'Kein Datum';
|
|
}
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
|
const [year, month, day] = dateString.split('-');
|
|
const monthLabel = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1];
|
|
return `${Number(day)}. ${monthLabel} ${year}`;
|
|
}
|
|
|
|
return dateString;
|
|
}
|
|
|
|
// Vereinheitlicht die Zeitanzeige fuer die Profilseite.
|
|
function formatEventTime(timeString) {
|
|
if (!timeString) {
|
|
return 'Keine Uhrzeit';
|
|
}
|
|
|
|
return timeString.includes('UHR') ? timeString.replace('UHR', 'Uhr').trim() : timeString;
|
|
}
|
|
|
|
// Normalisiert Vergleichswerte fuer robuste String-Matches.
|
|
function normalizeText(value) {
|
|
return String(value || '').trim().toLowerCase();
|
|
}
|
|
});
|