mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
18
src/components/layout/app-shell.tsx
Normal file
18
src/components/layout/app-shell.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { Sidebar } from "./sidebar";
|
||||||
|
|
||||||
|
interface AppShellProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppShell = ({ children }: AppShellProps) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-primary">
|
||||||
|
<Sidebar activeUrl={pathname} />
|
||||||
|
<main className="flex flex-1 flex-col overflow-auto">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
126
src/components/layout/sidebar.tsx
Normal file
126
src/components/layout/sidebar.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
src/components/layout/top-bar.tsx
Normal file
30
src/components/layout/top-bar.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
|
||||||
|
interface TopBarProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TopBar = ({ title, subtitle }: TopBarProps) => {
|
||||||
|
return (
|
||||||
|
<header className="flex h-16 items-center justify-between border-b border-secondary bg-primary px-6">
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h1 className="text-display-xs font-bold text-primary">{title}</h1>
|
||||||
|
{subtitle && <p className="text-sm text-tertiary">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-64">
|
||||||
|
<Input
|
||||||
|
placeholder="Search..."
|
||||||
|
icon={SearchLg}
|
||||||
|
aria-label="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Avatar initials="SM" size="sm" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
src/main.tsx
25
src/main.tsx
@@ -1,8 +1,14 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router";
|
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
|
||||||
import { HomeScreen } from "@/pages/home-screen";
|
import { AppShell } from "@/components/layout/app-shell";
|
||||||
import { NotFound } from "@/pages/not-found";
|
import { NotFound } from "@/pages/not-found";
|
||||||
|
import { AllLeadsPage } from "@/pages/all-leads";
|
||||||
|
import { CampaignDetailPage } from "@/pages/campaign-detail";
|
||||||
|
import { CampaignsPage } from "@/pages/campaigns";
|
||||||
|
import { LeadWorkspacePage } from "@/pages/lead-workspace";
|
||||||
|
import { LoginPage } from "@/pages/login";
|
||||||
|
import { OutreachPage } from "@/pages/outreach";
|
||||||
import { RouteProvider } from "@/providers/router-provider";
|
import { RouteProvider } from "@/providers/router-provider";
|
||||||
import { ThemeProvider } from "@/providers/theme-provider";
|
import { ThemeProvider } from "@/providers/theme-provider";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
@@ -13,8 +19,21 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<RouteProvider>
|
<RouteProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomeScreen />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<AppShell>
|
||||||
|
<Outlet />
|
||||||
|
</AppShell>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route path="/" element={<LeadWorkspacePage />} />
|
||||||
|
<Route path="/leads" element={<AllLeadsPage />} />
|
||||||
|
<Route path="/campaigns" element={<CampaignsPage />} />
|
||||||
|
<Route path="/campaigns/:id" element={<CampaignDetailPage />} />
|
||||||
|
<Route path="/outreach" element={<OutreachPage />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</RouteProvider>
|
</RouteProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
12
src/pages/all-leads.tsx
Normal file
12
src/pages/all-leads.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
|
|
||||||
|
export const AllLeadsPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<TopBar title="All Leads" />
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8">
|
||||||
|
<p className="text-tertiary">All Leads — coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/pages/campaign-detail.tsx
Normal file
12
src/pages/campaign-detail.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
|
|
||||||
|
export const CampaignDetailPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<TopBar title="Campaign Detail" />
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8">
|
||||||
|
<p className="text-tertiary">Campaign Detail — coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/pages/campaigns.tsx
Normal file
12
src/pages/campaigns.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
|
|
||||||
|
export const CampaignsPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<TopBar title="Campaigns" />
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8">
|
||||||
|
<p className="text-tertiary">Campaigns — coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/pages/lead-workspace.tsx
Normal file
12
src/pages/lead-workspace.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
|
|
||||||
|
export const LeadWorkspacePage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<TopBar title="Lead Workspace" subtitle="Ramaiah Memorial Hospital · Last 24 hours" />
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8">
|
||||||
|
<p className="text-tertiary">Lead Workspace — coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
src/pages/login.tsx
Normal file
3
src/pages/login.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const LoginPage = () => {
|
||||||
|
return <div>Login placeholder</div>;
|
||||||
|
};
|
||||||
12
src/pages/outreach.tsx
Normal file
12
src/pages/outreach.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
|
|
||||||
|
export const OutreachPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<TopBar title="Outreach" />
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8">
|
||||||
|
<p className="text-tertiary">Outreach — coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user