${item.title}
${item.teaser}
document.addEventListener('DOMContentLoaded', () => {
const body = document.body;
const isLoggedIn = body.dataset.auth === '1';
const childGender = body.dataset.childGender || '';
const locationPreference = body.dataset.locationPreference || 'prompt';
const header = document.querySelector('.site-header');
const headerSentinel = document.getElementById('headerSentinel');
// Logo-Logik
const pickLogo = (gender) => {
if (gender === 'female') return 'logo_female.png';
if (gender === 'male') return 'logo_male.png';
return Math.random() < 0.5 ? 'logo_female.png' : 'logo_male.png';
};
const chosenLogo = pickLogo(childGender);
document.querySelectorAll('[data-logo-img]').forEach(img => {
img.src = `/assets/bilder/${chosenLogo}`;
});
// Header shrink via IntersectionObserver (no flicker around threshold)
if (header && headerSentinel && 'IntersectionObserver' in window) {
const observer = new IntersectionObserver(([entry]) => {
header.classList.toggle('is-scrolled', !entry.isIntersecting);
}, {
rootMargin: '-1px 0px 0px 0px',
threshold: 0,
});
observer.observe(headerSentinel);
} else if (header) {
// Fallback with simple threshold
const limit = 120;
const onScroll = () => header.classList.toggle('is-scrolled', window.scrollY > limit);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
}
// Mobile Menü
const mobileMenu = document.getElementById('mobileMenu');
document.querySelectorAll('.menu-toggle').forEach(btn => {
btn.addEventListener('click', () => {
mobileMenu?.classList.toggle('open');
});
});
// Scroll zu Events
const scrollBtn = document.getElementById('scrollToEvents');
if (scrollBtn) {
scrollBtn.addEventListener('click', () => {
document.getElementById('events')?.scrollIntoView({ behavior: 'smooth' });
});
}
// Events from backend (injected on page); fallback to empty
const events = Array.isArray(window.__events) ? window.__events : [];
const threads = Array.isArray(window.__threads) ? window.__threads : [];
const EVENTS_RADIUS_KM = 20;
const LOCATION_STORAGE_KEY = 'pkt_user_location';
const LOCATION_SESSION_STORAGE_KEY = 'pkt_user_location_session';
const LOCATION_SESSION_PREFERENCE_KEY = 'pkt_user_location_session_preference';
const el = {
sliderTrack: document.getElementById('eventSlider'),
sliderViewport: document.querySelector('.slider__viewport'),
sliderPrev: document.querySelector('[data-slider-prev]'),
sliderNext: document.querySelector('[data-slider-next]'),
threadSliderTrack: document.getElementById('threadSlider'),
threadSliderViewport: document.getElementById('threadSlider')?.closest('.slider__viewport') || null,
threadSliderPrev: document.querySelector('[data-thread-slider-prev]'),
threadSliderNext: document.querySelector('[data-thread-slider-next]'),
modal: document.getElementById('eventModal'),
modalBody: document.getElementById('eventModalBody'),
modalTitle: document.getElementById('eventModalTitle'),
quickForm: document.getElementById('quickSearchForm'),
quickLoc: document.getElementById('qsLoc'),
quickQuery: document.getElementById('qsQuery'),
quickLat: document.getElementById('qsLat'),
quickLng: document.getElementById('qsLng'),
quickGeo: document.getElementById('quickGeo'),
eventsSection: document.getElementById('events'),
locationPreferenceModal: document.getElementById('locationPreferenceModal'),
};
let effectiveLocationPreference = locationPreference;
const fmtDate = (iso) => {
const d = new Date(iso);
return d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
};
const now = () => new Date();
const setCookie = (name, value, days = 30) => {
const expires = new Date(Date.now() + (days * 86400000)).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
};
const parseCookie = (name) => {
const prefix = `${name}=`;
return document.cookie
.split(';')
.map(item => item.trim())
.find(item => item.startsWith(prefix))
?.slice(prefix.length) || '';
};
const persistLocation = (location, mode = 'persistent') => {
if (!location || !Number.isFinite(location.lat) || !Number.isFinite(location.lng)) return;
const payload = {
lat: Number(location.lat.toFixed(6)),
lng: Number(location.lng.toFixed(6)),
label: location.label || 'Mein Standort',
savedAt: new Date().toISOString(),
};
if (mode === 'session') {
try {
window.sessionStorage.setItem(LOCATION_SESSION_STORAGE_KEY, JSON.stringify(payload));
} catch (err) {
// ignore
}
} else {
try {
window.localStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(payload));
} catch (err) {
// ignore
}
setCookie('pkt_user_location', JSON.stringify(payload), 30);
}
if (el.quickLat) el.quickLat.value = payload.lat.toFixed(6);
if (el.quickLng) el.quickLng.value = payload.lng.toFixed(6);
if (el.quickLoc && (!el.quickLoc.value || el.quickLoc.value === 'Mein Standort')) {
el.quickLoc.value = payload.label;
}
};
const clearStoredLocation = () => {
try {
window.localStorage.removeItem(LOCATION_STORAGE_KEY);
} catch (err) {
// ignore
}
try {
window.sessionStorage.removeItem(LOCATION_SESSION_STORAGE_KEY);
} catch (err) {
// ignore
}
document.cookie = 'pkt_user_location=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax';
if (el.quickLat) el.quickLat.value = '';
if (el.quickLng) el.quickLng.value = '';
if (el.quickLoc) el.quickLoc.value = '';
};
const getStoredLocation = () => {
const candidates = [];
try {
const session = window.sessionStorage.getItem(LOCATION_SESSION_STORAGE_KEY);
if (session) candidates.push(session);
} catch (err) {
// ignore
}
try {
const local = window.localStorage.getItem(LOCATION_STORAGE_KEY);
if (local) candidates.push(local);
} catch (err) {
// ignore
}
const cookieValue = parseCookie('pkt_user_location');
if (cookieValue) candidates.push(decodeURIComponent(cookieValue));
for (const entry of candidates) {
try {
const parsed = JSON.parse(entry);
if (parsed && Number.isFinite(parsed.lat) && Number.isFinite(parsed.lng)) {
return {
lat: Number(parsed.lat),
lng: Number(parsed.lng),
label: parsed.label || 'Mein Standort',
};
}
} catch (err) {
// ignore invalid payloads
}
}
return null;
};
const haversineKm = (lat1, lng1, lat2, lng2) => {
const toRad = (value) => value * Math.PI / 180;
const earthRadiusKm = 6371;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
return earthRadiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
const smallTag = (label) => `${label}`;
const getRankedEvents = (sourceEvents, location) => {
const normalized = sourceEvents.map(item => {
const startsAtDate = new Date(item.startsAt);
const isFuture = startsAtDate >= now();
const hasGeo = Number.isFinite(item.lat) && Number.isFinite(item.lng);
const distanceKm = (location && hasGeo)
? haversineKm(location.lat, location.lng, item.lat, item.lng)
: null;
return {
...item,
startsAtDate,
isFuture,
distanceKm,
};
});
if (!location) {
return normalized
.filter(item => item.isFuture)
.sort((a, b) => a.startsAtDate - b.startsAtDate)
.slice(0, 10);
}
const nearby = normalized.filter(item => item.distanceKm !== null && item.distanceKm <= EVENTS_RADIUS_KM);
const byDistanceThenDate = (a, b) => {
if (a.distanceKm !== b.distanceKm) return a.distanceKm - b.distanceKm;
return a.startsAtDate - b.startsAtDate;
};
const upcomingNearby = nearby
.filter(item => item.isFuture)
.sort(byDistanceThenDate);
if (upcomingNearby.length) {
return upcomingNearby.slice(0, 10);
}
const missedNearby = nearby
.filter(item => !item.isFuture)
.sort((a, b) => {
if (a.distanceKm !== b.distanceKm) return a.distanceKm - b.distanceKm;
return b.startsAtDate - a.startsAtDate;
})
.map(item => ({ ...item, missed: true }));
if (missedNearby.length) {
return missedNearby.slice(0, 10);
}
return normalized
.filter(item => item.isFuture)
.sort((a, b) => a.startsAtDate - b.startsAtDate)
.slice(0, 10);
};
const renderCard = (item) => {
const guest = !isLoggedIn;
const kids = item.allowKids ? 'Mit Kindern' : 'Ohne Kinder';
const distanceTag = Number.isFinite(item.distanceKm) ? smallTag(`${item.distanceKm.toFixed(1)} km`) : '';
const missedTag = item.missed ? smallTag('Das hast du verpasst') : '';
return `
${item.teaser}${item.title}
Keine Events vorhanden.
'; return; } el.sliderTrack.innerHTML = rankedEvents.map(renderCard).join(''); el.sliderTrack.querySelectorAll('[data-event-detail]').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.getAttribute('data-event-detail'), 10); const ev = rankedEvents.find(e => e.id === id); if (!ev || !el.modal || !el.modalBody || !el.modalTitle) return; el.modalTitle.textContent = ev.title; el.modalBody.innerHTML = `${ev.teaser}
${ev.description}
${ev.locationLabel ? `Ort: ${ev.locationLabel}
` : ''} ${Number.isFinite(ev.distanceKm) ? `Entfernung: ${ev.distanceKm.toFixed(1)} km
` : ''} ${ev.missed ? 'Hinweis: Das hast du verpasst.
' : ''}Kinder: ${ev.allowKids ? 'Mit Kindern' : 'Ohne Kinder'}
Sichtbarkeit: ${ev.visibility === 'public' ? 'Öffentlich' : 'Nur Mitglieder'}
`; el.modal.classList.add('open'); }); }); }; const renderThreadCard = (item) => { const preview = item.body.length > 170 ? `${item.body.slice(0, 170)}…` : item.body; return `${preview}
Noch keine Community-Beiträge vorhanden.
'; return; } el.threadSliderTrack.innerHTML = threads.map(renderThreadCard).join(''); }; const scrollSlider = (viewport, dir) => { if (!viewport) return; const amount = dir === 'next' ? 320 : -320; viewport.scrollBy({ left: amount, behavior: 'smooth' }); }; el.sliderPrev?.addEventListener('click', () => scrollSlider(el.sliderViewport, 'prev')); el.sliderNext?.addEventListener('click', () => scrollSlider(el.sliderViewport, 'next')); el.threadSliderPrev?.addEventListener('click', () => scrollSlider(el.threadSliderViewport, 'prev')); el.threadSliderNext?.addEventListener('click', () => scrollSlider(el.threadSliderViewport, 'next')); const applyStoredLocationToForm = () => { const stored = getStoredLocation(); if (!stored) return null; if (el.quickLat) el.quickLat.value = stored.lat.toFixed(6); if (el.quickLng) el.quickLng.value = stored.lng.toFixed(6); if (el.quickLoc && !el.quickLoc.value) el.quickLoc.value = stored.label || 'Mein Standort'; return stored; }; const requestLocation = (options = {}) => new Promise((resolve) => { if (!navigator.geolocation) { resolve(null); return; } navigator.geolocation.getCurrentPosition( (pos) => { const location = { lat: pos.coords.latitude, lng: pos.coords.longitude, label: 'Mein Standort', }; persistLocation(location, options.persistMode || 'persistent'); resolve(location); }, () => resolve(null), { enableHighAccuracy: true, timeout: options.timeout || 10000, maximumAge: options.maximumAge || 300000, } ); }); const closeLocationPreferenceModal = () => { el.locationPreferenceModal?.classList.remove('open'); }; document.querySelectorAll('[data-location-pref-close]').forEach(btn => { btn.addEventListener('click', closeLocationPreferenceModal); }); el.locationPreferenceModal?.addEventListener('click', (event) => { if (event.target === el.locationPreferenceModal) { closeLocationPreferenceModal(); } }); const saveLocationPreference = async (preference) => { effectiveLocationPreference = preference; if (!isLoggedIn) { try { window.localStorage.setItem('pkt_location_preference_guest', preference); } catch (err) { // ignore } return true; } try { const body = new URLSearchParams({ preference }); const response = await fetch('/api/location-preference', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: body.toString(), }); return response.ok; } catch (err) { return false; } }; const getSessionPreference = () => { try { return window.sessionStorage.getItem(LOCATION_SESSION_PREFERENCE_KEY); } catch (err) { return null; } }; const setSessionPreference = (value) => { try { if (!value) { window.sessionStorage.removeItem(LOCATION_SESSION_PREFERENCE_KEY); } else { window.sessionStorage.setItem(LOCATION_SESSION_PREFERENCE_KEY, value); } } catch (err) { // ignore } }; const resolvePromptChoice = () => new Promise((resolve) => { if (!el.locationPreferenceModal) { resolve('disabled'); return; } const buttons = el.locationPreferenceModal.querySelectorAll('[data-location-pref-choice]'); const cleanup = () => { buttons.forEach(button => button.removeEventListener('click', handleClick)); document.removeEventListener('keydown', handleKeydown); }; const finish = (value) => { cleanup(); closeLocationPreferenceModal(); resolve(value); }; const handleClick = (event) => { const value = event.currentTarget.getAttribute('data-location-pref-choice') || 'disabled'; finish(value); }; const handleKeydown = (event) => { if (event.key === 'Escape') { finish('disabled'); } }; buttons.forEach(button => button.addEventListener('click', handleClick)); document.addEventListener('keydown', handleKeydown); el.locationPreferenceModal.classList.add('open'); }); const ensureEventsLocation = async () => { const sessionPreference = getSessionPreference(); if (effectiveLocationPreference === 'disabled') { clearStoredLocation(); renderSlider(null); return null; } const stored = applyStoredLocationToForm(); if (stored && effectiveLocationPreference !== 'enabled' && sessionPreference !== 'session') { renderSlider(stored); return stored; } if (effectiveLocationPreference === 'prompt') { const choice = sessionPreference || await resolvePromptChoice(); if (choice === 'session') { setSessionPreference('session'); const location = await requestLocation({ persistMode: 'session' }); renderSlider(location || stored); return location; } if (choice === 'disabled') { await saveLocationPreference('disabled'); clearStoredLocation(); renderSlider(null); return null; } if (choice === 'enabled') { await saveLocationPreference('enabled'); effectiveLocationPreference = 'enabled'; } } const location = await requestLocation({ persistMode: effectiveLocationPreference === 'enabled' ? 'persistent' : 'session', }); renderSlider(location || stored); return location; }; // Quick search with geocoding const geocode = async (query) => { const url = 'https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=' + encodeURIComponent(query); const res = await fetch(url, { headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' } }); const data = await res.json(); if (Array.isArray(data) && data[0]) { return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) }; } return null; }; el.quickGeo?.addEventListener('click', () => { if (!navigator.geolocation) { alert('Geolocation wird nicht unterstützt.'); return; } navigator.geolocation.getCurrentPosition( (pos) => { persistLocation({ lat: pos.coords.latitude, lng: pos.coords.longitude, label: 'Mein Standort', }, effectiveLocationPreference === 'enabled' ? 'persistent' : 'session'); renderSlider(getStoredLocation()); }, () => alert('Standort konnte nicht ermittelt werden.') ); }); if (el.quickForm) { el.quickForm.addEventListener('submit', async (e) => { if (!el.quickLoc || !el.quickLat || !el.quickLng) return; const hasCoords = el.quickLat.value && el.quickLng.value; const locVal = (el.quickLoc.value || '').trim(); if (hasCoords || locVal === '') return; e.preventDefault(); try { const coords = await geocode(locVal); if (coords) { el.quickLat.value = coords.lat.toFixed(6); el.quickLng.value = coords.lng.toFixed(6); } } catch (err) { // ignore } finally { el.quickForm.submit(); } }); } // Close modal document.querySelectorAll('[data-event-modal-close]').forEach(btn => { btn.addEventListener('click', () => el.modal?.classList.remove('open')); }); el.modal?.addEventListener('click', (e) => { if (e.target === el.modal) el.modal.classList.remove('open'); }); document.querySelectorAll('[data-nav-events]').forEach(link => { link.addEventListener('click', async () => { if (window.location.pathname === '/' && window.location.hash === '#events') { await ensureEventsLocation(); } }); }); try { if (!isLoggedIn) { const guestPreference = window.localStorage.getItem('pkt_location_preference_guest'); if (guestPreference && ['disabled', 'prompt', 'enabled'].includes(guestPreference)) { effectiveLocationPreference = guestPreference; } } } catch (err) { // ignore } const initialLocation = effectiveLocationPreference === 'disabled' ? null : applyStoredLocationToForm(); if (effectiveLocationPreference === 'disabled') { clearStoredLocation(); } renderSlider(initialLocation); renderThreadSlider(); if (effectiveLocationPreference === 'enabled' && window.location.pathname === '/') { ensureEventsLocation(); } else if (window.location.hash === '#events' || window.location.hash === '#quicksearch') { ensureEventsLocation(); } window.addEventListener('hashchange', () => { if (window.location.hash === '#events' || window.location.hash === '#quicksearch') { ensureEventsLocation(); } }); });