diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index f2f96260..89ffc135 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,6 +1,7 @@ import { cn } from "@/lib/utils"; import { Link, useRouterState } from "@tanstack/react-router"; import { APP_LOGO } from "@/lib/logo"; +import { Avatar } from "@/components/shared/primitives"; import { LayoutDashboard, FolderKanban, @@ -64,9 +65,7 @@ const groups: Group[] = [ }, { label: "Account", - items: [ - { label: "Settings", to: "/settings", icon: }, - ], + items: [{ label: "Settings", to: "/settings", icon: }], }, ]; @@ -111,10 +110,11 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void onClick={onClose} className="flex items-center gap-3 rounded-md px-2 py-2 hover:bg-sidebar-accent" > -

@@ -147,10 +147,7 @@ function SidebarGroup({ group, onNav }: { group: Group; onNav: () => void }) { className="flex w-full items-center justify-between px-2 py-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground" > {group.label} - + {open && ( @@ -161,7 +158,8 @@ function SidebarGroup({ group, onNav }: { group: Group; onNav: () => void }) { className="overflow-hidden" > {group.items.map((item) => { - const active = pathname === item.to || pathname.startsWith(item.to.split("?")[0] + "/"); + const active = + pathname === item.to || pathname.startsWith(item.to.split("?")[0] + "/"); return (

  • void }) { params={{ username: currentUser.handle }} className="ml-1 flex items-center gap-2 rounded-md p-1 hover:bg-muted" > - +

    {currentUser.name} diff --git a/frontend/src/components/shared/primitives.tsx b/frontend/src/components/shared/primitives.tsx index 85a28fa2..d3bb415a 100644 --- a/frontend/src/components/shared/primitives.tsx +++ b/frontend/src/components/shared/primitives.tsx @@ -1,6 +1,6 @@ -import { cn } from "@/lib/utils"; +import { cn, getInitials } from "@/lib/utils"; import { Link } from "@tanstack/react-router"; -import type { ReactNode } from "react"; +import { useEffect, useState, type ReactNode } from "react"; export function SectionHeader({ title, @@ -18,16 +18,11 @@ export function SectionHeader({

    {title}

    {action && (actionTo ? ( - + {action} ) : ( - + ))}
    ); @@ -107,21 +102,42 @@ export function Avatar({ alt, size = 32, online, + name, }: { - src: string; + src?: string | null; alt: string; size?: number; online?: boolean; + name?: string | null; }) { + const [hasError, setHasError] = useState(false); + const normalizedSrc = typeof src === "string" ? src.trim() : ""; + const shouldRenderImage = Boolean(normalizedSrc) && !hasError; + const fallbackLabel = alt || name || "User avatar"; + + useEffect(() => { + setHasError(false); + }, [normalizedSrc]); + return (
    - {alt} + {shouldRenderImage ? ( + {alt} setHasError(true)} + className="h-full w-full rounded-full border border-border bg-muted object-cover" + /> + ) : ( +
    + {getInitials(name ?? alt)} +
    + )} {online !== undefined && ( diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index a5ef1935..372982e8 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -4,3 +4,24 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function getInitials(name?: string | null, fallback = "?") { + const normalizedName = name?.trim(); + + if (!normalizedName) { + return fallback; + } + + const parts = normalizedName.split(/\s+/).filter(Boolean); + + if (parts.length === 0) { + return fallback; + } + + const initials = parts + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(""); + + return initials || fallback; +}