xccxv
All checks were successful
Deploy / deploy (push) Successful in 51s

This commit is contained in:
2026-07-24 21:09:33 +02:00
parent 5256dd4080
commit 5630ff2308
12 changed files with 139 additions and 29 deletions

View File

@@ -48,7 +48,7 @@ Stand: 2026-07-24
- `src/App/ProfileSettings.php` - `src/App/ProfileSettings.php`
Persistente Profileinstellungen für Standortfreigabe und Avatar-Merkmale. Persistente Profileinstellungen für Standortfreigabe und Avatar-Merkmale.
- `src/App/Avatar.php` - `src/App/Avatar.php`
Avatar-Generator, Optionen und HTML-Rendering. Avatar-Generator, Optionen und HTML-Rendering im realistischerem Comic-Stil.
- `src/App/Search.php` - `src/App/Search.php`
Eventsuche inkl. Distanzfilter. Eventsuche inkl. Distanzfilter.
@@ -67,7 +67,7 @@ Stand: 2026-07-24
- Kein eigener Top-Level-Punkt `Termine`; Event-Fokus liegt derzeit auf Startseite und Suche. - Kein eigener Top-Level-Punkt `Termine`; Event-Fokus liegt derzeit auf Startseite und Suche.
- Community mit Kategorien, Boards, Thread-Liste, Thread-Ansicht, Rollen-/Moderationslogik. - Community mit Kategorien, Boards, Thread-Liste, Thread-Ansicht, Rollen-/Moderationslogik.
- Mitgliederbereich als Bereichslayout mit linker Navigation und separaten Seiten für Profil, Kinder, Termine, Community und Einstellungen. - Mitgliederbereich als Bereichslayout mit linker Navigation und separaten Seiten für Profil, Kinder, Termine, Community und Einstellungen.
- Profilbereich mit Avatar-Generator für Haut, Haare, Augen, Bart, Brille und Kopfform. - Profilbereich mit Avatar-Generator für Haut, Haare, Augen, Bart, Brille, Kopfform, Mütze und Outfit-Farbe.
- Community-Admin-Bereich für Bewerbungen, Meldungen, Rollen und Migration. - Community-Admin-Bereich für Bewerbungen, Meldungen, Rollen und Migration.
- Impressum, Datenschutz-&-Cookies-Seite und Über-uns vorhanden. - Impressum, Datenschutz-&-Cookies-Seite und Über-uns vorhanden.
- Standortsortierung auf der Startseite für Treffen in der Nähe. - Standortsortierung auf der Startseite für Treffen in der Nähe.

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 einen integrierten Avatar-Generator statt über Bild-Uploads erzeugt. - Profilbilder werden aktuell über einen integrierten Avatar-Generator im realistischeren Comic-Stil statt über 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 sein Avatar-Bild aus festen Merkmalen zusammenstellen. - Im Profil kann der Nutzer sein Avatar-Bild aus festen Merkmalen wie Gesicht, Haare, Bart, Brille, Mütze und Outfit 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
- Avatar-Generator im Profil mit Haut-, Haar-, Augen-, Bart-, Brillen- und Kopfform-Auswahl - Avatar-Generator im Profil mit realistischerem Comic-Stil, Haut-, Haar-, Augen-, Bart-, Brillen-, Kopfform-, Mützen- und Outfit-Auswahl
- 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

