adasd
This commit is contained in:
@@ -94,6 +94,13 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
|
||||
<li>Ort: <?= htmlspecialchars($profile['city'], ENT_QUOTES) ?> <?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?></li>
|
||||
<li>E-Mail: <?= htmlspecialchars($profile['email'], ENT_QUOTES) ?></li>
|
||||
<li>Telefon: <?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?></li>
|
||||
<li>Standortfreigabe: <?=
|
||||
htmlspecialchars(match ($profile['location_tracking_preference'] ?? 'prompt') {
|
||||
'enabled' => 'Immer verwenden',
|
||||
'disabled' => 'Deaktiviert',
|
||||
default => 'Beim nächsten Mal fragen',
|
||||
}, ENT_QUOTES)
|
||||
?></li>
|
||||
<li>Beruf: <?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></li>
|
||||
<li>Sprachen: <?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></li>
|
||||
<li>About: <?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></li>
|
||||
@@ -256,6 +263,15 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
|
||||
<label class="label" for="pProf">Beruf</label>
|
||||
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>">
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="pLocationPref">Standortfreigabe</label>
|
||||
<select id="pLocationPref" name="location_tracking_preference" class="select">
|
||||
<option value="disabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'disabled') ? 'selected' : '' ?>>Deaktiviert</option>
|
||||
<option value="prompt" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'prompt') ? 'selected' : '' ?>>Beim nächsten Mal fragen</option>
|
||||
<option value="enabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'enabled') ? 'selected' : '' ?>>Immer verwenden</option>
|
||||
</select>
|
||||
<p class="muted small">Bei aktiver Freigabe wird dein aktueller Standort für passende Treffen in deiner Nähe verwendet. Du kannst die Abfrage hier jederzeit wieder auf Nachfrage oder komplett aus stellen.</p>
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="pAbout">Kurzvorstellung</label>
|
||||
<textarea id="pAbout" name="about" class="textarea" rows="3"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
|
||||
|
||||
@@ -7,7 +7,14 @@ $threadsForJs = [];
|
||||
try {
|
||||
$pdo = $app->pdo();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->prepare('SELECT id, title, teaser_public, description, city, region, zip, starts_at, allow_kids, visibility, location_label, lat, lng, created_at FROM events WHERE starts_at >= NOW() AND status != "cancelled" ORDER BY created_at DESC, starts_at ASC LIMIT 10');
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, title, teaser_public, description, city, region, zip, starts_at, allow_kids, visibility, location_label, lat, lng, created_at
|
||||
FROM events
|
||||
WHERE starts_at >= DATE_SUB(NOW(), INTERVAL 120 DAY)
|
||||
AND status != "cancelled"
|
||||
ORDER BY starts_at ASC
|
||||
LIMIT 60
|
||||
');
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
foreach ($rows as $r) {
|
||||
@@ -81,7 +88,7 @@ try {
|
||||
<section class="section alt" id="quicksearch">
|
||||
<div class="container">
|
||||
<div class="section__intro">
|
||||
<h2>Starte direkt mit der Suche nach passenden Treffen.</h2>
|
||||
<h2>Suche nach Treffen in deiner Nähe</h2>
|
||||
</div>
|
||||
<form id="quickSearchForm" class="grid grid-3 quicksearch-form" style="gap: 12px; align-items:flex-end;" action="/search" method="get">
|
||||
<div class="stack gap-6">
|
||||
|
||||
@@ -13,6 +13,18 @@ if (!in_array($childGender, ['male', 'female', 'mixed'], true)) {
|
||||
}
|
||||
|
||||
$debugEnabled = defined('APP_DEBUG') && APP_DEBUG === true;
|
||||
$locationTrackingPreference = 'prompt';
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
$pdo = $app->pdo();
|
||||
if ($pdo) {
|
||||
$profileSettings = new \App\ProfileSettings($pdo);
|
||||
$locationTrackingPreference = $profileSettings->getLocationTrackingPreference((int)$_SESSION['user_id']);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$locationTrackingPreference = 'prompt';
|
||||
}
|
||||
}
|
||||
$debugFiles = [];
|
||||
if ($debugEnabled) {
|
||||
$debugDir = __DIR__ . '/../../debug';
|
||||
@@ -57,7 +69,7 @@ if ($debugEnabled) {
|
||||
<?php asset_styles(); ?>
|
||||
<?php asset_scripts('header'); ?>
|
||||
</head>
|
||||
<body data-auth="<?= isset($_SESSION['user_id']) ? '1' : '0' ?>" data-child-gender="<?= htmlspecialchars($childGender, ENT_QUOTES) ?>">
|
||||
<body data-auth="<?= isset($_SESSION['user_id']) ? '1' : '0' ?>" data-child-gender="<?= htmlspecialchars($childGender, ENT_QUOTES) ?>" data-location-preference="<?= htmlspecialchars($locationTrackingPreference, ENT_QUOTES) ?>">
|
||||
<?php tpl('matomo', 'structure'); ?>
|
||||
<?php tpl('nav', 'structure'); ?>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ $isDebug = defined('APP_DEBUG') && APP_DEBUG === true;
|
||||
<nav class="nav-links" aria-label="Hauptmenü">
|
||||
<a href="/">Home</a>
|
||||
<a href="/search">Suche</a>
|
||||
<a href="/#events">Termine</a>
|
||||
<a href="/#events" data-nav-events>Termine</a>
|
||||
<a href="/community">Community</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
@@ -35,7 +35,7 @@ $isDebug = defined('APP_DEBUG') && APP_DEBUG === true;
|
||||
<div class="mobile-menu" id="mobileMenu">
|
||||
<a href="/">Home</a>
|
||||
<a href="/search">Suche</a>
|
||||
<a href="/#events">Termine</a>
|
||||
<a href="/#events" data-nav-events>Termine</a>
|
||||
<a href="/community">Community</a>
|
||||
<?php if ($isLoggedIn): ?>
|
||||
<a class="btn ghost" href="/dashboard">Dashboard</a>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ CREATE TABLE user_profiles (
|
||||
region VARCHAR(120) NULL,
|
||||
lat DECIMAL(10,7) NULL,
|
||||
lng DECIMAL(10,7) NULL,
|
||||
location_tracking_preference ENUM('disabled','prompt','enabled') NOT NULL DEFAULT 'prompt',
|
||||
contact_phone VARBINARY(512) NULL,
|
||||
contact_email VARBINARY(512) NULL,
|
||||
profession VARBINARY(512) NULL,
|
||||
|
||||
@@ -158,6 +158,7 @@ final class AccountPages
|
||||
$community = $pdo ? new Community($pdo, $communityConfig) : null;
|
||||
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
||||
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
|
||||
$profileSettings = $pdo ? new ProfileSettings($pdo) : null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
@@ -168,7 +169,11 @@ final class AccountPages
|
||||
$languages = implode(', ', array_map('trim', $languages));
|
||||
}
|
||||
$phoneEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['contact_phone'])) : trim((string)$_POST['contact_phone']);
|
||||
$stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, updated_at=NOW() WHERE user_id=:id');
|
||||
$locationPreference = (string)($_POST['location_tracking_preference'] ?? 'prompt');
|
||||
if ($profileSettings) {
|
||||
$profileSettings->ensureSchema();
|
||||
}
|
||||
$stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, location_tracking_preference=:locationPref, updated_at=NOW() WHERE user_id=:id');
|
||||
$stmt?->execute([
|
||||
'name' => trim((string)$_POST['display_name']),
|
||||
'fname' => trim((string)$_POST['first_name']),
|
||||
@@ -179,6 +184,7 @@ final class AccountPages
|
||||
'langs' => trim((string)$languages),
|
||||
'about' => trim((string)$_POST['about']),
|
||||
'phone' => $phoneEnc,
|
||||
'locationPref' => in_array($locationPreference, ['disabled', 'prompt', 'enabled'], true) ? $locationPreference : 'prompt',
|
||||
'id' => $userId,
|
||||
]);
|
||||
$info = 'Profil gespeichert.';
|
||||
@@ -321,8 +327,12 @@ final class AccountPages
|
||||
'about' => '',
|
||||
'email' => '',
|
||||
'contact_phone' => '',
|
||||
'location_tracking_preference' => 'prompt',
|
||||
];
|
||||
$stmt = $pdo?->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
|
||||
if ($profileSettings) {
|
||||
$profileSettings->ensureSchema();
|
||||
}
|
||||
$stmt = $pdo?->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
|
||||
$stmt?->execute(['id' => $userId]);
|
||||
$row = $stmt?->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
|
||||
83
src/App/ProfileSettings.php
Normal file
83
src/App/ProfileSettings.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class ProfileSettings
|
||||
{
|
||||
private bool $schemaEnsured = false;
|
||||
private array $columnCache = [];
|
||||
|
||||
public function __construct(private \PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function ensureSchema(): void
|
||||
{
|
||||
if ($this->schemaEnsured) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->hasColumn('user_profiles', 'location_tracking_preference')) {
|
||||
$this->pdo->exec(
|
||||
"ALTER TABLE user_profiles
|
||||
ADD COLUMN location_tracking_preference ENUM('disabled','prompt','enabled')
|
||||
NOT NULL DEFAULT 'prompt'
|
||||
AFTER lng"
|
||||
);
|
||||
$this->columnCache['user_profiles.location_tracking_preference'] = true;
|
||||
}
|
||||
|
||||
$this->schemaEnsured = true;
|
||||
}
|
||||
|
||||
public function getLocationTrackingPreference(int $userId): string
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return 'prompt';
|
||||
}
|
||||
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare('SELECT location_tracking_preference FROM user_profiles WHERE user_id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $userId]);
|
||||
$value = (string)($stmt->fetchColumn() ?: 'prompt');
|
||||
return in_array($value, ['disabled', 'prompt', 'enabled'], true) ? $value : 'prompt';
|
||||
}
|
||||
|
||||
public function updateLocationTrackingPreference(int $userId, string $preference): void
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureSchema();
|
||||
$preference = in_array($preference, ['disabled', 'prompt', 'enabled'], true) ? $preference : 'prompt';
|
||||
$stmt = $this->pdo->prepare('UPDATE user_profiles SET location_tracking_preference = :pref, updated_at = NOW() WHERE user_id = :id');
|
||||
$stmt->execute([
|
||||
'pref' => $preference,
|
||||
'id' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
$cacheKey = $table . '.' . $column;
|
||||
if (array_key_exists($cacheKey, $this->columnCache)) {
|
||||
return $this->columnCache[$cacheKey];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = :tableName
|
||||
AND COLUMN_NAME = :columnName
|
||||
");
|
||||
$stmt->execute([
|
||||
'tableName' => $table,
|
||||
'columnName' => $column,
|
||||
]);
|
||||
|
||||
return $this->columnCache[$cacheKey] = ((int)$stmt->fetchColumn() > 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user