Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -64,9 +65,7 @@ const groups: Group[] = [
},
{
label: "Account",
items: [
{ label: "Settings", to: "/settings", icon: <Settings size={16} /> },
],
items: [{ label: "Settings", to: "/settings", icon: <Settings size={16} /> }],
},
];

Expand Down Expand Up @@ -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"
>
<img
<Avatar
src={currentUser.avatar}
alt=""
className="h-9 w-9 rounded-full border border-border bg-muted"
alt={currentUser.name}
name={currentUser.name}
size={36}
/>
<div className="min-w-0 flex-1">
<p className="truncate text-[13px] font-semibold text-foreground">
Expand Down Expand Up @@ -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}
<ChevronRight
size={12}
className={cn("transition-transform", open && "rotate-90")}
/>
<ChevronRight size={12} className={cn("transition-transform", open && "rotate-90")} />
</button>
<AnimatePresence initial={false}>
{open && (
Expand All @@ -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 (
<li key={item.label}>
<Link
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/components/layout/Topbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Bell, MessageSquare, Plus, Search, Sparkles, Menu, Moon, Sun } from "lucide-react";
import { Link } from "@tanstack/react-router";
import { Avatar } from "@/components/shared/primitives";
import { currentUser } from "@/mocks/seed";
import { useTheme } from "@/hooks/useTheme";

Expand Down Expand Up @@ -59,11 +60,7 @@ export function Topbar({ onMenu }: { onMenu: () => void }) {
params={{ username: currentUser.handle }}
className="ml-1 flex items-center gap-2 rounded-md p-1 hover:bg-muted"
>
<img
src={currentUser.avatar}
alt=""
className="h-8 w-8 rounded-full border border-border bg-muted"
/>
<Avatar src={currentUser.avatar} alt={currentUser.name} name={currentUser.name} size={32} />
<div className="hidden text-left sm:block">
<p className="text-[12px] font-semibold leading-tight text-foreground">
{currentUser.name}
Expand Down
50 changes: 33 additions & 17 deletions frontend/src/components/shared/primitives.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,16 +18,11 @@ export function SectionHeader({
<h3 className="text-[14px] font-semibold text-foreground">{title}</h3>
{action &&
(actionTo ? (
<Link
to={actionTo}
className="text-[12px] font-medium text-primary hover:underline"
>
<Link to={actionTo} className="text-[12px] font-medium text-primary hover:underline">
{action}
</Link>
) : (
<button className="text-[12px] font-medium text-primary hover:underline">
{action}
</button>
<button className="text-[12px] font-medium text-primary hover:underline">{action}</button>
))}
</div>
);
Expand Down Expand Up @@ -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 (
<div className="relative shrink-0" style={{ width: size, height: size }}>
<img
src={src}
alt={alt}
width={size}
height={size}
className="h-full w-full rounded-full border border-border bg-muted object-cover"
/>
{shouldRenderImage ? (
<img
src={normalizedSrc}
alt={alt}
width={size}
height={size}
onError={() => setHasError(true)}
className="h-full w-full rounded-full border border-border bg-muted object-cover"
/>
) : (
<div
aria-label={fallbackLabel}
className="flex h-full w-full items-center justify-center rounded-full border border-border bg-primary/10 text-[0.7rem] font-semibold uppercase tracking-[0.12em] text-primary"
>
{getInitials(name ?? alt)}
</div>
)}
{online !== undefined && (
<span className="absolute -bottom-0.5 -right-0.5">
<StatusDot online={online} />
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading