feat: Replace user avatar with Facehash faces (#86)

## Summary
- Replaced the default initials-based avatar fallback with
[Facehash](https://facehash.dev/) — deterministic, unique avatar faces
generated from the user's name
- Same name always produces the same face, no API calls needed
- Fully rounded with `rounded-full` styling

## Preview

![Facehash avatars](https://facehash.dev/og-image.png)

Each user gets a unique, consistent face based on their name — no more
generic initials.

## Changes
- Installed `facehash` package
- Updated `resources/js/components/user-info.tsx` to use `<Facehash>`
instead of `<Avatar>` with initials fallback

## Test plan
- [x] Verify avatar renders correctly in the sidebar footer (desktop)
- [x] Verify avatar renders correctly in the mobile header
- [x] Verify avatar renders correctly in the user dropdown menu
- [x] Confirm the same user always gets the same face
- [x] Check dark mode appearance
This commit is contained in:
Víctor Falcón 2026-01-31 14:58:30 +01:00 committed by Víctor Falcón
parent cc666a3111
commit 6aa9da3df3
3 changed files with 40 additions and 9 deletions

View File

@ -36,6 +36,7 @@
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"facehash": "^0.0.5",
"globals": "^15.14.0",
"input-otp": "^1.4.2",
"json-logic-js": "^2.0.5",
@ -1029,6 +1030,8 @@
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"facehash": ["facehash@0.0.5", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-txM9veJ9IPNWsimpHD+Qqr5aFcSdgJPBY2oTRzRgIfSPcLxwZQY4++7vv9fHjco25ErUxC9pdq9tPB9NK1UhTQ=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],

View File

@ -74,6 +74,7 @@
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"facehash": "^0.0.5",
"globals": "^15.14.0",
"input-otp": "^1.4.2",
"json-logic-js": "^2.0.5",

View File

@ -1,7 +1,32 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/hooks/use-initials';
import { cn } from '@/lib/utils';
import { type User } from '@/types';
import { Facehash } from 'facehash';
import { Avatar } from './ui/avatar';
export const tailwindColorClasses = [
'bg-slate-200 dark:bg-slate-800',
'bg-gray-200 dark:bg-gray-800',
'bg-zinc-200 dark:bg-zinc-800',
'bg-neutral-200 dark:bg-neutral-800',
'bg-stone-200 dark:bg-stone-800',
'bg-red-200 dark:bg-red-800',
'bg-orange-200 dark:bg-orange-800',
'bg-amber-200 dark:bg-amber-800',
'bg-yellow-200 dark:bg-yellow-800',
'bg-lime-200 dark:bg-lime-800',
'bg-green-200 dark:bg-green-800',
'bg-emerald-200 dark:bg-emerald-800',
'bg-teal-200 dark:bg-teal-800',
'bg-cyan-200 dark:bg-cyan-800',
'bg-sky-200 dark:bg-sky-800',
'bg-blue-200 dark:bg-blue-800',
'bg-indigo-200 dark:bg-indigo-800',
'bg-violet-200 dark:bg-violet-800',
'bg-purple-200 dark:bg-purple-800',
'bg-fuchsia-200 dark:bg-fuchsia-800',
'bg-pink-200 dark:bg-pink-800',
'bg-rose-200 dark:bg-rose-800',
];
export function UserInfo({
user,
@ -12,16 +37,18 @@ export function UserInfo({
showEmail?: boolean;
hideNameOnMobile?: boolean;
}) {
const getInitials = useInitials();
return (
<>
<Avatar className="h-8 w-8 overflow-hidden rounded-full">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{getInitials(user.name)}
</AvatarFallback>
<Avatar>
<Facehash
name={user.name}
size={32}
colorClasses={tailwindColorClasses}
intensity3d="dramatic"
className="rounded-full"
/>
</Avatar>
<div
className={cn([
'grid flex-1 text-left text-sm leading-tight',