ycyxcx
All checks were successful
Deploy / deploy (push) Successful in 1m13s

This commit is contained in:
2026-07-24 21:43:37 +02:00
parent 9a5f1b1a5d
commit 0956a984cb
31 changed files with 326 additions and 266 deletions

View File

@@ -1,5 +1,12 @@
Anweisung: Projektstruktur (Basis-Template) wie in „papa-kind-treff“ 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.
- Relevante Logik liegt in `src/App/Avatar.php`.
- Die Layer-Assets liegen unter `public/assets/avatars/layers/modern/`.
- Ä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 Ziel
- Erstelle ein neues Projekt bzw. aktualisiere das aktuelle mit exakt der gleichen Grundstruktur wie in „papa-kind-treff“. - Erstelle ein neues Projekt bzw. aktualisiere das aktuelle mit exakt der gleichen Grundstruktur wie in „papa-kind-treff“.
- Fokus auf Hauptordner und deren Zweck; keine projektspezifischen Sonderordner (z. B. Community) anlegen. - Fokus auf Hauptordner und deren Zweck; keine projektspezifischen Sonderordner (z. B. Community) anlegen.

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. - Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
- Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich. - Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich.
- Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut. - Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut.
- Profilbilder werden aktuell über moderne Preset-Portraits im Stil der Referenz-Assets statt über freie Bild-Uploads erzeugt. - Profilbilder werden aktuell über einen modernen Layer-Generator mit getrennten SVG-Ebenen statt über freie Bild-Uploads erzeugt.
## Navigation / Konto ## Navigation / Konto
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons. - Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
- Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`. - Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`.
- Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut. - Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut.
- Im Profil kann der Nutzer aktuell aus einer hochwertigen modernen Avatar-Preset-Familie wählen. - Im Profil kann der Nutzer aktuell seinen Avatar über Layer für Kopfform, Haare, Bart, Brille, Mütze, Outfit und Farbwelten zusammenstellen.
- Debug ist kein Menüpunkt und erscheint nur als Floating-Käfer für `site_admin` bei aktivem Debug-Modus. - Debug ist kein Menüpunkt und erscheint nur als Floating-Käfer für `site_admin` bei aktivem Debug-Modus.
## Community und Rechte ## 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 - Impressum, Datenschutz-&-Cookies-Seite und Über-uns
- standortbasierte Sortierung für die neuesten Treffen - standortbasierte Sortierung für die neuesten Treffen
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung - eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
- modernes Avatar-System auf Basis hochwertiger Preset-Assets im Stil der Referenzportraits - moderner Avatar-Generator auf Layer-Basis mit kombinierbaren SVG-Elementen für Kopf, Haare, Bart, Brille, Mütze, Outfit und Farben
- Consent-Manager für Analyse und externe Dienste - Consent-Manager für Analyse und externe Dienste
- Navigation aktuell mit `Home`, `Suche`, `Community` - Navigation aktuell mit `Home`, `Suche`, `Community`
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen` - eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`

View File

