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 `
${fmtDate(item.startsAt)}

${item.title}

${item.teaser}

📍 ${item.city || item.region || ''}
${smallTag(kids)} ${item.visibility === 'public' ? smallTag('Öffentlich') : smallTag('Mitglieder')} ${distanceTag} ${missedTag}
${guest || item.missed ? '' : ''}
`; }; const renderSlider = (location = null) => { if (!el.sliderTrack) return; const rankedEvents = getRankedEvents(events, location); if (!rankedEvents.length) { el.sliderTrack.innerHTML = '

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 = `
${fmtDate(ev.startsAt)} · ${ev.region || ev.city || ''}

${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 `
${fmtDate(item.createdAt)}

${item.title}

${preview}

${item.displayName} Antworten: ${item.answers}
Beitrag öffnen
`; }; const renderThreadSlider = () => { if (!el.threadSliderTrack) return; if (!threads.length) { el.threadSliderTrack.innerHTML = '

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