refactor: enhance URL normalization logic and update sidebar navigation active state handling

This commit is contained in:
Yoga Pangestu 2026-04-16 13:38:36 +07:00
parent fe3e6767ce
commit 2e1aebae97
5 changed files with 34 additions and 20 deletions

View File

@ -19,7 +19,7 @@ import product from '@/routes/product';
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: dashboard(),
href: dashboard().url,
icon: LayoutGrid,
},
];
@ -44,7 +44,7 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href={dashboard()} prefetch>
<Link href={dashboard().url} prefetch>
<AppLogo />
</Link>
</SidebarMenuButton>

View File

@ -16,7 +16,7 @@ export function NavMain({
items: NavItem[];
label?: string;
}) {
const { isCurrentUrl } = useCurrentUrl();
const { isCurrentOrParentUrl } = useCurrentUrl();
return (
<SidebarGroup className="px-2 py-1">
@ -26,7 +26,7 @@ export function NavMain({
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={isCurrentUrl(item.href)}
isActive={item.isActive || isCurrentOrParentUrl(item.href)}
tooltip={{ children: item.title }}
>
<Link href={item.href} prefetch>

View File

@ -506,7 +506,7 @@ function SidebarMenuButton({
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
data-active={isActive || undefined}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
@ -662,7 +662,7 @@ function SidebarMenuSubButton({
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
data-active={isActive || undefined}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
className

View File

@ -40,23 +40,34 @@ export function useCurrentUrl(): UseCurrentUrlReturn {
currentUrl?: string,
startsWith: boolean = false,
) => {
const urlToCompare = currentUrl ?? currentUrlPath;
const urlString = toUrl(urlToCheck);
const currentPath = currentUrl ?? currentUrlPath;
const comparePath = (path: string): boolean =>
startsWith ? urlToCompare.startsWith(path) : path === urlToCompare;
const clean = (p: string) => {
if (!p) return '/';
// Remove query and hash, then trailing slash
let path = p.split('?')[0].split('#')[0];
if (path.startsWith('http')) {
try {
path = new URL(path).pathname;
} catch {
// Ignore
}
}
return path.replace(/\/+$/, '') || '/';
};
if (!urlString.startsWith('http')) {
return comparePath(urlString);
const normCurrent = clean(currentPath);
const normCheck = clean(urlString);
if (startsWith) {
if (normCheck === '/') {
return normCurrent === '/';
}
return normCurrent.startsWith(normCheck);
}
try {
const absoluteUrl = new URL(urlString);
return comparePath(absoluteUrl.pathname);
} catch {
return false;
}
return normCurrent === normCheck;
};
const isCurrentOrParentUrl: IsCurrentOrParentUrlFn = (

View File

@ -7,6 +7,9 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function toUrl(url: NonNullable<InertiaLinkProps['href']>): string {
return typeof url === 'string' ? url : url.url;
export function toUrl(url: InertiaLinkProps['href']): string {
if (!url) return '';
if (typeof url === 'string') return url;
if (typeof url === 'object' && url !== null && 'url' in url) return url.url as string;
return String(url);
}