sxsad
All checks were successful
Deploy / deploy (push) Successful in 1m43s

This commit is contained in:
2026-07-24 21:58:59 +02:00
parent 0956a984cb
commit 69fa2e029d
110 changed files with 150 additions and 152 deletions

View File

@@ -1,10 +1,9 @@
Anweisung: Projektstruktur (Basis-Template) wie in „papa-kind-treff“
Aktueller Projektstand
- Papa-Kind-Treff nutzt im Mitgliederbereich einen modernen Avatar-Generator auf Layer-Basis.
- Die Avatare bestehen aus getrennten SVG-Ebenen für Kopf, Haare, Bart, Brille, Mütze, Outfit und Farben.
- Papa-Kind-Treff nutzt im Mitgliederbereich 100 moderne Portrait-Presets im gewünschten Referenzstil.
- Relevante Logik liegt in `src/App/Avatar.php`.
- Die Layer-Assets liegen unter `public/assets/avatars/layers/modern/`.
- Die Preset-Assets liegen unter `public/assets/avatars/presets/`.
- Ä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.
Ziel

View File

@@ -19,13 +19,13 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
- Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich.
- Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut.
- Profilbilder werden aktuell über einen modernen Layer-Generator mit getrennten SVG-Ebenen statt über freie Bild-Uploads erzeugt.
- Profilbilder werden aktuell über 100 moderne Preset-Portraits im gewünschten Referenzstil statt über freie Bild-Uploads erzeugt.
## Navigation / Konto
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
- Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`.
- Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut.
- Im Profil kann der Nutzer aktuell seinen Avatar über Layer für Kopfform, Haare, Bart, Brille, Mütze, Outfit und Farbwelten zusammenstellen.
- Im Profil kann der Nutzer aktuell aus 100 modernen Preset-Varianten wählen, gruppiert nach Hauttönen.
- Debug ist kein Menüpunkt und erscheint nur als Floating-Käfer für `site_admin` bei aktivem Debug-Modus.
## Community und Rechte

View File

@@ -17,7 +17,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- Impressum, Datenschutz-&-Cookies-Seite und Über-uns
- standortbasierte Sortierung für die neuesten Treffen
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
- moderner Avatar-Generator auf Layer-Basis mit kombinierbaren SVG-Elementen für Kopf, Haare, Bart, Brille, Mütze, Outfit und Farben
- modernes Avatar-System auf Basis von 100 festen Portrait-Presets im Referenzstil, gruppiert nach Hauttönen
- Consent-Manager für Analyse und externe Dienste
- Navigation aktuell mit `Home`, `Suche`, `Community`
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`

View File

