@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
4
public/assets/avatars/layers/modern/beard/full.svg
Normal 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 |
3
public/assets/avatars/layers/modern/beard/mustache.svg
Normal 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 |
4
public/assets/avatars/layers/modern/beard/short.svg
Normal 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 |
14
public/assets/avatars/layers/modern/features/default.svg
Normal 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 |
5
public/assets/avatars/layers/modern/glasses/round.svg
Normal 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 |
5
public/assets/avatars/layers/modern/glasses/square.svg
Normal 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 |
7
public/assets/avatars/layers/modern/hair/curly.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hair/fade.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hair/parted.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hair/short.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hair/waves.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hat/beanie.svg
Normal 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 |
4
public/assets/avatars/layers/modern/hat/cap.svg
Normal 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 |
8
public/assets/avatars/layers/modern/head/oval.svg
Normal 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 |
8
public/assets/avatars/layers/modern/head/round.svg
Normal 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 |
7
public/assets/avatars/layers/modern/head/square.svg
Normal 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 |
4
public/assets/avatars/layers/modern/outfit/crew.svg
Normal 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 |
5
public/assets/avatars/layers/modern/outfit/hoodie.svg
Normal 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 |
5
public/assets/avatars/layers/modern/outfit/polo.svg
Normal 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 |
4
public/assets/avatars/layers/modern/outfit/tee.svg
Normal 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 |
@@ -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; }
|
||||||
|
|||||||
@@ -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));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|||||||