feat: enhance profile notifications and address visibility logic
This commit is contained in:
parent
f4463bbd9a
commit
d46b65aa73
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
106
js/navigation.js
106
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 ? '<span class="notification-dot"></span>' : '';
|
||||
|
||||
return `
|
||||
<a
|
||||
@ -105,15 +195,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
title="${user.vorname || 'Profil'}"
|
||||
>
|
||||
${initial}
|
||||
${notificationMarkup}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
async function initNavigation() {
|
||||
const currentUser = getCurrentUser();
|
||||
const nextMarkup = currentUser ? buildLoggedInNavigation(currentUser) : buildLoggedOutNavigation();
|
||||
let 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();
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user