@@ -302,19 +302,118 @@ $sectionLinks = [
<div data-avatar-preview> <div data-avatar-preview>
<?= \App\Avatar::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?> <?= \App\Avatar::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
</div> </div>
<p class="muted small">Wähle ein modernes Preset aus deiner Stilfamilie. Die Vorschau wird sofort aktualisiert.</p> <p class="muted small">Stell dein Portrait direkt aus Haaren, Bart, Brille, Mütze, Outfit und Farben zusammen. Die Vorschau wird sofort aktualisiert.</p>
</div> </div>
<div class="avatar-builder__controls"> <div class="avatar-builder__controls">
<div class="stack gap-6"> <div class="stack gap-6">
<span class="label">Modernes Avatar-Preset</span> <label class="label" for="avatarHeadShape">Kopfform</label>
<div class="avatar-preset-grid"> <select id="avatarHeadShape" name="avatar_head_shape" class="select" data-avatar-field="avatar_head_shape">
<?php foreach ($avatarPresetOptions as $value => $meta): ?> <?php foreach ($avatarOptions['avatar_head_shape'] as $value => $label): ?>
<label class="avatar-preset-card"> <option value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" <?= $profile['avatar_head_shape'] === $value ? 'selected' : '' ?>><?= htmlspecialchars((string)$label, ENT_QUOTES) ?></option>
<input type="radio" name="avatar_preset" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-preset-option data-avatar-src="<?= htmlspecialchars((string)$meta['src'], ENT_QUOTES) ?>" <?= ($profile['avatar_preset'] ?? 'modern_01') === $value ? 'checked' : '' ?>> <?php endforeach; ?>
<span class="avatar-preset-card__thumb"> </select>
<img src="<?= htmlspecialchars((string)$meta['src'], ENT_QUOTES) ?>" alt=""> </div>
</span> <div class="stack gap-6">
<span class="avatar-preset-card__label"><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span> <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> </label>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@@ -11,7 +11,7 @@ if ($isLoggedIn) {
if ($pdo) { if ($pdo) {
$profileSettings = new \App\ProfileSettings($pdo); $profileSettings = new \App\ProfileSettings($pdo);
$profileSettings->ensureSchema(); $profileSettings->ensureSchema();
$stmt = $pdo->prepare('SELECT display_name, first_name, avatar_preset, avatar_skin_tone, avatar_hair_style, avatar_hair_color, avatar_eye_color, avatar_beard_style, avatar_glasses_style, avatar_head_shape, avatar_hat_style, avatar_hat_color, avatar_outfit_color FROM user_profiles WHERE user_id = :id LIMIT 1'); $stmt = $pdo->prepare('SELECT display_name, first_name, avatar_preset, avatar_skin_tone, avatar_hair_style, avatar_hair_color, avatar_eye_color, avatar_beard_style, avatar_glasses_style, avatar_head_shape, avatar_hat_style, avatar_hat_color, avatar_outfit_style, avatar_outfit_color FROM user_profiles WHERE user_id = :id LIMIT 1');
$stmt->execute(['id' => (int)$_SESSION['user_id']]); $stmt->execute(['id' => (int)$_SESSION['user_id']]);
$profileRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; $profileRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$displayName = trim((string)($profileRow['display_name'] ?? '')) ?: trim((string)($profileRow['first_name'] ?? '')) ?: 'Profil'; $displayName = trim((string)($profileRow['display_name'] ?? '')) ?: trim((string)($profileRow['first_name'] ?? '')) ?: 'Profil';

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M55 89c5 7 14 11 25 11 12 0 20-4 25-11-1 23-10 36-25 39-16-3-24-16-25-39z" fill="var(--avatar-hair)"/>
<path d="M61 94c5 5 11 8 19 8 7 0 14-3 19-8-3 11-8 18-19 21-11-3-16-10-19-21z" fill="rgba(255,255,255,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M64 97c4-4 10-5 16-5 7 0 12 1 16 5-4 1-7 4-10 8-2-2-4-3-6-3s-4 1-6 3c-3-4-6-7-10-8z" fill="var(--avatar-hair)"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M60 93c4 5 10 8 20 8s16-3 20-8c0 15-6 25-20 28-14-3-20-13-20-28z" fill="var(--avatar-hair)"/>
<path d="M66 95c4 3 8 4 14 4 5 0 10-1 14-4-2 7-6 12-14 14-8-2-12-7-14-14z" fill="rgba(255,255,255,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1,14 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<ellipse cx="60" cy="92" rx="8" ry="4" fill="rgba(204,112,100,0.11)"/>
<ellipse cx="100" cy="92" rx="8" ry="4" fill="rgba(204,112,100,0.11)"/>
<path d="M56 70c5-3 13-4 20-1" stroke="color-mix(in srgb, var(--avatar-hair) 92%, black 8%)" stroke-width="4" stroke-linecap="round" fill="none"/>
<path d="M84 69c7-3 15-2 20 2" stroke="color-mix(in srgb, var(--avatar-hair) 92%, black 8%)" stroke-width="4" stroke-linecap="round" fill="none"/>
<ellipse cx="66" cy="79" rx="8" ry="5" fill="#fff"/>
<ellipse cx="94" cy="79" rx="8" ry="5" fill="#fff"/>
<circle cx="66" cy="79" r="3.6" fill="var(--avatar-eye)"/>
<circle cx="94" cy="79" r="3.6" fill="var(--avatar-eye)"/>
<circle cx="65" cy="78" r="1.1" fill="rgba(255,255,255,0.92)"/>
<circle cx="93" cy="78" r="1.1" fill="rgba(255,255,255,0.92)"/>
<path d="M80 81c-2 5-3 9-3 15 0 3 2 5 5 5" stroke="rgba(111,76,49,0.18)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M69 103c7 4 15 4 22 0" stroke="rgba(98,64,42,0.72)" stroke-width="3" stroke-linecap="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="54" y="70" width="22" height="16" rx="8" fill="none" stroke="rgba(41,41,41,0.82)" stroke-width="2.8"/>
<rect x="84" y="70" width="22" height="16" rx="8" fill="none" stroke="rgba(41,41,41,0.82)" stroke-width="2.8"/>
<path d="M76 78h8" stroke="rgba(41,41,41,0.82)" stroke-width="2.8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="54" y="70" width="22" height="16" rx="4" fill="none" stroke="rgba(41,41,41,0.82)" stroke-width="2.8"/>
<rect x="84" y="70" width="22" height="16" rx="4" fill="none" stroke="rgba(41,41,41,0.82)" stroke-width="2.8"/>
<path d="M76 78h8" stroke="rgba(41,41,41,0.82)" stroke-width="2.8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,7 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M38 71c4-17 10-28 18-35 7-6 15-9 24-9 12 0 22 4 30 12 8 7 12 18 13 32-5-3-9-5-13-6-4-1-7 4-11 4-4 0-6-5-10-5-4 0-6 5-10 5-4 0-6-5-10-5-5 0-9 4-13 4-4 0-5-4-8-4-4 1-7 3-10 7z" fill="var(--avatar-hair)"/>
<circle cx="58" cy="43" r="9" fill="var(--avatar-hair)"/>
<circle cx="74" cy="37" r="10" fill="var(--avatar-hair)"/>
<circle cx="90" cy="38" r="11" fill="var(--avatar-hair)"/>
<circle cx="106" cy="45" r="9" fill="var(--avatar-hair)"/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M44 69c4-22 18-35 36-35 18 0 32 12 36 35-8-6-15-9-22-11-9-2-19-2-28 1-7 2-13 5-22 10z" fill="var(--avatar-hair)"/>
<path d="M50 71c8-6 18-8 30-8 12 0 22 2 30 8-7 4-16 6-30 6s-23-2-30-6z" fill="rgba(0,0,0,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M43 67c3-17 14-30 27-37 14-7 31-6 43 7 6 7 9 16 9 28-8-8-12-12-18-15-10-5-21-4-28 4-4-6-9-10-16-10-7 0-12 3-17 8z" fill="var(--avatar-hair)"/>
<path d="M60 37c5-5 14-8 22-8 14 0 25 6 31 16-7-4-13-6-20-7-10-2-19 0-33 11z" fill="rgba(255,255,255,0.12)"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M42 69c2-20 14-33 28-39 8-3 18-4 27-1 11 4 20 14 23 38-8-6-16-10-23-12-10-3-20-2-29 2-9 3-16 8-26 12z" fill="var(--avatar-hair)"/>
<path d="M60 35c11-6 29-5 41 1-7-1-14-1-22 1-9 1-14 4-19 8z" fill="rgba(255,255,255,0.12)"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M41 70c3-16 10-27 20-34 8-5 17-8 28-8 16 0 29 11 33 40-6-6-11-9-17-12-4-2-8-2-12-3-5 0-7 6-12 6-4 0-8-7-13-7-10 0-18 6-27 18z" fill="var(--avatar-hair)"/>
<path d="M54 42c6-5 14-8 22-8 14 0 26 8 31 18-6-4-11-6-19-7-13-1-22 0-34-3z" fill="rgba(255,255,255,0.12)"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M36 63c2-23 18-39 44-39 24 0 40 14 44 39H36z" fill="var(--avatar-hat)"/>
<path d="M38 61h84c0 11-6 18-14 18H52c-8 0-14-7-14-18z" fill="color-mix(in srgb, var(--avatar-hat) 84%, black 16%)"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M40 63c5-19 18-30 40-30 23 0 35 10 40 30H40z" fill="var(--avatar-hat)"/>
<path d="M80 63c15 0 31 5 44 14-7 4-16 7-26 8-18 2-28 1-39-4 3-8 9-18 21-18z" fill="color-mix(in srgb, var(--avatar-hat) 82%, black 18%)"/>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M68 101h24v21c0 8-5 14-12 14s-12-6-12-14v-21z" fill="color-mix(in srgb, var(--avatar-skin) 90%, #6e452f 10%)"/>
<path d="M49 79c0-8 5-13 10-13v25c-5 0-10-5-10-12z" fill="var(--avatar-skin)"/>
<path d="M111 66c5 0 10 5 10 13 0 7-5 12-10 12V66z" fill="var(--avatar-skin)"/>
<path d="M80 25c-22 0-37 18-37 43v20c0 28 16 46 37 46s37-18 37-46V68c0-25-15-43-37-43z" fill="var(--avatar-skin)"/>
<path d="M52 92c4 21 14 35 28 39V48c-10 1-19 7-24 17-7 12-7 27-4 27z" fill="rgba(255,255,255,0.12)"/>
<path d="M108 92c-4 21-14 35-28 39V48c10 1 19 7 24 17 7 12 7 27 4 27z" fill="rgba(120,72,42,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M68 100h24v22c0 7-5 13-12 13s-12-6-12-13v-22z" fill="color-mix(in srgb, var(--avatar-skin) 90%, #6e452f 10%)"/>
<path d="M48 78c0-8 5-13 11-13v26c-6 0-11-5-11-13z" fill="var(--avatar-skin)"/>
<path d="M112 65c6 0 11 5 11 13s-5 13-11 13V65z" fill="var(--avatar-skin)"/>
<path d="M80 27c-25 0-40 18-40 43v18c0 28 18 46 40 46s40-18 40-46V70c0-25-15-43-40-43z" fill="var(--avatar-skin)"/>
<path d="M49 91c3 22 15 38 31 41V50c-12 1-23 8-29 19-6 11-6 22-2 22v0z" fill="rgba(255,255,255,0.12)"/>
<path d="M111 91c-3 22-15 38-31 41V50c12 1 23 8 29 19 6 11 6 22 2 22v0z" fill="rgba(120,72,42,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@@ -0,0 +1,7 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M68 100h24v22c0 7-5 13-12 13s-12-6-12-13v-22z" fill="color-mix(in srgb, var(--avatar-skin) 90%, #6e452f 10%)"/>
<path d="M48 77c0-8 5-13 10-13v26c-5 0-10-5-10-13z" fill="var(--avatar-skin)"/>
<path d="M112 64c5 0 10 5 10 13s-5 13-10 13V64z" fill="var(--avatar-skin)"/>
<path d="M80 27c-24 0-39 18-39 41v20c0 27 17 45 39 45s39-18 39-45V68c0-23-15-41-39-41z" fill="var(--avatar-skin)"/>
<path d="M53 112c5 12 15 21 27 21s22-9 27-21c-6 7-15 12-27 12s-21-5-27-12z" fill="rgba(120,72,42,0.08)"/>
</svg>

After

Width:  |  Height:  |  Size: 599 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M24 160c3-23 18-36 56-36s53 13 56 36H24z" fill="var(--avatar-outfit)"/>
<path d="M62 126c4 7 10 11 18 11s14-4 18-11c-5-2-11-3-18-3s-13 1-18 3z" fill="color-mix(in srgb, var(--avatar-outfit) 76%, black 24%)"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M22 160c3-24 19-38 58-38s55 14 58 38H22z" fill="var(--avatar-outfit)"/>
<path d="M56 126c6 11 13 18 24 18s18-7 24-18c-6-4-15-7-24-7s-18 3-24 7z" fill="color-mix(in srgb, var(--avatar-outfit) 78%, black 22%)"/>
<path d="M73 126v12M87 126v12" stroke="rgba(255,255,255,0.46)" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M24 160c4-23 19-37 56-37s52 14 56 37H24z" fill="var(--avatar-outfit)"/>
<path d="M61 124l19 15 19-15" fill="color-mix(in srgb, var(--avatar-outfit) 70%, white 30%)"/>
<path d="M80 123v18" stroke="rgba(255,255,255,0.4)" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M26 160c3-22 18-35 54-35s51 13 54 35H26z" fill="var(--avatar-outfit)"/>
<path d="M58 126c5 9 12 14 22 14s17-5 22-14c-7-3-15-4-22-4s-15 1-22 4z" fill="color-mix(in srgb, var(--avatar-outfit) 80%, black 20%)"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -114,84 +114,22 @@ body {
.step { text-align: left; } .step { text-align: left; }
.step__icon { width: 34px; height:34px; border-radius: 10px; background: var(--color-accent); color: #fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom: 8px; } .step__icon { width: 34px; height:34px; border-radius: 10px; background: var(--color-accent); color: #fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom: 8px; }
.pkt-avatar { --avatar-size: 52px; --avatar-skin: #efc1a3; --avatar-hair: #5b392b; --avatar-eye: #5f4431; --avatar-hat: #38475f; --avatar-outfit: #5f7387; position: relative; display: inline-block; width: var(--avatar-size); height: var(--avatar-size); border-radius: 20px; overflow: hidden; background: #f7efe6; border: 1px solid rgba(74, 52, 38, 0.12); box-shadow: 0 10px 24px rgba(39, 28, 21, 0.08); } .pkt-avatar { --avatar-size: 52px; --avatar-skin: #efc1a3; --avatar-hair: #5b392b; --avatar-eye: #5f4431; --avatar-hat: #38475f; --avatar-outfit: #5f7387; position: relative; display: inline-block; width: var(--avatar-size); height: var(--avatar-size); border-radius: 20px; overflow: hidden; background: radial-gradient(circle at 50% 22%, #fffdfa 0%, #f7efe6 62%, #ecdac6 100%); border: 1px solid rgba(74, 52, 38, 0.12); box-shadow: 0 10px 24px rgba(39, 28, 21, 0.08); isolation: isolate; }
.pkt-avatar--nav { --avatar-size: 34px; border-radius: 999px; } .pkt-avatar--nav { --avatar-size: 34px; border-radius: 999px; }
.pkt-avatar--forum-list { --avatar-size: 42px; border-radius: 14px; } .pkt-avatar--forum-list { --avatar-size: 42px; border-radius: 14px; }
.pkt-avatar--forum { --avatar-size: 52px; border-radius: 16px; } .pkt-avatar--forum { --avatar-size: 52px; border-radius: 16px; }
.pkt-avatar--xl { --avatar-size: 120px; border-radius: 28px; } .pkt-avatar--xl { --avatar-size: 120px; border-radius: 28px; }
.pkt-avatar--xxl { --avatar-size: 168px; border-radius: 36px; } .pkt-avatar--xxl { --avatar-size: 168px; border-radius: 36px; }
.pkt-avatar__img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center top; } .pkt-avatar--layered::before { content: ""; position: absolute; inset: auto 8% -8% 8%; height: 28%; border-radius: 50%; background: radial-gradient(circle at center, rgba(74, 52, 38, 0.16), transparent 70%); filter: blur(10px); z-index: 0; pointer-events: none; }
.pkt-avatar__svg { display: block; width: 100%; height: 100%; } .pkt-avatar__layer { position: absolute; inset: 0; display: block; z-index: 1; pointer-events: none; }
.pkt-avatar__bg { fill: url(#none); } .pkt-avatar__layer--outfit { z-index: 1; }
.pkt-avatar__bg { fill: #f7efe6; } .pkt-avatar__layer--head { z-index: 2; }
.pkt-avatar__shadow { fill: rgba(70, 52, 40, 0.10); } .pkt-avatar__layer--hair { z-index: 3; }
.pkt-avatar__outfit { fill: color-mix(in srgb, var(--avatar-outfit) 88%, black 12%); } .pkt-avatar__layer--hat { z-index: 4; }
.pkt-avatar__outfit-detail { fill: none; stroke: rgba(255,255,255,0.45); stroke-width: 3.5; stroke-linecap: round; } .pkt-avatar__layer--features { z-index: 5; }
.pkt-avatar__neck, .pkt-avatar__layer--beard { z-index: 6; }
.pkt-avatar__ear, .pkt-avatar__layer--glasses { z-index: 7; }
.pkt-avatar__face { fill: var(--avatar-skin); } .pkt-avatar__layer svg { display: block; width: 100%; height: 100%; }
.pkt-avatar__face-shade { fill: rgba(88, 58, 35, 0.10); }
.pkt-avatar__cheek { fill: rgba(204, 112, 100, 0.12); }
.pkt-avatar__hair-base,
.pkt-avatar__hair-short,
.pkt-avatar__hair-fade,
.pkt-avatar__hair-parted,
.pkt-avatar__hair-curly,
.pkt-avatar__hair-waves,
.pkt-avatar__beard-mustache,
.pkt-avatar__beard-short,
.pkt-avatar__beard-full,
.pkt-avatar__brow { fill: var(--avatar-hair); stroke: none; }
.pkt-avatar__hair-base { opacity: .15; }
.pkt-avatar__hair-short,
.pkt-avatar__hair-fade,
.pkt-avatar__hair-parted,
.pkt-avatar__hair-curly,
.pkt-avatar__hair-waves { display: none; }
.pkt-avatar--hair-short .pkt-avatar__hair-short,
.pkt-avatar--hair-fade .pkt-avatar__hair-fade,
.pkt-avatar--hair-parted .pkt-avatar__hair-parted,
.pkt-avatar--hair-curly .pkt-avatar__hair-curly,
.pkt-avatar--hair-waves .pkt-avatar__hair-waves { display: block; }
.pkt-avatar--hair-bald .pkt-avatar__hair-base,
.pkt-avatar--hair-bald .pkt-avatar__hair-short,
.pkt-avatar--hair-bald .pkt-avatar__hair-fade,
.pkt-avatar--hair-bald .pkt-avatar__hair-parted,
.pkt-avatar--hair-bald .pkt-avatar__hair-curly,
.pkt-avatar--hair-bald .pkt-avatar__hair-waves { display: none; }
.pkt-avatar__hat-beanie,
.pkt-avatar__hat-beanie-rib,
.pkt-avatar__hat-cap,
.pkt-avatar__hat-cap-brim { display: none; fill: var(--avatar-hat); }
.pkt-avatar__hat-beanie-rib,
.pkt-avatar__hat-cap-brim { fill: color-mix(in srgb, var(--avatar-hat) 86%, black 14%); }
.pkt-avatar--hat-beanie .pkt-avatar__hat-beanie,
.pkt-avatar--hat-beanie .pkt-avatar__hat-beanie-rib,
.pkt-avatar--hat-cap .pkt-avatar__hat-cap,
.pkt-avatar--hat-cap .pkt-avatar__hat-cap-brim { display: block; }
.pkt-avatar__brow { fill: none; stroke: color-mix(in srgb, var(--avatar-hair) 92%, black 8%); stroke-width: 4; stroke-linecap: round; opacity: .9; }
.pkt-avatar__eye-shell { fill: #fff; }
.pkt-avatar__eye { fill: var(--avatar-eye); }
.pkt-avatar__eye-highlight { fill: rgba(255,255,255,0.9); }
.pkt-avatar__nose { fill: none; stroke: rgba(113, 76, 49, 0.18); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.pkt-avatar__mouth { fill: none; stroke: rgba(102, 66, 42, 0.7); stroke-width: 3; stroke-linecap: round; }
.pkt-avatar__beard-mustache,
.pkt-avatar__beard-short,
.pkt-avatar__beard-full { display: none; }
.pkt-avatar--beard-mustache .pkt-avatar__beard-mustache,
.pkt-avatar--beard-short .pkt-avatar__beard-short,
.pkt-avatar--beard-full .pkt-avatar__beard-full { display: block; }
.pkt-avatar__glasses-round-left,
.pkt-avatar__glasses-round-right,
.pkt-avatar__glasses-square-left,
.pkt-avatar__glasses-square-right,
.pkt-avatar__glasses-bridge { display: none; fill: none; stroke: rgba(41, 41, 41, 0.78); stroke-width: 2.8; stroke-linecap: round; }
.pkt-avatar--glasses-round .pkt-avatar__glasses-round-left,
.pkt-avatar--glasses-round .pkt-avatar__glasses-round-right,
.pkt-avatar--glasses-round .pkt-avatar__glasses-bridge { display: block; }
.pkt-avatar--glasses-square .pkt-avatar__glasses-square-left,
.pkt-avatar--glasses-square .pkt-avatar__glasses-square-right,
.pkt-avatar--glasses-square .pkt-avatar__glasses-bridge { display: block; }
.faq details { border:1px solid var(--color-border); border-radius: var(--radius-sm); padding: 10px 12px; background: #fff; } .faq details { border:1px solid var(--color-border); border-radius: var(--radius-sm); padding: 10px 12px; background: #fff; }
.faq summary { cursor:pointer; font-weight: 600; } .faq summary { cursor:pointer; font-weight: 600; }
@@ -311,18 +249,11 @@ body {
.avatar-builder__layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; align-items: start; } .avatar-builder__layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; align-items: start; }
.avatar-builder__preview { position: sticky; top: 0; display: grid; justify-items: center; gap: 12px; padding: 18px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; } .avatar-builder__preview { position: sticky; top: 0; display: grid; justify-items: center; gap: 12px; padding: 18px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
.avatar-builder__controls { display: grid; gap: 18px; } .avatar-builder__controls { display: grid; gap: 18px; }
.avatar-preset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 12px; }
.avatar-preset-card { display: grid; gap: 8px; align-content: start; padding: 10px; border: 1px solid var(--color-border); border-radius: 16px; background: #fff; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
.avatar-preset-card:hover { border-color: var(--color-primary); box-shadow: 0 10px 20px rgba(41, 31, 22, 0.08); transform: translateY(-1px); }
.avatar-preset-card input { position: absolute; opacity: 0; pointer-events: none; }
.avatar-preset-card__thumb { display: block; aspect-ratio: 1; border-radius: 14px; overflow: hidden; background: linear-gradient(180deg, #f7efe6, #f0e4d7); border: 1px solid rgba(74, 52, 38, 0.08); }
.avatar-preset-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; }
.avatar-preset-card__label { font-size: 13px; font-weight: 700; color: var(--color-text); }
.avatar-preset-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
.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); }
.avatar-swatch input { margin: 0; } .avatar-swatch input { margin: 0; }
.avatar-swatch:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
.settings-help { display: grid; gap: 14px; } .settings-help { display: grid; gap: 14px; }
.settings-help details { border: 1px solid var(--color-border); border-radius: 12px; padding: 12px 14px; background: #fff; } .settings-help details { border: 1px solid var(--color-border); border-radius: 12px; padding: 12px 14px; background: #fff; }
.settings-help summary { cursor: pointer; font-weight: 700; } .settings-help summary { cursor: pointer; font-weight: 700; }

View File

@@ -732,22 +732,13 @@ document.addEventListener('DOMContentLoaded', () => {
'pkt-avatar--beard-', 'pkt-avatar--beard-',
'pkt-avatar--glasses-', 'pkt-avatar--glasses-',
'pkt-avatar--hat-', 'pkt-avatar--hat-',
'pkt-avatar--outfit-',
]; ];
const updateAvatarPreview = (form) => { const updateAvatarPreview = (form) => {
const preview = form.querySelector('[data-avatar-preview] .pkt-avatar'); const preview = form.querySelector('[data-avatar-preview] .pkt-avatar');
if (!preview) return; if (!preview) return;
const presetChoice = form.querySelector('[data-avatar-preset-option]:checked');
if (presetChoice) {
const img = preview.querySelector('.pkt-avatar__img');
const src = presetChoice.getAttribute('data-avatar-src') || '';
if (img && src) {
img.src = src;
}
return;
}
const readValue = (field) => { const readValue = (field) => {
const checked = form.querySelector(`[data-avatar-field="${field}"]:checked`); const checked = form.querySelector(`[data-avatar-field="${field}"]:checked`);
if (checked) return checked.value; if (checked) return checked.value;
@@ -770,6 +761,7 @@ document.addEventListener('DOMContentLoaded', () => {
preview.classList.add(`pkt-avatar--beard-${readValue('avatar_beard_style')}`); preview.classList.add(`pkt-avatar--beard-${readValue('avatar_beard_style')}`);
preview.classList.add(`pkt-avatar--glasses-${readValue('avatar_glasses_style')}`); preview.classList.add(`pkt-avatar--glasses-${readValue('avatar_glasses_style')}`);
preview.classList.add(`pkt-avatar--hat-${readValue('avatar_hat_style')}`); preview.classList.add(`pkt-avatar--hat-${readValue('avatar_hat_style')}`);
preview.classList.add(`pkt-avatar--outfit-${readValue('avatar_outfit_style')}`);
preview.style.setProperty('--avatar-skin', readColor('avatar_skin_tone')); preview.style.setProperty('--avatar-skin', readColor('avatar_skin_tone'));
preview.style.setProperty('--avatar-hair', readColor('avatar_hair_color')); preview.style.setProperty('--avatar-hair', readColor('avatar_hair_color'));
preview.style.setProperty('--avatar-eye', readColor('avatar_eye_color')); preview.style.setProperty('--avatar-eye', readColor('avatar_eye_color'));
@@ -779,7 +771,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-avatar-builder]').forEach((form) => { document.querySelectorAll('[data-avatar-builder]').forEach((form) => {
updateAvatarPreview(form); updateAvatarPreview(form);
form.querySelectorAll('[data-avatar-field], [data-avatar-preset-option]').forEach((field) => { form.querySelectorAll('[data-avatar-field]').forEach((field) => {
field.addEventListener('change', () => updateAvatarPreview(form)); field.addEventListener('change', () => updateAvatarPreview(form));
field.addEventListener('input', () => updateAvatarPreview(form)); field.addEventListener('input', () => updateAvatarPreview(form));
}); });

View File

@@ -40,6 +40,7 @@ CREATE TABLE user_profiles (
avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round', avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round',
avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none', avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none',
avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy', avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy',
avatar_outfit_style VARCHAR(24) NOT NULL DEFAULT 'tee',
avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate', avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate',
contact_phone VARBINARY(512) NULL, contact_phone VARBINARY(512) NULL,
contact_email VARBINARY(512) NULL, contact_email VARBINARY(512) NULL,

View File

@@ -348,12 +348,13 @@ final class AccountPages
'avatar_head_shape' => 'round', 'avatar_head_shape' => 'round',
'avatar_hat_style' => 'none', 'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy', 'avatar_hat_color' => 'navy',
'avatar_outfit_style' => 'tee',
'avatar_outfit_color' => 'slate', 'avatar_outfit_color' => 'slate',
]; ];
if ($profileSettings) { if ($profileSettings) {
$profileSettings->ensureSchema(); $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, p.avatar_preset, p.avatar_skin_tone, p.avatar_hair_style, p.avatar_hair_color, p.avatar_eye_color, p.avatar_beard_style, p.avatar_glasses_style, p.avatar_head_shape, p.avatar_hat_style, p.avatar_hat_color, p.avatar_outfit_color FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1'); $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, p.avatar_preset, p.avatar_skin_tone, p.avatar_hair_style, p.avatar_hair_color, p.avatar_eye_color, p.avatar_beard_style, p.avatar_glasses_style, p.avatar_head_shape, p.avatar_hat_style, p.avatar_hat_color, p.avatar_outfit_style, p.avatar_outfit_color FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
$stmt?->execute(['id' => $userId]); $stmt?->execute(['id' => $userId]);
$row = $stmt?->fetch(\PDO::FETCH_ASSOC); $row = $stmt?->fetch(\PDO::FETCH_ASSOC);
if ($row) { if ($row) {
@@ -420,7 +421,6 @@ final class AccountPages
$section = 'profile'; $section = 'profile';
} }
$avatarOptions = Avatar::options(); $avatarOptions = Avatar::options();
$avatarPresetOptions = Avatar::presetOptions();
return compact( return compact(
'flash', 'flash',
@@ -438,7 +438,6 @@ final class AccountPages
'communityCanApply', 'communityCanApply',
'communityRestrictions', 'communityRestrictions',
'avatarOptions', 'avatarOptions',
'avatarPresetOptions',
'section', 'section',
'allowedSections' 'allowedSections'
); );

View File

@@ -10,14 +10,15 @@ final class Avatar
return [ return [
'avatar_preset' => 'modern_01', 'avatar_preset' => 'modern_01',
'avatar_skin_tone' => 'warm', 'avatar_skin_tone' => 'warm',
'avatar_hair_style' => 'short', 'avatar_hair_style' => 'parted',
'avatar_hair_color' => 'brown', 'avatar_hair_color' => 'brown',
'avatar_eye_color' => 'brown', 'avatar_eye_color' => 'brown',
'avatar_beard_style' => 'none', 'avatar_beard_style' => 'short',
'avatar_glasses_style' => 'none', 'avatar_glasses_style' => 'none',
'avatar_head_shape' => 'round', 'avatar_head_shape' => 'oval',
'avatar_hat_style' => 'none', 'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy', 'avatar_hat_color' => 'navy',
'avatar_outfit_style' => 'tee',
'avatar_outfit_color' => 'slate', 'avatar_outfit_color' => 'slate',
]; ];
} }
@@ -25,34 +26,33 @@ final class Avatar
public static function options(): array public static function options(): array
{ {
return [ return [
'avatar_preset' => self::presetOptions(),
'avatar_skin_tone' => [ 'avatar_skin_tone' => [
'fair' => ['label' => 'Hell', 'color' => '#f5d7c2'], 'fair' => ['label' => 'Hell', 'color' => '#f6d7c1'],
'warm' => ['label' => 'Warm', 'color' => '#efc1a3'], 'warm' => ['label' => 'Warm', 'color' => '#eec0a0'],
'golden' => ['label' => 'Gold', 'color' => '#dca57f'], 'golden' => ['label' => 'Gold', 'color' => '#d7a179'],
'deep' => ['label' => 'Tief', 'color' => '#a86d49'], 'deep' => ['label' => 'Tief', 'color' => '#9c633e'],
'rich' => ['label' => 'Kräftig', 'color' => '#7d4d31'], 'rich' => ['label' => 'Kräftig', 'color' => '#6f432b'],
], ],
'avatar_hair_style' => [ 'avatar_hair_style' => [
'parted' => 'Seitenscheitel',
'short' => 'Kurz', 'short' => 'Kurz',
'fade' => 'Fade', 'fade' => 'Fade',
'parted' => 'Seitlich',
'curly' => 'Lockig', 'curly' => 'Lockig',
'waves' => 'Wellen', 'waves' => 'Wellig',
'bald' => 'Kahl', 'bald' => 'Kahl',
], ],
'avatar_hair_color' => [ 'avatar_hair_color' => [
'black' => ['label' => 'Schwarz', 'color' => '#1d1b1b'], 'black' => ['label' => 'Schwarz', 'color' => '#191919'],
'brown' => ['label' => 'Braun', 'color' => '#5b392b'], 'brown' => ['label' => 'Braun', 'color' => '#65442f'],
'blonde' => ['label' => 'Blond', 'color' => '#d7b15f'], 'blonde' => ['label' => 'Blond', 'color' => '#d3a053'],
'red' => ['label' => 'Rot', 'color' => '#9e4f35'], 'red' => ['label' => 'Rot', 'color' => '#9f5a34'],
'gray' => ['label' => 'Grau', 'color' => '#767676'], 'gray' => ['label' => 'Grau', 'color' => '#8f8c87'],
], ],
'avatar_eye_color' => [ 'avatar_eye_color' => [
'brown' => ['label' => 'Braun', 'color' => '#5f4431'], 'brown' => ['label' => 'Braun', 'color' => '#5e4330'],
'hazel' => ['label' => 'Hasel', 'color' => '#7f6845'], 'hazel' => ['label' => 'Hasel', 'color' => '#7c6848'],
'green' => ['label' => 'Grün', 'color' => '#54704e'], 'green' => ['label' => 'Grün', 'color' => '#4f6c52'],
'blue' => ['label' => 'Blau', 'color' => '#4d6e92'], 'blue' => ['label' => 'Blau', 'color' => '#507196'],
], ],
'avatar_beard_style' => [ 'avatar_beard_style' => [
'none' => 'Ohne', 'none' => 'Ohne',
@@ -76,11 +76,17 @@ final class Avatar
'cap' => 'Cap', 'cap' => 'Cap',
], ],
'avatar_hat_color' => [ 'avatar_hat_color' => [
'navy' => ['label' => 'Navy', 'color' => '#38475f'], 'navy' => ['label' => 'Navy', 'color' => '#42526a'],
'charcoal' => ['label' => 'Anthrazit', 'color' => '#3c3f45'], 'charcoal' => ['label' => 'Anthrazit', 'color' => '#43464d'],
'forest' => ['label' => 'Waldgrün', 'color' => '#48614d'], 'forest' => ['label' => 'Waldgrün', 'color' => '#51634f'],
'brick' => ['label' => 'Ziegel', 'color' => '#975244'], 'brick' => ['label' => 'Ziegel', 'color' => '#9a5a47'],
'sand' => ['label' => 'Sand', 'color' => '#b79e79'], 'sand' => ['label' => 'Sand', 'color' => '#baa078'],
],
'avatar_outfit_style' => [
'tee' => 'T-Shirt',
'hoodie' => 'Hoodie',
'polo' => 'Polo',
'crew' => 'Pullover',
], ],
'avatar_outfit_color' => [ 'avatar_outfit_color' => [
'slate' => ['label' => 'Schiefer', 'color' => '#5f7387'], 'slate' => ['label' => 'Schiefer', 'color' => '#5f7387'],
@@ -92,29 +98,6 @@ final class Avatar
]; ];
} }
public static function presetOptions(): array
{
$options = [];
for ($i = 1; $i <= 30; $i++) {
$id = sprintf('modern_%02d', $i);
$options[$id] = [
'label' => 'Modern ' . str_pad((string)$i, 2, '0', STR_PAD_LEFT),
'src' => self::presetPublicPath($id),
];
}
return $options;
}
public static function presetPublicPath(string $preset): string
{
return '/assets/avatars/presets/' . rawurlencode($preset) . '.png';
}
public static function presetFilePath(string $preset): string
{
return dirname(__DIR__, 2) . '/public/assets/avatars/presets/' . $preset . '.png';
}
public static function selectColumns(string $alias = 'p'): string public static function selectColumns(string $alias = 'p'): string
{ {
$columns = array_keys(self::defaults()); $columns = array_keys(self::defaults());
@@ -132,7 +115,7 @@ final class Avatar
foreach ($defaults as $key => $defaultValue) { foreach ($defaults as $key => $defaultValue) {
$value = (string)($input[$key] ?? $defaultValue); $value = (string)($input[$key] ?? $defaultValue);
if (!array_key_exists($value, $options[$key])) { if (isset($options[$key]) && !array_key_exists($value, $options[$key])) {
$value = $defaultValue; $value = $defaultValue;
} }
$normalized[$key] = $value; $normalized[$key] = $value;
@@ -143,117 +126,70 @@ final class Avatar
public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string
{ {
static $instanceCounter = 0;
$instanceCounter++;
$avatar = self::normalize($row); $avatar = self::normalize($row);
$options = self::options(); $options = self::options();
$preset = (string)($avatar['avatar_preset'] ?? '');
if ($preset !== '' && is_file(self::presetFilePath($preset))) {
$classes = [
'pkt-avatar',
'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
'pkt-avatar--preset',
];
return sprintf(
'<span class="%s" role="img" aria-label="%s"><img class="pkt-avatar__img" src="%s" alt=""></span>',
htmlspecialchars(implode(' ', $classes), ENT_QUOTES),
htmlspecialchars('Avatar von ' . $name, ENT_QUOTES),
htmlspecialchars(self::presetPublicPath($preset), ENT_QUOTES)
);
}
$skin = $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color'];
$hair = $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color'];
$eyes = $options['avatar_eye_color'][$avatar['avatar_eye_color']]['color'];
$hat = $options['avatar_hat_color'][$avatar['avatar_hat_color']]['color'];
$outfit = $options['avatar_outfit_color'][$avatar['avatar_outfit_color']]['color'];
$classes = [ $classes = [
'pkt-avatar', 'pkt-avatar',
'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)), 'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
'pkt-avatar--layered',
'pkt-avatar--head-' . $avatar['avatar_head_shape'], 'pkt-avatar--head-' . $avatar['avatar_head_shape'],
'pkt-avatar--hair-' . $avatar['avatar_hair_style'], 'pkt-avatar--hair-' . $avatar['avatar_hair_style'],
'pkt-avatar--beard-' . $avatar['avatar_beard_style'], 'pkt-avatar--beard-' . $avatar['avatar_beard_style'],
'pkt-avatar--glasses-' . $avatar['avatar_glasses_style'], 'pkt-avatar--glasses-' . $avatar['avatar_glasses_style'],
'pkt-avatar--hat-' . $avatar['avatar_hat_style'], 'pkt-avatar--hat-' . $avatar['avatar_hat_style'],
'pkt-avatar--outfit-' . $avatar['avatar_outfit_style'],
]; ];
$style = sprintf( $style = sprintf(
'--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;--avatar-hat:%s;--avatar-outfit:%s;', '--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;--avatar-hat:%s;--avatar-outfit:%s;',
$skin, $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color'],
$hair, $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color'],
$eyes, $options['avatar_eye_color'][$avatar['avatar_eye_color']]['color'],
$hat, $options['avatar_hat_color'][$avatar['avatar_hat_color']]['color'],
$outfit $options['avatar_outfit_color'][$avatar['avatar_outfit_color']]['color']
); );
$label = htmlspecialchars('Avatar von ' . $name, ENT_QUOTES); $layers = [
$clipId = 'pktAvatarClip' . $instanceCounter; self::renderLayer('outfit', $avatar['avatar_outfit_style']),
self::renderLayer('head', $avatar['avatar_head_shape']),
self::renderLayer('hair', $avatar['avatar_hair_style']),
self::renderLayer('hat', $avatar['avatar_hat_style']),
self::renderLayer('features', 'default'),
self::renderLayer('beard', $avatar['avatar_beard_style']),
self::renderLayer('glasses', $avatar['avatar_glasses_style']),
];
return sprintf( return sprintf(
'<span class="%s" style="%s" role="img" aria-label="%s">' . '<span class="%s" style="%s" role="img" aria-label="%s">%s</span>',
'<svg class="pkt-avatar__svg" viewBox="0 0 160 160" aria-hidden="true">' .
'<defs>' .
'<clipPath id="%s"><rect x="0" y="0" width="160" height="160" rx="32" ry="32"></rect></clipPath>' .
'</defs>' .
'<g clip-path="url(#%s)">' .
'<rect class="pkt-avatar__bg" x="0" y="0" width="160" height="160"></rect>' .
'<ellipse class="pkt-avatar__shadow" cx="80" cy="150" rx="62" ry="18"></ellipse>' .
'<path class="pkt-avatar__outfit" d="M22 164c2-30 22-46 58-46s56 16 58 46H22z"></path>' .
'<path class="pkt-avatar__outfit-detail" d="M48 126c8 14 18 20 32 20s24-6 32-20"></path>' .
'<path class="pkt-avatar__neck" d="M68 98h24v20c0 8-6 14-12 14s-12-6-12-14V98z"></path>' .
'<path class="pkt-avatar__ear" d="M48 76c0-7 5-12 11-12v24c-6 0-11-5-11-12z"></path>' .
'<path class="pkt-avatar__ear" d="M112 64c6 0 11 5 11 12s-5 12-11 12V64z"></path>' .
'<path class="pkt-avatar__face" d="M80 28c-24 0-39 17-39 40v18c0 28 17 45 39 45s39-17 39-45V68c0-23-15-40-39-40z"></path>' .
'<path class="pkt-avatar__face-shade" d="M46 90c5 24 20 38 34 38 15 0 31-15 36-40-2 27-18 43-36 43-17 0-32-12-34-41z"></path>' .
'<ellipse class="pkt-avatar__cheek" cx="60" cy="90" rx="9" ry="5"></ellipse>' .
'<ellipse class="pkt-avatar__cheek" cx="100" cy="90" rx="9" ry="5"></ellipse>' .
'<g class="pkt-avatar__hair-set">' .
'<path class="pkt-avatar__hair-base" d="M40 70c0-31 17-48 40-48s40 17 40 48c0 0-2-20-10-30-9-11-22-16-30-16s-21 5-30 16c-8 10-10 30-10 30z"></path>' .
'<path class="pkt-avatar__hair-short" d="M42 69c2-18 12-30 22-36 7-5 18-8 28-5 14 4 24 16 26 41-7-8-14-15-23-20-8-4-16-6-24-5-11 1-20 5-29 25z"></path>' .
'<path class="pkt-avatar__hair-fade" d="M44 70c4-24 18-38 36-38 19 0 33 13 36 38-10-8-17-12-24-14-10-3-21-3-31 0-6 2-12 6-17 14z"></path>' .
'<path class="pkt-avatar__hair-parted" d="M42 69c3-18 13-29 24-35 13-7 30-6 42 6 7 7 10 17 10 29-7-10-12-17-19-20-11-5-21 0-26 7-3-6-9-10-15-10-8 0-12 4-16 8z"></path>' .
'<path class="pkt-avatar__hair-curly" d="M38 72c3-14 8-24 15-31 8-8 16-12 27-12 12 0 22 4 30 13 7 7 11 17 12 30-5-6-9-9-13-10-1 0-4 2-7 1-3-1-4-5-7-5s-5 5-8 4c-3 0-4-4-7-4-4 0-6 6-11 5-4 0-6-5-10-4-5 1-9 5-14 13z"></path>' .
'<path class="pkt-avatar__hair-waves" d="M41 70c2-14 8-24 17-32 8-7 17-10 28-10 17 0 31 12 34 41-5-8-10-12-16-15-4-1-7-2-11-3-3 0-6 6-10 6s-7-7-11-7c-12 0-21 8-31 20z"></path>' .
'</g>' .
'<g class="pkt-avatar__hat-set">' .
'<path class="pkt-avatar__hat-beanie" d="M36 63c2-24 18-40 44-40 24 0 40 14 44 40H36z"></path>' .
'<path class="pkt-avatar__hat-beanie-rib" d="M38 61h84c0 10-6 18-14 18H52c-8 0-14-8-14-18z"></path>' .
'<path class="pkt-avatar__hat-cap" d="M40 63c5-19 18-30 40-30 23 0 35 10 40 30H40z"></path>' .
'<path class="pkt-avatar__hat-cap-brim" d="M80 63c15 0 31 5 44 14-6 3-14 6-26 8-17 2-28 1-39-4 3-7 9-18 21-18z"></path>' .
'</g>' .
'<path class="pkt-avatar__brow pkt-avatar__brow--left" d="M58 67c5-4 12-5 19-2"></path>' .
'<path class="pkt-avatar__brow pkt-avatar__brow--right" d="M83 65c7-3 14-2 19 2"></path>' .
'<ellipse class="pkt-avatar__eye-shell" cx="66" cy="77" rx="8" ry="5"></ellipse>' .
'<ellipse class="pkt-avatar__eye-shell" cx="94" cy="77" rx="8" ry="5"></ellipse>' .
'<circle class="pkt-avatar__eye" cx="66" cy="77" r="3.4"></circle>' .
'<circle class="pkt-avatar__eye" cx="94" cy="77" r="3.4"></circle>' .
'<circle class="pkt-avatar__eye-highlight" cx="65" cy="76" r="1.1"></circle>' .
'<circle class="pkt-avatar__eye-highlight" cx="93" cy="76" r="1.1"></circle>' .
'<path class="pkt-avatar__nose" d="M80 78c-2 5-3 9-3 15 0 3 2 5 5 5"></path>' .
'<path class="pkt-avatar__mouth" d="M69 102c7 5 15 5 22 0"></path>' .
'<g class="pkt-avatar__beard-set">' .
'<path class="pkt-avatar__beard-mustache" d="M64 97c4-4 9-5 16-5 6 0 11 1 16 5-5 1-8 4-10 8-2-3-4-4-6-4s-4 1-6 4c-2-4-5-7-10-8z"></path>' .
'<path class="pkt-avatar__beard-short" d="M60 93c3 4 10 7 20 7s17-3 20-7c0 16-6 27-20 29-14-2-20-13-20-29z"></path>' .
'<path class="pkt-avatar__beard-full" d="M55 90c5 6 13 10 25 10 11 0 20-4 25-10-1 22-9 36-25 38-16-2-24-16-25-38z"></path>' .
'</g>' .
'<g class="pkt-avatar__glasses-set">' .
'<rect class="pkt-avatar__glasses-round-left" x="54" y="70" width="22" height="16" rx="8"></rect>' .
'<rect class="pkt-avatar__glasses-round-right" x="84" y="70" width="22" height="16" rx="8"></rect>' .
'<rect class="pkt-avatar__glasses-square-left" x="54" y="70" width="22" height="16" rx="4"></rect>' .
'<rect class="pkt-avatar__glasses-square-right" x="84" y="70" width="22" height="16" rx="4"></rect>' .
'<path class="pkt-avatar__glasses-bridge" d="M76 78h8"></path>' .
'</g>' .
'</g>' .
'</svg>' .
'</span>',
htmlspecialchars(implode(' ', $classes), ENT_QUOTES), htmlspecialchars(implode(' ', $classes), ENT_QUOTES),
htmlspecialchars($style, ENT_QUOTES), htmlspecialchars($style, ENT_QUOTES),
$label, htmlspecialchars('Avatar von ' . $name, ENT_QUOTES),
htmlspecialchars($clipId, ENT_QUOTES), implode('', array_filter($layers))
htmlspecialchars($clipId, ENT_QUOTES)
); );
} }
private static function renderLayer(string $category, string $variant): string
{
if ($variant === 'none') {
return '';
}
$file = self::layerFilePath($category, $variant);
if (!is_file($file)) {
return '';
}
$svg = file_get_contents($file);
if ($svg === false) {
return '';
}
return '<span class="pkt-avatar__layer pkt-avatar__layer--' . htmlspecialchars($category, ENT_QUOTES) . '">' . $svg . '</span>';
}
public static function layerFilePath(string $category, string $variant): string
{
return dirname(__DIR__, 2) . '/public/assets/avatars/layers/modern/' . $category . '/' . $variant . '.svg';
}
} }

View File

@@ -39,6 +39,7 @@ final class ProfileSettings
'avatar_head_shape' => "ALTER TABLE user_profiles ADD COLUMN avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round' AFTER avatar_glasses_style", 'avatar_head_shape' => "ALTER TABLE user_profiles ADD COLUMN avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round' AFTER avatar_glasses_style",
'avatar_hat_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_head_shape", 'avatar_hat_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_head_shape",
'avatar_hat_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy' AFTER avatar_hat_style", 'avatar_hat_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy' AFTER avatar_hat_style",
'avatar_outfit_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_outfit_style VARCHAR(24) NOT NULL DEFAULT 'tee' AFTER avatar_hat_color",
'avatar_outfit_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate' AFTER avatar_hat_color", 'avatar_outfit_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate' AFTER avatar_hat_color",
]; ];
foreach ($avatarColumns as $column => $sql) { foreach ($avatarColumns as $column => $sql) {
@@ -86,27 +87,11 @@ final class ProfileSettings
} }
$this->ensureSchema(); $this->ensureSchema();
$preset = (string)($avatarConfig['avatar_preset'] ?? '');
if ($preset !== '') {
$options = Avatar::presetOptions();
$preset = array_key_exists($preset, $options) ? $preset : 'modern_01';
$stmt = $this->pdo->prepare(
'UPDATE user_profiles
SET avatar_preset = :preset,
updated_at = NOW()
WHERE user_id = :id'
);
$stmt->execute([
'preset' => $preset,
'id' => $userId,
]);
return;
}
$avatar = Avatar::normalize($avatarConfig); $avatar = Avatar::normalize($avatarConfig);
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
'UPDATE user_profiles 'UPDATE user_profiles
SET avatar_skin_tone = :skin, SET avatar_preset = :preset,
avatar_skin_tone = :skin,
avatar_hair_style = :hairStyle, avatar_hair_style = :hairStyle,
avatar_hair_color = :hairColor, avatar_hair_color = :hairColor,
avatar_eye_color = :eyeColor, avatar_eye_color = :eyeColor,
@@ -115,11 +100,13 @@ final class ProfileSettings
avatar_head_shape = :headShape, avatar_head_shape = :headShape,
avatar_hat_style = :hatStyle, avatar_hat_style = :hatStyle,
avatar_hat_color = :hatColor, avatar_hat_color = :hatColor,
avatar_outfit_style = :outfitStyle,
avatar_outfit_color = :outfitColor, avatar_outfit_color = :outfitColor,
updated_at = NOW() updated_at = NOW()
WHERE user_id = :id' WHERE user_id = :id'
); );
$stmt->execute([ $stmt->execute([
'preset' => $avatar['avatar_preset'],
'skin' => $avatar['avatar_skin_tone'], 'skin' => $avatar['avatar_skin_tone'],
'hairStyle' => $avatar['avatar_hair_style'], 'hairStyle' => $avatar['avatar_hair_style'],
'hairColor' => $avatar['avatar_hair_color'], 'hairColor' => $avatar['avatar_hair_color'],
@@ -129,6 +116,7 @@ final class ProfileSettings
'headShape' => $avatar['avatar_head_shape'], 'headShape' => $avatar['avatar_head_shape'],
'hatStyle' => $avatar['avatar_hat_style'], 'hatStyle' => $avatar['avatar_hat_style'],
'hatColor' => $avatar['avatar_hat_color'], 'hatColor' => $avatar['avatar_hat_color'],
'outfitStyle' => $avatar['avatar_outfit_style'],
'outfitColor' => $avatar['avatar_outfit_color'], 'outfitColor' => $avatar['avatar_outfit_color'],
'id' => $userId, 'id' => $userId,
]); ]);