Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions frontend/components/layout/DashboardAuthGuard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { usePathname, useRouter } from 'next/navigation';
import { Sidebar } from '@/components/layout/Sidebar';
import { Header } from '@/components/layout/Header';
import { ErrorBoundary } from '@/components/errors/ErrorBoundary';
import { SidebarProvider, useSidebar } from '@/contexts/SidebarContext';
import { cn } from '@/lib/utils';

export function DashboardAuthGuard({ children }: { children: React.ReactNode }) {
function DashboardContent({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const router = useRouter();
const pathname = usePathname();
const { isCollapsed } = useSidebar();

const mainRef = React.useRef<HTMLDivElement>(null);
const scrollPositions = React.useRef<Record<string, number>>({});
Expand Down Expand Up @@ -43,7 +46,7 @@ export function DashboardAuthGuard({ children }: { children: React.ReactNode })
return (
<div className="flex h-screen bg-gray-50">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden lg:ml-64">
<div className={cn('flex-1 flex flex-col overflow-hidden transition-all duration-200', isCollapsed ? 'lg:ml-20' : 'lg:ml-64')}>
<Header />
<main ref={mainRef} className="flex-1 overflow-y-auto p-4 sm:p-6">
<ErrorBoundary context="dashboard-page" resetKey={pathname}>
Expand All @@ -54,3 +57,11 @@ export function DashboardAuthGuard({ children }: { children: React.ReactNode })
</div>
);
}

export function DashboardAuthGuard({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<DashboardContent>{children}</DashboardContent>
</SidebarProvider>
);
}
61 changes: 40 additions & 21 deletions frontend/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { LayoutDashboard, Folder, FileText, Wallet, Scale, Menu, X, QrCode, ShieldCheck } from 'lucide-react';
import { LayoutDashboard, Folder, FileText, Wallet, Scale, Menu, X, QrCode, ShieldCheck, ChevronLeft, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useSidebar } from '@/contexts/SidebarContext';

const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
Expand All @@ -21,6 +22,7 @@ export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const [isMobileOpen, setIsMobileOpen] = useState(false);
const { isCollapsed, setIsCollapsed } = useSidebar();

return (
<>
Expand All @@ -45,25 +47,38 @@ export function Sidebar() {
role="navigation"
aria-label="Main navigation"
className={cn(
'fixed inset-y-0 left-0 z-40 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out lg:translate-x-0',
'fixed inset-y-0 left-0 z-40 bg-white border-r border-gray-200 transform transition-all duration-200 ease-in-out lg:translate-x-0',
isCollapsed ? 'w-20' : 'w-64',
isMobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
)}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center gap-2 px-6 py-6 border-b border-gray-200">
<div
className="w-8 h-8 rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 flex items-center justify-center"
role="img"
aria-label="AgenticPay logo"
>
<Wallet className="h-5 w-5 text-white" aria-hidden="true" />
<div className={cn('flex items-center gap-2 px-6 py-6 border-b border-gray-200 justify-between', isCollapsed && 'px-3')}>
<div className="flex items-center gap-2 min-w-0">
<div
className="w-8 h-8 rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 flex items-center justify-center flex-shrink-0"
role="img"
aria-label="AgenticPay logo"
>
<Wallet className="h-5 w-5 text-white" aria-hidden="true" />
</div>
{!isCollapsed && <span className="text-xl font-bold text-gray-900 truncate">AgenticPay</span>}
</div>
<span className="text-xl font-bold text-gray-900">AgenticPay</span>
<Button
variant="ghost"
size="icon"
className="hidden lg:flex"
onClick={() => setIsCollapsed(!isCollapsed)}
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
aria-expanded={!isCollapsed}
>
{isCollapsed ? <ChevronRight className="h-5 w-5" /> : <ChevronLeft className="h-5 w-5" />}
</Button>
</div>

{/* Navigation */}
<nav className="flex-1 px-4 py-6 space-y-1">
<nav className={cn('flex-1 py-6 space-y-1', isCollapsed ? 'px-2' : 'px-4')}>
{navigation.map((item) => {
const isActive =
item.href === '/dashboard'
Expand All @@ -77,41 +92,45 @@ export function Sidebar() {
onMouseEnter={() => router.prefetch(item.href)}
onFocus={() => router.prefetch(item.href)}
onClick={() => setIsMobileOpen(false)}
title={isCollapsed ? item.name : undefined}
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500',
'flex items-center gap-3 rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500',
isCollapsed ? 'px-2 py-3 justify-center' : 'px-4 py-3',
isActive
? 'bg-gradient-to-r from-blue-50 to-purple-50 text-blue-700 border border-blue-100'
: 'text-gray-700 hover:bg-gray-50'
)}
aria-current={isActive ? 'page' : undefined}
>
<item.icon
className={cn('h-5 w-5', isActive ? 'text-blue-600' : 'text-gray-500')}
className={cn('h-5 w-5 flex-shrink-0', isActive ? 'text-blue-600' : 'text-gray-500')}
aria-hidden="true"
/>
{item.name}
{!isCollapsed && <span className="truncate">{item.name}</span>}
</Link>
);
})}
</nav>

{/* User section */}
<div className="px-4 py-4 border-t border-gray-200">
<div className={cn('py-4 border-t border-gray-200', isCollapsed ? 'px-2' : 'px-4')}>
<div
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-gray-50"
className={cn('rounded-lg bg-gray-50', isCollapsed ? 'px-2 py-3 flex justify-center' : 'px-4 py-3 flex items-center gap-3')}
role="region"
aria-label="User information"
>
<div
className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold"
className={cn('rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold flex-shrink-0', isCollapsed ? 'w-8 h-8 text-xs' : 'w-10 h-10')}
aria-hidden="true"
>
JD
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">John Doe</p>
<p className="text-xs text-gray-500 truncate">Freelancer</p>
</div>
{!isCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">John Doe</p>
<p className="text-xs text-gray-500 truncate">Freelancer</p>
</div>
)}
</div>
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions frontend/contexts/SidebarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { createContext, useContext, useState, ReactNode } from 'react';

interface SidebarContextType {
isCollapsed: boolean;
setIsCollapsed: (collapsed: boolean) => void;
}

const SidebarContext = createContext<SidebarContextType | undefined>(undefined);

export function SidebarProvider({ children }: { children: ReactNode }) {
const [isCollapsed, setIsCollapsed] = useState(false);

return (
<SidebarContext.Provider value={{ isCollapsed, setIsCollapsed }}>
{children}
</SidebarContext.Provider>
);
}

export function useSidebar() {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within SidebarProvider');
}
return context;
}
Loading