feat: add app shell with sidebar navigation, routing, and placeholder pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 14:41:59 +05:30
parent dc68577477
commit 2984545dde
10 changed files with 260 additions and 4 deletions

View File

@@ -0,0 +1,126 @@
import type { FC, HTMLAttributes } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBullhorn,
faChartMixed,
faCommentDots,
faGear,
faGrid2,
faPlug,
} from "@fortawesome/pro-regular-svg-icons";
import { MobileNavigationHeader } from "@/components/application/app-navigation/base-components/mobile-header";
import { NavAccountCard } from "@/components/application/app-navigation/base-components/nav-account-card";
import { NavItemBase } from "@/components/application/app-navigation/base-components/nav-item";
import type { NavItemType } from "@/components/application/app-navigation/config";
// TODO: Wire to useAuth() once auth-provider.tsx is implemented
const isAdmin = false;
const MAIN_SIDEBAR_WIDTH = 292;
// FontAwesome icon wrappers that satisfy FC<HTMLAttributes<HTMLOrSVGElement>>
const IconGrid2: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faGrid2} className={className} />
);
const IconBullhorn: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faBullhorn} className={className} />
);
const IconCommentDots: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faCommentDots} className={className} />
);
const IconChartMixed: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faChartMixed} className={className} />
);
const IconPlug: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faPlug} className={className} />
);
const IconGear: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faGear} className={className} />
);
const mainItems: NavItemType[] = [
{ label: "Lead Workspace", href: "/", icon: IconGrid2 },
{ label: "Campaigns", href: "/campaigns", icon: IconBullhorn },
{ label: "Outreach", href: "/outreach", icon: IconCommentDots },
];
const insightsItems: NavItemType[] = [
{ label: "Analytics", href: "/analytics", icon: IconChartMixed },
];
const adminItems: NavItemType[] = [
{ label: "Integrations", href: "/integrations", icon: IconPlug },
{ label: "Settings", href: "/settings", icon: IconGear },
];
interface SidebarProps {
activeUrl?: string;
}
export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
const navSections = [
{ label: "Main", items: mainItems },
{ label: "Insights", items: insightsItems },
...(isAdmin ? [{ label: "Admin", items: adminItems }] : []),
];
const content = (
<aside
style={{ "--width": `${MAIN_SIDEBAR_WIDTH}px` } as React.CSSProperties}
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
>
{/* Logo */}
<div className="flex flex-col gap-1 px-4 lg:px-5">
<span className="text-md font-bold text-primary">Helix Engage</span>
<span className="text-xs text-tertiary">Ramaiah Memorial Hospital</span>
</div>
{/* Nav sections */}
<ul className="mt-8">
{navSections.map((group) => (
<li key={group.label}>
<div className="px-5 pb-1">
<p className="text-xs font-bold text-quaternary uppercase">{group.label}</p>
</div>
<ul className="px-4 pb-5">
{group.items.map((item) => (
<li key={item.label} className="py-0.5">
<NavItemBase
icon={item.icon}
href={item.href}
badge={item.badge}
type="link"
current={item.href === activeUrl}
>
{item.label}
</NavItemBase>
</li>
))}
</ul>
</li>
))}
</ul>
{/* Account card */}
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
<NavAccountCard />
</div>
</aside>
);
return (
<>
{/* Mobile header navigation */}
<MobileNavigationHeader>{content}</MobileNavigationHeader>
{/* Desktop sidebar navigation */}
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
<div
style={{ paddingLeft: MAIN_SIDEBAR_WIDTH + 4 }}
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
/>
</>
);
};