@@ -337,6 +337,14 @@ $sectionLinks = [
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </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"> <div class="stack gap-6">
<span class="label">Hautfarbe</span> <span class="label">Hautfarbe</span>
@@ -376,6 +384,32 @@ $sectionLinks = [
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</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">Hoodie / Shirt</span>
<div class="avatar-swatch-group">
<?php foreach ($avatarOptions['avatar_outfit_color'] as $value => $meta): ?>
<label class="avatar-swatch">
<input type="radio" name="avatar_outfit_color" value="<?= htmlspecialchars((string)$value, ENT_QUOTES) ?>" data-avatar-field="avatar_outfit_color" data-avatar-color="<?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>" <?= $profile['avatar_outfit_color'] === $value ? 'checked' : '' ?>>
<span class="avatar-swatch__color" style="--swatch-color: <?= htmlspecialchars((string)$meta['color'], ENT_QUOTES) ?>;"></span>
<span><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div> </div>
</div> </div>
<div class="flex gap-12"> <div class="flex gap-12">

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_skin_tone, avatar_hair_style, avatar_hair_color, avatar_eye_color, avatar_beard_style, avatar_glasses_style, avatar_head_shape FROM user_profiles WHERE user_id = :id LIMIT 1'); $stmt = $pdo->prepare('SELECT display_name, first_name, 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->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

@@ -114,39 +114,51 @@ 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; position: relative; display: inline-block; width: var(--avatar-size); height: var(--avatar-size); border-radius: 18px; overflow: hidden; background: linear-gradient(180deg, #fbf6ee, #efe2d2); border: 1px solid rgba(74, 52, 38, 0.12); box-shadow: inset 0 1px 0 rgba(255,255,255,0.65); } .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 28% 22%, #ffffff, #f5ece2 34%, #dfd0c0 100%); border: 1px solid rgba(74, 52, 38, 0.14); box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 10px 22px rgba(39, 28, 21, 0.08); }
.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__bg, .pkt-avatar__bg,
.pkt-avatar__shirt, .pkt-avatar__shadow,
.pkt-avatar__hoodie,
.pkt-avatar__neck, .pkt-avatar__neck,
.pkt-avatar__ears, .pkt-avatar__ears,
.pkt-avatar__head, .pkt-avatar__head,
.pkt-avatar__cheek,
.pkt-avatar__hair, .pkt-avatar__hair,
.pkt-avatar__hat,
.pkt-avatar__eye, .pkt-avatar__eye,
.pkt-avatar__brow,
.pkt-avatar__nose, .pkt-avatar__nose,
.pkt-avatar__mouth, .pkt-avatar__mouth,
.pkt-avatar__beard, .pkt-avatar__beard,
.pkt-avatar__glasses { position: absolute; display: block; } .pkt-avatar__glasses { position: absolute; display: block; }
.pkt-avatar__bg { inset: 0; background: radial-gradient(circle at top, rgba(255,255,255,0.82), rgba(255,255,255,0) 48%); } .pkt-avatar__bg { inset: 0; background: radial-gradient(circle at 30% 16%, rgba(255,255,255,0.92), rgba(255,255,255,0.04) 42%), radial-gradient(circle at 78% 82%, rgba(93,70,48,0.08), rgba(93,70,48,0) 34%); }
.pkt-avatar__shirt { left: 16%; bottom: -4%; width: 68%; height: 34%; border-radius: 50% 50% 18% 18%; background: linear-gradient(180deg, #607d94, #44586b); } .pkt-avatar__shadow { left: 18%; top: 18%; width: 64%; height: 62%; border-radius: 44% 44% 42% 42%; background: radial-gradient(circle at 52% 56%, rgba(44, 29, 21, 0.20), rgba(44, 29, 21, 0) 68%); filter: blur(calc(var(--avatar-size) * 0.045)); }
.pkt-avatar__neck { left: 43%; bottom: 24%; width: 14%; height: 16%; border-radius: 10px; background: var(--avatar-skin); } .pkt-avatar__hoodie { left: 7%; bottom: -4%; width: 86%; height: 38%; border-radius: 42% 42% 16% 16%; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-outfit) 82%, white 18%), color-mix(in srgb, var(--avatar-outfit) 72%, black 28%)); }
.pkt-avatar__hoodie::before { content: ""; position: absolute; left: 29%; top: -18%; width: 42%; height: 54%; border-radius: 50% 50% 42% 42%; border: calc(var(--avatar-size) * 0.035) solid color-mix(in srgb, var(--avatar-outfit) 78%, white 22%); border-bottom: 0; opacity: .95; }
.pkt-avatar__hoodie::after { content: ""; position: absolute; left: 47%; top: 12%; width: 6%; height: 20%; border-radius: 999px; background: rgba(255,255,255,0.42); box-shadow: calc(var(--avatar-size) * -0.08) 0 0 rgba(255,255,255,0.42); }
.pkt-avatar__neck { left: 43%; bottom: 24%; width: 14%; height: 16%; border-radius: 10px; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-skin) 90%, white 10%), color-mix(in srgb, var(--avatar-skin) 80%, black 20%)); }
.pkt-avatar__ears { left: 20%; top: 37%; width: 60%; height: 18%; } .pkt-avatar__ears { left: 20%; top: 37%; width: 60%; height: 18%; }
.pkt-avatar__ears::before, .pkt-avatar__ears::before,
.pkt-avatar__ears::after { content: ""; position: absolute; top: 0; width: 12%; height: 100%; border-radius: 999px; background: var(--avatar-skin); } .pkt-avatar__ears::after { content: ""; position: absolute; top: 0; width: 12%; height: 100%; border-radius: 999px; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-skin) 90%, white 10%), color-mix(in srgb, var(--avatar-skin) 78%, black 22%)); }
.pkt-avatar__ears::before { left: -6%; } .pkt-avatar__ears::before { left: -6%; }
.pkt-avatar__ears::after { right: -6%; } .pkt-avatar__ears::after { right: -6%; }
.pkt-avatar__head { left: 22%; top: 19%; width: 56%; height: 54%; background: var(--avatar-skin); } .pkt-avatar__head { left: 22%; top: 19%; width: 56%; height: 54%; background: radial-gradient(circle at 44% 26%, color-mix(in srgb, var(--avatar-skin) 88%, white 12%) 0, color-mix(in srgb, var(--avatar-skin) 92%, white 8%) 35%, color-mix(in srgb, var(--avatar-skin) 84%, black 16%) 100%); box-shadow: inset 0 calc(var(--avatar-size) * -0.02) calc(var(--avatar-size) * 0.03) rgba(255,255,255,0.22), inset 0 calc(var(--avatar-size) * -0.04) calc(var(--avatar-size) * 0.08) rgba(118, 75, 47, 0.12); }
.pkt-avatar--head-round .pkt-avatar__head { border-radius: 48% 48% 42% 42%; } .pkt-avatar--head-round .pkt-avatar__head { border-radius: 48% 48% 42% 42%; }
.pkt-avatar--head-oval .pkt-avatar__head { border-radius: 44% 44% 46% 46%; transform: scaleX(0.94); } .pkt-avatar--head-oval .pkt-avatar__head { border-radius: 44% 44% 46% 46%; transform: scaleX(0.94); }
.pkt-avatar--head-square .pkt-avatar__head { border-radius: 26% 26% 32% 32%; } .pkt-avatar--head-square .pkt-avatar__head { border-radius: 26% 26% 32% 32%; }
.pkt-avatar__hair { left: 18%; top: 10%; width: 64%; height: 27%; background: var(--avatar-hair); border-radius: 52% 52% 26% 26%; } .pkt-avatar__cheek { top: 54%; width: 11%; height: 6%; border-radius: 999px; background: rgba(194, 109, 97, 0.16); filter: blur(calc(var(--avatar-size) * 0.018)); }
.pkt-avatar__cheek--left { left: 29%; }
.pkt-avatar__cheek--right { right: 29%; }
.pkt-avatar__hair { left: 17%; top: 8%; width: 66%; height: 30%; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-hair) 92%, white 8%), color-mix(in srgb, var(--avatar-hair) 72%, black 28%)); border-radius: 52% 52% 26% 26%; box-shadow: inset 0 calc(var(--avatar-size) * -0.015) calc(var(--avatar-size) * 0.04) rgba(255,255,255,0.14); }
.pkt-avatar__hair::before, .pkt-avatar__hair::before,
.pkt-avatar__hair::after { content: ""; position: absolute; background: var(--avatar-hair); } .pkt-avatar__hair::after { content: ""; position: absolute; background: var(--avatar-hair); }
.pkt-avatar--hair-short .pkt-avatar__hair::before { left: 10%; right: 10%; top: 52%; height: 36%; border-radius: 0 0 40% 40%; } .pkt-avatar--hair-short .pkt-avatar__hair::before { left: 10%; right: 10%; top: 52%; height: 36%; border-radius: 0 0 40% 40%; }
.pkt-avatar--hair-fade .pkt-avatar__hair::before { left: 12%; right: 12%; top: 48%; height: 28%; border-radius: 0 0 38% 38%; }
.pkt-avatar--hair-fade .pkt-avatar__hair::after { left: 6%; right: 6%; top: 66%; height: 12%; border-radius: 0 0 50% 50%; opacity: .72; }
.pkt-avatar--hair-parted .pkt-avatar__hair::before { left: 8%; width: 36%; top: 54%; height: 38%; border-radius: 0 0 46% 42%; } .pkt-avatar--hair-parted .pkt-avatar__hair::before { left: 8%; width: 36%; top: 54%; height: 38%; border-radius: 0 0 46% 42%; }
.pkt-avatar--hair-parted .pkt-avatar__hair::after { right: 8%; width: 28%; top: 44%; height: 44%; border-radius: 0 0 40% 40%; } .pkt-avatar--hair-parted .pkt-avatar__hair::after { right: 8%; width: 28%; top: 44%; height: 44%; border-radius: 0 0 40% 40%; }
.pkt-avatar--hair-curly .pkt-avatar__hair { height: 30%; border-radius: 52% 52% 38% 38%; } .pkt-avatar--hair-curly .pkt-avatar__hair { height: 30%; border-radius: 52% 52% 38% 38%; }
@@ -154,21 +166,31 @@ body {
.pkt-avatar--hair-curly .pkt-avatar__hair::after { top: 56%; width: 26%; height: 44%; border-radius: 50%; } .pkt-avatar--hair-curly .pkt-avatar__hair::after { top: 56%; width: 26%; height: 44%; border-radius: 50%; }
.pkt-avatar--hair-curly .pkt-avatar__hair::before { left: 8%; } .pkt-avatar--hair-curly .pkt-avatar__hair::before { left: 8%; }
.pkt-avatar--hair-curly .pkt-avatar__hair::after { right: 8%; } .pkt-avatar--hair-curly .pkt-avatar__hair::after { right: 8%; }
.pkt-avatar--hair-waves .pkt-avatar__hair::before { left: 7%; width: 34%; top: 48%; height: 42%; border-radius: 48% 42% 38% 42%; }
.pkt-avatar--hair-waves .pkt-avatar__hair::after { right: 5%; width: 38%; top: 44%; height: 46%; border-radius: 46% 46% 40% 44%; }
.pkt-avatar--hair-bald .pkt-avatar__hair { display: none; } .pkt-avatar--hair-bald .pkt-avatar__hair { display: none; }
.pkt-avatar__eye { top: 42%; width: 10%; height: 10%; border-radius: 999px; background: var(--avatar-eye); box-shadow: inset 0 0 0 3px rgba(255,255,255,0.65); } .pkt-avatar__hat { left: 11%; top: 7%; width: 78%; height: 26%; display: none; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-hat) 86%, white 14%), color-mix(in srgb, var(--avatar-hat) 70%, black 30%)); box-shadow: inset 0 calc(var(--avatar-size) * -0.015) calc(var(--avatar-size) * 0.035) rgba(255,255,255,0.14); }
.pkt-avatar--hat-beanie .pkt-avatar__hat { display: block; border-radius: 48% 48% 22% 22%; }
.pkt-avatar--hat-beanie .pkt-avatar__hat::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 32%; border-radius: 12px; background: color-mix(in srgb, var(--avatar-hat) 88%, black 12%); }
.pkt-avatar--hat-cap .pkt-avatar__hat { display: block; top: 9%; height: 22%; border-radius: 52% 52% 18% 18%; }
.pkt-avatar--hat-cap .pkt-avatar__hat::before { content: ""; position: absolute; left: 42%; top: 68%; width: 42%; height: 24%; border-radius: 0 999px 999px 999px; background: color-mix(in srgb, var(--avatar-hat) 82%, black 18%); transform: skewX(-18deg); }
.pkt-avatar__eye { top: 44%; width: 9%; height: 9%; border-radius: 999px; background: radial-gradient(circle at 42% 42%, rgba(255,255,255,0.85) 0 18%, var(--avatar-eye) 22% 100%); box-shadow: 0 calc(var(--avatar-size) * 0.01) 0 rgba(39,27,19,0.14); }
.pkt-avatar__eye--left { left: 35%; } .pkt-avatar__eye--left { left: 35%; }
.pkt-avatar__eye--right { right: 35%; } .pkt-avatar__eye--right { right: 35%; }
.pkt-avatar__nose { left: 47%; top: 48%; width: 6%; height: 12%; border-radius: 999px; background: rgba(126, 85, 54, 0.18); } .pkt-avatar__brow { top: 38%; width: 14%; height: 4%; background: color-mix(in srgb, var(--avatar-hair) 85%, black 15%); border-radius: 999px; opacity: .82; }
.pkt-avatar__mouth { left: 40%; top: 60%; width: 20%; height: 8%; border-bottom: 2px solid rgba(103, 66, 43, 0.55); border-radius: 0 0 999px 999px; } .pkt-avatar__brow--left { left: 32%; transform: rotate(-8deg); }
.pkt-avatar__beard { left: 27%; top: 56%; width: 46%; background: var(--avatar-hair); display: none; } .pkt-avatar__brow--right { right: 32%; transform: rotate(8deg); }
.pkt-avatar__nose { left: 47%; top: 49%; width: 6%; height: 12%; border-radius: 999px; background: linear-gradient(180deg, rgba(126, 85, 54, 0.08), rgba(126, 85, 54, 0.26)); }
.pkt-avatar__mouth { left: 39%; top: 62%; width: 22%; height: 8%; border-bottom: 2px solid rgba(103, 66, 43, 0.56); border-radius: 0 0 999px 999px; }
.pkt-avatar__beard { left: 26%; top: 55%; width: 48%; background: linear-gradient(180deg, color-mix(in srgb, var(--avatar-hair) 88%, white 12%), color-mix(in srgb, var(--avatar-hair) 70%, black 30%)); display: none; }
.pkt-avatar--beard-mustache .pkt-avatar__beard { display: block; height: 7%; top: 57%; border-radius: 999px 999px 18px 18px; } .pkt-avatar--beard-mustache .pkt-avatar__beard { display: block; height: 7%; top: 57%; border-radius: 999px 999px 18px 18px; }
.pkt-avatar--beard-short .pkt-avatar__beard { display: block; height: 18%; top: 56%; border-radius: 14px 14px 38% 38%; } .pkt-avatar--beard-short .pkt-avatar__beard { display: block; height: 19%; top: 56%; border-radius: 16px 16px 40% 40%; }
.pkt-avatar--beard-full .pkt-avatar__beard { display: block; height: 24%; top: 54%; border-radius: 16px 16px 42% 42%; } .pkt-avatar--beard-full .pkt-avatar__beard { display: block; height: 25%; top: 54%; border-radius: 18px 18px 44% 44%; }
.pkt-avatar__glasses { inset: 0; display: none; } .pkt-avatar__glasses { inset: 0; display: none; }
.pkt-avatar__glasses::before, .pkt-avatar__glasses::before,
.pkt-avatar__glasses::after { content: ""; position: absolute; top: 40%; width: 18%; height: 12%; border: 2px solid rgba(53, 53, 53, 0.75); background: rgba(255,255,255,0.18); } .pkt-avatar__glasses::after { content: ""; position: absolute; top: 40%; width: 18%; height: 12%; border: 2px solid rgba(53, 53, 53, 0.78); background: rgba(255,255,255,0.18); }
.pkt-avatar__glasses::before { left: 28%; border-radius: 999px; } .pkt-avatar__glasses::before { left: 28%; border-radius: 999px; }
.pkt-avatar__glasses::after { right: 28%; border-radius: 999px; box-shadow: -24px 0 0 -22px rgba(53,53,53,0.75); } .pkt-avatar__glasses::after { right: 28%; border-radius: 999px; box-shadow: calc(var(--avatar-size) * -0.145) 0 0 calc(var(--avatar-size) * -0.132) rgba(53,53,53,0.78); }
.pkt-avatar--glasses-round .pkt-avatar__glasses, .pkt-avatar--glasses-round .pkt-avatar__glasses,
.pkt-avatar--glasses-square .pkt-avatar__glasses { display: block; } .pkt-avatar--glasses-square .pkt-avatar__glasses { display: block; }
.pkt-avatar--glasses-square .pkt-avatar__glasses::before, .pkt-avatar--glasses-square .pkt-avatar__glasses::before,

