blob: 9a4ce3d4a57abbd324c0a9455caba28dc5109756 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
"use client";
import { cn } from "@/lib/utils";
import { useAbility } from "@/shared/casl/AbilityProvider";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { NAV_GROUP_LABELS, type NavGroup, type NavItem, portalNav } from "./nav";
const GROUP_ORDER: NavGroup[] = ["allocations", "admin"];
export function Sidebar() {
const pathname = usePathname();
const ability = useAbility();
const visible = portalNav.filter((item) => {
if (!item.ability) return true;
return ability.can(item.ability.action, item.ability.subject);
});
const groups = GROUP_ORDER.map((group) => ({
group,
items: visible.filter((item) => item.group === group),
})).filter((g) => g.items.length > 0);
return (
<aside className="flex w-[240px] shrink-0 flex-col border-r border-border bg-sidebar text-sidebar-foreground">
<div className="px-6 pt-8 pb-6">
<Link
href="/"
className="font-display text-2xl font-extrabold uppercase tracking-tight text-brand"
>
Custos
</Link>
</div>
<nav className="flex flex-col">
{groups.map(({ group, items }, idx) => (
<div key={group} className={cn("flex flex-col", idx > 0 && "mt-4")}>
<div className="px-6 pt-2 pb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{NAV_GROUP_LABELS[group]}
</div>
{items.map((item) => (
<SidebarLink key={item.href} item={item} active={isActive(pathname, item.href)} />
))}
</div>
))}
</nav>
</aside>
);
}
function isActive(pathname: string, href: string): boolean {
return pathname === href || pathname.startsWith(`${href}/`);
}
function SidebarLink({ item, active }: { item: NavItem; active: boolean }) {
const Icon = item.icon;
return (
<Link
href={item.href}
aria-current={active ? "page" : undefined}
className={cn(
"relative flex h-11 items-center gap-3 px-6 text-sm font-medium transition",
active
? "bg-[var(--sidebar-active)] font-semibold text-brand"
: "text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground",
)}
>
<Icon className="h-5 w-5 stroke-[1.75]" />
<span className="truncate">{item.label}</span>
{active && <span className="absolute top-2 right-0 bottom-2 w-1 rounded-l-full bg-brand" />}
</Link>
);
}