avatar
All checks were successful
Deploy / deploy (push) Successful in 1m7s

This commit is contained in:
2026-07-28 02:03:01 +02:00
parent 0ac69fdfd5
commit aa86001760
18 changed files with 827 additions and 385 deletions

View File

@@ -3,6 +3,10 @@ $app = app();
$vm = \App\AccountPages::dashboard($app);
extract($vm, EXTR_OVERWRITE);
$avatarPlaceholder = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 12 12%22%3E%3Crect width=%2212%22 height=%2212%22 fill=%22%23f7efe6%22/%3E%3C/svg%3E';
$avatarStyleKeys = array_keys($avatarBuilder ?? []);
$currentAvatarStyle = (string)($profile['avatar_style'] ?? ($avatarStyleKeys[0] ?? 'lorelei'));
$currentAvatarComponents = $avatarBuilder[$currentAvatarStyle]['components'] ?? [];
$dashboardUserId = (int)($_SESSION['user_id'] ?? 0);
$editing = isset($editEvent) && $editEvent !== null;
$actionEvent = $editing ? 'event_update' : 'event_add';
$startVal = $editEvent ? date('Y-m-d\TH:i', strtotime((string)$editEvent['starts_at'])) : '';
@@ -64,11 +68,11 @@ $sectionLinks = [
<div class="account-panel__body">
<div class="profile-avatar-panel">
<div class="profile-avatar-panel__preview">
<?= \App\Avatar\Lorelei::render($profile, $profile['display_name'] ?: 'Papa', 'xl') ?>
<?= \App\Avatar\AvatarManager::render($profile, $profile['display_name'] ?: 'Papa', 'xl', $dashboardUserId) ?>
</div>
<div class="profile-avatar-panel__content">
<h3>Dein Profilbild</h3>
<p class="muted">Dein Profilbild wird jetzt mit DiceBear im Stil Lorelei erzeugt. Du wählst eine Variante aus und speicherst deren Seed dauerhaft in deinem Profil.</p>
<p class="muted">Dein Profilbild wird mit dem Avatar-Generator erzeugt und als feste SVG-Datei zu deinem Konto gespeichert. Stil, Seed und Varianten bleiben dabei in deinem Profil hinterlegt.</p>
<button class="btn" type="button" data-modal-open="modalAvatar">Profilbild gestalten</button>
</div>
</div>
@@ -298,69 +302,88 @@ $sectionLinks = [
</div>
<form method="post" class="stack gap-12 avatar-builder" data-avatar-builder style="margin-top:12px;">
<input type="hidden" name="action" value="avatar_update">
<input type="hidden" name="avatar_preset" value="<?= htmlspecialchars((string)$profile['avatar_preset'], ENT_QUOTES) ?>" data-avatar-field="avatar_preset">
<?php if (count($avatarStyleKeys) <= 1): ?>
<input type="hidden" name="avatar_style" value="<?= htmlspecialchars($currentAvatarStyle, ENT_QUOTES) ?>" data-avatar-style-select>
<?php endif; ?>
<input type="hidden" name="avatar_seed" value="<?= htmlspecialchars((string)$profile['avatar_seed'], ENT_QUOTES) ?>" data-avatar-seed>
<div class="avatar-builder__layout">
<div class="avatar-builder__preview">
<div data-avatar-preview>
<?= \App\Avatar\Lorelei::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
<?= \App\Avatar\AvatarManager::render($profile, $profile['display_name'] ?: 'Papa', 'xxl', $dashboardUserId) ?>
</div>
<p class="muted small">Wähle eine Komponente und passe dein neues Profilbild Schritt für Schritt an. Gespeichert wird erst nach Klick auf den Button unten.</p>
<?php if (count($avatarStyleKeys) > 1): ?>
<div class="stack gap-6" style="max-width:320px; margin:0 auto;">
<label class="label" for="avatarStyleSelect">Stil</label>
<select id="avatarStyleSelect" name="avatar_style" class="select" data-avatar-style-select>
<?php foreach ($avatarBuilder as $styleKey => $styleMeta): ?>
<option value="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" <?= $styleKey === $currentAvatarStyle ? 'selected' : '' ?>>
<?= htmlspecialchars((string)$styleMeta['label'], ENT_QUOTES) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="flex gap-12" style="flex-wrap:wrap; justify-content:center;">
<button class="btn ghost" type="button" data-avatar-random>Random Profilbild</button>
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit">Profilbild speichern</button>
</div>
<div class="avatar-component-nav" role="tablist" aria-label="Avatar-Komponenten">
<?php foreach (\App\Avatar\Lorelei::componentDefinitions() as $field => $meta): ?>
<?php
$componentOptions = $avatarOptions[$field] ?? [];
$componentCount = count(array_filter(array_keys($componentOptions), static fn ($value): bool => (string) $value !== ''));
?>
<button
type="button"
class="avatar-component-nav__button <?= $field === 'avatar_lorelei_eyes_variant' ? 'is-active' : '' ?>"
data-avatar-component-tab="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
aria-selected="<?= $field === 'avatar_lorelei_eyes_variant' ? 'true' : 'false' ?>"
>
<?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?>
<span class="muted small">(<?= $componentCount ?>)</span>
</button>
<?php endforeach; ?>
</div>
<div class="avatar-component-panels">
<?php foreach (\App\Avatar\Lorelei::componentDefinitions() as $field => $meta): ?>
<section class="avatar-component-panel avatar-component-panel--<?= htmlspecialchars(str_replace(['avatar_lorelei_', '_variant'], '', (string)$field), ENT_QUOTES) ?> <?= $field === 'avatar_lorelei_eyes_variant' ? 'is-active' : '' ?>" data-avatar-component-panel="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>">
<div class="avatar-component-panel__head">
<strong><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></strong>
<span class="muted small">Gesamtes Profilbild zur sicheren Vorschau</span>
</div>
<div class="avatar-component-grid">
<?php foreach (\App\Avatar\Lorelei::previewChoices($profile, $field) as $choice): ?>
<label class="avatar-component-card">
<input
type="radio"
name="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
value="<?= htmlspecialchars((string)$choice['value'], ENT_QUOTES) ?>"
data-avatar-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
<?= $choice['checked'] ? 'checked' : '' ?>
>
<span class="avatar-component-card__thumb">
<img
src="<?= htmlspecialchars($avatarPlaceholder, ENT_QUOTES) ?>"
alt="<?= htmlspecialchars((string)$choice['label'], ENT_QUOTES) ?>"
data-avatar-option-thumb
data-avatar-option-src="<?= htmlspecialchars((string)$choice['image'], ENT_QUOTES) ?>"
data-avatar-option-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
data-avatar-option-value="<?= htmlspecialchars((string)$choice['value'], ENT_QUOTES) ?>"
>
</span>
<span class="avatar-component-card__label"><?= htmlspecialchars((string)$choice['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<?php foreach ($avatarBuilder as $styleKey => $styleMeta): ?>
<?php $styleComponents = $styleMeta['components'] ?? []; ?>
<div class="avatar-style-group <?= $styleKey === $currentAvatarStyle ? 'is-active' : '' ?>" data-avatar-style-panel="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" <?= $styleKey === $currentAvatarStyle ? '' : 'hidden' ?>>
<div class="avatar-component-nav" role="tablist" aria-label="Avatar-Komponenten">
<?php $firstField = array_key_first($styleComponents); ?>
<?php foreach ($styleComponents as $field => $component): ?>
<button
type="button"
class="avatar-component-nav__button <?= $field === $firstField ? 'is-active' : '' ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-component-tab="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
aria-selected="<?= $field === $firstField ? 'true' : 'false' ?>"
>
<?= htmlspecialchars((string)($component['label'] ?? $field), ENT_QUOTES) ?>
<span class="muted small">(<?= (int)($component['count'] ?? 0) ?>)</span>
</button>
<?php endforeach; ?>
</div>
<div class="avatar-component-panels">
<?php foreach ($styleComponents as $field => $component): ?>
<section class="avatar-component-panel avatar-component-panel--<?= htmlspecialchars((string)$field, ENT_QUOTES) ?> <?= $field === $firstField ? 'is-active' : '' ?>" data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" data-avatar-component-panel="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>">
<div class="avatar-component-panel__head">
<strong><?= htmlspecialchars((string)($component['label'] ?? $field), ENT_QUOTES) ?></strong>
<span class="muted small">Gesamtes Profilbild zur sicheren Vorschau</span>
</div>
<div class="avatar-component-grid">
<?php foreach (($component['options'] ?? []) as $choice): ?>
<label class="avatar-component-card">
<input
type="radio"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>]"
value="<?= htmlspecialchars((string)($choice['value'] ?? ''), ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
<?= !empty($choice['checked']) ? 'checked' : '' ?>
>
<span class="avatar-component-card__thumb">
<img
src="<?= htmlspecialchars($avatarPlaceholder, ENT_QUOTES) ?>"
alt="<?= htmlspecialchars((string)($choice['label'] ?? ''), ENT_QUOTES) ?>"
data-avatar-option-thumb
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-option-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
data-avatar-option-value="<?= htmlspecialchars((string)($choice['value'] ?? ''), ENT_QUOTES) ?>"
>
</span>
<span class="avatar-component-card__label"><?= htmlspecialchars((string)($choice['label'] ?? ''), ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</form>

View File

@@ -115,7 +115,7 @@ $threads = $community
?>
<article class="forum-row">
<div class="forum-row__topic">
<div class="forum-row__avatar"><?= \App\Avatar\Lorelei::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></div>
<div class="forum-row__avatar"><?= \App\Avatar\AvatarManager::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list', (int)($t['uid'] ?? 0)) ?></div>
<div>
<h3><a href="/community_thread?id=<?= (int)$t['id'] ?>"><?= htmlspecialchars((string)$t['title'], ENT_QUOTES) ?></a></h3>
<div class="forum-row__meta">
@@ -206,7 +206,7 @@ $threads = $community
?>
<article class="forum-row">
<div class="forum-row__topic">
<div class="forum-row__avatar"><?= \App\Avatar\Lorelei::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></div>
<div class="forum-row__avatar"><?= \App\Avatar\AvatarManager::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list', (int)($t['uid'] ?? 0)) ?></div>
<div>
<h3><a href="/community_thread?id=<?= (int)$t['id'] ?>"><?= htmlspecialchars((string)$t['title'], ENT_QUOTES) ?></a></h3>
<div class="forum-row__meta">

View File

@@ -154,7 +154,7 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
<article class="forum-post forum-post--lead">
<aside class="forum-post__author">
<div class="forum-post__avatar"><?= \App\Avatar\Lorelei::render($thread, $threadAuthor, 'forum') ?></div>
<div class="forum-post__avatar"><?= \App\Avatar\AvatarManager::render($thread, $threadAuthor, 'forum', (int)($thread['user_id'] ?? 0)) ?></div>
<strong><?= htmlspecialchars($threadAuthor, ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars(trim((string)($threadLevel['icon'] ?? '') . ' ' . (string)($threadLevel['label'] ?? '')), ENT_QUOTES) ?></span>
<span><?= number_format($threadPoints, 1) ?> Punkte</span>
@@ -203,7 +203,7 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
?>
<article class="forum-post<?= $isHighlighted ? ' forum-post--highlighted' : '' ?>">
<aside class="forum-post__author">
<div class="forum-post__avatar"><?= \App\Avatar\Lorelei::render($p, $postAuthor, 'forum') ?></div>
<div class="forum-post__avatar"><?= \App\Avatar\AvatarManager::render($p, $postAuthor, 'forum', (int)($p['user_id'] ?? 0)) ?></div>
<strong><?= htmlspecialchars($postAuthor, ENT_QUOTES) ?></strong>
<?php if ($isHighlighted): ?>
<span class="forum-highlight-badge">Hilfreich hervorgehoben</span>

View File

@@ -11,7 +11,11 @@ if ($isLoggedIn) {
if ($pdo) {
$profileSettings = new \App\ProfileSettings($pdo);
$profileSettings->ensureSchema();
$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');
$avatarColumns = implode(', ', array_map(
static fn(string $column): string => $column,
\App\Avatar\AvatarManager::allProfileColumns()
));
$stmt = $pdo->prepare("SELECT user_id, display_name, first_name, $avatarColumns 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';
@@ -48,7 +52,7 @@ if ($isLoggedIn) {
<?php if ($isLoggedIn): ?>
<div class="profile-menu" data-profile-menu>
<button class="profile-menu__trigger" type="button" aria-haspopup="menu" aria-expanded="false" data-profile-menu-trigger>
<span class="profile-menu__avatar" aria-hidden="true"><?= \App\Avatar\Lorelei::render($profileRow ?? [], $displayName, 'nav') ?></span>
<span class="profile-menu__avatar" aria-hidden="true"><?= \App\Avatar\AvatarManager::render($profileRow ?? [], $displayName, 'nav', (int)($_SESSION['user_id'] ?? 0)) ?></span>
<span class="profile-menu__name"><?= htmlspecialchars($displayName, ENT_QUOTES) ?></span>
</button>
<div class="profile-menu__dropdown" role="menu">