${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');
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) => `${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; } 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 readAvatarValue = (form, field) => { const checked = form.querySelector(`[data-avatar-field="${field}"]:checked`); if (checked) return checked.value; const input = form.querySelector(`[data-avatar-field="${field}"]`); return input ? input.value : ''; }; const avatarComponentFieldMap = { avatar_lorelei_eyes_variant: 'eyesVariant', avatar_lorelei_eyebrows_variant: 'eyebrowsVariant', avatar_lorelei_mouth_variant: 'mouthVariant', avatar_lorelei_glasses_variant: 'glassesVariant', avatar_lorelei_hair_variant: 'hairVariant', avatar_lorelei_beard_variant: 'beardVariant', avatar_lorelei_earrings_variant: 'earringsVariant', }; const avatarComponentFields = Object.keys(avatarComponentFieldMap); const avatarEndpoint = '/api/avatar.php'; const avatarOptionalProbabilityMap = { avatar_lorelei_glasses_variant: 'glassesProbability', avatar_lorelei_hair_variant: 'hairProbability', avatar_lorelei_beard_variant: 'beardProbability', avatar_lorelei_earrings_variant: 'earringsProbability', }; const buildAvatarParams = (form, overrides = {}) => { const seed = overrides.avatar_preset || readAvatarValue(form, 'avatar_preset'); if (!seed) return null; const params = new URLSearchParams({ seed }); avatarComponentFields.forEach((field) => { const value = Object.prototype.hasOwnProperty.call(overrides, field) ? overrides[field] : readAvatarValue(form, field); const param = avatarComponentFieldMap[field]; const probabilityParam = avatarOptionalProbabilityMap[field]; 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 params = buildAvatarParams(form); if (!params) return; const url = `${avatarEndpoint}?${params.toString()}`; if (!url) return; image.src = url; }; const getActiveAvatarField = (form) => { const activeTab = form.querySelector('[data-avatar-component-tab].is-active'); return activeTab?.getAttribute('data-avatar-component-tab') || 'avatar_lorelei_eyes_variant'; }; const setAvatarLoaderState = (form, active, loaded = 0, total = 0, label = 'Varianten werden geladen', runId = '') => { const loader = form.querySelector('[data-avatar-loader]'); const fill = form.querySelector('[data-avatar-loader-fill]'); const progress = form.querySelector('[data-avatar-loader-progress]'); const labelNode = form.querySelector('[data-avatar-loader-label]'); if (!loader || !fill || !progress || !labelNode) return; if (!active) { if (runId && form.dataset.avatarActiveLoadRun && form.dataset.avatarActiveLoadRun !== runId) { return; } form.dataset.avatarActiveLoadRun = ''; loader.hidden = true; fill.style.width = '0%'; progress.textContent = '0%'; labelNode.textContent = label; return; } if (runId) { form.dataset.avatarActiveLoadRun = runId; } const percent = total > 0 ? Math.max(0, Math.min(100, Math.round((loaded / total) * 100))) : 0; loader.hidden = false; fill.style.width = `${percent}%`; progress.textContent = `${percent}%`; labelNode.textContent = label; }; const updateAvatarOptionPreviews = (form, activeField = null) => { const selector = activeField ? `[data-avatar-component-panel="${activeField}"] [data-avatar-option-thumb]` : '[data-avatar-option-thumb]'; const images = Array.from(form.querySelectorAll(selector)); if (!images.length) { setAvatarLoaderState(form, false); return; } let loaded = 0; const label = activeField ? `Varianten fuer ${form.querySelector(`[data-avatar-component-tab="${activeField}"]`)?.textContent?.trim() || 'diesen Bereich'}` : 'Varianten werden geladen'; const loadRunId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; setAvatarLoaderState(form, true, 0, images.length, label, loadRunId); images.forEach((image) => { const field = image.getAttribute('data-avatar-option-field') || ''; const value = image.getAttribute('data-avatar-option-value') || ''; const params = buildAvatarParams(form, { [field]: value }); if (!params) { loaded += 1; setAvatarLoaderState(form, true, loaded, images.length, label, loadRunId); return; } const src = `${avatarEndpoint}?${params.toString()}`; image.dataset.avatarLoadRun = loadRunId; if (image.dataset.avatarLoadedSrc === src) { loaded += 1; setAvatarLoaderState(form, true, loaded, images.length, label, loadRunId); if (loaded >= images.length) { window.setTimeout(() => setAvatarLoaderState(form, false, 0, 0, 'Varianten werden geladen', loadRunId), 180); } return; } let finished = false; const finalize = () => { if (finished || image.dataset.avatarLoadRun !== loadRunId) return; finished = true; loaded += 1; setAvatarLoaderState(form, true, loaded, images.length, label, loadRunId); if (loaded >= images.length) { window.setTimeout(() => setAvatarLoaderState(form, false, 0, 0, 'Varianten werden geladen', loadRunId), 180); } }; const timeoutId = window.setTimeout(() => { finalize(); }, 2500); image.onload = () => { window.clearTimeout(timeoutId); image.dataset.avatarLoadedSrc = src; finalize(); }; image.onerror = () => { window.clearTimeout(timeoutId); finalize(); }; image.src = src; }); }; const ensureAvatarBuilderLoaded = (form) => { if (form.dataset.avatarInitialized === 'true') return; form.dataset.avatarInitialized = 'true'; updateAvatarPreview(form); updateAvatarOptionPreviews(form, getActiveAvatarField(form)); }; const renderAvatarSeedChoices = (form, seeds) => { const picker = form.querySelector('[data-avatar-seed-picker]'); const grid = form.querySelector('[data-avatar-seed-picker-grid]'); if (!picker || !grid) return; grid.innerHTML = ''; seeds.forEach((seed, index) => { const params = buildAvatarParams(form, { avatar_preset: seed }); if (!params) return; const card = document.createElement('button'); card.type = 'button'; card.className = 'avatar-seed-card'; card.setAttribute('data-avatar-seed-option', seed); card.innerHTML = `