Files
papa-kind-treff.info/public/assets/js/app.js
2025-12-29 01:32:13 +01:00

225 lines
8.9 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const body = document.body;
const isLoggedIn = body.dataset.auth === '1';
const childGender = body.dataset.childGender || '';
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 state = { topic: 'all', age: '', region: '', query: '', geo: null };
const el = {
list: document.getElementById('eventList'),
chips: document.querySelectorAll('[data-filter]'),
locInput: document.getElementById('locInput'),
topicSelect: document.getElementById('topicSelect'),
ageSelect: document.getElementById('ageSelect'),
btnSearch: document.getElementById('btnSearch'),
btnGeo: document.getElementById('btnGeo'),
modal: document.getElementById('eventModal'),
modalBody: document.getElementById('eventModalBody'),
modalTitle: document.getElementById('eventModalTitle'),
};
const searchState = { active: false };
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 haversine = (lat1, lon1, lat2, lon2) => {
const toRad = (d) => d * Math.PI / 180;
const R = 6371e3;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat/2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
};
const tag = (label) => `<span class="badge">${label}</span>`;
const renderCard = (item) => {
const guest = !isLoggedIn;
const access = item.visibility === 'members' && guest ? '<div class="event__access">Nur für Mitglieder</div>' : '';
const desc = guest ? `<p class="muted">Melde dich an, um die volle Beschreibung zu sehen.</p>` : `<p>${item.description}</p>`;
const contact = !guest ? `<div class="muted small">Kontakt: ${item.contact}</div>` : '';
const kids = item.allowKids ? 'Mit Kindern' : 'Ohne Kinder';
const tags = [
tag(item.topic === 'kaffee' ? 'Kaffee & Austausch' : item.topic.charAt(0).toUpperCase() + item.topic.slice(1)),
tag(kids),
item.ageGroup ? tag(`Alter: ${item.ageGroup}`) : '',
].filter(Boolean).join('');
return `
<article class="card">
<div class="event__body">
${access}
<div class="event__meta">
<span>${fmtDate(item.startsAt)}</span>
<span>📍 ${item.region || item.city}</span>
<span>${item.visibility === 'public' ? 'Öffentlich' : 'Mitglieder'}</span>
</div>
<h3>${item.title}</h3>
<p class="muted">${guest ? item.teaser : item.description.slice(0, 140) + '...'}</p>
${guest ? '' : desc}
<div class="event__tags">${tags}</div>
${contact}
<div class="flex gap-12">
<button class="btn ghost" data-event-detail="${item.id}">Details</button>
${guest ? '' : '<button class="btn">Teilnehmen</button>'}
</div>
</div>
</article>`;
};
const matchesFilter = (ev) => {
const topicOk = state.topic === 'all' || state.topic === ev.topic || state.topic === '';
const ageOk = !state.age || ev.ageGroup === state.age || (state.age === 'baby' && ev.ageGroup === 'kids'); // simple fallback
const regionField = (ev.region || '').toLowerCase();
const cityField = (ev.city || '').toLowerCase();
const zipField = (ev.zip || '');
const queryHaystack = (ev.title + ev.teaser + ev.description + (ev.city || '') + (ev.region || '')).toLowerCase();
const regionOk = !state.region || regionField.includes(state.region) || cityField.includes(state.region) || zipField.startsWith(state.region);
const queryOk = !state.query || queryHaystack.includes(state.query);
return topicOk && ageOk && regionOk && queryOk;
};
const renderEvents = () => {
if (!el.list) return;
let filtered = events.filter(matchesFilter);
filtered.sort((a,b) => new Date(a.startsAt) - new Date(b.startsAt));
if (state.geo && !searchState.active) {
const withCoords = filtered.filter(ev => ev.lat !== null && ev.lng !== null);
withCoords.forEach(ev => { ev._distance = haversine(state.geo.lat, state.geo.lng, ev.lat, ev.lng); });
withCoords.sort((a,b) => (a._distance || Infinity) - (b._distance || Infinity));
const near = withCoords.filter(ev => ev._distance <= 5000).slice(0,5);
const fallback = withCoords.slice(0,5);
filtered = (near.length ? near : fallback).map(ev => {
const copy = { ...ev };
if (ev._distance) {
copy.teaser = `${ev.teaser} · ca. ${(ev._distance/1000).toFixed(1)} km entfernt`;
}
return copy;
});
} else if (!searchState.active) {
filtered = filtered.slice(0,5);
}
el.list.innerHTML = filtered.map(renderCard).join('') || '<p class="muted">Keine Events gefunden.</p>';
// wire detail buttons
el.list.querySelectorAll('[data-event-detail]').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.getAttribute('data-event-detail'), 10);
const ev = events.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>
${ev.locationLabel ? `<p><strong>Ort:</strong> ${ev.locationLabel}</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');
});
});
};
if (el.chips.length > 0) {
el.chips[0].classList.add('active');
}
el.chips.forEach(chip => {
chip.addEventListener('click', () => {
el.chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
state.topic = chip.dataset.filter;
renderEvents();
});
});
if (el.btnSearch) {
el.btnSearch.addEventListener('click', () => {
state.region = (el.locInput?.value || '').trim().toLowerCase();
state.topic = el.topicSelect?.value || 'all';
state.age = el.ageSelect?.value || '';
state.query = state.region; // keep simple search for now
searchState.active = true;
renderEvents();
});
}
if (el.btnGeo) {
el.btnGeo.addEventListener('click', () => {
if (!navigator.geolocation) {
alert('Geolocation wird nicht unterstützt.');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
state.geo = { lat: pos.coords.latitude, lng: pos.coords.longitude };
searchState.active = false;
renderEvents();
},
() => alert('Standort konnte nicht ermittelt werden.')
);
});
}
// 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'); });
renderEvents();
});