feat(dashboard): refactor navigation items to use NavItem component and enhance styling
This commit is contained in:
parent
2d91325937
commit
f0f53bcc73
|
|
@ -2,16 +2,17 @@
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Bot,
|
Bot,
|
||||||
Boxes,
|
Boxes,
|
||||||
|
Building2,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
CircleDot,
|
||||||
Folder,
|
Folder,
|
||||||
FolderGit,
|
FolderGit,
|
||||||
CircleDot,
|
|
||||||
Building2,
|
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Network,
|
Network,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -19,15 +20,85 @@ import {
|
||||||
Tags,
|
Tags,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useAuth } from "@/auth/clerk";
|
|
||||||
import { ApiError } from "@/api/mutator";
|
import { ApiError } from "@/api/mutator";
|
||||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
|
||||||
import {
|
import {
|
||||||
type healthzHealthzGetResponse,
|
type healthzHealthzGetResponse,
|
||||||
useHealthzHealthzGet,
|
useHealthzHealthzGet,
|
||||||
} from "@/api/generated/default/default";
|
} from "@/api/generated/default/default";
|
||||||
|
import { useAuth } from "@/auth/clerk";
|
||||||
|
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type NavTone = "blue" | "cyan" | "emerald" | "violet" | "amber" | "rose";
|
||||||
|
|
||||||
|
const iconToneClass: Record<NavTone, string> = {
|
||||||
|
blue: "bg-blue-500/15 text-blue-300 ring-blue-400/20",
|
||||||
|
cyan: "bg-cyan-500/15 text-cyan-300 ring-cyan-400/20",
|
||||||
|
emerald: "bg-emerald-500/15 text-emerald-300 ring-emerald-400/20",
|
||||||
|
violet: "bg-violet-500/15 text-violet-300 ring-violet-400/20",
|
||||||
|
amber: "bg-amber-500/15 text-amber-300 ring-amber-400/20",
|
||||||
|
rose: "bg-rose-500/15 text-rose-300 ring-rose-400/20",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionHeaderClass =
|
||||||
|
"px-3 text-[13px] font-bold uppercase tracking-[0.18em] text-[color:var(--text)]";
|
||||||
|
|
||||||
|
const navItemClass = (active: boolean) =>
|
||||||
|
cn(
|
||||||
|
"group relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-[15px] font-semibold text-[color:var(--text-muted)] transition duration-200",
|
||||||
|
"before:absolute before:inset-y-2 before:left-0 before:w-1 before:rounded-full before:bg-transparent before:transition",
|
||||||
|
active
|
||||||
|
? "bg-[color:var(--accent-soft)] text-[color:var(--text)] shadow-sm before:bg-[color:var(--accent)]"
|
||||||
|
: "hover:bg-[color:var(--surface-muted)] hover:text-[color:var(--text)] hover:before:bg-[color:var(--border-strong)]",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface)]",
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconClass = (active: boolean, tone: NavTone) =>
|
||||||
|
cn(
|
||||||
|
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md ring-1 transition duration-200",
|
||||||
|
iconToneClass[tone],
|
||||||
|
active &&
|
||||||
|
"bg-[color:var(--accent)] text-[color:var(--primary-foreground)] ring-[color:var(--accent)]",
|
||||||
|
!active && "group-hover:scale-105 group-hover:ring-[color:var(--accent)]",
|
||||||
|
);
|
||||||
|
|
||||||
|
function isNavActive(pathname: string, href: string) {
|
||||||
|
if (href === "/git-projects") {
|
||||||
|
return (
|
||||||
|
pathname === href ||
|
||||||
|
pathname.startsWith("/git-projects/connections") ||
|
||||||
|
pathname.startsWith("/git-projects/repositories")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathname === href || pathname.startsWith(`${href}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavItem({
|
||||||
|
href,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
tone,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
tone: NavTone;
|
||||||
|
active: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={navItemClass(active)}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<span className={iconClass(active, tone)}>{icon}</span>
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardSidebar() {
|
export function DashboardSidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { isSignedIn } = useAuth();
|
const { isSignedIn } = useAuth();
|
||||||
|
|
@ -58,177 +129,162 @@ export function DashboardSidebar() {
|
||||||
: systemStatus === "unknown"
|
: systemStatus === "unknown"
|
||||||
? "System status unavailable"
|
? "System status unavailable"
|
||||||
: "System degraded";
|
: "System degraded";
|
||||||
const navItemClass = (active: boolean) =>
|
const isActive = (href: string) => isNavActive(pathname, href);
|
||||||
cn(
|
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-[color:var(--text-muted)] transition",
|
|
||||||
active
|
|
||||||
? "bg-[color:var(--accent-soft)] font-medium text-[color:var(--accent-strong)]"
|
|
||||||
: "hover:bg-[color:var(--surface-muted)] hover:text-[color:var(--text)]",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="fixed inset-y-0 left-0 z-40 flex w-[280px] -translate-x-full flex-col border-r border-[color:var(--border)] bg-[color:var(--surface)] pt-16 shadow-lg transition-transform duration-200 ease-in-out [[data-sidebar=open]_&]:translate-x-0 md:relative md:inset-auto md:z-auto md:w-[260px] md:translate-x-0 md:pt-0 md:shadow-none md:transition-none">
|
<aside className="fixed inset-y-0 left-0 z-40 flex w-[280px] -translate-x-full flex-col border-r border-[color:var(--border)] bg-[linear-gradient(180deg,var(--surface)_0%,var(--surface-muted)_100%)] pt-16 shadow-lg transition-transform duration-200 ease-in-out [[data-sidebar=open]_&]:translate-x-0 md:relative md:inset-auto md:z-auto md:w-[260px] md:translate-x-0 md:pt-0 md:shadow-none md:transition-none">
|
||||||
<div className="flex-1 px-3 py-4">
|
<div className="flex-1 overflow-y-auto px-3 py-5">
|
||||||
<p className="px-3 text-xs font-semibold uppercase tracking-wider text-[color:var(--text-muted)]">
|
<p className="px-3 font-heading text-lg font-bold text-[color:var(--text)]">
|
||||||
Navigation
|
Navigation
|
||||||
</p>
|
</p>
|
||||||
<nav className="mt-3 space-y-4 text-sm">
|
<nav className="mt-5 space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
|
<p className={sectionHeaderClass}>Overview</p>
|
||||||
Overview
|
<div className="mt-2 space-y-1.5">
|
||||||
</p>
|
<NavItem
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
<Link
|
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className={navItemClass(pathname === "/dashboard")}
|
label="Dashboard"
|
||||||
>
|
icon={<BarChart3 className="h-4 w-4" />}
|
||||||
<BarChart3 className="h-4 w-4" />
|
tone="blue"
|
||||||
Dashboard
|
active={isActive("/dashboard")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/activity"
|
href="/activity"
|
||||||
className={navItemClass(pathname === "/activity")}
|
label="Live feed"
|
||||||
>
|
icon={<Activity className="h-4 w-4" />}
|
||||||
<Activity className="h-4 w-4" />
|
tone="cyan"
|
||||||
Live feed
|
active={isActive("/activity")}
|
||||||
</Link>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
|
<p className={sectionHeaderClass}>Boards</p>
|
||||||
Boards
|
<div className="mt-2 space-y-1.5">
|
||||||
</p>
|
<NavItem
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
<Link
|
|
||||||
href="/board-groups"
|
href="/board-groups"
|
||||||
className={navItemClass(pathname === "/board-groups")}
|
label="Board groups"
|
||||||
>
|
icon={<Folder className="h-4 w-4" />}
|
||||||
<Folder className="h-4 w-4" />
|
tone="emerald"
|
||||||
Board groups
|
active={isActive("/board-groups")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/boards"
|
href="/boards"
|
||||||
className={navItemClass(pathname === "/boards")}
|
label="Boards"
|
||||||
>
|
icon={<LayoutGrid className="h-4 w-4" />}
|
||||||
<LayoutGrid className="h-4 w-4" />
|
tone="blue"
|
||||||
Boards
|
active={isActive("/boards")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/git-projects"
|
href="/git-projects"
|
||||||
className={navItemClass(pathname === "/git-projects")}
|
label="Git projects"
|
||||||
>
|
icon={<FolderGit className="h-4 w-4" />}
|
||||||
<FolderGit className="h-4 w-4" />
|
tone="violet"
|
||||||
Git Projects
|
active={isActive("/git-projects")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/git-projects/issues"
|
href="/git-projects/issues"
|
||||||
className={navItemClass(pathname === "/git-projects/issues")}
|
label="Issues"
|
||||||
>
|
icon={<CircleDot className="h-4 w-4" />}
|
||||||
<CircleDot className="h-4 w-4" />
|
tone="amber"
|
||||||
Issues
|
active={isActive("/git-projects/issues")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/tags"
|
href="/tags"
|
||||||
className={navItemClass(pathname === "/tags")}
|
label="Tags"
|
||||||
>
|
icon={<Tags className="h-4 w-4" />}
|
||||||
<Tags className="h-4 w-4" />
|
tone="cyan"
|
||||||
Tags
|
active={isActive("/tags")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/approvals"
|
href="/approvals"
|
||||||
className={navItemClass(pathname === "/approvals")}
|
label="Approvals"
|
||||||
>
|
icon={<CheckCircle2 className="h-4 w-4" />}
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
tone="emerald"
|
||||||
Approvals
|
active={isActive("/approvals")}
|
||||||
</Link>
|
/>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Link
|
<NavItem
|
||||||
href="/custom-fields"
|
href="/custom-fields"
|
||||||
className={navItemClass(pathname === "/custom-fields")}
|
label="Custom fields"
|
||||||
>
|
icon={<Settings className="h-4 w-4" />}
|
||||||
<Settings className="h-4 w-4" />
|
tone="rose"
|
||||||
Custom fields
|
active={isActive("/custom-fields")}
|
||||||
</Link>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{isAdmin ? (
|
||||||
{isAdmin ? (
|
<div>
|
||||||
<>
|
<p className={sectionHeaderClass}>Skills</p>
|
||||||
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
|
<div className="mt-2 space-y-1.5">
|
||||||
Skills
|
<NavItem
|
||||||
</p>
|
href="/skills/marketplace"
|
||||||
<div className="mt-1 space-y-1">
|
label="Marketplace"
|
||||||
<Link
|
icon={<Store className="h-4 w-4" />}
|
||||||
href="/skills/marketplace"
|
tone="violet"
|
||||||
className={navItemClass(pathname === "/skills/marketplace")}
|
active={isActive("/skills/marketplace")}
|
||||||
>
|
/>
|
||||||
<Store className="h-4 w-4" />
|
<NavItem
|
||||||
Marketplace
|
href="/skills/packs"
|
||||||
</Link>
|
label="Packs"
|
||||||
<Link
|
icon={<Boxes className="h-4 w-4" />}
|
||||||
href="/skills/packs"
|
tone="cyan"
|
||||||
className={navItemClass(pathname === "/skills/packs")}
|
active={isActive("/skills/packs")}
|
||||||
>
|
/>
|
||||||
<Boxes className="h-4 w-4" />
|
</div>
|
||||||
Packs
|
</div>
|
||||||
</Link>
|
) : null}
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
|
<p className={sectionHeaderClass}>Administration</p>
|
||||||
Administration
|
<div className="mt-2 space-y-1.5">
|
||||||
</p>
|
<NavItem
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
<Link
|
|
||||||
href="/organization"
|
href="/organization"
|
||||||
className={navItemClass(pathname === "/organization")}
|
label="Organization"
|
||||||
>
|
icon={<Building2 className="h-4 w-4" />}
|
||||||
<Building2 className="h-4 w-4" />
|
tone="blue"
|
||||||
Organization
|
active={isActive("/organization")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<NavItem
|
||||||
href="/settings"
|
href="/settings"
|
||||||
className={navItemClass(pathname === "/settings")}
|
label="Settings"
|
||||||
>
|
icon={<Settings className="h-4 w-4" />}
|
||||||
<Settings className="h-4 w-4" />
|
tone="amber"
|
||||||
Settings
|
active={isActive("/settings")}
|
||||||
</Link>
|
/>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Link
|
<NavItem
|
||||||
href="/gateways"
|
href="/gateways"
|
||||||
className={navItemClass(pathname === "/gateways")}
|
label="Gateways"
|
||||||
>
|
icon={<Network className="h-4 w-4" />}
|
||||||
<Network className="h-4 w-4" />
|
tone="emerald"
|
||||||
Gateways
|
active={isActive("/gateways")}
|
||||||
</Link>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Link
|
<NavItem
|
||||||
href="/agents"
|
href="/agents"
|
||||||
className={navItemClass(pathname === "/agents")}
|
label="Agents"
|
||||||
>
|
icon={<Bot className="h-4 w-4" />}
|
||||||
<Bot className="h-4 w-4" />
|
tone="violet"
|
||||||
Agents
|
active={isActive("/agents")}
|
||||||
</Link>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-[color:var(--border)] p-4">
|
<div className="border-t border-[color:var(--border)] bg-[color:var(--surface)]/70 p-4">
|
||||||
<div className="flex items-center gap-2 text-xs text-[color:var(--text-muted)]">
|
<div className="flex items-center gap-2 text-xs font-medium text-[color:var(--text-muted)]">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-2 w-2 rounded-full",
|
"h-2.5 w-2.5 rounded-full shadow-[0_0_18px_currentColor]",
|
||||||
systemStatus === "operational" && "bg-emerald-500",
|
systemStatus === "operational" && "bg-emerald-500 text-emerald-500",
|
||||||
systemStatus === "degraded" && "bg-rose-500",
|
systemStatus === "degraded" && "bg-rose-500 text-rose-500",
|
||||||
systemStatus === "unknown" && "bg-[color:var(--text-quiet)]",
|
systemStatus === "unknown" &&
|
||||||
|
"bg-[color:var(--text-quiet)] text-[color:var(--text-quiet)]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue