applayout added, sidebar updated
This commit is contained in:
25
apps/Frontend/src/components/layout/app-layout.tsx
Normal file
25
apps/Frontend/src/components/layout/app-layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SidebarProvider defaultOpen>
|
||||
<div className="flex flex-col h-screen">
|
||||
{/* Fixed top bar */}
|
||||
<TopAppBar />
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex flex-1 pt-16 min-h-0 bg-gray-100">
|
||||
{/* Sidebar (collapsible on mobile) */}
|
||||
<Sidebar />
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 min-w-0 min-h-0 overflow-y-auto p-4">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -9,96 +9,115 @@ import {
|
||||
CreditCard,
|
||||
FolderOpen,
|
||||
Database,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
|
||||
interface SidebarProps {
|
||||
isMobileOpen: boolean;
|
||||
setIsMobileOpen: (open: boolean) => void;
|
||||
}
|
||||
const WIDTH_ANIM_MS = 100;
|
||||
|
||||
export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
|
||||
export function Sidebar() {
|
||||
const [location] = useLocation();
|
||||
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/",
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Appointments",
|
||||
path: "/appointments",
|
||||
icon: <Calendar className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Patients",
|
||||
path: "/patients",
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Insurance Eligibility",
|
||||
path: "/insurance-eligibility",
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Claims",
|
||||
path: "/claims",
|
||||
icon: <FileCheck className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Payments",
|
||||
path: "/payments",
|
||||
icon: <CreditCard className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Documents",
|
||||
path: "/documents",
|
||||
icon: <FolderOpen className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Backup Database",
|
||||
path: "/database-management",
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
// Delay label visibility until the width animation completes
|
||||
const [showLabels, setShowLabels] = useState(state !== "collapsed");
|
||||
useEffect(() => {
|
||||
let timer: number | undefined;
|
||||
|
||||
if (state === "expanded") {
|
||||
timer = window.setTimeout(() => setShowLabels(true), WIDTH_ANIM_MS);
|
||||
} else {
|
||||
setShowLabels(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
const navItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/",
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Appointments",
|
||||
path: "/appointments",
|
||||
icon: <Calendar className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Patients",
|
||||
path: "/patients",
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Insurance Eligibility",
|
||||
path: "/insurance-eligibility",
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Claims",
|
||||
path: "/claims",
|
||||
icon: <FileCheck className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Payments",
|
||||
path: "/payments",
|
||||
icon: <CreditCard className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Documents",
|
||||
path: "/documents",
|
||||
icon: <FolderOpen className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
path: "/reports",
|
||||
icon: <FileText className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Backup Database",
|
||||
path: "/database-management",
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white w-64 border-r border-gray-200 shadow-sm z-10 fixed h-full md:static",
|
||||
isMobileOpen ? "block" : "hidden md:block"
|
||||
// original look
|
||||
"bg-white border-r border-gray-200 shadow-sm z-20",
|
||||
// clip during width animation to avoid text peeking
|
||||
"overflow-hidden will-change-[width]",
|
||||
// animate width only
|
||||
"transition-[width] duration-200 ease-in-out",
|
||||
// MOBILE: overlay below topbar (h = 100vh - 4rem)
|
||||
openMobile
|
||||
? "fixed top-16 left-0 h-[calc(100vh-4rem)] w-64 block md:hidden"
|
||||
: "hidden md:block",
|
||||
// DESKTOP: participates in row layout
|
||||
"md:static md:top-auto md:h-auto md:flex-shrink-0",
|
||||
state === "collapsed" ? "md:w-16" : "md:w-64"
|
||||
)}
|
||||
>
|
||||
<div className="p-4 border-b border-gray-200 flex items-center space-x-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-5 w-5 text-primary"
|
||||
>
|
||||
<path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" />
|
||||
<path d="M19 14v-4a7 7 0 0 0-14 0v4" />
|
||||
<path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" />
|
||||
</svg>
|
||||
<h1 className="text-lg font-medium text-primary">DentalConnect</h1>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
<nav>
|
||||
<nav role="navigation" aria-label="Main">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.path}>
|
||||
<Link to={item.path} onClick={() => setIsMobileOpen(false)}>
|
||||
<Link to={item.path} onClick={() => setOpenMobile(false)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer",
|
||||
@@ -108,7 +127,12 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
{/* show label only after expand animation completes */}
|
||||
{showLabels && (
|
||||
<span className="whitespace-nowrap select-none">
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Bell, Menu } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -11,47 +10,45 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useLocation } from "wouter";
|
||||
import { NotificationsBell } from "@/components/layout/notification-bell";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
||||
interface TopAppBarProps {
|
||||
toggleMobileMenu: () => void;
|
||||
}
|
||||
|
||||
export function TopAppBar({ toggleMobileMenu }: TopAppBarProps) {
|
||||
export function TopAppBar() {
|
||||
const { user, logoutMutation } = useAuth();
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
const handleLogout = () => {
|
||||
logoutMutation.mutate();
|
||||
};
|
||||
|
||||
const getInitials = (username: string) => {
|
||||
return username.substring(0, 2).toUpperCase();
|
||||
};
|
||||
const handleLogout = () => logoutMutation.mutate();
|
||||
const getInitials = (username: string) =>
|
||||
username.substring(0, 2).toUpperCase();
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm z-10">
|
||||
<header className="bg-white shadow-sm z-30 fixed top-0 left-0 right-0">
|
||||
<div className="flex items-center justify-between h-16 px-4">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden mr-2"
|
||||
onClick={toggleMobileMenu}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="md:hidden text-lg font-medium text-primary">
|
||||
DentalConnect
|
||||
</h1>
|
||||
</div>
|
||||
{/* both desktop + mobile triggers */}
|
||||
<SidebarTrigger className="mr-2" />
|
||||
|
||||
<div className="hidden md:flex md:flex-1 items-center justify-center">
|
||||
{/* Search bar removed */}
|
||||
<div className="p-4 border-gray-200 flex items-center space-x-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-5 w-5 text-primary"
|
||||
>
|
||||
<path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" />
|
||||
<path d="M19 14v-4a7 7 0 0 0-14 0v4" />
|
||||
<path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" />
|
||||
</svg>
|
||||
|
||||
<h1 className="text-lg font-medium text-primary">DentalConnect</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<NotificationsBell />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user