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();
});