feat: enhance profile notifications and address visibility logic

This commit is contained in:
viiivo 2026-04-23 21:15:44 +02:00
parent f4463bbd9a
commit d46b65aa73
3 changed files with 184 additions and 9 deletions

View File

@ -597,6 +597,7 @@ label {
} }
.category-item-profile { .category-item-profile {
position: relative;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -651,6 +652,7 @@ label {
} }
.profile-pill { .profile-pill {
position: relative;
width: 2.375rem; width: 2.375rem;
height: 2.375rem; height: 2.375rem;
border-radius: 1.1875rem; border-radius: 1.1875rem;
@ -666,6 +668,18 @@ label {
text-decoration: none; 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 */ /* Utilities */
.text-center { .text-center {
text-align: center; text-align: center;

View File

@ -213,6 +213,11 @@
const isActive = panel.getAttribute('data-profile-panel') === tabName; const isActive = panel.getAttribute('data-profile-panel') === tabName;
panel.classList.toggle('hidden', !isActive); 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. // Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation.
@ -477,6 +482,35 @@
}, 'hosting'); }, '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. // Rendert angemeldete Events inkl. Zähler.
function renderMyRegistrations(events, user) { function renderMyRegistrations(events, user) {
const registeredEvents = getMyRegisteredEvents(events, user); const registeredEvents = getMyRegisteredEvents(events, user);
@ -486,12 +520,37 @@
myRegistrationsCount.textContent = String(count); myRegistrationsCount.textContent = String(count);
if (myRegistrationsBtnCount) myRegistrationsBtnCount.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, { renderEventCards(myRegistrationsList, registeredEvents, {
title: 'Noch keine Anmeldungen', title: 'Noch keine Anmeldungen',
text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.', text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.',
buttonLabel: 'Events entdecken', buttonLabel: 'Events entdecken',
href: 'event_overview.html' 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). // 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. // 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 = ''; container.innerHTML = '';
if (events.length === 0) { if (events.length === 0) {

View File

@ -6,6 +6,8 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const CURRENT_USER_KEY = 'socialCookingCurrentUser'; const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const navcontainers = document.querySelectorAll('.nav-tab-links'); const navcontainers = document.querySelectorAll('.nav-tab-links');
const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase(); const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase();
@ -31,6 +33,93 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.href = 'index.html'; 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. // Baut die Navigation für ausgeloggte Besucher.
function buildLoggedOutNavigation() { function buildLoggedOutNavigation() {
const loginIsActive = currentPage === 'login.html'; const loginIsActive = currentPage === 'login.html';
@ -71,10 +160,11 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Baut die Navigation für eingeloggte Benutzer. // Baut die Navigation für eingeloggte Benutzer.
function buildLoggedInNavigation(user) { function buildLoggedInNavigation(user, hasNotifications) {
const initial = (user.vorname || 'U').charAt(0).toUpperCase(); const initial = (user.vorname || 'U').charAt(0).toUpperCase();
const isEventOverview = currentPage === 'event_overview.html'; const isEventOverview = currentPage === 'event_overview.html';
const isEventCreate = currentPage === 'event_create.html'; const isEventCreate = currentPage === 'event_create.html';
const notificationMarkup = hasNotifications ? '<span class="notification-dot"></span>' : '';
return ` return `
<a <a
@ -105,15 +195,27 @@ document.addEventListener('DOMContentLoaded', () => {
title="${user.vorname || 'Profil'}" title="${user.vorname || 'Profil'}"
> >
${initial} ${initial}
${notificationMarkup}
</a> </a>
`; `;
} }
const currentUser = getCurrentUser(); async function initNavigation() {
const nextMarkup = currentUser ? buildLoggedInNavigation(currentUser) : buildLoggedOutNavigation(); const currentUser = getCurrentUser();
let nextMarkup;
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an. if (currentUser) {
navcontainers.forEach(container => { const hasNotifications = await hasUnreadNotifications(currentUser);
container.innerHTML = nextMarkup; nextMarkup = buildLoggedInNavigation(currentUser, hasNotifications);
}); } else {
nextMarkup = buildLoggedOutNavigation();
}
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
navcontainers.forEach(container => {
container.innerHTML = nextMarkup;
});
}
initNavigation();
}); });