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))}