From 32985d5785db020fcc7f5e9d7e3b46a9be0b2ee3 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 27 Jul 2026 01:50:38 +0200 Subject: [PATCH] avatar 2 --- partials/landing/account/dashboard.php | 64 +++++++++++++++++ partials/structure/nav.php | 2 +- public/assets/js/app.js | 19 ++++- public/page/api/avatar.php | 14 +++- schema.sql | 18 ++--- src/App/AccountPages.php | 22 +++--- src/App/Avatar/Lorelei.php | 96 +++++++++++++++++++++----- src/App/Community.php | 6 +- src/App/ProfileSettings.php | 54 ++++++--------- 9 files changed, 216 insertions(+), 79 deletions(-) diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index adff6fb..a9d0b3a 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -329,6 +329,70 @@ $sectionLinks = [ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
diff --git a/partials/structure/nav.php b/partials/structure/nav.php index c57b68f..1b26aa1 100755 --- a/partials/structure/nav.php +++ b/partials/structure/nav.php @@ -11,7 +11,7 @@ if ($isLoggedIn) { if ($pdo) { $profileSettings = new \App\ProfileSettings($pdo); $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_style, avatar_outfit_color FROM user_profiles WHERE user_id = :id LIMIT 1'); + $stmt = $pdo->prepare('SELECT display_name, first_name, avatar_preset, avatar_lorelei_eyes_variant, avatar_lorelei_eyebrows_variant, avatar_lorelei_mouth_variant, avatar_lorelei_glasses_variant, avatar_lorelei_hair_variant, avatar_lorelei_beard_variant, avatar_lorelei_earrings_variant FROM user_profiles WHERE user_id = :id LIMIT 1'); $stmt->execute(['id' => (int)$_SESSION['user_id']]); $profileRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; $displayName = trim((string)($profileRow['display_name'] ?? '')) ?: trim((string)($profileRow['first_name'] ?? '')) ?: 'Profil'; diff --git a/public/assets/js/app.js b/public/assets/js/app.js index 7da9edb..82c7d60 100755 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -740,7 +740,24 @@ document.addEventListener('DOMContentLoaded', () => { if (!image) return; const checked = form.querySelector('[data-avatar-field="avatar_preset"]:checked'); - const url = checked?.getAttribute('data-avatar-url') || ''; + const seed = checked?.value || ''; + if (!seed) return; + + const params = new URLSearchParams({ seed }); + [ + ['avatar_lorelei_eyes_variant', 'eyesVariant'], + ['avatar_lorelei_eyebrows_variant', 'eyebrowsVariant'], + ['avatar_lorelei_mouth_variant', 'mouthVariant'], + ['avatar_lorelei_glasses_variant', 'glassesVariant'], + ['avatar_lorelei_hair_variant', 'hairVariant'], + ['avatar_lorelei_beard_variant', 'beardVariant'], + ['avatar_lorelei_earrings_variant', 'earringsVariant'], + ].forEach(([field, param]) => { + const value = readValue(field); + if (value) params.set(param, value); + }); + + const url = `/api/avatar?${params.toString()}`; if (!url) return; image.src = url; }; diff --git a/public/page/api/avatar.php b/public/page/api/avatar.php index 5ee3339..dce0eb2 100644 --- a/public/page/api/avatar.php +++ b/public/page/api/avatar.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Avatar\Lorelei; +$allowedParams = ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant']; $seed = Lorelei::normalizeSeed((string)($_GET['seed'] ?? 'papa-kind-treff')); $cacheDir = dirname(__DIR__, 3) . '/assets/avatars/dicebear-cache'; @@ -10,11 +11,20 @@ if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0775, true); } -$cacheFile = $cacheDir . '/' . $seed . '.svg'; +$query = ['seed' => $seed, 'size' => '256']; +foreach ($allowedParams as $param) { + $value = Lorelei::normalizeVariant((string)($_GET[$param] ?? '')); + if ($value !== '') { + $query[$param] = $value; + } +} + +$cacheKey = sha1(json_encode($query, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: $seed); +$cacheFile = $cacheDir . '/' . $cacheKey . '.svg'; $svg = is_file($cacheFile) ? file_get_contents($cacheFile) : false; if ($svg === false) { - $url = 'https://api.dicebear.com/10.x/lorelei/svg?seed=' . rawurlencode($seed) . '&size=256'; + $url = 'https://api.dicebear.com/10.x/lorelei/svg?' . http_build_query($query); $context = stream_context_create([ 'http' => [ 'method' => 'GET', diff --git a/schema.sql b/schema.sql index 57065bd..b1fafe0 100755 --- a/schema.sql +++ b/schema.sql @@ -31,17 +31,13 @@ CREATE TABLE user_profiles ( lng DECIMAL(10,7) NULL, location_tracking_preference ENUM('disabled','prompt','enabled') NOT NULL DEFAULT 'prompt', avatar_preset VARCHAR(64) NOT NULL DEFAULT 'papa-kind-treff', - avatar_skin_tone VARCHAR(24) NOT NULL DEFAULT 'warm', - avatar_hair_style VARCHAR(24) NOT NULL DEFAULT 'short', - avatar_hair_color VARCHAR(24) NOT NULL DEFAULT 'brown', - avatar_eye_color VARCHAR(24) NOT NULL DEFAULT 'brown', - avatar_beard_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_hat_style VARCHAR(24) NOT NULL DEFAULT 'none', - 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_lorelei_eyes_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_eyebrows_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_mouth_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_glasses_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_hair_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_beard_variant VARCHAR(24) NOT NULL DEFAULT '', + avatar_lorelei_earrings_variant VARCHAR(24) NOT NULL DEFAULT '', contact_phone VARBINARY(512) NULL, contact_email VARBINARY(512) NULL, profession VARBINARY(512) NULL, diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index d4f6591..68f3dd4 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -341,22 +341,18 @@ final class AccountPages 'contact_phone' => '', 'location_tracking_preference' => 'prompt', 'avatar_preset' => 'papa-kind-treff', - 'avatar_skin_tone' => 'warm', - 'avatar_hair_style' => 'short', - 'avatar_hair_color' => 'brown', - 'avatar_eye_color' => 'brown', - 'avatar_beard_style' => 'none', - 'avatar_glasses_style' => 'none', - 'avatar_head_shape' => 'round', - 'avatar_hat_style' => 'none', - 'avatar_hat_color' => 'navy', - 'avatar_outfit_style' => 'tee', - 'avatar_outfit_color' => 'slate', + 'avatar_lorelei_eyes_variant' => '', + 'avatar_lorelei_eyebrows_variant' => '', + 'avatar_lorelei_mouth_variant' => '', + 'avatar_lorelei_glasses_variant' => '', + 'avatar_lorelei_hair_variant' => '', + 'avatar_lorelei_beard_variant' => '', + 'avatar_lorelei_earrings_variant' => '', ]; if ($profileSettings) { $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_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 = $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_lorelei_eyes_variant, p.avatar_lorelei_eyebrows_variant, p.avatar_lorelei_mouth_variant, p.avatar_lorelei_glasses_variant, p.avatar_lorelei_hair_variant, p.avatar_lorelei_beard_variant, p.avatar_lorelei_earrings_variant FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1'); $stmt?->execute(['id' => $userId]); $row = $stmt?->fetch(\PDO::FETCH_ASSOC); if ($row) { @@ -422,6 +418,7 @@ final class AccountPages if (!in_array($section, $allowedSections, true)) { $section = 'profile'; } + $avatarOptions = Lorelei::options(); $avatarPresetOptions = Lorelei::seedChoices((string)$profile['avatar_preset']); return compact( @@ -439,6 +436,7 @@ final class AccountPages 'communityApplication', 'communityCanApply', 'communityRestrictions', + 'avatarOptions', 'avatarPresetOptions', 'section', 'allowedSections' diff --git a/src/App/Avatar/Lorelei.php b/src/App/Avatar/Lorelei.php index f1516bd..d1dce1d 100644 --- a/src/App/Avatar/Lorelei.php +++ b/src/App/Avatar/Lorelei.php @@ -9,23 +9,30 @@ final class Lorelei { return [ 'avatar_preset' => 'papa-kind-treff', - 'avatar_skin_tone' => 'warm', - 'avatar_hair_style' => 'short', - 'avatar_hair_color' => 'brown', - 'avatar_eye_color' => 'brown', - 'avatar_beard_style' => 'none', - 'avatar_glasses_style' => 'none', - 'avatar_head_shape' => 'round', - 'avatar_hat_style' => 'none', - 'avatar_hat_color' => 'navy', - 'avatar_outfit_style' => 'tee', - 'avatar_outfit_color' => 'slate', + 'avatar_lorelei_eyes_variant' => '', + 'avatar_lorelei_eyebrows_variant' => '', + 'avatar_lorelei_mouth_variant' => '', + 'avatar_lorelei_glasses_variant' => '', + 'avatar_lorelei_hair_variant' => '', + 'avatar_lorelei_beard_variant' => '', + 'avatar_lorelei_earrings_variant' => '', ]; } public static function options(): array { - return []; + $variants24 = self::variantOptions(24); + $optional24 = ['' => 'Automatisch'] + $variants24; + + return [ + 'avatar_lorelei_eyes_variant' => $variants24, + 'avatar_lorelei_eyebrows_variant' => $variants24, + 'avatar_lorelei_mouth_variant' => $variants24, + 'avatar_lorelei_glasses_variant' => $optional24, + 'avatar_lorelei_hair_variant' => $optional24, + 'avatar_lorelei_beard_variant' => $optional24, + 'avatar_lorelei_earrings_variant' => $optional24, + ]; } public static function selectColumns(string $alias = 'p'): string @@ -44,9 +51,17 @@ final class Lorelei foreach ($defaults as $key => $defaultValue) { $value = (string)($input[$key] ?? $defaultValue); - $normalized[$key] = $key === 'avatar_preset' - ? self::normalizeSeed($value) - : $value; + if ($key === 'avatar_preset') { + $normalized[$key] = self::normalizeSeed($value); + continue; + } + + if (str_starts_with($key, 'avatar_lorelei_')) { + $normalized[$key] = self::normalizeVariant($value); + continue; + } + + $normalized[$key] = $value; } return $normalized; @@ -119,10 +134,38 @@ final class Lorelei return '/api/avatar?seed=' . rawurlencode(self::normalizeSeed($seed)); } + public static function apiQuery(array $row): array + { + $avatar = self::normalize($row); + $query = ['seed' => $avatar['avatar_preset']]; + $map = [ + 'avatar_lorelei_eyes_variant' => 'eyesVariant', + 'avatar_lorelei_eyebrows_variant' => 'eyebrowsVariant', + 'avatar_lorelei_mouth_variant' => 'mouthVariant', + 'avatar_lorelei_glasses_variant' => 'glassesVariant', + 'avatar_lorelei_hair_variant' => 'hairVariant', + 'avatar_lorelei_beard_variant' => 'beardVariant', + 'avatar_lorelei_earrings_variant' => 'earringsVariant', + ]; + + foreach ($map as $key => $param) { + if ($avatar[$key] !== '') { + $query[$param] = $avatar[$key]; + } + } + + return $query; + } + + public static function buildProxyUrl(array $row): string + { + return '/api/avatar?' . http_build_query(self::apiQuery($row)); + } + public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string { $avatar = self::normalize($row); - $src = self::dicebearUrl($avatar['avatar_preset']); + $src = self::buildProxyUrl($avatar); return sprintf( '', @@ -153,4 +196,25 @@ final class Lorelei htmlspecialchars($initials, ENT_QUOTES) ); } + + private static function variantOptions(int $count): array + { + $options = []; + for ($index = 1; $index <= $count; $index++) { + $key = sprintf('variant%02d', $index); + $options[$key] = 'Variante ' . $index; + } + + return $options; + } + + public static function normalizeVariant(string $value): string + { + $value = strtolower(trim($value)); + if ($value === '') { + return ''; + } + + return preg_match('/^variant\d{2}$/', $value) === 1 ? $value : ''; + } } diff --git a/src/App/Community.php b/src/App/Community.php index ab3a995..d73a14f 100755 --- a/src/App/Community.php +++ b/src/App/Community.php @@ -152,7 +152,7 @@ final class Community $where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : ''; - $avatarSelect = $this->hasColumn('user_profiles', 'avatar_outfit_color') + $avatarSelect = $this->hasColumn('user_profiles', 'avatar_preset') ? ', ' . Lorelei::selectColumns('p') : ''; $sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at, @@ -208,7 +208,7 @@ final class Community public function getThread(int $id): ?array { $select = 'ft.*, p.display_name'; - if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) { + if ($this->hasColumn('user_profiles', 'avatar_preset')) { $select .= ', ' . Lorelei::selectColumns('p'); } $join = ''; @@ -228,7 +228,7 @@ final class Community public function listPosts(int $threadId): array { $select = 'fp.*, p.display_name'; - if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) { + if ($this->hasColumn('user_profiles', 'avatar_preset')) { $select .= ', ' . Lorelei::selectColumns('p'); } if ($this->hasColumn('forum_posts', 'highlighted_at')) { diff --git a/src/App/ProfileSettings.php b/src/App/ProfileSettings.php index b77846c..b907279 100644 --- a/src/App/ProfileSettings.php +++ b/src/App/ProfileSettings.php @@ -32,17 +32,13 @@ final class ProfileSettings $avatarColumns = [ 'avatar_preset' => "ALTER TABLE user_profiles ADD COLUMN avatar_preset VARCHAR(64) NOT NULL DEFAULT 'papa-kind-treff' AFTER location_tracking_preference", - 'avatar_skin_tone' => "ALTER TABLE user_profiles ADD COLUMN avatar_skin_tone VARCHAR(24) NOT NULL DEFAULT 'warm' AFTER location_tracking_preference", - 'avatar_hair_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_hair_style VARCHAR(24) NOT NULL DEFAULT 'short' AFTER avatar_skin_tone", - 'avatar_hair_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_hair_color VARCHAR(24) NOT NULL DEFAULT 'brown' AFTER avatar_hair_style", - 'avatar_eye_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_eye_color VARCHAR(24) NOT NULL DEFAULT 'brown' AFTER avatar_hair_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_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_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_lorelei_eyes_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_eyes_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_preset", + 'avatar_lorelei_eyebrows_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_eyebrows_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_eyes_variant", + 'avatar_lorelei_mouth_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_mouth_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_eyebrows_variant", + 'avatar_lorelei_glasses_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_glasses_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_mouth_variant", + 'avatar_lorelei_hair_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_hair_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_glasses_variant", + 'avatar_lorelei_beard_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_beard_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_hair_variant", + 'avatar_lorelei_earrings_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_earrings_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_beard_variant", ]; foreach ($avatarColumns as $column => $sql) { if (!$this->hasColumn('user_profiles', $column)) { @@ -93,33 +89,25 @@ final class ProfileSettings $stmt = $this->pdo->prepare( 'UPDATE user_profiles SET avatar_preset = :preset, - avatar_skin_tone = :skin, - avatar_hair_style = :hairStyle, - avatar_hair_color = :hairColor, - avatar_eye_color = :eyeColor, - avatar_beard_style = :beardStyle, - avatar_glasses_style = :glassesStyle, - avatar_head_shape = :headShape, - avatar_hat_style = :hatStyle, - avatar_hat_color = :hatColor, - avatar_outfit_style = :outfitStyle, - avatar_outfit_color = :outfitColor, + avatar_lorelei_eyes_variant = :eyesVariant, + avatar_lorelei_eyebrows_variant = :eyebrowsVariant, + avatar_lorelei_mouth_variant = :mouthVariant, + avatar_lorelei_glasses_variant = :glassesVariant, + avatar_lorelei_hair_variant = :hairVariant, + avatar_lorelei_beard_variant = :beardVariant, + avatar_lorelei_earrings_variant = :earringsVariant, updated_at = NOW() WHERE user_id = :id' ); $stmt->execute([ 'preset' => $avatar['avatar_preset'], - 'skin' => $avatar['avatar_skin_tone'], - 'hairStyle' => $avatar['avatar_hair_style'], - 'hairColor' => $avatar['avatar_hair_color'], - 'eyeColor' => $avatar['avatar_eye_color'], - 'beardStyle' => $avatar['avatar_beard_style'], - 'glassesStyle' => $avatar['avatar_glasses_style'], - 'headShape' => $avatar['avatar_head_shape'], - 'hatStyle' => $avatar['avatar_hat_style'], - 'hatColor' => $avatar['avatar_hat_color'], - 'outfitStyle' => $avatar['avatar_outfit_style'], - 'outfitColor' => $avatar['avatar_outfit_color'], + 'eyesVariant' => $avatar['avatar_lorelei_eyes_variant'], + 'eyebrowsVariant' => $avatar['avatar_lorelei_eyebrows_variant'], + 'mouthVariant' => $avatar['avatar_lorelei_mouth_variant'], + 'glassesVariant' => $avatar['avatar_lorelei_glasses_variant'], + 'hairVariant' => $avatar['avatar_lorelei_hair_variant'], + 'beardVariant' => $avatar['avatar_lorelei_beard_variant'], + 'earringsVariant' => $avatar['avatar_lorelei_earrings_variant'], 'id' => $userId, ]); }