@@ -67,7 +67,7 @@ $sectionLinks = [
</div>
<div class="profile-avatar-panel__content">
<h3>Dein Profilbild</h3>
<p class="muted">Stell dir mit wenigen Klicks ein wiedererkennbares Community-Bild zusammen. Du kannst Haut, Haare, Augen, Bart, Brille und Kopfform direkt anpassen.</p>
<p class="muted">Wähle ein modernes Portrait im Stil der Community. Die Varianten bleiben konsistent, frisch und deutlich näher an der gewünschten Optik.</p>
<button class="btn" type="button" data-modal-open="modalAvatar">Profilbild gestalten</button>
</div>
</div>
@@ -302,121 +302,39 @@ $sectionLinks = [
<div data-avatar-preview>
<?= \App\Avatar::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
</div>
<p class="muted small">Stell dein Portrait direkt aus Haaren, Bart, Brille, Mütze, Outfit und Farben zusammen. Die Vorschau wird sofort aktualisiert.</p>
<p class="muted small">Wähle zuerst einen Hautton-Bereich und dann eine passende Variante. Die Vorschau aktualisiert sich sofort.</p>
</div>
<div class="avatar-builder__controls">
<div class="stack gap-6">
<label class="label" for="avatarHeadShape">Kopfform</label>
<select id="avatarHeadShape" name="avatar_head_shape" class="select" data-avatar-field="avatar_head_shape">
<?php foreach ($avatarOptions['avatar_head_shape'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_head_shape'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
<div class="avatar-preset-filters" role="tablist" aria-label="Hauttöne filtern">
<?php foreach ($avatarPresetGroups as $groupKey => $groupMeta): ?>
<button
type="button"
class="avatar-preset-filter <?= $groupKey === 'fair' ? 'is-active' : '' ?>"
data-avatar-preset-filter="<?= htmlspecialchars((string)$groupKey, ENT_QUOTES) ?>"
role="tab"
aria-selected="<?= $groupKey === 'fair' ? 'true' : 'false' ?>"
>
<?= htmlspecialchars((string)$groupMeta['label'], ENT_QUOTES) ?>
</button>
<?php endforeach; ?>
</div>
<div class="stack gap-6">
<label class="label" for="avatarHairStyle">Haarform</label>
<select id="avatarHairStyle" name="avatar_hair_style" class="select" data-avatar-field="avatar_hair_style">
<?php foreach ($avatarOptions['avatar_hair_style'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_hair_style'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="stack gap-6">
<label class="label" for="avatarBeardStyle">Bart</label>
<select id="avatarBeardStyle" name="avatar_beard_style" class="select" data-avatar-field="avatar_beard_style">
<?php foreach ($avatarOptions['avatar_beard_style'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_beard_style'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="stack gap-6">
<label class="label" for="avatarGlassesStyle">Brille</label>
<select id="avatarGlassesStyle" name="avatar_glasses_style" class="select" data-avatar-field="avatar_glasses_style">
<?php foreach ($avatarOptions['avatar_glasses_style'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_glasses_style'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="stack gap-6">
<label class="label" for="avatarHatStyle">Mütze</label>
<select id="avatarHatStyle" name="avatar_hat_style" class="select" data-avatar-field="avatar_hat_style">
<?php foreach ($avatarOptions['avatar_hat_style'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_hat_style'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="stack gap-6">
<label class="label" for="avatarOutfitStyle">Outfit</label>
<select id="avatarOutfitStyle" name="avatar_outfit_style" class="select" data-avatar-field="avatar_outfit_style">
<?php foreach ($avatarOptions['avatar_outfit_style'] as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_outfit_style'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="stack gap-6">
<span class="label">Hautfarbe</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_skin_tone'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_skin_tone" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_skin_tone" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_skin_tone'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="stack gap-6">
<span class="label">Haarfarbe</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_hair_color'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_hair_color" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_hair_color" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_hair_color'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="stack gap-6">
<span class="label">Augenfarbe</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_eye_color'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_eye_color" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_eye_color" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_eye_color'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="stack gap-6">
<span class="label">Mützenfarbe</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_hat_color'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_hat_color" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_hat_color" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_hat_color'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="stack gap-6">
<span class="label">Outfit-Farbe</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_outfit_color'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_outfit_color" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_outfit_color" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_outfit_color'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
<span class="label">Variante wählen</span>
<?php foreach ($avatarPresetOptions as $groupKey => $groupPresets): ?>
<div class="avatar-preset-group <?= $groupKey === 'fair' ? 'is-active' : '' ?>" data-avatar-preset-group="<?= htmlspecialchars((string)$groupKey, ENT_QUOTES) ?>">
<div class="avatar-preset-grid">
<?php foreach ($groupPresets as $value => $meta): ?>
<label class="avatar-preset-card">
<input type="radio" name="avatar_preset" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_preset" <?= $profile['avatar_preset'] === $value ? 'checked' : '' ?>>
<span class="avatar-preset-card__thumb">
<img src="<?= htmlspecialchars((string)($meta['image'] ?? ''), ENT_QUOTES) ?>" alt="<?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?>">
</span>
<span class="avatar-preset-card__label"><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Some files were not shown because too many files have changed in this diff Show More