This commit is contained in:
@@ -190,6 +190,11 @@ final class AccountPages
|
||||
'id' => $userId,
|
||||
]);
|
||||
$info = 'Profil gespeichert.';
|
||||
} elseif ($action === 'avatar_update') {
|
||||
if ($profileSettings) {
|
||||
$profileSettings->updateAvatar($userId, $_POST);
|
||||
}
|
||||
$info = 'Profilbild gespeichert.';
|
||||
} elseif ($action === 'settings_location') {
|
||||
$locationPreference = (string)($_POST['location_tracking_preference'] ?? 'prompt');
|
||||
if ($profileSettings) {
|
||||
@@ -333,11 +338,18 @@ final class AccountPages
|
||||
'email' => '',
|
||||
'contact_phone' => '',
|
||||
'location_tracking_preference' => 'prompt',
|
||||
'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',
|
||||
];
|
||||
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 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 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) {
|
||||
@@ -346,6 +358,7 @@ final class AccountPages
|
||||
$profile['contact_phone'] = $crypto->decrypt((string)$profile['contact_phone']) ?: '';
|
||||
}
|
||||
}
|
||||
$profile = array_merge($profile, Avatar::normalize($profile));
|
||||
|
||||
$children = [];
|
||||
$stmt = $pdo?->prepare('SELECT id, encrypted_first_name AS first_name, note, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC');
|
||||
@@ -402,6 +415,7 @@ final class AccountPages
|
||||
if (!in_array($section, $allowedSections, true)) {
|
||||
$section = 'profile';
|
||||
}
|
||||
$avatarOptions = Avatar::options();
|
||||
|
||||
return compact(
|
||||
'flash',
|
||||
@@ -418,6 +432,7 @@ final class AccountPages
|
||||
'communityApplication',
|
||||
'communityCanApply',
|
||||
'communityRestrictions',
|
||||
'avatarOptions',
|
||||
'section',
|
||||
'allowedSections'
|
||||
);
|
||||
|
||||
142
src/App/Avatar.php
Normal file
142
src/App/Avatar.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class Avatar
|
||||
{
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return [
|
||||
'avatar_skin_tone' => [
|
||||
'fair' => ['label' => 'Hell', 'color' => '#f5d7c2'],
|
||||
'warm' => ['label' => 'Warm', 'color' => '#efc1a3'],
|
||||
'golden' => ['label' => 'Gold', 'color' => '#dca57f'],
|
||||
'deep' => ['label' => 'Tief', 'color' => '#a86d49'],
|
||||
'rich' => ['label' => 'Kräftig', 'color' => '#7d4d31'],
|
||||
],
|
||||
'avatar_hair_style' => [
|
||||
'short' => 'Kurz',
|
||||
'parted' => 'Seitlich',
|
||||
'curly' => 'Lockig',
|
||||
'bald' => 'Kahl',
|
||||
],
|
||||
'avatar_hair_color' => [
|
||||
'black' => ['label' => 'Schwarz', 'color' => '#1d1b1b'],
|
||||
'brown' => ['label' => 'Braun', 'color' => '#5b392b'],
|
||||
'blonde' => ['label' => 'Blond', 'color' => '#d7b15f'],
|
||||
'red' => ['label' => 'Rot', 'color' => '#9e4f35'],
|
||||
'gray' => ['label' => 'Grau', 'color' => '#767676'],
|
||||
],
|
||||
'avatar_eye_color' => [
|
||||
'brown' => ['label' => 'Braun', 'color' => '#5f4431'],
|
||||
'hazel' => ['label' => 'Hasel', 'color' => '#7f6845'],
|
||||
'green' => ['label' => 'Grün', 'color' => '#54704e'],
|
||||
'blue' => ['label' => 'Blau', 'color' => '#4d6e92'],
|
||||
],
|
||||
'avatar_beard_style' => [
|
||||
'none' => 'Ohne',
|
||||
'mustache' => 'Schnurrbart',
|
||||
'short' => 'Kurz',
|
||||
'full' => 'Vollbart',
|
||||
],
|
||||
'avatar_glasses_style' => [
|
||||
'none' => 'Ohne',
|
||||
'round' => 'Rund',
|
||||
'square' => 'Eckig',
|
||||
],
|
||||
'avatar_head_shape' => [
|
||||
'round' => 'Rund',
|
||||
'oval' => 'Oval',
|
||||
'square' => 'Eckig',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function selectColumns(string $alias = 'p'): string
|
||||
{
|
||||
$columns = array_keys(self::defaults());
|
||||
return implode(', ', array_map(
|
||||
static fn(string $column): string => sprintf('%s.%s', $alias, $column),
|
||||
$columns
|
||||
));
|
||||
}
|
||||
|
||||
public static function normalize(array $input): array
|
||||
{
|
||||
$defaults = self::defaults();
|
||||
$options = self::options();
|
||||
$normalized = [];
|
||||
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
$value = (string)($input[$key] ?? $defaultValue);
|
||||
if (!array_key_exists($value, $options[$key])) {
|
||||
$value = $defaultValue;
|
||||
}
|
||||
$normalized[$key] = $value;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string
|
||||
{
|
||||
$avatar = self::normalize($row);
|
||||
$options = self::options();
|
||||
|
||||
$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'];
|
||||
|
||||
$classes = [
|
||||
'pkt-avatar',
|
||||
'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
|
||||
'pkt-avatar--head-' . $avatar['avatar_head_shape'],
|
||||
'pkt-avatar--hair-' . $avatar['avatar_hair_style'],
|
||||
'pkt-avatar--beard-' . $avatar['avatar_beard_style'],
|
||||
'pkt-avatar--glasses-' . $avatar['avatar_glasses_style'],
|
||||
];
|
||||
|
||||
$style = sprintf(
|
||||
'--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;',
|
||||
$skin,
|
||||
$hair,
|
||||
$eyes
|
||||
);
|
||||
|
||||
$label = htmlspecialchars('Avatar von ' . $name, ENT_QUOTES);
|
||||
|
||||
return sprintf(
|
||||
'<span class="%s" style="%s" role="img" aria-label="%s">' .
|
||||
'<span class="pkt-avatar__bg"></span>' .
|
||||
'<span class="pkt-avatar__shirt"></span>' .
|
||||
'<span class="pkt-avatar__neck"></span>' .
|
||||
'<span class="pkt-avatar__ears"></span>' .
|
||||
'<span class="pkt-avatar__head"></span>' .
|
||||
'<span class="pkt-avatar__hair"></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__nose"></span>' .
|
||||
'<span class="pkt-avatar__mouth"></span>' .
|
||||
'<span class="pkt-avatar__beard"></span>' .
|
||||
'<span class="pkt-avatar__glasses"></span>' .
|
||||
'</span>',
|
||||
htmlspecialchars(implode(' ', $classes), ENT_QUOTES),
|
||||
htmlspecialchars($style, ENT_QUOTES),
|
||||
$label
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -150,9 +150,12 @@ final class Community
|
||||
|
||||
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
|
||||
|
||||
$avatarSelect = $this->hasColumn('user_profiles', 'avatar_skin_tone')
|
||||
? ', ' . Avatar::selectColumns('p')
|
||||
: '';
|
||||
$sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at,
|
||||
u.id as uid, u.created_at as user_created,
|
||||
p.display_name,
|
||||
p.display_name$avatarSelect,
|
||||
$boardSelect,
|
||||
(SELECT COUNT(*) FROM forum_posts fp WHERE fp.thread_id = ft.id) AS answers,
|
||||
COALESCE(
|
||||
@@ -203,6 +206,9 @@ final class Community
|
||||
public function getThread(int $id): ?array
|
||||
{
|
||||
$select = 'ft.*, p.display_name';
|
||||
if ($this->hasColumn('user_profiles', 'avatar_skin_tone')) {
|
||||
$select .= ', ' . Avatar::selectColumns('p');
|
||||
}
|
||||
$join = '';
|
||||
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
|
||||
$select .= ', fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
|
||||
@@ -220,6 +226,9 @@ final class Community
|
||||
public function listPosts(int $threadId): array
|
||||
{
|
||||
$select = 'fp.*, p.display_name';
|
||||
if ($this->hasColumn('user_profiles', 'avatar_skin_tone')) {
|
||||
$select .= ', ' . Avatar::selectColumns('p');
|
||||
}
|
||||
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
|
||||
$select .= ', hp.display_name AS highlighted_by_name';
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,22 @@ final class ProfileSettings
|
||||
$this->columnCache['user_profiles.location_tracking_preference'] = true;
|
||||
}
|
||||
|
||||
$avatarColumns = [
|
||||
'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",
|
||||
];
|
||||
foreach ($avatarColumns as $column => $sql) {
|
||||
if (!$this->hasColumn('user_profiles', $column)) {
|
||||
$this->pdo->exec($sql);
|
||||
$this->columnCache['user_profiles.' . $column] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->schemaEnsured = true;
|
||||
}
|
||||
|
||||
@@ -59,6 +75,38 @@ final class ProfileSettings
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateAvatar(int $userId, array $avatarConfig): void
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureSchema();
|
||||
$avatar = Avatar::normalize($avatarConfig);
|
||||
$stmt = $this->pdo->prepare(
|
||||
'UPDATE user_profiles
|
||||
SET 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,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = :id'
|
||||
);
|
||||
$stmt->execute([
|
||||
'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'],
|
||||
'id' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
$cacheKey = $table . '.' . $column;
|
||||
|
||||
Reference in New Issue
Block a user