diff --git a/package-lock.json b/package-lock.json index 1581c96..4788fa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5012b2f..085655f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/app.js b/public/app.js index 2c2a482..cef369a 100644 --- a/public/app.js +++ b/public/app.js @@ -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') { diff --git a/public/kundenbereich.css b/public/kundenbereich.css index ef3a821..ae12b92 100644 --- a/public/kundenbereich.css +++ b/public/kundenbereich.css @@ -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 { diff --git a/server/routes/customerData.js b/server/routes/customerData.js index eb719d7..3e0260a 100644 --- a/server/routes/customerData.js +++ b/server/routes/customerData.js @@ -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