From 9a02dfff2e81cf631e16fe159fa9b8a08c74cdfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 19:43:21 +0100 Subject: [PATCH] fix(settings): build the avatar initials from the display name The settings avatar took its fallback letters and its alt text from the login, while the top bar and the sidebar spell the display name, so the same account showed different initials on two screens. The three copies of the initials function become one `nameInitials()` beside `accountName()`, and the settings avatar reads the same name. --- src/components/layout/sidebar-nav.tsx | 16 ++------ src/components/layout/top-bar.tsx | 12 +----- .../__tests__/avatar-initials.test.tsx | 39 +++++++++++++++++++ .../account-section/avatar-section.tsx | 13 ++----- 4 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 src/components/settings/account-section/__tests__/avatar-initials.test.tsx diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx index 28062cbb8..cae4fb514 100644 --- a/src/components/layout/sidebar-nav.tsx +++ b/src/components/layout/sidebar-nav.tsx @@ -40,7 +40,7 @@ import { import { cn } from "@/lib/utils"; import { Logo } from "@/components/ui/logo"; import { useAuth, useLogout } from "@/hooks/use-auth"; -import { accountName } from "@/lib/account-name"; +import { accountName, nameInitials } from "@/lib/account-name"; import { useIsMobile } from "@/hooks/use-is-mobile"; import { useNavModules } from "@/hooks/use-nav-modules"; import { useMounted } from "@/hooks/use-mounted"; @@ -98,14 +98,6 @@ export function writeSidebarCollapsedPref(collapsed: boolean): void { } } -function getInitials(name: string): string { - return name - .split(/[\s._-]+/) - .slice(0, 2) - .map((s) => s[0]?.toUpperCase() ?? "") - .join(""); -} - /** * The sidebar footer's name-and-avatar block. * @@ -170,7 +162,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) { {avatarUrl && } - {getInitials(name)} + {nameInitials(name)}
@@ -286,7 +278,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) { @@ -313,7 +305,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
diff --git a/src/components/layout/top-bar.tsx b/src/components/layout/top-bar.tsx index 4d499d1a2..53c220210 100644 --- a/src/components/layout/top-bar.tsx +++ b/src/components/layout/top-bar.tsx @@ -26,7 +26,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Skeleton } from "@/components/ui/skeleton"; import Link from "next/link"; import { useAuth, useLogout } from "@/hooks/use-auth"; -import { accountName } from "@/lib/account-name"; +import { accountName, nameInitials } from "@/lib/account-name"; import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; import { useTheme } from "@/components/providers"; import { useTranslations } from "@/lib/i18n/context"; @@ -41,14 +41,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -function getInitials(name: string): string { - return name - .split(/[\s._-]+/) - .slice(0, 2) - .map((s) => s[0]?.toUpperCase() ?? "") - .join(""); -} - export function TopBar() { const { user, isLoading } = useAuth(); // v1.36.0 — acting on somebody else's record; see the menu below. Read from @@ -147,7 +139,7 @@ export function TopBar() { )} - {getInitials(accountName(user))} + {nameInitials(accountName(user))} {accountName(user)} diff --git a/src/components/settings/account-section/__tests__/avatar-initials.test.tsx b/src/components/settings/account-section/__tests__/avatar-initials.test.tsx new file mode 100644 index 000000000..0e9bd00a3 --- /dev/null +++ b/src/components/settings/account-section/__tests__/avatar-initials.test.tsx @@ -0,0 +1,39 @@ +/** + * The avatar fallback on the settings card spells the name the rest of the + * app shows. It used to take its letters from the login, so an account named + * "Robin Example" with the login "rex" read "RE" in the top bar and the + * sidebar and "R" here. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("@tanstack/react-query", () => ({ + useMutation: () => ({ mutate: vi.fn(), isPending: false }), +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { + id: "u1", + username: "rex", + displayName: "Robin Example", + avatarUrl: null, + }, + refetch: vi.fn(), + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { AvatarSection } from "../avatar-section"; + +describe("the settings avatar fallback", () => { + it("builds its initials from the display name, not the login", () => { + const html = renderToStaticMarkup( + + + , + ); + expect(html).toContain(">RE<"); + expect(html).not.toContain(">R<"); + }); +}); diff --git a/src/components/settings/account-section/avatar-section.tsx b/src/components/settings/account-section/avatar-section.tsx index bff3eb9e5..6297cdb61 100644 --- a/src/components/settings/account-section/avatar-section.tsx +++ b/src/components/settings/account-section/avatar-section.tsx @@ -13,6 +13,7 @@ import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { apiFetchRaw } from "@/lib/api/api-fetch"; import { statusText, type StatusMessage } from "./account-section-utils"; +import { accountName, nameInitials } from "@/lib/account-name"; // Accepted upload types + byte cap mirror the server contract in // `src/app/api/user/avatar/route.ts` so the client rejects an obvious @@ -21,14 +22,6 @@ import { statusText, type StatusMessage } from "./account-section-utils"; const ACCEPTED_AVATAR_TYPES = ["image/jpeg", "image/png", "image/webp"]; const AVATAR_MAX_UPLOAD_BYTES = 2 * 1024 * 1024; -function getAvatarInitials(name: string): string { - return name - .split(/[\s._-]+/) - .slice(0, 2) - .map((s) => s[0]?.toUpperCase() ?? "") - .join(""); -} - export function AvatarSection() { const { t } = useTranslations(); const { user, refetch } = useAuth(); @@ -123,9 +116,9 @@ export function AvatarSection() {

- {avatarUrl && } + {avatarUrl && } - {getAvatarInitials(user.username)} + {nameInitials(accountName(user))}