This commit is contained in:
@@ -17,6 +17,7 @@ Aktueller Projektstand
|
|||||||
- Wenn bei einem Kind ein Geburtsdatum hinterlegt ist, wird das Alter automatisch daraus berechnet und spaeter bei Bedarf jaehrlich aktualisiert; ohne Geburtsdatum bleibt das manuell gepflegte Alter massgeblich.
|
- Wenn bei einem Kind ein Geburtsdatum hinterlegt ist, wird das Alter automatisch daraus berechnet und spaeter bei Bedarf jaehrlich aktualisiert; ohne Geburtsdatum bleibt das manuell gepflegte Alter massgeblich.
|
||||||
- Profilbezogene sensible Angaben wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; vorhandene Klartextwerte werden bei der Nutzung schrittweise nachmigriert.
|
- Profilbezogene sensible Angaben wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; vorhandene Klartextwerte werden bei der Nutzung schrittweise nachmigriert.
|
||||||
- Das Profil besitzt jetzt zusaetzlich eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie eine optionale Adress-Lokalisierung ueber die vorhandenen externen Karten-/Geocoding-Dienste.
|
- Das Profil besitzt jetzt zusaetzlich eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie eine optionale Adress-Lokalisierung ueber die vorhandenen externen Karten-/Geocoding-Dienste.
|
||||||
|
- Die Profiladresse unterstuetzt jetzt ausserdem eine Auswahl moeglicher Adress-Treffer, die Uebernahme aus dem Browser-Standort und einen kurzen Hinweisdialog zur lokalen Relevanz.
|
||||||
- Alte statische Avatar-Presets und frühere Layer-Assets sind entfernt.
|
- Alte statische Avatar-Presets und frühere Layer-Assets sind entfernt.
|
||||||
- Änderungen an Funktionen mit Cookies, LocalStorage, SessionStorage, Geolocation, Tracking oder Drittanbietern müssen immer auch in Datenschutz-/Cookie-Hinweisen und im Consent-Flow berücksichtigt werden.
|
- Änderungen an Funktionen mit Cookies, LocalStorage, SessionStorage, Geolocation, Tracking oder Drittanbietern müssen immer auch in Datenschutz-/Cookie-Hinweisen und im Consent-Flow berücksichtigt werden.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Papa-Kind-Treff – Chat-Kontext
|
# Papa-Kind-Treff – Chat-Kontext
|
||||||
|
|
||||||
Stand: 2026-07-29
|
Stand: 2026-07-31
|
||||||
|
|
||||||
## Kurzbeschreibung
|
## Kurzbeschreibung
|
||||||
Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind lokale Treffen/Events, ein Community-Forum, Mitgliederprofile mit optionalen Kinderinfos und ein Moderations-/Adminbereich.
|
Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind lokale Treffen/Events, ein Community-Forum, Mitgliederprofile mit optionalen Kinderinfos und ein Moderations-/Adminbereich.
|
||||||
@@ -37,6 +37,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
|||||||
- Wenn ein Geburtsdatum gesetzt ist, wird das Alter im Mitgliederbereich automatisch berechnet und spaeter bei Bedarf anhand des Datums aktualisiert; ohne Geburtsdatum gilt der manuelle Alterswert.
|
- Wenn ein Geburtsdatum gesetzt ist, wird das Alter im Mitgliederbereich automatisch berechnet und spaeter bei Bedarf anhand des Datums aktualisiert; ohne Geburtsdatum gilt der manuelle Alterswert.
|
||||||
- Sensible Profildaten wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden bei Nutzung schrittweise in den verschluesselten Zustand ueberfuehrt.
|
- Sensible Profildaten wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden bei Nutzung schrittweise in den verschluesselten Zustand ueberfuehrt.
|
||||||
- Im Profil koennen jetzt auch Strasse/Hausnummer, PLZ, Ort und Region gepflegt werden; fuer die Adress-Lokalisierung werden die bereits vorhandenen externen Karten-/Geocoding-Dienste wiederverwendet.
|
- Im Profil koennen jetzt auch Strasse/Hausnummer, PLZ, Ort und Region gepflegt werden; fuer die Adress-Lokalisierung werden die bereits vorhandenen externen Karten-/Geocoding-Dienste wiederverwendet.
|
||||||
|
- Die Profiladresse bietet jetzt zusaetzlich eine Trefferauswahl fuer die Suche, eine Uebernahme aus dem Browser-Standort und einen kurzen Hinweisdialog zur Relevanz fuer lokale Termin-Hinweise.
|
||||||
- Im Profil kann der Nutzer aktuell Komponenten wie Augen, Augenbrauen, Mund, Brille, Haare, Bart und Ohrringe direkt per Vorschaubild auswählen.
|
- Im Profil kann der Nutzer aktuell Komponenten wie Augen, Augenbrauen, Mund, Brille, Haare, Bart und Ohrringe direkt per Vorschaubild auswählen.
|
||||||
- Der Button `Random Profilbild` setzt Seed und Varianten des aktiven Styles zufaellig neu und aktualisiert die Vorschau sofort.
|
- Der Button `Random Profilbild` setzt Seed und Varianten des aktiven Styles zufaellig neu und aktualisiert die Vorschau sofort.
|
||||||
- Beim Oeffnen des Avatar-Generators wird nur der aktive Style-/Komponenten-Tab geladen; weitere Varianten werden erst beim Wechsel nachgeladen.
|
- Beim Oeffnen des Avatar-Generators wird nur der aktive Style-/Komponenten-Tab geladen; weitere Varianten werden erst beim Wechsel nachgeladen.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Papa-Kind-Treff
|
# Papa-Kind-Treff
|
||||||
|
|
||||||
Stand: 2026-07-29
|
Stand: 2026-07-31
|
||||||
|
|
||||||
Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale Treffen, Community-Austausch und einen geschützten Mitgliederbereich.
|
Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale Treffen, Community-Austausch und einen geschützten Mitgliederbereich.
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
|||||||
- bei Kindern mit Geburtsdatum wird das Alter automatisch berechnet und bei Aufruf des Mitgliederbereichs bei Bedarf jaehrlich nachgezogen; ohne Geburtsdatum gilt das manuell gepflegte Alter
|
- bei Kindern mit Geburtsdatum wird das Alter automatisch berechnet und bei Aufruf des Mitgliederbereichs bei Bedarf jaehrlich nachgezogen; ohne Geburtsdatum gilt das manuell gepflegte Alter
|
||||||
- sensible Profildaten werden im Mitgliederbereich jetzt konsequent app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden beim Lesen schrittweise in den verschluesselten Zustand ueberfuehrt
|
- sensible Profildaten werden im Mitgliederbereich jetzt konsequent app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden beim Lesen schrittweise in den verschluesselten Zustand ueberfuehrt
|
||||||
- das Profil unterstuetzt jetzt eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie optionaler Adress-Lokalisierung ueber die bereits vorhandenen Karten-/Geocoding-Dienste
|
- das Profil unterstuetzt jetzt eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie optionaler Adress-Lokalisierung ueber die bereits vorhandenen Karten-/Geocoding-Dienste
|
||||||
|
- bei der Profiladresse gibt es jetzt eine Trefferauswahl fuer Adresssuche, eine Uebernahme aus dem Browser-Standort sowie einen mobilen Hinweisdialog, warum die Adresse fuer lokale Termine relevant ist
|
||||||
- Debug-Floating-Button nur für `site_admin` im Debug-Modus
|
- Debug-Floating-Button nur für `site_admin` im Debug-Modus
|
||||||
|
|
||||||
## Technik
|
## Technik
|
||||||
|
|||||||
@@ -530,8 +530,19 @@ $sectionLinks = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-12" style="flex-wrap:wrap;">
|
<div class="flex gap-12" style="flex-wrap:wrap;">
|
||||||
<button class="btn ghost" type="button" id="btnProfileLocate">Adresse lokalisieren</button>
|
<button class="btn ghost" type="button" id="btnProfileLocate">Adresse suchen</button>
|
||||||
<p class="muted small" style="margin:0;">Verwendet dieselben Karten-/Adressdienste wie bei Events und nur nach Einwilligung in externe Dienste.</p>
|
<button class="btn ghost" type="button" id="btnProfileBrowserLocation">Browser-Adresse übernehmen</button>
|
||||||
|
<button class="btn ghost" type="button" data-modal-open="modalProfileAddressHelp" aria-label="Hilfe zur Profiladresse">?</button>
|
||||||
|
</div>
|
||||||
|
<p class="muted small" style="margin:0;">Für Suche und Browser-Übernahme werden dieselben Karten-/Adressdienste wie bei Events genutzt und nur nach Einwilligung in externe Dienste.</p>
|
||||||
|
<div id="profileAddressStatus" class="muted small" style="margin:0;"></div>
|
||||||
|
<div id="profileAddressSelectedLabel" class="muted small" style="margin:0;<?= empty($profile['street']) && empty($profile['city']) ? ' display:none;' : '' ?>">
|
||||||
|
<?php if (!empty($profile['street']) || !empty($profile['city'])): ?>
|
||||||
|
Gewaehlte Adresse: <?= htmlspecialchars(trim(implode(', ', array_filter([(string)($profile['street'] ?? ''), trim((string)$profile['zip'] . ' ' . (string)$profile['city']), (string)($profile['region'] ?? '')]))), ENT_QUOTES) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div id="profileAddressNeedsValidation" class="muted small" style="margin:0; display:none;">
|
||||||
|
Die Adresse wurde manuell geaendert. Vor dem Speichern bitte erneut suchen oder die Browser-Adresse uebernehmen.
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label">Sprachen (Mehrfachauswahl)</label>
|
<label class="label">Sprachen (Mehrfachauswahl)</label>
|
||||||
@@ -566,6 +577,39 @@ $sectionLinks = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="modalProfileAddressHelp">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="head flex between center-y">
|
||||||
|
<h3>Warum wir die Adresse abfragen</h3>
|
||||||
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-12">
|
||||||
|
<p class="muted" style="margin:0;">Deine Adresse hilft uns, dir die neuesten Termine und Events in deiner Umgebung gezielter anzuzeigen.</p>
|
||||||
|
<p class="muted" style="margin:0;">Wenn keine Adresse hinterlegt ist, koennen wir lokale Bedarfe und passende Angebote deutlich schlechter einschaetzen.</p>
|
||||||
|
<p class="muted small" style="margin:0;">Die Adresse wird nur fuer diese Plattform-Funktionen genutzt. Strasse und Hausnummer werden verschluesselt gespeichert.</p>
|
||||||
|
<div class="flex gap-12">
|
||||||
|
<button class="btn" type="button" data-modal-close>Verstanden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="modalProfileAddressResults">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="head flex between center-y">
|
||||||
|
<h3>Adresse auswählen</h3>
|
||||||
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-12">
|
||||||
|
<p class="muted small" style="margin:0;">Waehle den passenden Treffer fuer dein Profil.</p>
|
||||||
|
<div id="profileAddressResults" class="stack gap-12"></div>
|
||||||
|
<div class="flex gap-12">
|
||||||
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal" id="modalChild">
|
<div class="modal" id="modalChild">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="head flex between center-y">
|
<div class="head flex between center-y">
|
||||||
@@ -746,6 +790,7 @@ $sectionLinks = [
|
|||||||
const cityInput = document.getElementById('evCity');
|
const cityInput = document.getElementById('evCity');
|
||||||
const regionInput = document.getElementById('evRegion');
|
const regionInput = document.getElementById('evRegion');
|
||||||
const profileLocateButton = document.getElementById('btnProfileLocate');
|
const profileLocateButton = document.getElementById('btnProfileLocate');
|
||||||
|
const profileBrowserLocationButton = document.getElementById('btnProfileBrowserLocation');
|
||||||
const profileStreetInput = document.getElementById('pStreet');
|
const profileStreetInput = document.getElementById('pStreet');
|
||||||
const profileZipInput = document.getElementById('pZip');
|
const profileZipInput = document.getElementById('pZip');
|
||||||
const profileCityInput = document.getElementById('pCity');
|
const profileCityInput = document.getElementById('pCity');
|
||||||
@@ -753,6 +798,11 @@ $sectionLinks = [
|
|||||||
const profileRegionViewInput = document.getElementById('pRegionView');
|
const profileRegionViewInput = document.getElementById('pRegionView');
|
||||||
const profileLatInput = document.getElementById('pLat');
|
const profileLatInput = document.getElementById('pLat');
|
||||||
const profileLngInput = document.getElementById('pLng');
|
const profileLngInput = document.getElementById('pLng');
|
||||||
|
const profileAddressStatus = document.getElementById('profileAddressStatus');
|
||||||
|
const profileAddressSelectedLabel = document.getElementById('profileAddressSelectedLabel');
|
||||||
|
const profileAddressNeedsValidation = document.getElementById('profileAddressNeedsValidation');
|
||||||
|
const profileAddressResults = document.getElementById('profileAddressResults');
|
||||||
|
const profileAddressResultsModal = document.getElementById('modalProfileAddressResults');
|
||||||
let map, marker;
|
let map, marker;
|
||||||
|
|
||||||
function ensureLeaflet(callback) {
|
function ensureLeaflet(callback) {
|
||||||
@@ -843,24 +893,50 @@ $sectionLinks = [
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function geocodeAddressData(query, onSuccess) {
|
function geocodeAddressCandidates(query, onSuccess) {
|
||||||
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
||||||
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
||||||
window.PKTConsent?.openPreferences?.();
|
window.PKTConsent?.openPreferences?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&addressdetails=1&q=' + encodeURIComponent(query), {
|
fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&addressdetails=1&q=' + encodeURIComponent(query), {
|
||||||
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (Array.isArray(data) && data[0]) {
|
if (Array.isArray(data) && data.length) {
|
||||||
onSuccess?.(data[0]);
|
onSuccess?.(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
||||||
alert('Adresse konnte nicht eindeutig lokalisiert werden.');
|
alert('Adresse konnte nicht eindeutig lokalisiert werden.');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
||||||
|
alert('Adresssuche aktuell nicht erreichbar.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reverseGeocodeProfileAddress(lat, lng, onSuccess) {
|
||||||
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
||||||
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
||||||
|
window.PKTConsent?.openPreferences?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&addressdetails=1&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}`, {
|
||||||
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.lat && data.lon) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
||||||
|
alert('Die Browser-Position konnte nicht in eine Adresse umgewandelt werden.');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
||||||
alert('Adresssuche aktuell nicht erreichbar.');
|
alert('Adresssuche aktuell nicht erreichbar.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -911,6 +987,85 @@ $sectionLinks = [
|
|||||||
});
|
});
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
|
const setProfileStatus = (message = '') => {
|
||||||
|
if (!profileAddressStatus) return;
|
||||||
|
profileAddressStatus.textContent = message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSelectedAddress = (label) => {
|
||||||
|
if (!profileAddressSelectedLabel) return;
|
||||||
|
if (!label) {
|
||||||
|
profileAddressSelectedLabel.textContent = '';
|
||||||
|
profileAddressSelectedLabel.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profileAddressSelectedLabel.textContent = `Gewaehlte Adresse: ${label}`;
|
||||||
|
profileAddressSelectedLabel.style.display = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNeedsValidation = (needsValidation) => {
|
||||||
|
if (profileAddressNeedsValidation) {
|
||||||
|
profileAddressNeedsValidation.style.display = needsValidation ? '' : 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeResult = (result) => {
|
||||||
|
const address = result.address || {};
|
||||||
|
const street = [address.road || '', address.house_number || ''].filter(Boolean).join(' ').trim();
|
||||||
|
const region = address.city_district || address.suburb || address.state || address.county || address.region || address.state_district || '';
|
||||||
|
const city = address.city || address.town || address.village || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: result.display_name || '',
|
||||||
|
street,
|
||||||
|
zip: address.postcode || '',
|
||||||
|
city,
|
||||||
|
region,
|
||||||
|
lat: parseFloat(result.lat),
|
||||||
|
lng: parseFloat(result.lon),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyProfileAddress = (result) => {
|
||||||
|
const normalized = normalizeResult(result);
|
||||||
|
|
||||||
|
if (profileStreetInput && normalized.street) profileStreetInput.value = normalized.street;
|
||||||
|
if (profileZipInput && normalized.zip) profileZipInput.value = normalized.zip;
|
||||||
|
if (profileCityInput && normalized.city) profileCityInput.value = normalized.city;
|
||||||
|
if (profileRegionViewInput) profileRegionViewInput.value = normalized.region;
|
||||||
|
if (profileRegionInput) profileRegionInput.value = normalized.region;
|
||||||
|
if (profileLatInput) profileLatInput.value = Number.isNaN(normalized.lat) ? '' : normalized.lat.toFixed(7);
|
||||||
|
if (profileLngInput) profileLngInput.value = Number.isNaN(normalized.lng) ? '' : normalized.lng.toFixed(7);
|
||||||
|
|
||||||
|
renderSelectedAddress(normalized.label || [normalized.street, `${normalized.zip} ${normalized.city}`.trim(), normalized.region].filter(Boolean).join(', '));
|
||||||
|
setNeedsValidation(false);
|
||||||
|
setProfileStatus('Adresse uebernommen.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProfileResultsModal = (results) => {
|
||||||
|
if (!profileAddressResults || !profileAddressResultsModal) return;
|
||||||
|
profileAddressResults.innerHTML = '';
|
||||||
|
setProfileStatus('Bitte waehle den passenden Adress-Treffer aus.');
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const normalized = normalizeResult(result);
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'btn ghost';
|
||||||
|
button.style.width = '100%';
|
||||||
|
button.style.textAlign = 'left';
|
||||||
|
button.style.justifyContent = 'flex-start';
|
||||||
|
button.innerHTML = `<strong>${normalized.street || normalized.city || 'Treffer'}</strong><br><span class="muted small">${normalized.label || ''}</span>`;
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
applyProfileAddress(result);
|
||||||
|
profileAddressResultsModal.classList.remove('open');
|
||||||
|
});
|
||||||
|
profileAddressResults.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
profileAddressResultsModal.classList.add('open');
|
||||||
|
};
|
||||||
|
|
||||||
const syncProfileRegionView = () => {
|
const syncProfileRegionView = () => {
|
||||||
if (profileRegionViewInput && profileRegionInput) {
|
if (profileRegionViewInput && profileRegionInput) {
|
||||||
profileRegionInput.value = profileRegionViewInput.value.trim();
|
profileRegionInput.value = profileRegionViewInput.value.trim();
|
||||||
@@ -920,6 +1075,9 @@ $sectionLinks = [
|
|||||||
const clearProfileCoordinates = () => {
|
const clearProfileCoordinates = () => {
|
||||||
if (profileLatInput) profileLatInput.value = '';
|
if (profileLatInput) profileLatInput.value = '';
|
||||||
if (profileLngInput) profileLngInput.value = '';
|
if (profileLngInput) profileLngInput.value = '';
|
||||||
|
renderSelectedAddress('');
|
||||||
|
setNeedsValidation(true);
|
||||||
|
setProfileStatus('');
|
||||||
};
|
};
|
||||||
|
|
||||||
[profileStreetInput, profileZipInput, profileCityInput].forEach((input) => {
|
[profileStreetInput, profileZipInput, profileCityInput].forEach((input) => {
|
||||||
@@ -944,26 +1102,41 @@ $sectionLinks = [
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
geocodeAddressData(parts.join(', '), (result) => {
|
setProfileStatus('Suche passende Adressen ...');
|
||||||
const address = result.address || {};
|
geocodeAddressCandidates(parts.join(', '), (results) => openProfileResultsModal(results));
|
||||||
const street = [address.road || '', address.house_number || ''].filter(Boolean).join(' ').trim();
|
});
|
||||||
const region = address.suburb || address.state || address.county || '';
|
|
||||||
const city = address.city || address.town || address.village || '';
|
|
||||||
|
|
||||||
if (profileStreetInput && street) profileStreetInput.value = street;
|
profileBrowserLocationButton?.addEventListener('click', () => {
|
||||||
if (profileZipInput && address.postcode) profileZipInput.value = address.postcode;
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
||||||
if (profileCityInput && city) profileCityInput.value = city;
|
alert('Für die Browser-Adresse bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
||||||
if (profileRegionViewInput) profileRegionViewInput.value = region;
|
window.PKTConsent?.openPreferences?.();
|
||||||
if (profileRegionInput) profileRegionInput.value = region;
|
return;
|
||||||
|
}
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
alert('Dieser Browser unterstuetzt keine Geolokalisierung.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lat = parseFloat(result.lat);
|
setProfileStatus('Browser-Standort wird abgefragt ...');
|
||||||
const lng = parseFloat(result.lon);
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
if (profileLatInput) profileLatInput.value = Number.isNaN(lat) ? '' : lat.toFixed(7);
|
const lat = position.coords.latitude;
|
||||||
if (profileLngInput) profileLngInput.value = Number.isNaN(lng) ? '' : lng.toFixed(7);
|
const lng = position.coords.longitude;
|
||||||
|
reverseGeocodeProfileAddress(lat, lng, (result) => {
|
||||||
|
applyProfileAddress(result);
|
||||||
|
setProfileStatus('Adresse aus dem Browser-Standort uebernommen.');
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
setProfileStatus('');
|
||||||
|
alert('Die Browser-Position konnte nicht gelesen werden.');
|
||||||
|
}, {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000,
|
||||||
|
maximumAge: 300000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
syncProfileRegionView();
|
syncProfileRegionView();
|
||||||
|
setNeedsValidation(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
<?php if ($editing): ?>
|
<?php if ($editing): ?>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ $clientCookie = $config->cookiePrefix() . 'client';
|
|||||||
<main class="container section legal-page">
|
<main class="container section legal-page">
|
||||||
<div class="content-card content-card--narrow page-copy">
|
<div class="content-card content-card--narrow page-copy">
|
||||||
<h1>Datenschutz & Cookies</h1>
|
<h1>Datenschutz & Cookies</h1>
|
||||||
<p class="muted small">Stand: 26. Juli 2026</p>
|
<p class="muted small">Stand: 31. Juli 2026</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Diese Hinweise erklären, welche personenbezogenen Daten beim Besuch und bei der Nutzung von Papa-Kind-Treff
|
Diese Hinweise erklären, welche personenbezogenen Daten beim Besuch und bei der Nutzung von Papa-Kind-Treff
|
||||||
@@ -139,8 +139,10 @@ $clientCookie = $config->cookiePrefix() . 'client';
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Zusätzlich kannst du im Mitgliederbereich deine Profiladresse über die gleichen externen Adressdienste
|
Zusätzlich kannst du im Mitgliederbereich deine Profiladresse über die gleichen externen Adressdienste
|
||||||
lokalisieren lassen, die auch für Event-Adressen genutzt werden. Dabei werden die von dir eingegebenen
|
lokalisieren lassen, die auch für Event-Adressen genutzt werden. Dabei kannst du entweder nach passenden
|
||||||
Adressbestandteile an den jeweiligen Geocoding-Dienst übermittelt.
|
Adress-Treffern suchen oder die vom Browser ermittelte Position in eine Profiladresse übernehmen.
|
||||||
|
In beiden Fällen werden die von dir eingegebenen oder vom Browser bereitgestellten Positionsdaten
|
||||||
|
an den jeweiligen Geocoding-Dienst übermittelt.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Für Karten- und Adressfunktionen werden derzeit externe Dienste genutzt:
|
Für Karten- und Adressfunktionen werden derzeit externe Dienste genutzt:
|
||||||
|
|||||||
@@ -192,6 +192,40 @@ final class AccountPages
|
|||||||
$languagesEnc = self::encryptOptionalProfileField($crypto, trim((string)$languages));
|
$languagesEnc = self::encryptOptionalProfileField($crypto, trim((string)$languages));
|
||||||
$aboutEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['about']));
|
$aboutEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['about']));
|
||||||
$locationPreference = (string)($_POST['location_tracking_preference'] ?? 'prompt');
|
$locationPreference = (string)($_POST['location_tracking_preference'] ?? 'prompt');
|
||||||
|
$street = trim((string)($_POST['street'] ?? ''));
|
||||||
|
$zip = trim((string)($_POST['zip'] ?? ''));
|
||||||
|
$city = trim((string)($_POST['city'] ?? ''));
|
||||||
|
$region = trim((string)($_POST['region'] ?? ''));
|
||||||
|
$lat = isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null;
|
||||||
|
$lng = isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null;
|
||||||
|
|
||||||
|
$hasAddress = $street !== '' || $zip !== '' || $city !== '' || $region !== '';
|
||||||
|
if ($hasAddress) {
|
||||||
|
$resolvedAddress = null;
|
||||||
|
if ($lat !== null && $lng !== null) {
|
||||||
|
$resolvedAddress = self::reverseGeocodeAddress($lat, $lng);
|
||||||
|
}
|
||||||
|
if ($resolvedAddress === null) {
|
||||||
|
$searchResults = self::geocodeAddressCandidates($street, $zip, $city, $region, 5);
|
||||||
|
if (!$searchResults) {
|
||||||
|
throw new \RuntimeException('Die Profiladresse konnte nicht validiert werden. Bitte waehle eine Adresse aus der Suche oder ueber den Browser-Standort.');
|
||||||
|
}
|
||||||
|
$resolvedAddress = $searchResults[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$street = $resolvedAddress['street'] !== '' ? $resolvedAddress['street'] : $street;
|
||||||
|
$zip = $resolvedAddress['zip'] !== '' ? $resolvedAddress['zip'] : $zip;
|
||||||
|
$city = $resolvedAddress['city'] !== '' ? $resolvedAddress['city'] : $city;
|
||||||
|
$region = $resolvedAddress['region'] !== '' ? $resolvedAddress['region'] : $region;
|
||||||
|
$lat = $resolvedAddress['lat'];
|
||||||
|
$lng = $resolvedAddress['lng'];
|
||||||
|
} else {
|
||||||
|
$lat = null;
|
||||||
|
$lng = null;
|
||||||
|
$region = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$streetEnc = self::encryptOptionalProfileField($crypto, $street);
|
||||||
if ($profileSettings) {
|
if ($profileSettings) {
|
||||||
$profileSettings->ensureSchema();
|
$profileSettings->ensureSchema();
|
||||||
}
|
}
|
||||||
@@ -201,11 +235,11 @@ final class AccountPages
|
|||||||
'fname' => $firstNameEnc,
|
'fname' => $firstNameEnc,
|
||||||
'lname' => $lastNameEnc,
|
'lname' => $lastNameEnc,
|
||||||
'street' => $streetEnc,
|
'street' => $streetEnc,
|
||||||
'zip' => trim((string)$_POST['zip']),
|
'zip' => $zip,
|
||||||
'city' => trim((string)$_POST['city']),
|
'city' => $city,
|
||||||
'region' => trim((string)($_POST['region'] ?? '')),
|
'region' => $region,
|
||||||
'lat' => isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null,
|
'lat' => $lat,
|
||||||
'lng' => isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null,
|
'lng' => $lng,
|
||||||
'prof' => $professionEnc,
|
'prof' => $professionEnc,
|
||||||
'langs' => $languagesEnc,
|
'langs' => $languagesEnc,
|
||||||
'about' => $aboutEnc,
|
'about' => $aboutEnc,
|
||||||
@@ -629,6 +663,17 @@ final class AccountPages
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static function geocodeAddress(?string $street, ?string $zip, ?string $city, ?string $region): array
|
private static function geocodeAddress(?string $street, ?string $zip, ?string $city, ?string $region): array
|
||||||
|
{
|
||||||
|
$candidates = self::geocodeAddressCandidates($street, $zip, $city, $region, 1);
|
||||||
|
if (!$candidates) {
|
||||||
|
return [null, null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
$best = $candidates[0];
|
||||||
|
return [$best['lat'], $best['lng'], $best['region'] !== '' ? $best['region'] : null];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function geocodeAddressCandidates(?string $street, ?string $zip, ?string $city, ?string $region, int $limit = 5): array
|
||||||
{
|
{
|
||||||
$parts = array_filter([
|
$parts = array_filter([
|
||||||
$street ?: null,
|
$street ?: null,
|
||||||
@@ -643,10 +688,75 @@ final class AccountPages
|
|||||||
$query = implode(', ', $parts);
|
$query = implode(', ', $parts);
|
||||||
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
|
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
|
||||||
'format' => 'jsonv2',
|
'format' => 'jsonv2',
|
||||||
'limit' => 1,
|
'limit' => max(1, min(10, $limit)),
|
||||||
|
'addressdetails' => 1,
|
||||||
'q' => $query,
|
'q' => $query,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resp = self::fetchGeoJson($url);
|
||||||
|
if ($resp === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$json = json_decode($resp, true);
|
||||||
|
if (!is_array($json)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($json as $item) {
|
||||||
|
if (!is_array($item) || empty($item['lat']) || empty($item['lon'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$results[] = self::normalizeGeoAddressResult($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function reverseGeocodeAddress(float $lat, float $lng): ?array
|
||||||
|
{
|
||||||
|
$url = 'https://nominatim.openstreetmap.org/reverse?' . http_build_query([
|
||||||
|
'format' => 'jsonv2',
|
||||||
|
'addressdetails' => 1,
|
||||||
|
'lat' => $lat,
|
||||||
|
'lon' => $lng,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resp = self::fetchGeoJson($url);
|
||||||
|
if ($resp === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$json = json_decode($resp, true);
|
||||||
|
if (!is_array($json) || empty($json['lat']) || empty($json['lon'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::normalizeGeoAddressResult($json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeGeoAddressResult(array $item): array
|
||||||
|
{
|
||||||
|
$addr = is_array($item['address'] ?? null) ? $item['address'] : [];
|
||||||
|
$street = trim(implode(' ', array_filter([
|
||||||
|
(string)($addr['road'] ?? ''),
|
||||||
|
(string)($addr['house_number'] ?? ''),
|
||||||
|
])));
|
||||||
|
$city = (string)($addr['city'] ?? $addr['town'] ?? $addr['village'] ?? '');
|
||||||
|
$region = (string)($addr['city_district'] ?? $addr['suburb'] ?? $addr['state'] ?? $addr['county'] ?? $addr['region'] ?? $addr['state_district'] ?? '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => trim((string)($item['display_name'] ?? '')),
|
||||||
|
'street' => $street,
|
||||||
|
'zip' => (string)($addr['postcode'] ?? ''),
|
||||||
|
'city' => $city,
|
||||||
|
'region' => $region,
|
||||||
|
'lat' => round((float)$item['lat'], 7),
|
||||||
|
'lng' => round((float)$item['lon'], 7),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function fetchGeoJson(string $url): ?string
|
||||||
|
{
|
||||||
$ctx = stream_context_create([
|
$ctx = stream_context_create([
|
||||||
'http' => [
|
'http' => [
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
@@ -656,15 +766,6 @@ final class AccountPages
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$resp = @file_get_contents($url, false, $ctx);
|
$resp = @file_get_contents($url, false, $ctx);
|
||||||
if ($resp === false) {
|
return $resp === false ? null : $resp;
|
||||||
return [null, null, null];
|
|
||||||
}
|
|
||||||
$json = json_decode($resp, true);
|
|
||||||
if (!is_array($json) || empty($json[0]['lat']) || empty($json[0]['lon'])) {
|
|
||||||
return [null, null, null];
|
|
||||||
}
|
|
||||||
$addr = $json[0]['address'] ?? [];
|
|
||||||
$regionGuess = $addr['city_district'] ?? $addr['suburb'] ?? $addr['state'] ?? $addr['county'] ?? $addr['region'] ?? $addr['state_district'] ?? null;
|
|
||||||
return [round((float)$json[0]['lat'], 7), round((float)$json[0]['lon'], 7), $regionGuess];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user