diff --git a/css/stylesheet_global.css b/css/stylesheet_global.css index 4d0f4df..c650c64 100644 --- a/css/stylesheet_global.css +++ b/css/stylesheet_global.css @@ -597,6 +597,7 @@ label { } .category-item-profile { + position: relative; font-size: 1.25rem; font-weight: 400; line-height: 1; @@ -651,6 +652,7 @@ label { } .profile-pill { + position: relative; width: 2.375rem; height: 2.375rem; border-radius: 1.1875rem; @@ -666,6 +668,18 @@ label { text-decoration: none; } +.notification-dot { + position: absolute; + top: -2px; + right: -2px; + width: 12px; + height: 12px; + background-color: var(--error); + border-radius: 50%; + border: 2px solid var(--butter-light); + z-index: 10; +} + /* Utilities */ .text-center { text-align: center; diff --git a/js/my_profil.js b/js/my_profil.js index aa4d7d4..a4d1b9b 100644 --- a/js/my_profil.js +++ b/js/my_profil.js @@ -213,6 +213,11 @@ const isActive = panel.getAttribute('data-profile-panel') === tabName; panel.classList.toggle('hidden', !isActive); }); + + if (tabName === 'teilnehmen') { + const registeredEvents = getMyRegisteredEvents(allEvents, currentUser); + markRegistrationsAsRead(registeredEvents); + } } // Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation. @@ -477,6 +482,35 @@ }, 'hosting'); } + function getSeenAddresses() { + try { + const raw = localStorage.getItem('socialCookingSeenAddresses'); + return raw ? JSON.parse(raw) : []; + } catch (err) { + return []; + } + } + + function markRegistrationsAsRead(events) { + const seen = getSeenAddresses(); + let changed = false; + events.forEach(event => { + if (isAddressVisibleWindow(event) && !seen.includes(Number(event.id))) { + seen.push(Number(event.id)); + changed = true; + } + }); + if (changed) { + localStorage.setItem('socialCookingSeenAddresses', JSON.stringify(seen)); + // Remove dots from UI + const tabDot = document.querySelector('[data-category-item="teilnehmen"] .notification-dot'); + if (tabDot) tabDot.remove(); + + const navDot = document.querySelector('.profile-pill .notification-dot'); + if (navDot) navDot.remove(); + } + } + // Rendert angemeldete Events inkl. Zähler. function renderMyRegistrations(events, user) { const registeredEvents = getMyRegisteredEvents(events, user); @@ -486,12 +520,37 @@ myRegistrationsCount.textContent = String(count); if (myRegistrationsBtnCount) myRegistrationsBtnCount.textContent = String(count); + const seenAddresses = getSeenAddresses(); + const unreadEvents = registeredEvents.filter(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id))); + const hasNotifications = unreadEvents.length > 0; + + const tabButton = document.querySelector('[data-category-item="teilnehmen"]'); + if (tabButton) { + let dot = tabButton.querySelector('.notification-dot'); + if (hasNotifications) { + if (!dot) { + dot = document.createElement('span'); + dot.className = 'notification-dot'; + tabButton.appendChild(dot); + } + } else if (dot) { + dot.remove(); + } + } + 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'); + }, 'registrations', seenAddresses); + + // Falls wir bereits auf dem Tab sind, direkt als gelesen markieren + const activeTab = document.querySelector('[data-category-item="teilnehmen"].is-active'); + if (activeTab && hasNotifications) { + // Kurze Verzögerung, damit UI sich erst aufbaut + setTimeout(() => markRegistrationsAsRead(registeredEvents), 500); + } } // Gibt true zurück, wenn die Abmeldung gesperrt ist (innerhalb von 24h oder in der Vergangenheit). @@ -505,7 +564,7 @@ } // Baut die Eventkarten für beide Listen in einheitlichem Markup. - function renderEventCards(container, events, emptyStateConfig, mode) { + function renderEventCards(container, events, emptyStateConfig, mode, seenAddresses = []) { container.innerHTML = ''; if (events.length === 0) { diff --git a/js/navigation.js b/js/navigation.js index 9206f87..2ac6878 100644 --- a/js/navigation.js +++ b/js/navigation.js @@ -6,6 +6,8 @@ document.addEventListener('DOMContentLoaded', () => { const CURRENT_USER_KEY = 'socialCookingCurrentUser'; + const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations'; + const EVENTS_STORAGE_KEY = 'socialCookingEvents'; const navcontainers = document.querySelectorAll('.nav-tab-links'); const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase(); @@ -31,6 +33,93 @@ document.addEventListener('DOMContentLoaded', () => { window.location.href = 'index.html'; }; + // Hilfsfunktionen für Datumsberechnungen + 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, month, day; + + if (isoDateMatch) { + year = Number(isoDateMatch[1]); + month = Number(isoDateMatch[2]); + day = Number(isoDateMatch[3]); + } else { + const monthMap = { + jan: 1, januar: 1, + feb: 2, februar: 2, + 'mär': 3, mrz: 3, mar: 3, maerz: 3, märz: 3, + apr: 4, april: 4, + mai: 5, + jun: 6, juni: 6, + jul: 7, juli: 7, + aug: 8, august: 8, + sep: 9, sept: 9, september: 9, + okt: 10, oktober: 10, + nov: 11, november: 11, + dez: 12, dezember: 12 + }; + const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/); + if (!localizedMatch) return null; + day = Number(localizedMatch[1]); + month = monthMap[String(localizedMatch[2]).toLowerCase()]; + 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 isAddressVisibleWindow(event) { + const eventDateTime = parseEventDateTime(event); + if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false; + const now = Date.now(); + const start = eventDateTime.getTime(); + const revealStart = start - (24 * 60 * 60 * 1000); + const revealEnd = start + (1 * 60 * 60 * 1000); + return now >= revealStart && now <= revealEnd; + } + + async function hasUnreadNotifications(user) { + if (!user || !user.email) return false; + + let events = []; + try { + const rawStored = localStorage.getItem(EVENTS_STORAGE_KEY); + const storedEvents = rawStored ? JSON.parse(rawStored) : []; + + const response = await fetch('data/events.json'); + const apiEvents = await response.json(); + events = [...storedEvents, ...apiEvents]; + } catch (err) { + console.error('Fehler beim Laden der Events für Benachrichtigungen', err); + return false; + } + + let map = {}; + try { + const rawReg = localStorage.getItem(REGISTRATION_STORAGE_KEY); + map = rawReg ? JSON.parse(rawReg) : {}; + } catch (err) {} + + let seenAddresses = []; + try { + const rawSeen = localStorage.getItem('socialCookingSeenAddresses'); + seenAddresses = rawSeen ? JSON.parse(rawSeen) : []; + } catch (err) {} + + const registeredIds = Array.isArray(map[user.email]) ? map[user.email] : []; + const idSet = new Set(registeredIds.map(id => Number(id))); + + const myRegisteredEvents = events.filter(e => idSet.has(Number(e.id))); + + // Unread = address visible AND NOT marked as seen + return myRegisteredEvents.some(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id))); + } + // Baut die Navigation für ausgeloggte Besucher. function buildLoggedOutNavigation() { const loginIsActive = currentPage === 'login.html'; @@ -71,10 +160,11 @@ document.addEventListener('DOMContentLoaded', () => { } // Baut die Navigation für eingeloggte Benutzer. - function buildLoggedInNavigation(user) { + function buildLoggedInNavigation(user, hasNotifications) { const initial = (user.vorname || 'U').charAt(0).toUpperCase(); const isEventOverview = currentPage === 'event_overview.html'; const isEventCreate = currentPage === 'event_create.html'; + const notificationMarkup = hasNotifications ? '' : ''; return ` { title="${user.vorname || 'Profil'}" > ${initial} + ${notificationMarkup} `; } - const currentUser = getCurrentUser(); - const nextMarkup = currentUser ? buildLoggedInNavigation(currentUser) : buildLoggedOutNavigation(); + async function initNavigation() { + const currentUser = getCurrentUser(); + let nextMarkup; - // Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an. - navcontainers.forEach(container => { - container.innerHTML = nextMarkup; - }); + if (currentUser) { + const hasNotifications = await hasUnreadNotifications(currentUser); + nextMarkup = buildLoggedInNavigation(currentUser, hasNotifications); + } else { + nextMarkup = buildLoggedOutNavigation(); + } + + // Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an. + navcontainers.forEach(container => { + container.innerHTML = nextMarkup; + }); + } + + initNavigation(); });