csadas
All checks were successful
Deploy / deploy (push) Successful in 58s

This commit is contained in:
2026-07-31 21:45:19 +02:00
parent 002108ac83
commit c335fd6f5c
6 changed files with 158 additions and 88 deletions

View File

@@ -20,6 +20,7 @@ Aktueller Projektstand
- Der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist auf der Datenschutz-Seite jetzt zusaetzlich deutlich hervorgehoben. - Der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist auf der Datenschutz-Seite jetzt zusaetzlich deutlich hervorgehoben.
- 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. - Die Profiladresse unterstuetzt jetzt ausserdem eine Auswahl moeglicher Adress-Treffer, die Uebernahme aus dem Browser-Standort und einen kurzen Hinweisdialog zur lokalen Relevanz.
- Der Profil-Editor im Mitgliederbereich ist jetzt kompakter aufgebaut, in ausklappbare Bereiche gegliedert und enthaelt die bearbeitbare Konto-E-Mail direkt im Formular.
- Fuer Umkreis- und Distanzfunktionen bleiben nur `lat`/`lng` technisch im Klartext; die eigentliche Strasse/Hausnummer ist verschluesselt. - Fuer Umkreis- und Distanzfunktionen bleiben nur `lat`/`lng` technisch im Klartext; die eigentliche Strasse/Hausnummer ist verschluesselt.
- 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.

View File

@@ -40,6 +40,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist auf der Datenschutz-Seite jetzt zusaetzlich deutlich hervorgehoben. - Der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist auf der Datenschutz-Seite jetzt zusaetzlich deutlich hervorgehoben.
- 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. - 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.
- Der Profil-Editor ist jetzt kompakter gegliedert, nutzt ausklappbare Bereiche fuer Basisdaten, Adresse, Sprache/Beruf und Kurzvorstellung und enthaelt die bearbeitbare Konto-E-Mail direkt im Formular.
- 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.

View File

@@ -34,6 +34,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist jetzt auch auf der Datenschutz-Seite deutlich hervorgehoben - der Hinweis auf die verschluesselte Speicherung personenbezogener Profilangaben ist jetzt auch auf der Datenschutz-Seite deutlich hervorgehoben
- 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 - 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
- der Profil-Editor im Mitgliederbereich ist jetzt kompakter gruppiert, nutzt ausklappbare Abschnitte und enthaelt die bearbeitbare Konto-E-Mail wieder direkt im Formular
- Debug-Floating-Button nur für `site_admin` im Debug-Modus - Debug-Floating-Button nur für `site_admin` im Debug-Modus
## Technik ## Technik

View File

