This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<nav class="site-footer__links" aria-label="Footer">
|
||||
<a href="/impressum">Impressum</a>
|
||||
<a href="/ueber-uns">Über uns</a>
|
||||
<button class="site-footer__link-button" type="button" data-consent-open>Cookie-Einstellungen</button>
|
||||
</nav>
|
||||
<p class="site-footer__copy">© Copyright <?= date('Y') ?> - Lars Gebhardt-Kusche</p>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +70,93 @@ if ($debugEnabled) {
|
||||
<?php asset_scripts('header'); ?>
|
||||
</head>
|
||||
<body data-auth="<?= isset($_SESSION['user_id']) ? '1' : '0' ?>" data-child-gender="<?= htmlspecialchars($childGender, ENT_QUOTES) ?>" data-location-preference="<?= htmlspecialchars($locationTrackingPreference, ENT_QUOTES) ?>">
|
||||
<script>
|
||||
(function () {
|
||||
const storageKey = 'pkt_cookie_consent';
|
||||
const read = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
window.PKTConsent = window.PKTConsent || {
|
||||
storageKey,
|
||||
get() {
|
||||
return read();
|
||||
},
|
||||
has(category) {
|
||||
if (category === 'necessary') return true;
|
||||
const consent = read();
|
||||
return Boolean(consent && consent[category] === true);
|
||||
},
|
||||
openPreferences() {
|
||||
document.dispatchEvent(new CustomEvent('pkt:open-consent'));
|
||||
},
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php tpl('matomo', 'structure'); ?>
|
||||
<?php tpl('nav', 'structure'); ?>
|
||||
<div class="cookie-consent" id="cookieConsentBanner" hidden>
|
||||
<div class="cookie-consent__inner">
|
||||
<div class="cookie-consent__copy">
|
||||
<strong>Cookies, Analyse und externe Dienste</strong>
|
||||
<p>Notwendige Cookies für Sitzung und Login sind immer aktiv. Analyse mit Matomo sowie Karten-, Standort- und Geocoding-Funktionen werden erst nach deiner Auswahl geladen.</p>
|
||||
</div>
|
||||
<div class="cookie-consent__actions">
|
||||
<button class="btn ghost" type="button" data-consent-necessary>Nur notwendige</button>
|
||||
<button class="btn ghost" type="button" data-consent-open>Auswahl</button>
|
||||
<button class="btn" type="button" data-consent-accept-all>Alle akzeptieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="cookieConsentModal">
|
||||
<div class="panel">
|
||||
<div class="head flex between center-y">
|
||||
<h3 style="margin:0;">Cookie- und Diensteinstellungen</h3>
|
||||
<button class="btn ghost" type="button" data-consent-close>✕</button>
|
||||
</div>
|
||||
<div class="stack gap-12" style="margin-top:12px;">
|
||||
<p class="muted" style="margin:0;">Du kannst nicht notwendige Funktionen freiwillig aktivieren oder später wieder widerrufen.</p>
|
||||
<div class="card">
|
||||
<strong>Notwendig</strong>
|
||||
<p class="muted small">Sitzung, Login und Sicherheitsfunktionen. Immer aktiv.</p>
|
||||
</div>
|
||||
<label class="card" style="display:block; cursor:pointer;">
|
||||
<input type="checkbox" id="consentAnalytics" style="margin-right:8px;">
|
||||
<strong>Analyse mit Matomo</strong>
|
||||
<p class="muted small" style="margin:8px 0 0;">Reichweitenmessung und Nutzungsanalyse.</p>
|
||||
</label>
|
||||
<label class="card" style="display:block; cursor:pointer;">
|
||||
<input type="checkbox" id="consentExternalServices" style="margin-right:8px;">
|
||||
<strong>Externe Dienste, Karten und Standort</strong>
|
||||
<p class="muted small" style="margin:8px 0 0;">Browser-Geolocation, lokale Standortspeicherung, Leaflet von `unpkg.com` und Geocoding/Reverse-Geocoding über OpenStreetMap Nominatim.</p>
|
||||
</label>
|
||||
<div class="flex gap-12" style="flex-wrap:wrap;">
|
||||
<button class="btn ghost" type="button" data-consent-necessary>Nur notwendige</button>
|
||||
<button class="btn" type="button" data-consent-save>Auswahl speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="locationPreferenceModal">
|
||||
<div class="panel">
|
||||
<div class="head flex between center-y">
|
||||
<h3 style="margin:0;">Standortfreigabe</h3>
|
||||
<button class="btn ghost" type="button" data-location-pref-close>✕</button>
|
||||
</div>
|
||||
<div class="stack gap-12" style="margin-top:12px;">
|
||||
<p class="muted" style="margin:0;">Damit wir dir passende Treffen in deiner Nähe zeigen können, dürfen wir deinen Standort verwenden. Wie soll das gehandhabt werden?</p>
|
||||
<div class="stack gap-10">
|
||||
<button class="btn" type="button" data-location-pref-choice="session">Nur für diesen Besuch</button>
|
||||
<button class="btn ghost" type="button" data-location-pref-choice="enabled">Immer erlauben</button>
|
||||
<button class="btn ghost" type="button" data-location-pref-choice="disabled">Niemals</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($debugEnabled): ?>
|
||||
<style>
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
<?php if (!defined('MATOMO_SITE_ID')) return; ?>
|
||||
<?php if (!defined('MATOMO_ENABLED') || !MATOMO_ENABLED) return; ?>
|
||||
<?php
|
||||
$primaryDomain = app_primary_domain();
|
||||
$primaryDomain = app_primary_domain();
|
||||
$fakecheckDomain = app_fakecheck_domain();
|
||||
|
||||
// Build allowed domains for tracking (hosts only, no paths)
|
||||
$matomoDomains = array_values(array_unique(array_filter([
|
||||
$primaryDomain ? '*.' . $primaryDomain : null,
|
||||
($fakecheckDomain && $fakecheckDomain !== $primaryDomain) ? '*.' . $fakecheckDomain : null,
|
||||
])));
|
||||
?>
|
||||
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(["setDomains", <?= json_encode($matomoDomains, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
|
||||
(function() {
|
||||
var u = "<?= rtrim(MATOMO_URL, '/') ?>/";
|
||||
_paq.push(['setTrackerUrl', u + 'matomo.php']);
|
||||
_paq.push(['setSiteId', '<?= MATOMO_SITE_ID ?>']);
|
||||
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true;
|
||||
g.src=u + 'matomo.js';
|
||||
s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<p>
|
||||
<img referrerpolicy="no-referrer-when-downgrade"
|
||||
src="<?= rtrim(MATOMO_URL,'/') ?>/matomo.php?idsite=<?= MATOMO_SITE_ID ?>&rec=1"
|
||||
style="border:0;" alt="" />
|
||||
</p>
|
||||
</noscript>
|
||||
<!-- End Matomo -->
|
||||
<script type="application/json" id="matomoConfig"><?= json_encode([
|
||||
'url' => rtrim(MATOMO_URL, '/') . '/',
|
||||
'siteId' => (string) MATOMO_SITE_ID,
|
||||
'domains' => $matomoDomains,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>
|
||||
|
||||
@@ -137,7 +137,14 @@ body {
|
||||
.site-footer__inner { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px 30px; }
|
||||
.site-footer__links { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; font-weight: 600; }
|
||||
.site-footer__links a:hover { color: var(--color-primary); }
|
||||
.site-footer__link-button { border: 0; background: transparent; padding: 0; font: inherit; color: var(--color-text); cursor: pointer; }
|
||||
.site-footer__link-button:hover { color: var(--color-primary); }
|
||||
.site-footer__copy { margin: 0; color: var(--color-muted); font-size: 14px; text-align: center; }
|
||||
.cookie-consent { position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 220; }
|
||||
.cookie-consent[hidden] { display: none; }
|
||||
.cookie-consent__inner { max-width: 1120px; margin: 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: center; padding: 18px 20px; background: rgba(255,255,255,0.98); border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: 0 18px 40px rgba(0,0,0,0.16); backdrop-filter: blur(10px); }
|
||||
.cookie-consent__copy p { margin: 6px 0 0; color: var(--color-muted); }
|
||||
.cookie-consent__actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.legal-page { min-height: 40vh; }
|
||||
.content-card { background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 24px; box-shadow: var(--shadow-card); }
|
||||
.content-card--narrow { max-width: 860px; margin: 0 auto; }
|
||||
@@ -279,6 +286,8 @@ body {
|
||||
.forum-admin-item { padding-left: 16px; padding-right: 16px; }
|
||||
.forum-post__body,
|
||||
.forum-reply-form { padding: 18px 16px; }
|
||||
.cookie-consent__inner { grid-template-columns: 1fr; }
|
||||
.cookie-consent__actions { justify-content: stretch; }
|
||||
}
|
||||
|
||||
/* Auth & Dashboard */
|
||||
|
||||
@@ -55,6 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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'),
|
||||
@@ -75,7 +77,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
@@ -98,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
?.slice(prefix.length) || '';
|
||||
};
|
||||
|
||||
const persistLocation = (location) => {
|
||||
const persistLocation = (location, mode = 'persistent') => {
|
||||
if (!location || !Number.isFinite(location.lat) || !Number.isFinite(location.lng)) return;
|
||||
const payload = {
|
||||
lat: Number(location.lat.toFixed(6)),
|
||||
@@ -106,12 +110,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
label: location.label || 'Mein Standort',
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
try {
|
||||
window.localStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(payload));
|
||||
} catch (err) {
|
||||
// ignore
|
||||
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);
|
||||
}
|
||||
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')) {
|
||||
@@ -125,6 +137,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} 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 = '';
|
||||
@@ -133,6 +150,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
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);
|
||||
@@ -333,7 +356,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
lng: pos.coords.longitude,
|
||||
label: 'Mein Standort',
|
||||
};
|
||||
persistLocation(location);
|
||||
persistLocation(location, options.persistMode || 'persistent');
|
||||
resolve(location);
|
||||
},
|
||||
() => resolve(null),
|
||||
@@ -345,19 +368,133 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
if (locationPreference === 'disabled') {
|
||||
const sessionPreference = getSessionPreference();
|
||||
if (effectiveLocationPreference === 'disabled') {
|
||||
clearStoredLocation();
|
||||
renderSlider(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const stored = applyStoredLocationToForm();
|
||||
if (stored && locationPreference !== 'enabled') {
|
||||
if (stored && effectiveLocationPreference !== 'enabled' && sessionPreference !== 'session') {
|
||||
renderSlider(stored);
|
||||
return stored;
|
||||
}
|
||||
const location = await requestLocation();
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -384,7 +521,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
lat: pos.coords.latitude,
|
||||
lng: pos.coords.longitude,
|
||||
label: 'Mein Standort',
|
||||
});
|
||||
}, effectiveLocationPreference === 'enabled' ? 'persistent' : 'session');
|
||||
renderSlider(getStoredLocation());
|
||||
},
|
||||
() => alert('Standort konnte nicht ermittelt werden.')
|
||||
@@ -426,14 +563,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const initialLocation = locationPreference === 'disabled' ? null : applyStoredLocationToForm();
|
||||
if (locationPreference === 'disabled') {
|
||||
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 (locationPreference === 'enabled' && window.location.pathname === '/') {
|
||||
if (effectiveLocationPreference === 'enabled' && window.location.pathname === '/') {
|
||||
ensureEventsLocation();
|
||||
} else if (window.location.hash === '#events' || window.location.hash === '#quicksearch') {
|
||||
ensureEventsLocation();
|
||||
|
||||
@@ -68,6 +68,9 @@ $targetReal = realpath($target);
|
||||
if ($targetReal && str_starts_with($targetReal, realpath(__DIR__ . '/page/retool'))) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
if ($targetReal && str_starts_with($targetReal, realpath(__DIR__ . '/page/api'))) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Ausgabe
|
||||
|
||||
38
public/page/api/location-preference.php
Normal file
38
public/page/api/location-preference.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['ok' => false, 'error' => 'method_not_allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'unauthorized']);
|
||||
return;
|
||||
}
|
||||
|
||||
$preference = (string)($_POST['preference'] ?? '');
|
||||
if (!in_array($preference, ['disabled', 'prompt', 'enabled'], true)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['ok' => false, 'error' => 'invalid_preference']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = app()->pdo();
|
||||
if (!$pdo) {
|
||||
throw new RuntimeException('db_unavailable');
|
||||
}
|
||||
|
||||
$settings = new \App\ProfileSettings($pdo);
|
||||
$settings->updateLocationTrackingPreference((int)$_SESSION['user_id'], $preference);
|
||||
|
||||
echo json_encode(['ok' => true, 'preference' => $preference]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'save_failed']);
|
||||
}
|
||||
Reference in New Issue
Block a user