Profilbild: zufälliger DiceBear-Avatar (Big Smile) pro Kunde

Der Server erzeugt das SVG lokal unter /api/avatar.svg (Seed = Kunden-ID),
es geht also keine Anfrage an dicebear.com. Die Initialen bleiben als
Fallback, bis das Bild geladen ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-29 16:36:49 +00:00
1 parent e4efdac226
commit 8c9d28c30f
5 files changed
+70 -2

No files matched your search

+32
View File
@@ -8,6 +8,8 @@
"name": "webklar-kundenbereich",
"version": "1.0.0",
"dependencies": {
"@dicebear/big-smile": "^9.4.2",
"@dicebear/core": "^9.4.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
@@ -20,6 +22,36 @@
"node": ">=18"
}
},
"node_modules/@dicebear/big-smile": {
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.4.2.tgz",
"integrity": "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ==",
"license": "(MIT AND CC-BY-4.0)",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/core": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.3.tgz",
"integrity": "sha512-9ITrQI57k3p5hKuU8HZJ1d0kPdrmTKEJDOdlKQ8CpaChbtmnD1R+dtWmNz5IesYThXM8F64pJ0ZvJ2rsY8Hciw==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+2
View File
@@ -15,6 +15,8 @@
"node": ">=18"
},
"dependencies": {
"@dicebear/big-smile": "^9.4.2",
"@dicebear/core": "^9.4.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
+20 -2
View File
@@ -1920,6 +1920,20 @@ function showView(name, { updateHash = true } = {}) {
document.dispatchEvent(new CustomEvent('kb:view', { detail: { name } }))
}
/* Profilbild (DiceBear Big Smile vom eigenen Server); die Initialen bleiben stehen, bis das Bild geladen ist,
und falls es nicht laedt. Die Kunden-ID im Query sorgt dafuer, dass "Als Kunde ansehen" nicht das gecachte Bild zeigt. */
function setAvatar(el, initials, seed) {
if (!el) return
el.textContent = initials
el.dataset.initials = initials
if (!seed) return
const img = new Image()
img.alt = ''
img.decoding = 'async'
img.onload = () => { el.textContent = ''; el.appendChild(img); el.classList.add('has-img') }
img.src = `/api/avatar.svg?c=${encodeURIComponent(seed)}`
}
function initialsOf(name) {
const words = String(name || '').split(/\s+/).filter((w) => /^[\p{L}\p{N}]/u.test(w))
const letters = words.length > 1 ? words[0][0] + words[1][0] : (words[0] || '').slice(0, 2)
@@ -1985,7 +1999,7 @@ async function initDashboardPage() {
document.getElementById('customer-name').textContent = displayName
document.getElementById('customer-meta').textContent =
[customer.code ? `Kundennr. ${customer.code}` : '', customer.email].filter(Boolean).join(' · ')
document.getElementById('customer-avatar').textContent = initialsOf(displayName)
setAvatar(document.getElementById('customer-avatar'), initialsOf(displayName), customer.id)
api('/api/contact')
.then((c) => {
@@ -2212,7 +2226,11 @@ function initAccountSheet() {
const fill = () => {
document.getElementById('kb-sheet-name').textContent = document.getElementById('customer-name')?.textContent || 'Mein Konto'
document.getElementById('kb-sheet-meta').textContent = document.getElementById('customer-meta')?.textContent || ''
document.getElementById('kb-sheet-avatar').textContent = avatar.textContent
const sheetAvatar = document.getElementById('kb-sheet-avatar')
const img = avatar.querySelector('img')
sheetAvatar.textContent = img ? '' : avatar.textContent
sheetAvatar.classList.toggle('has-img', Boolean(img))
if (img) sheetAvatar.appendChild(img.cloneNode())
renderInstallOffers()
}
if (typeof sheet.showModal !== 'function') {
+2
View File
@@ -54,6 +54,8 @@
flex: none; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center;
font-size: 13px; font-weight: 700; background: #26124a; border: 1px solid var(--kb-purple-line);
}
.kb-avatar.has-img { overflow: hidden; }
.kb-avatar img { width: 100%; height: 100%; display: block; border-radius: 50%; }
/* ---------------------------------------------------------------- Raster --- */
.kb-shell {
+14
View File
@@ -1,4 +1,6 @@
import { Router } from 'express'
import { createAvatar } from '@dicebear/core'
import * as bigSmile from '@dicebear/big-smile'
import { config } from '../config.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { getDocument, listDocuments, Query } from '../services/appwriteAdmin.js'
@@ -105,4 +107,16 @@ router.get('/contact', requireSession, async (req, res) => {
}
})
/** Profilbild: DiceBear "Big Smile", lokal erzeugt (keine Anfrage an dicebear.com).
Die Kunden-ID ist der Seed - zufaellig pro Kunde, aber bei jedem Login dasselbe Bild. */
router.get('/avatar.svg', requireSession, (req, res) => {
const svg = createAvatar(bigSmile, {
seed: String(getSessionCustomerId(req) || 'webklar'),
backgroundColor: ['b6e3f4', 'c0aede', 'd1d4f9', 'ffd5dc', 'ffdfbf'],
}).toString()
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8')
res.setHeader('Cache-Control', 'private, max-age=86400')
return res.send(svg)
})
export default router