@@ -78,16 +78,31 @@ $sectionLinks = [
<button class="btn" type="button" data-modal-open="modalAvatar">Profilbild gestalten</button> <button class="btn" type="button" data-modal-open="modalAvatar">Profilbild gestalten</button>
</div> </div>
</div> </div>
<dl class="account-kv"> <div class="profile-summary-grid">
<div><dt>Name</dt><dd><?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']), ENT_QUOTES) ?></dd></div> <section class="profile-summary-card">
<div><dt>Anzeigename</dt><dd><?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?></dd></div> <span class="profile-summary-card__eyebrow">Basis</span>
<div><dt>Adresse</dt><dd><?= htmlspecialchars(trim(implode(', ', array_filter([(string)($profile['street'] ?? ''), trim((string)$profile['zip'] . ' ' . (string)$profile['city']), (string)($profile['region'] ?? '')]))), ENT_QUOTES) ?></dd></div> <dl class="account-kv account-kv--compact">
<div><dt>E-Mail</dt><dd><?= htmlspecialchars($profile['email'], ENT_QUOTES) ?></dd></div> <div><dt>Anzeigename</dt><dd><?= htmlspecialchars($profile['display_name'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Telefon</dt><dd><?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?></dd></div> <div><dt>Name</dt><dd><?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']) ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Beruf</dt><dd><?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></dd></div> <div><dt>E-Mail</dt><dd><?= htmlspecialchars($profile['email'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Sprachen</dt><dd><?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></dd></div> <div><dt>Telefon</dt><dd><?= htmlspecialchars($profile['contact_phone'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Über dich</dt><dd><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></dd></div> </dl>
</dl> </section>
<section class="profile-summary-card">
<span class="profile-summary-card__eyebrow">Standort</span>
<dl class="account-kv account-kv--compact">
<div><dt>Adresse</dt><dd><?= htmlspecialchars(trim(implode(', ', array_filter([(string)($profile['street'] ?? ''), trim((string)$profile['zip'] . ' ' . (string)$profile['city']), (string)($profile['region'] ?? '')]))) ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Sprachen</dt><dd><?= htmlspecialchars($profile['languages'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
<div><dt>Beruf</dt><dd><?= htmlspecialchars($profile['profession'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
</dl>
</section>
</div>
<?php if (trim((string)$profile['about']) !== ''): ?>
<div class="profile-about-card">
<strong>Über dich</strong>
<p><?= nl2br(htmlspecialchars((string)$profile['about'], ENT_QUOTES)) ?></p>
</div>
<?php endif; ?>
<div class="flex gap-12" style="margin-top:18px;"> <div class="flex gap-12" style="margin-top:18px;">
<button class="btn" type="button" data-modal-open="modalProfile">Profil bearbeiten</button> <button class="btn" type="button" data-modal-open="modalProfile">Profil bearbeiten</button>
</div> </div>
@@ -484,91 +499,116 @@ $sectionLinks = [
<h3>Profil bearbeiten</h3> <h3>Profil bearbeiten</h3>
<button class="btn ghost" type="button" data-modal-close>✕</button> <button class="btn ghost" type="button" data-modal-close>✕</button>
</div> </div>
<form method="post" class="stack gap-12"> <form method="post" class="stack gap-12 profile-edit-form">
<input type="hidden" name="action" value="profile"> <input type="hidden" name="action" value="profile">
<input type="hidden" id="pRegion" name="region" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>"> <input type="hidden" id="pRegion" name="region" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>">
<input type="hidden" id="pLat" name="lat" value="<?= htmlspecialchars((string)($profile['lat'] ?? ''), ENT_QUOTES) ?>"> <input type="hidden" id="pLat" name="lat" value="<?= htmlspecialchars((string)($profile['lat'] ?? ''), ENT_QUOTES) ?>">
<input type="hidden" id="pLng" name="lng" value="<?= htmlspecialchars((string)($profile['lng'] ?? ''), ENT_QUOTES) ?>"> <input type="hidden" id="pLng" name="lng" value="<?= htmlspecialchars((string)($profile['lng'] ?? ''), ENT_QUOTES) ?>">
<div class="form-grid"> <p class="muted small" style="margin:0;">Persoenliche Angaben werden app-seitig verschluesselt gespeichert. Nur fuer Login und Kontofunktionen wird die E-Mail intern ueber einen separaten Lookup-Hash adressiert.</p>
<div class="stack gap-6"> <details class="profile-edit-section" open>
<label class="label" for="pName">Anzeigename</label> <summary>Basisdaten</summary>
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>"> <div class="profile-edit-section__body">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pName">Anzeigename</label>
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pEmail">E-Mail-Adresse</label>
<input id="pEmail" name="email" type="email" class="input" value="<?= htmlspecialchars((string)$profile['email'], ENT_QUOTES) ?>" autocomplete="email" required>
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pFirst">Vorname</label>
<input id="pFirst" name="first_name" class="input" value="<?= htmlspecialchars($profile['first_name'], ENT_QUOTES) ?>" autocomplete="given-name">
</div>
<div class="stack gap-6">
<label class="label" for="pLast">Nachname</label>
<input id="pLast" name="last_name" class="input" value="<?= htmlspecialchars($profile['last_name'], ENT_QUOTES) ?>" autocomplete="family-name">
</div>
</div>
<div class="stack gap-6">
<label class="label" for="pPhone">Telefon (mobil)</label>
<input id="pPhone" name="contact_phone" class="input" value="<?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?>" autocomplete="tel">
</div>
</div> </div>
<div class="stack gap-6"> </details>
<label class="label" for="pFirst">Vorname</label> <details class="profile-edit-section" open>
<input id="pFirst" name="first_name" class="input" value="<?= htmlspecialchars($profile['first_name'], ENT_QUOTES) ?>"> <summary>Adresse und Umgebung</summary>
<div class="profile-edit-section__body">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pStreet">Straße / Hausnummer</label>
<input id="pStreet" name="street" class="input" value="<?= htmlspecialchars((string)($profile['street'] ?? ''), ENT_QUOTES) ?>" placeholder="z. B. Musterstraße 12" autocomplete="street-address">
</div>
<div class="stack gap-6">
<label class="label" for="pZip">PLZ</label>
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>" autocomplete="postal-code">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pCity">Ort</label>
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>" autocomplete="address-level2">
</div>
<div class="stack gap-6">
<label class="label" for="pRegionView">Region / Bezirk</label>
<input id="pRegionView" class="input" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>" placeholder="wird bei Lokalisierung ergänzt" autocomplete="address-level1">
</div>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<button class="btn ghost" type="button" id="btnProfileLocate">Adresse suchen</button>
<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>
</div> </details>
<div class="form-grid"> <details class="profile-edit-section">
<div class="stack gap-6"> <summary>Sprache und Beruf</summary>
<label class="label" for="pStreet">Straße / Hausnummer</label> <div class="profile-edit-section__body">
<input id="pStreet" name="street" class="input" value="<?= htmlspecialchars((string)($profile['street'] ?? ''), ENT_QUOTES) ?>" placeholder="z. B. Musterstraße 12"> <div class="stack gap-6">
<label class="label">Sprachen (Mehrfachauswahl)</label>
<div class="chips" style="flex-wrap: wrap;">
<?php
$langOptions = ['Deutsch','Englisch','Französisch','Spanisch','Türkisch','Arabisch','Polnisch'];
$currentLangs = array_filter(array_map('trim', explode(',', (string)$profile['languages'])));
?>
<?php foreach ($langOptions as $opt): ?>
<label class="chip" style="cursor:pointer;">
<input type="checkbox" name="languages[]" value="<?= htmlspecialchars($opt, ENT_QUOTES) ?>" <?= in_array($opt, $currentLangs, true) ? 'checked' : '' ?> style="margin-right:6px;">
<?= htmlspecialchars($opt, ENT_QUOTES) ?>
</label>
<?php endforeach; ?>
</div>
<label class="label" for="pLangCustom">Weitere Sprachen (Kommagetrennt)</label>
<input id="pLangCustom" name="languages[]" class="input" placeholder="z. B. Italienisch, Niederländisch">
</div>
<div class="stack gap-6">
<label class="label" for="pProf">Beruf</label>
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>" autocomplete="organization-title">
</div>
</div> </div>
<div class="stack gap-6"> </details>
<label class="label" for="pLast">Nachname</label> <details class="profile-edit-section">
<input id="pLast" name="last_name" class="input" value="<?= htmlspecialchars($profile['last_name'], ENT_QUOTES) ?>"> <summary>Kurzvorstellung</summary>
<div class="profile-edit-section__body">
<div class="stack gap-6">
<label class="label" for="pAbout">Über dich</label>
<textarea id="pAbout" name="about" class="textarea" rows="4"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
</div>
</div> </div>
</div> </details>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pZip">PLZ</label>
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pCity">Ort</label>
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pRegionView">Region / Bezirk</label>
<input id="pRegionView" class="input" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>" placeholder="wird bei Lokalisierung ergänzt">
</div>
<div class="stack gap-6">
<label class="label" for="pPhone">Telefon (mobil)</label>
<input id="pPhone" name="contact_phone" class="input" value="<?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?>">
</div>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<button class="btn ghost" type="button" id="btnProfileLocate">Adresse suchen</button>
<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 class="stack gap-6">
<label class="label">Sprachen (Mehrfachauswahl)</label>
<div class="chips" style="flex-wrap: wrap;">
<?php
$langOptions = ['Deutsch','Englisch','Französisch','Spanisch','Türkisch','Arabisch','Polnisch'];
$currentLangs = array_filter(array_map('trim', explode(',', (string)$profile['languages'])));
?>
<?php foreach ($langOptions as $opt): ?>
<label class="chip" style="cursor:pointer;">
<input type="checkbox" name="languages[]" value="<?= htmlspecialchars($opt, ENT_QUOTES) ?>" <?= in_array($opt, $currentLangs, true) ? 'checked' : '' ?> style="margin-right:6px;">
<?= htmlspecialchars($opt, ENT_QUOTES) ?>
</label>
<?php endforeach; ?>
</div>
<label class="label" for="pLangCustom">Weitere Sprachen (Kommagetrennt)</label>
<input id="pLangCustom" name="languages[]" class="input" placeholder="z. B. Italienisch, Niederländisch">
</div>
<div class="stack gap-6">
<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="pAbout">Kurzvorstellung</label>
<textarea id="pAbout" name="about" class="textarea" rows="3"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
</div>
<div class="flex gap-12"> <div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button> <button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit">Speichern</button> <button class="btn" type="submit">Speichern</button>

View File

@@ -241,12 +241,27 @@ body {
.account-panel__body { padding: 24px; } .account-panel__body { padding: 24px; }
.account-kv { display: grid; gap: 10px; margin: 0; } .account-kv { display: grid; gap: 10px; margin: 0; }
.account-kv div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 14px; } .account-kv div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 14px; }
.account-kv--compact { gap: 8px; }
.account-kv--compact div { grid-template-columns: 120px minmax(0, 1fr); gap: 10px; }
.account-kv dt { font-weight: 700; color: var(--color-muted); } .account-kv dt { font-weight: 700; color: var(--color-muted); }
.account-kv dd { margin: 0; } .account-kv dd { margin: 0; }
.profile-avatar-panel { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 22px; align-items: center; margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, #fffdfa, #f7f0e6); border: 1px solid var(--color-border); border-radius: 18px; } .profile-avatar-panel { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 22px; align-items: center; margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, #fffdfa, #f7f0e6); border: 1px solid var(--color-border); border-radius: 18px; }
.profile-avatar-panel__preview { display: flex; justify-content: center; } .profile-avatar-panel__preview { display: flex; justify-content: center; }
.profile-avatar-panel__content h3 { margin: 0 0 8px; } .profile-avatar-panel__content h3 { margin: 0 0 8px; }
.profile-avatar-panel__content p { margin: 0 0 14px; } .profile-avatar-panel__content p { margin: 0 0 14px; }
.profile-summary-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.profile-summary-card { padding: 18px; background: #fffdfa; border: 1px solid var(--color-border); border-radius: 18px; }
.profile-summary-card__eyebrow { display: inline-block; margin-bottom: 12px; font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--color-highlight); }
.profile-about-card { margin-top: 16px; padding: 18px; background: linear-gradient(135deg, #fff, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
.profile-about-card strong { display: block; margin-bottom: 8px; }
.profile-about-card p { margin: 0; line-height: 1.65; color: var(--color-text); }
.profile-edit-form { max-width: 100%; }
.profile-edit-section { border: 1px solid var(--color-border); border-radius: 18px; background: #fffdfa; overflow: hidden; }
.profile-edit-section summary { cursor: pointer; list-style: none; padding: 16px 18px; font-weight: 800; color: var(--color-text); display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.profile-edit-section summary::-webkit-details-marker { display: none; }
.profile-edit-section summary::after { content: "+"; width: 28px; height: 28px; border-radius: 999px; border: 1px solid var(--color-border); display: inline-flex; align-items: center; justify-content: center; color: var(--color-primary); background: #fff; font-size: 18px; line-height: 1; flex: 0 0 auto; }
.profile-edit-section[open] summary::after { content: ""; }
.profile-edit-section__body { display: grid; gap: 14px; padding: 0 18px 18px; border-top: 1px solid rgba(196, 169, 120, 0.22); }
.avatar-builder { height: 100%; min-height: 0; } .avatar-builder { height: 100%; min-height: 0; }
.avatar-builder__shell { height: 100%; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 14px; } .avatar-builder__shell { height: 100%; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 14px; }
.avatar-builder__top { display: grid; gap: 14px; padding: 14px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; } .avatar-builder__top { display: grid; gap: 14px; padding: 14px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
@@ -287,7 +302,7 @@ body {
.avatar-component-card__label { font-size: 12px; font-weight: 700; color: var(--color-text); text-align: center; } .avatar-component-card__label { font-size: 12px; font-weight: 700; color: var(--color-text); text-align: center; }
.avatar-component-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); } .avatar-component-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
@media (max-width: 980px){ .avatar-component-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 980px){ .avatar-component-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 860px){ .avatar-builder__hero { grid-template-columns: 1fr; } .avatar-builder__actions { justify-content: center; } .avatar-component-colors { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } .avatar-component-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 860px){ .avatar-builder__hero { grid-template-columns: 1fr; } .avatar-builder__actions { justify-content: center; } .avatar-component-colors { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } .avatar-component-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .profile-summary-grid { grid-template-columns: 1fr; } }
.avatar-swatch-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; } .avatar-swatch-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.avatar-swatch { display: grid; grid-template-columns: 20px 18px minmax(0, 1fr); gap: 10px; align-items: center; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 14px; background: #fff; cursor: pointer; } .avatar-swatch { display: grid; grid-template-columns: 20px 18px minmax(0, 1fr); gap: 10px; align-items: center; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 14px; background: #fff; cursor: pointer; }
.avatar-swatch__color { width: 18px; height: 18px; border-radius: 999px; background: var(--swatch-color); border: 1px solid rgba(0,0,0,0.12); } .avatar-swatch__color { width: 18px; height: 18px; border-radius: 999px; background: var(--swatch-color); border: 1px solid rgba(0,0,0,0.12); }
@@ -393,6 +408,9 @@ body {
.cookie-consent__inner { grid-template-columns: 1fr; } .cookie-consent__inner { grid-template-columns: 1fr; }
.cookie-consent__actions { justify-content: stretch; } .cookie-consent__actions { justify-content: stretch; }
.account-kv div { grid-template-columns: 1fr; gap: 4px; } .account-kv div { grid-template-columns: 1fr; gap: 4px; }
.account-kv--compact div { grid-template-columns: 1fr; gap: 4px; }
.profile-edit-section summary,
.profile-edit-section__body { padding-left: 14px; padding-right: 14px; }
} }
/* Auth & Dashboard */ /* Auth & Dashboard */

View File

@@ -182,10 +182,14 @@ final class AccountPages
try { try {
if ($action === 'profile') { if ($action === 'profile') {
$crypto = self::requireCrypto($crypto, 'Profil'); $crypto = self::requireCrypto($crypto, 'Profil');
$email = trim((string)($_POST['email'] ?? ''));
$languages = $_POST['languages'] ?? ''; $languages = $_POST['languages'] ?? '';
if (is_array($languages)) { if (is_array($languages)) {
$languages = implode(', ', array_map('trim', $languages)); $languages = implode(', ', array_map('trim', $languages));
} }
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \RuntimeException('Bitte gib eine gueltige E-Mail-Adresse an.');
}
$firstNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['first_name'])); $firstNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['first_name']));
$lastNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['last_name'])); $lastNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['last_name']));
$streetEnc = self::encryptOptionalProfileField($crypto, trim((string)($_POST['street'] ?? ''))); $streetEnc = self::encryptOptionalProfileField($crypto, trim((string)($_POST['street'] ?? '')));
@@ -249,6 +253,11 @@ final class AccountPages
'locationPref' => in_array($locationPreference, ['disabled', 'prompt', 'enabled'], true) ? $locationPreference : 'prompt', 'locationPref' => in_array($locationPreference, ['disabled', 'prompt', 'enabled'], true) ? $locationPreference : 'prompt',
'id' => $userId, 'id' => $userId,
]); ]);
if ($pdo) {
$emailStore = new UserEmailStore($pdo);
$emailStore->ensureSchema();
$emailStore->updateUserEmail($userId, $email);
}
$info = 'Profil gespeichert.'; $info = 'Profil gespeichert.';
} elseif ($action === 'avatar_update') { } elseif ($action === 'avatar_update') {
if ($profileSettings) { if ($profileSettings) {