Files
papa-kind-treff.info/public/assets/js/app.js
Lars Gebhardt-Kusche aa86001760
All checks were successful
Deploy / deploy (push) Successful in 1m7s
avatar
2026-07-28 02:03:01 +02:00

1029 lines
38 KiB
JavaScript
Executable File

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');
const matomoConfigNode = document.getElementById('matomoConfig');
const consentStateKey = 'pkt_cookie_consent';
let matomoLoaded = false;
// 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');
});
});
const profileMenu = document.querySelector('[data-profile-menu]');
const profileMenuTrigger = document.querySelector('[data-profile-menu-trigger]');
profileMenuTrigger?.addEventListener('click', () => {
const next = !profileMenu?.classList.contains('is-open');
profileMenu?.classList.toggle('is-open', next);
profileMenuTrigger.setAttribute('aria-expanded', next ? 'true' : 'false');
});
document.addEventListener('click', (event) => {
if (!profileMenu || !profileMenuTrigger) return;
if (profileMenu.contains(event.target)) return;
profileMenu.classList.remove('is-open');
profileMenuTrigger.setAttribute('aria-expanded', 'false');
});
// 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'),
consentBanner: document.getElementById('cookieConsentBanner'),
consentModal: document.getElementById('cookieConsentModal'),
consentAnalytics: document.getElementById('consentAnalytics'),
consentExternalServices: document.getElementById('consentExternalServices'),
};
let effectiveLocationPreference = locationPreference;
let consentState = null;
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 deleteCookie = (name) => {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax`;
};
const readConsent = () => {
try {
const raw = window.localStorage.getItem(consentStateKey);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
return {
necessary: true,
analytics: Boolean(parsed.analytics),
external_services: Boolean(parsed.external_services),
updatedAt: parsed.updatedAt || null,
};
} catch (err) {
return null;
}
};
const writeConsent = (nextState) => {
consentState = {
necessary: true,
analytics: Boolean(nextState.analytics),
external_services: Boolean(nextState.external_services),
updatedAt: new Date().toISOString(),
};
try {
window.localStorage.setItem(consentStateKey, JSON.stringify(consentState));
} catch (err) {
// ignore
}
window.PKTConsent = window.PKTConsent || {};
window.PKTConsent.get = () => consentState;
window.PKTConsent.has = (category) => {
if (category === 'necessary') return true;
return Boolean(consentState?.[category]);
};
};
const closeConsentModal = () => {
el.consentModal?.classList.remove('open');
};
const openConsentModal = () => {
if (el.consentAnalytics) el.consentAnalytics.checked = Boolean(consentState?.analytics);
if (el.consentExternalServices) el.consentExternalServices.checked = Boolean(consentState?.external_services);
el.consentModal?.classList.add('open');
};
const deleteMatomoCookies = () => {
['_pk_id', '_pk_ses', '_pk_ref', '_pk_cvar', 'mtm_consent', 'mtm_consent_removed'].forEach((prefix) => {
deleteCookie(prefix);
const hostParts = window.location.hostname.split('.');
while (hostParts.length > 1) {
document.cookie = `${prefix}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${hostParts.join('.')}; SameSite=Lax`;
hostParts.shift();
}
});
};
const loadMatomo = () => {
if (matomoLoaded || !matomoConfigNode || !consentState?.analytics) return;
try {
const cfg = JSON.parse(matomoConfigNode.textContent || '{}');
if (!cfg.url || !cfg.siteId) return;
const _paq = window._paq = window._paq || [];
_paq.push(['setDomains', Array.isArray(cfg.domains) ? cfg.domains : []]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
_paq.push(['setTrackerUrl', cfg.url + 'matomo.php']);
_paq.push(['setSiteId', cfg.siteId]);
const script = document.createElement('script');
script.async = true;
script.src = cfg.url + 'matomo.js';
document.head.appendChild(script);
matomoLoaded = true;
} catch (err) {
// ignore
}
};
const showConsentBannerIfNeeded = () => {
if (!consentState && el.consentBanner) {
el.consentBanner.removeAttribute('hidden');
}
};
const applyConsent = (nextState, options = {}) => {
const previous = consentState;
writeConsent(nextState);
if (!consentState.external_services) {
clearStoredLocation();
}
if (!consentState.analytics) {
deleteMatomoCookies();
} else {
loadMatomo();
}
el.consentBanner?.setAttribute('hidden', 'hidden');
closeConsentModal();
if (previous && options.reloadOnChange !== false) {
const analyticsChanged = previous.analytics !== consentState.analytics;
const externalChanged = previous.external_services !== consentState.external_services;
if (analyticsChanged || externalChanged) {
window.location.reload();
}
}
};
const requireExternalServicesConsent = (message = 'Für diese Funktion werden externe Dienste benötigt.') => {
if (consentState?.external_services) {
return true;
}
alert(message);
openConsentModal();
return false;
};
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
}
deleteCookie('pkt_user_location');
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) => `<span class="badge">${label}</span>`;
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 `
<article class="card event-card-small">
<div class="muted small">${fmtDate(item.startsAt)}</div>
<h3>${item.title}</h3>
<p class="muted">${item.teaser}</p>
<div class="muted small">📍 ${item.city || item.region || ''}</div>
<div class="event__tags">${smallTag(kids)} ${item.visibility === 'public' ? smallTag('Öffentlich') : smallTag('Mitglieder')} ${distanceTag} ${missedTag}</div>
<div class="flex gap-8" style="margin-top:8px;">
<button class="btn ghost" data-event-detail="${item.id}">Details</button>
${guest || item.missed ? '' : '<button class="btn">Teilnehmen</button>'}
</div>
</article>`;
};
const renderSlider = (location = null) => {
if (!el.sliderTrack) return;
const rankedEvents = getRankedEvents(events, location);
if (!rankedEvents.length) {
el.sliderTrack.innerHTML = '<p class="muted">Keine Events vorhanden.</p>';
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 = `
<div class="muted small">${fmtDate(ev.startsAt)} · ${ev.region || ev.city || ''}</div>
<p class="muted">${ev.teaser}</p>
<p>${ev.description}</p>
${ev.locationLabel ? `<p><strong>Ort:</strong> ${ev.locationLabel}</p>` : ''}
${Number.isFinite(ev.distanceKm) ? `<p><strong>Entfernung:</strong> ${ev.distanceKm.toFixed(1)} km</p>` : ''}
${ev.missed ? '<p><strong>Hinweis:</strong> Das hast du verpasst.</p>' : ''}
<p><strong>Kinder:</strong> ${ev.allowKids ? 'Mit Kindern' : 'Ohne Kinder'}</p>
<p><strong>Sichtbarkeit:</strong> ${ev.visibility === 'public' ? 'Öffentlich' : 'Nur Mitglieder'}</p>
`;
el.modal.classList.add('open');
});
});
};
const renderThreadCard = (item) => {
const preview = item.body.length > 170 ? `${item.body.slice(0, 170)}` : item.body;
return `
<article class="card thread-card-small">
<div class="muted small">${fmtDate(item.createdAt)}</div>
<h3>${item.title}</h3>
<p class="muted clamp-4">${preview}</p>
<div class="event__meta">
<span>${item.displayName}</span>
<span>Antworten: ${item.answers}</span>
</div>
<div class="flex gap-8" style="margin-top:8px;">
<a class="btn ghost" href="/community_thread?id=${item.id}">Beitrag öffnen</a>
</div>
</article>`;
};
const renderThreadSlider = () => {
if (!el.threadSliderTrack) return;
if (!threads.length) {
el.threadSliderTrack.innerHTML = '<p class="muted">Noch keine Community-Beiträge vorhanden.</p>';
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;
}
if (!requireExternalServicesConsent('Für standortbezogene Treffen werden Standort- und Kartendienste erst nach deiner Einwilligung aktiviert.')) {
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 (!requireExternalServicesConsent('Für die Standortermittlung benötigen wir deine Einwilligung zu externen Diensten und Standortfunktionen.')) {
return;
}
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-consent-open]').forEach(btn => {
btn.addEventListener('click', openConsentModal);
});
document.querySelectorAll('[data-consent-close]').forEach(btn => {
btn.addEventListener('click', closeConsentModal);
});
document.querySelectorAll('[data-consent-necessary]').forEach(btn => {
btn.addEventListener('click', () => applyConsent({ analytics: false, external_services: false }));
});
document.querySelector('[data-consent-accept-all]')?.addEventListener('click', () => {
applyConsent({ analytics: true, external_services: true });
});
document.querySelector('[data-consent-save]')?.addEventListener('click', () => {
applyConsent({
analytics: Boolean(el.consentAnalytics?.checked),
external_services: Boolean(el.consentExternalServices?.checked),
});
});
el.consentModal?.addEventListener('click', (e) => {
if (e.target === el.consentModal) closeConsentModal();
});
document.addEventListener('pkt:open-consent', openConsentModal);
const avatarStyleConfigs = {
lorelei: {
fields: ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant'],
paramMap: {
eyesVariant: 'eyesVariant',
eyebrowsVariant: 'eyebrowsVariant',
mouthVariant: 'mouthVariant',
glassesVariant: 'glassesVariant',
hairVariant: 'hairVariant',
beardVariant: 'beardVariant',
earringsVariant: 'earringsVariant',
},
optionalProbabilityMap: {
glassesVariant: 'glassesProbability',
beardVariant: 'beardProbability',
earringsVariant: 'earringsProbability',
},
alwaysProbabilityMap: {
hairVariant: 'hairProbability',
},
},
};
const getCurrentAvatarStyle = (form) => {
const input = form.querySelector('[data-avatar-style-select]');
const value = input?.value || '';
if (value) return value;
return form.querySelector('[data-avatar-style-panel]')?.getAttribute('data-avatar-style-panel') || 'lorelei';
};
const getAvatarSeedInput = (form) => form.querySelector('[data-avatar-seed]');
const getAvatarStylePanel = (form, style) => form.querySelector(`[data-avatar-style-panel="${style}"]`);
const readAvatarValue = (form, style, field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return '';
const checked = panel.querySelector(`[data-avatar-field="${field}"]:checked`);
if (checked) return checked.value;
const input = panel.querySelector(`[data-avatar-field="${field}"]`);
return input ? input.value : '';
};
const getAvatarStyleConfig = (style) => {
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
return avatarStyleConfigs.lorelei;
};
const avatarEndpoint = '/api/avatar.php';
const buildAvatarParams = (form, style = getCurrentAvatarStyle(form), overrides = {}) => {
const styleConfig = getAvatarStyleConfig(style);
const seed = overrides.avatar_seed || getAvatarSeedInput(form)?.value || '';
if (!seed) return null;
const params = new URLSearchParams({ style, seed });
styleConfig.fields.forEach((field) => {
const value = Object.prototype.hasOwnProperty.call(overrides, field)
? overrides[field]
: readAvatarValue(form, style, field);
const param = styleConfig.paramMap[field];
const probabilityParam = styleConfig.optionalProbabilityMap[field];
const alwaysProbabilityParam = styleConfig.alwaysProbabilityMap[field];
if (alwaysProbabilityParam) {
params.set(alwaysProbabilityParam, value ? '100' : '0');
}
if (probabilityParam) {
params.set(probabilityParam, value ? '100' : '0');
}
if (value && param) params.set(param, value);
});
return params;
};
const updateAvatarPreview = (form) => {
const preview = form.querySelector('[data-avatar-preview] .pkt-avatar');
if (!preview) return;
const image = preview.querySelector('.pkt-avatar__img');
if (!image) return;
const style = getCurrentAvatarStyle(form);
const params = buildAvatarParams(form, style);
if (!params) return;
const url = `${avatarEndpoint}?${params.toString()}`;
if (!url) return;
image.src = url;
};
const getActiveAvatarField = (form, style) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return '';
const activeTab = panel.querySelector('[data-avatar-component-tab].is-active');
return activeTab?.getAttribute('data-avatar-component-tab') || panel.querySelector('[data-avatar-component-tab]')?.getAttribute('data-avatar-component-tab') || '';
};
const updateAvatarOptionPreviews = (form, style, activeField = null) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
const selector = activeField
? `[data-avatar-component-panel="${activeField}"] [data-avatar-option-thumb]`
: '[data-avatar-option-thumb]';
panel.querySelectorAll(selector).forEach((image) => {
const field = image.getAttribute('data-avatar-option-field') || '';
const value = image.getAttribute('data-avatar-option-value') || '';
const params = buildAvatarParams(form, style, { [field]: value });
if (!params) return;
const src = `${avatarEndpoint}?${params.toString()}`;
if (image.dataset.avatarLoadedSrc === src) return;
image.dataset.avatarLoadedSrc = src;
image.src = src;
});
};
const ensureAvatarBuilderLoaded = (form, style = getCurrentAvatarStyle(form)) => {
const loadedStyles = new Set((form.dataset.avatarInitializedStyles || '').split(',').filter(Boolean));
if (loadedStyles.has(style)) return;
loadedStyles.add(style);
form.dataset.avatarInitializedStyles = Array.from(loadedStyles).join(',');
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
};
document.querySelectorAll('[data-avatar-builder]').forEach((form) => {
form.querySelectorAll('[data-avatar-field]').forEach((field) => {
field.addEventListener('change', () => {
const style = field.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
field.addEventListener('input', () => {
const style = field.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
});
const setActiveComponentPanel = (style, field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
panel.querySelectorAll('[data-avatar-component-tab]').forEach((button) => {
const active = button.getAttribute('data-avatar-component-tab') === field;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
panel.querySelectorAll('[data-avatar-component-panel]').forEach((componentPanel) => {
componentPanel.classList.toggle('is-active', componentPanel.getAttribute('data-avatar-component-panel') === field);
});
updateAvatarOptionPreviews(form, style, field);
};
const setActiveStyle = (style) => {
form.querySelectorAll('[data-avatar-style-panel]').forEach((panel) => {
const active = panel.getAttribute('data-avatar-style-panel') === style;
panel.hidden = !active;
panel.classList.toggle('is-active', active);
panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
input.disabled = !active;
});
});
ensureAvatarBuilderLoaded(form, style);
setActiveComponentPanel(style, getActiveAvatarField(form, style));
updateAvatarPreview(form);
};
form.querySelector('[data-avatar-style-select]')?.addEventListener('change', (event) => {
const nextStyle = event.currentTarget?.value || getCurrentAvatarStyle(form);
setActiveStyle(nextStyle);
});
form.querySelectorAll('[data-avatar-component-tab]').forEach((button) => {
button.addEventListener('click', () => {
const style = button.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
const field = button.getAttribute('data-avatar-component-tab');
if (field) {
ensureAvatarBuilderLoaded(form, style);
setActiveComponentPanel(style, field);
}
});
});
Object.values(avatarStyleConfigs).forEach((styleConfig) => {
styleConfig.fields.forEach((fieldName) => {
form.querySelectorAll(`[data-avatar-field="${fieldName}"]`).forEach((input) => {
input.addEventListener('change', () => {
const style = input.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
setActiveComponentPanel(style, fieldName);
});
});
});
});
const avatarPrefixes = [
'abend', 'ahorn', 'anker', 'atlas', 'berg', 'blick', 'brise', 'echo',
'eiche', 'fjord', 'fluss', 'fokus', 'hafen', 'herz', 'horizont', 'insel',
'kiesel', 'kompass', 'kraft', 'linie', 'licht', 'lotse', 'mond', 'morgen',
'nord', 'pfad', 'quelle', 'rauch', 'runde', 'sand', 'sommer', 'stein',
'sturm', 'tal', 'ufer', 'wald', 'welle', 'wind', 'winkel', 'zeit',
];
const avatarSuffixes = [
'alpha', 'atlas', 'balu', 'bravo', 'caspar', 'dario', 'emil', 'felix',
'fiete', 'finn', 'gerrit', 'hanno', 'ilan', 'joris', 'kian', 'leon',
'linus', 'maik', 'malo', 'marlon', 'mats', 'mika', 'milan', 'noel',
'ole', 'oskar', 'pepe', 'quinn', 'remo', 'sam', 'taro', 'timo',
'veit', 'vito', 'yaro', 'yuri', 'zeno', 'zuri',
];
const slugifySeed = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'papa-kind-treff';
const buildSeed = () => {
const prefix = avatarPrefixes[Math.floor(Math.random() * avatarPrefixes.length)];
const suffix = avatarSuffixes[Math.floor(Math.random() * avatarSuffixes.length)];
const tail = Math.floor(Math.random() * 997);
return slugifySeed(`${prefix}-${suffix}-${tail}`);
};
form.querySelector('[data-avatar-random]')?.addEventListener('click', () => {
const style = getCurrentAvatarStyle(form);
const styleConfig = getAvatarStyleConfig(style);
const seedField = getAvatarSeedInput(form);
if (!(seedField instanceof HTMLInputElement)) return;
seedField.value = buildSeed();
styleConfig.fields.forEach((field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
const options = Array.from(panel.querySelectorAll(`[data-avatar-field="${field}"]`));
if (!options.length) return;
const randomOption = options[Math.floor(Math.random() * options.length)];
if (!(randomOption instanceof HTMLInputElement)) return;
randomOption.checked = true;
});
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
setActiveStyle(getCurrentAvatarStyle(form));
});
document.querySelectorAll('[data-modal-open="modalAvatar"]').forEach((button) => {
button.addEventListener('click', () => {
const form = document.querySelector('#modalAvatar [data-avatar-builder]');
if (!form) return;
const style = getCurrentAvatarStyle(form);
window.setTimeout(() => ensureAvatarBuilderLoaded(form, style), 20);
});
});
document.querySelectorAll('[data-nav-events]').forEach(link => {
link.addEventListener('click', async () => {
if (window.location.pathname === '/' && window.location.hash === '#events') {
await ensureEventsLocation();
}
});
});
consentState = readConsent();
if (consentState?.analytics) {
loadMatomo();
}
showConsentBannerIfNeeded();
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 === '/' && consentState?.external_services) {
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();
}
});
});