From c3e00ab9d9e6938628bce882a2f23fa7ec7f7fe3 Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Tue, 9 Jun 2026 10:58:23 +0200 Subject: [PATCH] refactor(web): generate DiceBear avatars locally instead of via HTTP API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted over the HTTP API: no runtime dependency on api.dicebear.com, works offline/LAN, and no player pseudo leaks to a third party. - @dicebear/core@10 + @dicebear/styles@10 (lorelei.json), CC0 1.0 - Avatar builds a data URI locally (new Style/Avatar), memoized per seed - same component API (seed/className) → no call-site changes - enable resolveJsonModule for the style import Cost: ~64 kB gzip added to the bundle — acceptable for a self-hosted game. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/package.json | 2 ++ apps/web/src/components/avatar.tsx | 22 +++++++++++++++++++--- apps/web/tsconfig.app.json | 1 + bun.lock | 6 ++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 5d5458d..ce1306e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,8 @@ "preview": "vite preview" }, "dependencies": { + "@dicebear/core": "^10", + "@dicebear/styles": "^10", "@nerdware/shared": "workspace:*", "@workspace/ui": "workspace:*", "framer-motion": "^11", diff --git a/apps/web/src/components/avatar.tsx b/apps/web/src/components/avatar.tsx index b38dbda..3a876c9 100644 --- a/apps/web/src/components/avatar.tsx +++ b/apps/web/src/components/avatar.tsx @@ -1,11 +1,27 @@ +import { Avatar as DicebearAvatar, Style } from "@dicebear/core" +import loreleiDefinition from "@dicebear/styles/lorelei.json" + +// Génération locale (pas d'appel à l'API DiceBear) : self-hosted, offline, +// aucun pseudo envoyé à un tiers. Lorelei par Lisa Wischofsky — licence CC0 1.0. +const style = new Style(loreleiDefinition) +const cache = new Map() + +/** Data URI de l'avatar pour un seed donné, mémoïsé. */ +function avatarUri(seed: string): string { + let uri = cache.get(seed) + if (!uri) { + uri = new DicebearAvatar(style, { seed }).toDataUri() + cache.set(seed, uri) + } + return uri +} + /** Avatar DiceBear (style lorelei), seedé par le pseudo → stable et reconnaissable. */ export function Avatar({ seed, className }: { seed: string; className?: string }) { - const url = `https://api.dicebear.com/9.x/lorelei/svg?seed=${encodeURIComponent(seed)}` return ( {`Avatar ) diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index 03ddd3f..60be5f1 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -9,6 +9,7 @@ /* Bundler mode */ "moduleResolution": "bundler", + "resolveJsonModule": true, "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", diff --git a/bun.lock b/bun.lock index 8ff9b27..5810c19 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,8 @@ "name": "web", "version": "0.0.1", "dependencies": { + "@dicebear/core": "^10", + "@dicebear/styles": "^10", "@nerdware/shared": "workspace:*", "@workspace/ui": "workspace:*", "framer-motion": "^11", @@ -163,6 +165,10 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@dicebear/core": ["@dicebear/core@10.1.0", "", {}, "sha512-OzwokGKfSJCTHNATS87Rl3Fbf00FvqKamitVaBsgm535paz09Vdgwv4QxwHR6CZUsrCdMJn+j/RfZX4o2iNk5g=="], + + "@dicebear/styles": ["@dicebear/styles@10.2.0", "", {}, "sha512-o4rw0+7Q7sSAyr9YxYrUe3jCxlOAzEcBvbQvl4hQKQ+9WNmBK4p7wGYvsmGeJbxFh8y/Ux6ltrCPyXBIDV6qYA=="], + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.71.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "enquirer": "^2.4.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-KEUw/mGu+EDRhYWRTNGHIimVCs9NvMFaIXOGrHSXoCteKLE5EsJnmPjOPpYorjXVg/0xG0fbdVw720azw1z4ag=="], "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="],