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

This commit is contained in:
2026-07-27 01:36:24 +02:00
parent e274e07f45
commit 708830fe0a
11 changed files with 26 additions and 18 deletions

View File

@@ -2,7 +2,7 @@ Anweisung: Projektstruktur (Basis-Template) wie in „papa-kind-treff“
Aktueller Projektstand
- Papa-Kind-Treff nutzt im Mitgliederbereich DiceBear `lorelei` über einen internen API-Proxy mit lokalem Cache.
- Relevante Logik liegt in `src/App/Avatar.php`.
- Relevante Logik liegt in `src/App/Avatar/Lorelei.php`.
- Der Avatar-Proxy liegt unter `public/page/api/avatar.php`.
- Ä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.

View File

@@ -11,6 +11,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Geschäftslogik unter `src/App/`
- MySQL/MariaDB-Schema in `schema.sql`
- Globales Frontend vor allem über `public/assets/js/app.js` und `public/assets/css/app.css`
- Avatar-Style `Lorelei` liegt getrennt unter `src/App/Avatar/Lorelei.php`
## Wichtige Produktentscheidungen
- Die Ansprache im Frontend soll immer den aktuellen Nutzer direkt ansprechen.

View File

@@ -37,6 +37,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- `owner`
Die Rechteverteilung und Community-Logik sind in `src/App/CommunityAccess.php` abgebildet.
Die Lorelei-Avatarlogik liegt in `src/App/Avatar/Lorelei.php`.
## Consent und rechtlich relevante Integrationen
Der aktuelle Code nutzt oder kann nutzen:

View File

@@ -63,7 +63,7 @@ $sectionLinks = [
<div class="account-panel__body">
<div class="profile-avatar-panel">
<div class="profile-avatar-panel__preview">
<?= \App\Avatar::render($profile, $profile['display_name'] ?: 'Papa', 'xl') ?>
<?= \App\Avatar\Lorelei::render($profile, $profile['display_name'] ?: 'Papa', 'xl') ?>
</div>
<div class="profile-avatar-panel__content">
<h3>Dein Profilbild</h3>
@@ -300,7 +300,7 @@ $sectionLinks = [
<div class="avatar-builder__layout">
<div class="avatar-builder__preview">
<div data-avatar-preview>
<?= \App\Avatar::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
<?= \App\Avatar\Lorelei::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
</div>
<p class="muted small">Wähle eine Lorelei-Variante oder lade dir direkt neue Vorschläge. Die Vorschau aktualisiert sich sofort.</p>
</div>

View File

@@ -115,7 +115,7 @@ $threads = $community
?>
<article class="forum-row">
<div class="forum-row__topic">
<div class="forum-row__avatar"><?= \App\Avatar::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></div>
<div class="forum-row__avatar"><?= \App\Avatar\Lorelei::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></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::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></div>
<div class="forum-row__avatar"><?= \App\Avatar\Lorelei::render($t, (string)($t['display_name'] ?: 'Mitglied'), 'forum-list') ?></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::render($thread, $threadAuthor, 'forum') ?></div>
<div class="forum-post__avatar"><?= \App\Avatar\Lorelei::render($thread, $threadAuthor, 'forum') ?></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::render($p, $postAuthor, 'forum') ?></div>
<div class="forum-post__avatar"><?= \App\Avatar\Lorelei::render($p, $postAuthor, 'forum') ?></div>
<strong><?= htmlspecialchars($postAuthor, ENT_QUOTES) ?></strong>
<?php if ($isHighlighted): ?>
<span class="forum-highlight-badge">Hilfreich hervorgehoben</span>

View File

@@ -48,7 +48,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::render($profileRow ?? [], $displayName, 'nav') ?></span>
<span class="profile-menu__avatar" aria-hidden="true"><?= \App\Avatar\Lorelei::render($profileRow ?? [], $displayName, 'nav') ?></span>
<span class="profile-menu__name"><?= htmlspecialchars($displayName, ENT_QUOTES) ?></span>
</button>
<div class="profile-menu__dropdown" role="menu">

View File

@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace App;
use App\Avatar\Lorelei;
final class AccountPages
{
public static function register(App $app): array
@@ -363,7 +365,7 @@ final class AccountPages
$profile['contact_phone'] = $crypto->decrypt((string)$profile['contact_phone']) ?: '';
}
}
$profile = array_merge($profile, Avatar::normalize($profile));
$profile = array_merge($profile, Lorelei::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');
@@ -420,7 +422,7 @@ final class AccountPages
if (!in_array($section, $allowedSections, true)) {
$section = 'profile';
}
$avatarPresetOptions = Avatar::seedChoices((string)$profile['avatar_preset']);
$avatarPresetOptions = Lorelei::seedChoices((string)$profile['avatar_preset']);
return compact(
'flash',

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace App;
namespace App\Avatar;
final class Avatar
final class Lorelei
{
public static function defaults(): array
{
@@ -90,7 +90,7 @@ final class Avatar
]];
$seen = [$selectedSeed => true];
$base = abs((int)crc32($selectedSeed));
$base = abs((int) crc32($selectedSeed));
$index = 0;
while (count($choices) < $count) {
@@ -141,7 +141,7 @@ final class Avatar
$seed = self::normalizeSeed($seed);
$initials = strtoupper(substr(preg_replace('/[^a-z]/', '', $seed) ?: 'pk', 0, 2));
$palette = ['#f5efe5', '#d9c7b3', '#8f6f54', '#37485a', '#c88f5b'];
$hash = abs((int)crc32($seed));
$hash = abs((int) crc32($seed));
$bg = $palette[$hash % count($palette)];
$fg = $hash % 2 === 0 ? '#243142' : '#ffffff';

View File

@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace App;
use App\Avatar\Lorelei;
final class Community
{
private ?array $forumStructure = null;
@@ -151,7 +153,7 @@ final class Community
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
$avatarSelect = $this->hasColumn('user_profiles', 'avatar_outfit_color')
? ', ' . Avatar::selectColumns('p')
? ', ' . Lorelei::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,
@@ -207,7 +209,7 @@ final class Community
{
$select = 'ft.*, p.display_name';
if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) {
$select .= ', ' . Avatar::selectColumns('p');
$select .= ', ' . Lorelei::selectColumns('p');
}
$join = '';
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
@@ -227,7 +229,7 @@ final class Community
{
$select = 'fp.*, p.display_name';
if ($this->hasColumn('user_profiles', 'avatar_outfit_color')) {
$select .= ', ' . Avatar::selectColumns('p');
$select .= ', ' . Lorelei::selectColumns('p');
}
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
$select .= ', hp.display_name AS highlighted_by_name';

View File

@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace App;
use App\Avatar\Lorelei;
final class ProfileSettings
{
private bool $schemaEnsured = false;
@@ -87,7 +89,7 @@ final class ProfileSettings
}
$this->ensureSchema();
$avatar = Avatar::normalize($avatarConfig);
$avatar = Lorelei::normalize($avatarConfig);
$stmt = $this->pdo->prepare(
'UPDATE user_profiles
SET avatar_preset = :preset,