mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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.
This commit is contained in:
1 parent
5cf4a7d213
commit
9a02dfff2e
4 files changed
+48
-32
No files matched your search
@@ -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 }) {
|
||||
<Avatar className="h-9 w-9 shrink-0">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(name)}
|
||||
{nameInitials(name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -286,7 +278,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
<Avatar className="h-8 w-8" aria-hidden="true">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(name)}
|
||||
{nameInitials(name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -313,7 +305,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
<Avatar className="h-8 w-8 shrink-0" aria-hidden="true">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(name)}
|
||||
{nameInitials(name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -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() {
|
||||
<AvatarImage src={avatarUrl} alt={accountName(user)} />
|
||||
)}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(accountName(user))}
|
||||
{nameInitials(accountName(user))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden sm:inline">{accountName(user)}</span>
|
||||
|
||||
@@ -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(
|
||||
<I18nProvider initialLocale="en">
|
||||
<AvatarSection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
expect(html).toContain(">RE<");
|
||||
expect(html).not.toContain(">R<");
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="size-16">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={user.username} />}
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={accountName(user)} />}
|
||||
<AvatarFallback className="text-base">
|
||||
{getAvatarInitials(user.username)}
|
||||
{nameInitials(accountName(user))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in new issue
Block a user