View File

@@ -731,6 +731,7 @@ document.addEventListener('DOMContentLoaded', () => {
'pkt-avatar--hair-', 'pkt-avatar--hair-',
'pkt-avatar--beard-', 'pkt-avatar--beard-',
'pkt-avatar--glasses-', 'pkt-avatar--glasses-',
'pkt-avatar--hat-',
]; ];
const updateAvatarPreview = (form) => { const updateAvatarPreview = (form) => {
@@ -758,9 +759,12 @@ document.addEventListener('DOMContentLoaded', () => {
preview.classList.add(`pkt-avatar--hair-${readValue('avatar_hair_style')}`); preview.classList.add(`pkt-avatar--hair-${readValue('avatar_hair_style')}`);
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.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'));
preview.style.setProperty('--avatar-hat', readColor('avatar_hat_color'));
preview.style.setProperty('--avatar-outfit', readColor('avatar_outfit_color'));
}; };
document.querySelectorAll('[data-avatar-builder]').forEach((form) => { document.querySelectorAll('[data-avatar-builder]').forEach((form) => {

View File

@@ -37,6 +37,9 @@ CREATE TABLE user_profiles (
avatar_beard_style VARCHAR(24) NOT NULL DEFAULT 'none', avatar_beard_style VARCHAR(24) NOT NULL DEFAULT 'none',
avatar_glasses_style VARCHAR(24) NOT NULL DEFAULT 'none', avatar_glasses_style VARCHAR(24) NOT NULL DEFAULT 'none',
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_color VARCHAR(24) NOT NULL DEFAULT 'navy',
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,
profession VARBINARY(512) NULL, profession VARBINARY(512) NULL,

View File

@@ -345,11 +345,14 @@ final class AccountPages
'avatar_beard_style' => 'none', 'avatar_beard_style' => 'none',
'avatar_glasses_style' => 'none', 'avatar_glasses_style' => 'none',
'avatar_head_shape' => 'round', 'avatar_head_shape' => 'round',
'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy',
'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_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 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_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?->execute(['id' => $userId]); $stmt?->execute(['id' => $userId]);
$row = $stmt?->fetch(\PDO::FETCH_ASSOC); $row = $stmt?->fetch(\PDO::FETCH_ASSOC);
if ($row) { if ($row) {

View File

@@ -15,6 +15,9 @@ final class Avatar
'avatar_beard_style' => 'none', 'avatar_beard_style' => 'none',
'avatar_glasses_style' => 'none', 'avatar_glasses_style' => 'none',
'avatar_head_shape' => 'round', 'avatar_head_shape' => 'round',
'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy',
'avatar_outfit_color' => 'slate',
]; ];
} }
@@ -30,8 +33,10 @@ final class Avatar
], ],
'avatar_hair_style' => [ 'avatar_hair_style' => [
'short' => 'Kurz', 'short' => 'Kurz',
'fade' => 'Fade',
'parted' => 'Seitlich', 'parted' => 'Seitlich',
'curly' => 'Lockig', 'curly' => 'Lockig',
'waves' => 'Wellen',
'bald' => 'Kahl', 'bald' => 'Kahl',
], ],
'avatar_hair_color' => [ 'avatar_hair_color' => [
@@ -63,6 +68,25 @@ final class Avatar
'oval' => 'Oval', 'oval' => 'Oval',
'square' => 'Eckig', 'square' => 'Eckig',
], ],
'avatar_hat_style' => [
'none' => 'Ohne',
'beanie' => 'Beanie',
'cap' => 'Cap',
],
'avatar_hat_color' => [
'navy' => ['label' => 'Navy', 'color' => '#38475f'],
'charcoal' => ['label' => 'Anthrazit', 'color' => '#3c3f45'],
'forest' => ['label' => 'Waldgrün', 'color' => '#48614d'],
'brick' => ['label' => 'Ziegel', 'color' => '#975244'],
'sand' => ['label' => 'Sand', 'color' => '#b79e79'],
],
'avatar_outfit_color' => [
'slate' => ['label' => 'Schiefer', 'color' => '#5f7387'],
'graphite' => ['label' => 'Graphit', 'color' => '#4c515a'],
'olive' => ['label' => 'Oliv', 'color' => '#667155'],
'rust' => ['label' => 'Rost', 'color' => '#9f6547'],
'ocean' => ['label' => 'Ozean', 'color' => '#4e7897'],
],
]; ];
} }
@@ -100,6 +124,8 @@ final class Avatar
$skin = $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color']; $skin = $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color'];
$hair = $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color']; $hair = $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color'];
$eyes = $options['avatar_eye_color'][$avatar['avatar_eye_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',
@@ -108,13 +134,16 @@ final class Avatar
'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'],
]; ];
$style = sprintf( $style = sprintf(
'--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;', '--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;--avatar-hat:%s;--avatar-outfit:%s;',
$skin, $skin,
$hair, $hair,
$eyes $eyes,
$hat,
$outfit
); );
$label = htmlspecialchars('Avatar von ' . $name, ENT_QUOTES); $label = htmlspecialchars('Avatar von ' . $name, ENT_QUOTES);
@@ -122,13 +151,19 @@ final class Avatar
return sprintf( return sprintf(
'<span class="%s" style="%s" role="img" aria-label="%s">' . '<span class="%s" style="%s" role="img" aria-label="%s">' .
'<span class="pkt-avatar__bg"></span>' . '<span class="pkt-avatar__bg"></span>' .
'<span class="pkt-avatar__shirt"></span>' . '<span class="pkt-avatar__shadow"></span>' .
'<span class="pkt-avatar__hoodie"></span>' .
'<span class="pkt-avatar__neck"></span>' . '<span class="pkt-avatar__neck"></span>' .
'<span class="pkt-avatar__ears"></span>' . '<span class="pkt-avatar__ears"></span>' .
'<span class="pkt-avatar__head"></span>' . '<span class="pkt-avatar__head"></span>' .
'<span class="pkt-avatar__cheek pkt-avatar__cheek--left"></span>' .
'<span class="pkt-avatar__cheek pkt-avatar__cheek--right"></span>' .
'<span class="pkt-avatar__hair"></span>' . '<span class="pkt-avatar__hair"></span>' .
'<span class="pkt-avatar__hat"></span>' .
'<span class="pkt-avatar__eye pkt-avatar__eye--left"></span>' . '<span class="pkt-avatar__eye pkt-avatar__eye--left"></span>' .
'<span class="pkt-avatar__eye pkt-avatar__eye--right"></span>' . '<span class="pkt-avatar__eye pkt-avatar__eye--right"></span>' .
'<span class="pkt-avatar__brow pkt-avatar__brow--left"></span>' .
'<span class="pkt-avatar__brow pkt-avatar__brow--right"></span>' .
'<span class="pkt-avatar__nose"></span>' . '<span class="pkt-avatar__nose"></span>' .
'<span class="pkt-avatar__mouth"></span>' . '<span class="pkt-avatar__mouth"></span>' .
'<span class="pkt-avatar__beard"></span>' . '<span class="pkt-avatar__beard"></span>' .

View File

@@ -150,7 +150,7 @@ final class Community
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : ''; $where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
$avatarSelect = $this->hasColumn('user_profiles', 'avatar_skin_tone') $avatarSelect = $this->hasColumn('user_profiles', 'avatar_outfit_color')
? ', ' . Avatar::selectColumns('p') ? ', ' . Avatar::selectColumns('p')
: ''; : '';
$sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at, $sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at,
@@ -206,7 +206,7 @@ final class Community
public function getThread(int $id): ?array public function getThread(int $id): ?array
{ {
$select = 'ft.*, p.display_name'; $select = 'ft.*, p.display_name';
if ($this->hasColumn('user_profiles', 'avatar_skin_tone')) { if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) {
$select .= ', ' . Avatar::selectColumns('p'); $select .= ', ' . Avatar::selectColumns('p');
} }
$join = ''; $join = '';
@@ -226,7 +226,7 @@ final class Community
public function listPosts(int $threadId): array public function listPosts(int $threadId): array
{ {
$select = 'fp.*, p.display_name'; $select = 'fp.*, p.display_name';
if ($this->hasColumn('user_profiles', 'avatar_skin_tone')) { if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) {
$select .= ', ' . Avatar::selectColumns('p'); $select .= ', ' . Avatar::selectColumns('p');
} }
if ($this->hasColumn('forum_posts', 'highlighted_at')) { if ($this->hasColumn('forum_posts', 'highlighted_at')) {

View File

@@ -36,6 +36,9 @@ final class ProfileSettings
'avatar_beard_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_beard_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_eye_color", 'avatar_beard_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_beard_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_eye_color",
'avatar_glasses_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_glasses_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_beard_style", 'avatar_glasses_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_glasses_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_beard_style",
'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_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy' AFTER avatar_hat_style",
'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) {
if (!$this->hasColumn('user_profiles', $column)) { if (!$this->hasColumn('user_profiles', $column)) {
@@ -92,6 +95,9 @@ final class ProfileSettings
avatar_beard_style = :beardStyle, avatar_beard_style = :beardStyle,
avatar_glasses_style = :glassesStyle, avatar_glasses_style = :glassesStyle,
avatar_head_shape = :headShape, avatar_head_shape = :headShape,
avatar_hat_style = :hatStyle,
avatar_hat_color = :hatColor,
avatar_outfit_color = :outfitColor,
updated_at = NOW() updated_at = NOW()
WHERE user_id = :id' WHERE user_id = :id'
); );
@@ -103,6 +109,9 @@ final class ProfileSettings
'beardStyle' => $avatar['avatar_beard_style'], 'beardStyle' => $avatar['avatar_beard_style'],
'glassesStyle' => $avatar['avatar_glasses_style'], 'glassesStyle' => $avatar['avatar_glasses_style'],
'headShape' => $avatar['avatar_head_shape'], 'headShape' => $avatar['avatar_head_shape'],
'hatStyle' => $avatar['avatar_hat_style'],
'hatColor' => $avatar['avatar_hat_color'],
'outfitColor' => $avatar['avatar_outfit_color'],
'id' => $userId, 'id' => $userId,
]); ]);
} }