adasd
This commit is contained in:
@@ -2,6 +2,7 @@ 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');
|
||||
|
||||
@@ -52,6 +53,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 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 el = {
|
||||
sliderTrack: document.getElementById('eventSlider'),
|
||||
@@ -71,6 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
quickLat: document.getElementById('qsLat'),
|
||||
quickLng: document.getElementById('qsLng'),
|
||||
quickGeo: document.getElementById('quickGeo'),
|
||||
eventsSection: document.getElementById('events'),
|
||||
};
|
||||
|
||||
const fmtDate = (iso) => {
|
||||
@@ -78,36 +82,182 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) => {
|
||||
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(),
|
||||
};
|
||||
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
|
||||
}
|
||||
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 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')}</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 ? '' : '<button class="btn">Teilnehmen</button>'}
|
||||
${guest || item.missed ? '' : '<button class="btn">Teilnehmen</button>'}
|
||||
</div>
|
||||
</article>`;
|
||||
};
|
||||
|
||||
const renderSlider = () => {
|
||||
const renderSlider = (location = null) => {
|
||||
if (!el.sliderTrack) return;
|
||||
if (!events.length) {
|
||||
const rankedEvents = getRankedEvents(events, location);
|
||||
if (!rankedEvents.length) {
|
||||
el.sliderTrack.innerHTML = '<p class="muted">Keine Events vorhanden.</p>';
|
||||
return;
|
||||
}
|
||||
el.sliderTrack.innerHTML = events.slice(0, 10).map(renderCard).join('');
|
||||
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 = events.find(e => e.id === id);
|
||||
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 = `
|
||||
@@ -115,6 +265,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<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>
|
||||
`;
|
||||
@@ -160,6 +312,56 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
resolve(location);
|
||||
},
|
||||
() => resolve(null),
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: options.timeout || 10000,
|
||||
maximumAge: options.maximumAge || 300000,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const ensureEventsLocation = async () => {
|
||||
if (locationPreference === 'disabled') {
|
||||
clearStoredLocation();
|
||||
renderSlider(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const stored = applyStoredLocationToForm();
|
||||
if (stored && locationPreference !== 'enabled') {
|
||||
renderSlider(stored);
|
||||
return stored;
|
||||
}
|
||||
const location = await requestLocation();
|
||||
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);
|
||||
@@ -178,11 +380,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
if (el.quickLat && el.quickLng) {
|
||||
el.quickLat.value = pos.coords.latitude.toFixed(6);
|
||||
el.quickLng.value = pos.coords.longitude.toFixed(6);
|
||||
if (el.quickLoc) el.quickLoc.value = 'Mein Standort';
|
||||
}
|
||||
persistLocation({
|
||||
lat: pos.coords.latitude,
|
||||
lng: pos.coords.longitude,
|
||||
label: 'Mein Standort',
|
||||
});
|
||||
renderSlider(getStoredLocation());
|
||||
},
|
||||
() => alert('Standort konnte nicht ermittelt werden.')
|
||||
);
|
||||
@@ -215,6 +418,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
el.modal?.addEventListener('click', (e) => { if (e.target === el.modal) el.modal.classList.remove('open'); });
|
||||
|
||||
renderSlider();
|
||||
document.querySelectorAll('[data-nav-events]').forEach(link => {
|
||||
link.addEventListener('click', async () => {
|
||||
if (window.location.pathname === '/' && window.location.hash === '#events') {
|
||||
await ensureEventsLocation();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const initialLocation = locationPreference === 'disabled' ? null : applyStoredLocationToForm();
|
||||
if (locationPreference === 'disabled') {
|
||||
clearStoredLocation();
|
||||
}
|
||||
renderSlider(initialLocation);
|
||||
renderThreadSlider();
|
||||
|
||||
if (locationPreference === '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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user