246 lines
8.5 KiB
TypeScript
246 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import {
|
|
LayoutDashboard,
|
|
Activity,
|
|
Calendar,
|
|
Kanban,
|
|
FolderKanban,
|
|
FileText,
|
|
Wrench,
|
|
Target,
|
|
Menu,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
} from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/sheet";
|
|
import { cn } from "@/lib/utils";
|
|
import { Breadcrumbs } from "./breadcrumbs";
|
|
import { QuickSearch } from "./quick-search";
|
|
|
|
const navItems = [
|
|
{ name: "Dashboard", href: "/", icon: LayoutDashboard },
|
|
{ name: "Activity", href: "/activity", icon: Activity },
|
|
{ name: "Calendar", href: "/calendar", icon: Calendar },
|
|
{ name: "Tasks", href: "/tasks", icon: Kanban },
|
|
{ name: "Projects", href: "/projects", icon: FolderKanban },
|
|
{ name: "Documents", href: "/documents", icon: FileText },
|
|
{ name: "Tools", href: "/tools", icon: Wrench },
|
|
{ name: "Mission", href: "/mission", icon: Target },
|
|
];
|
|
|
|
const MISSION_SHORT = "Build an iOS empire → retire on our terms → travel with Heidi → 53 is just the start";
|
|
|
|
function LogoutButton({ collapsed }: { collapsed: boolean }) {
|
|
const router = useRouter();
|
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
|
|
const handleLogout = async () => {
|
|
setIsLoggingOut(true);
|
|
try {
|
|
await fetch("/api/auth/logout", { method: "POST" });
|
|
router.push("/login");
|
|
} catch {
|
|
// Force redirect even if API call fails
|
|
router.push("/login");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleLogout}
|
|
disabled={isLoggingOut}
|
|
className={cn(
|
|
"flex items-center justify-center rounded-lg transition-colors",
|
|
"text-muted-foreground hover:text-destructive hover:bg-destructive/10",
|
|
collapsed ? "w-8 h-8" : "px-3 py-2 gap-2"
|
|
)}
|
|
title="Logout"
|
|
aria-label="Logout"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
{!collapsed && <span className="text-sm">Logout</span>}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function SidebarContent({ pathname, collapsed = false }: { pathname: string; collapsed?: boolean }) {
|
|
return (
|
|
<div className="flex flex-col h-full overflow-y-auto">
|
|
<div className={cn("flex items-center gap-2 py-6", collapsed ? "px-2 justify-center" : "px-4")}>
|
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shrink-0">
|
|
<span className="text-white font-bold text-sm">MC</span>
|
|
</div>
|
|
{!collapsed && <span className="font-bold text-lg">Mission Control</span>}
|
|
</div>
|
|
|
|
{/* Mission Statement - hide when collapsed */}
|
|
{!collapsed && (
|
|
<div className="px-4 py-3 mx-3 mb-2 rounded-lg bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-500/20">
|
|
<p className="text-[10px] text-muted-foreground leading-tight">
|
|
{MISSION_SHORT}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<nav className={cn("flex-1 py-4 space-y-1", collapsed ? "px-2" : "px-3")} aria-label="Main navigation">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
title={collapsed ? item.name : undefined}
|
|
aria-current={isActive ? "page" : undefined}
|
|
className={cn(
|
|
"flex items-center rounded-lg text-sm font-medium transition-colors",
|
|
collapsed
|
|
? "justify-center px-2 py-3"
|
|
: "gap-3 px-3 py-2",
|
|
isActive
|
|
? "bg-primary text-primary-foreground"
|
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0" aria-hidden="true" />
|
|
{!collapsed && <span>{item.name}</span>}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className={cn("border-t border-border", collapsed ? "p-2" : "p-4")}>
|
|
<div className={cn("flex items-center", collapsed ? "justify-center" : "gap-3")}>
|
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-green-400 to-blue-500 shrink-0" />
|
|
{!collapsed && (
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">Matt Bruce</p>
|
|
<p className="text-xs text-muted-foreground truncate">TopDogLabs</p>
|
|
</div>
|
|
)}
|
|
<LogoutButton collapsed={collapsed} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MobileSidebar({ pathname }: { pathname: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetTrigger asChild className="lg:hidden">
|
|
<Button variant="ghost" size="icon" className="shrink-0" aria-label="Open navigation menu">
|
|
<Menu className="w-5 h-5" aria-hidden="true" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-64 p-0 h-full overflow-y-auto">
|
|
<SheetTitle className="sr-only">Navigation Menu</SheetTitle>
|
|
<SidebarContent pathname={pathname} />
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|
|
|
|
interface DashboardLayoutProps {
|
|
children: React.ReactNode;
|
|
showBreadcrumbs?: boolean;
|
|
}
|
|
|
|
export function DashboardLayout({ children, showBreadcrumbs = true }: DashboardLayoutProps) {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
const pathname = usePathname();
|
|
|
|
// Check for mobile on mount and resize
|
|
useEffect(() => {
|
|
const checkMobile = () => {
|
|
setIsMobile(window.innerWidth < 1024);
|
|
// Auto-collapse on smaller screens but not mobile
|
|
if (window.innerWidth < 1280 && window.innerWidth >= 1024) {
|
|
setCollapsed(true);
|
|
}
|
|
};
|
|
|
|
checkMobile();
|
|
window.addEventListener("resize", checkMobile);
|
|
return () => window.removeEventListener("resize", checkMobile);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-screen bg-background flex overflow-hidden">
|
|
{/* Desktop Sidebar - Collapsible */}
|
|
<div
|
|
className={cn(
|
|
"hidden lg:flex flex-col fixed inset-y-0 left-0 border-r border-border bg-card transition-all duration-300 z-40 h-screen",
|
|
collapsed ? "w-16" : "w-64"
|
|
)}
|
|
>
|
|
{/* Collapse Toggle Button */}
|
|
<button
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className={cn(
|
|
"absolute -right-4 top-8 w-8 h-8 rounded-full border-2 border-blue-500 bg-blue-500 text-white shadow-lg",
|
|
"hover:bg-blue-600 hover:scale-110 transition-all z-50 flex items-center justify-center"
|
|
)}
|
|
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
aria-expanded={!collapsed}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="w-5 h-5" aria-hidden="true" />
|
|
) : (
|
|
<ChevronLeft className="w-5 h-5" aria-hidden="true" />
|
|
)}
|
|
</button>
|
|
|
|
<SidebarContent pathname={pathname} collapsed={collapsed} />
|
|
</div>
|
|
|
|
<main
|
|
className={cn(
|
|
"flex-1 h-screen overflow-y-auto transition-all duration-300",
|
|
collapsed ? "lg:pl-16" : "lg:pl-64"
|
|
)}
|
|
>
|
|
<div className="min-h-full">
|
|
{/* Top Bar - Mobile header with search */}
|
|
<div className="sticky top-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border lg:border-none lg:bg-transparent lg:static lg:backdrop-blur-none">
|
|
<div className="flex items-center justify-between gap-4 p-4 lg:hidden">
|
|
<div className="flex items-center gap-3">
|
|
<MobileSidebar pathname={pathname} />
|
|
<span className="font-bold text-lg">Mission Control</span>
|
|
</div>
|
|
<QuickSearch />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="p-4 lg:p-8">
|
|
{/* Desktop Search Bar */}
|
|
<div className="hidden lg:flex items-center justify-end gap-4 mb-6">
|
|
<QuickSearch />
|
|
</div>
|
|
|
|
{/* Breadcrumbs */}
|
|
{showBreadcrumbs && <Breadcrumbs />}
|
|
|
|
{/* Page Content */}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Note: Use DashboardLayout for all pages - Sidebar export removed to prevent duplicate menu icons
|