Skip to main content

Navigation System

OneLibro uses a responsive navigation system with sidebar for desktop and bottom navigation for mobile, providing seamless access to all app features.

Overview

The navigation system adapts to different screen sizes:

  • Desktop (≥1024px): Sidebar navigation + top bar
  • Tablet (768px-1023px): Collapsible sidebar + top bar
  • Mobile (<768px): Bottom navigation + top bar

Architecture

┌─────────────────────────────────────────────────────────────┐
│ Navigation Structure │
└─────────────────────────────────────────────────────────────┘

DashboardLayout

├─> Sidebar (Desktop/Tablet)
│ ├─> Logo
│ ├─> Navigation Links
│ │ ├─> Dashboard
│ │ ├─> Accounts
│ │ ├─> Transactions
│ │ ├─> Budgets
│ │ └─> Settings
│ └─> User Menu

├─> TopBar (All Devices)
│ ├─> Page Title
│ ├─> User Avatar
│ └─> Mobile Menu Toggle

└─> BottomNav (Mobile Only)
├─> Dashboard Icon
├─> Accounts Icon
├─> Transactions Icon
└─> Budgets Icon

Components

Location: components/finance/Sidebar.tsx

Features:

  • Active link highlighting
  • Icon + text labels
  • Collapsible on tablet
  • Smooth transitions
  • Logout button
  • User profile section

Desktop Layout:

┌──────────────────┐
│ OneLibro Logo │
├──────────────────┤
│ 📊 Dashboard │ ← Active (highlighted)
│ 🏦 Accounts │
│ 💳 Transactions │
│ 📈 Budgets │
│ ⚙️ Settings │
├──────────────────┤
│ User: John Doe │
│ 🚪 Logout │
└──────────────────┘

Navigation Links:

PagePathIconDescription
Dashboard/financeLayoutDashboardOverview of finances
Accounts/finance/accountsBuilding2Connected bank accounts
Transactions/finance/transactionsReceiptTransaction history
Budgets/finance/budgetsPiggyBankBudget management
Settings/finance/settingsSettingsUser preferences

Implementation:

import { Sidebar } from '@/components/finance/Sidebar';

export default function FinanceLayout({ children }) {
return (
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
);
}

Active Link Styling:

const isActive = pathname === href;

<Link
href={href}
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg transition-colors',
isActive
? 'bg-[#10b981] text-white'
: 'text-gray-300 hover:bg-[#1a1a1a]'
)}
>
<Icon className="w-5 h-5" />
<span>{label}</span>
</Link>

TopBar

Location: components/finance/TopBar.tsx

Features:

  • Page title display
  • User avatar with dropdown menu
  • Mobile menu toggle
  • Breadcrumb navigation (optional)
  • Notifications badge (future)

Layout:

┌────────────────────────────────────────────────────────────┐
│ ☰ Dashboard 🔔 👤 John Doe ▼ |
└────────────────────────────────────────────────────────────┘

User Dropdown Menu:

  • Profile
  • Settings
  • Help & Support
  • Logout

Implementation:

import { TopBar } from '@/components/finance/TopBar';

<TopBar
title="Dashboard"
user={user}
showMobileMenu={isMobileMenuOpen}
onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>

BottomNav

Location: components/finance/BottomNav.tsx

Features:

  • Fixed position at bottom
  • Icon-only navigation
  • Active state highlighting
  • Touch-optimized targets (48x48px minimum)
  • Safe area insets for notched devices

Mobile Layout:

┌────────────────────────────────────────────────────────────┐
│ App Content │
│ │
├────────────────────────────────────────────────────────────┤
│ 📊 🏦 💳 📈 ⚙️ │
│ Home Accounts Trans Budgets Settings │
└────────────────────────────────────────────────────────────┘

Active State:

  • Active icon: Green (#10b981)
  • Inactive icons: Gray (#9ca3af)
  • Active label: Bold

Implementation:

import { BottomNav } from '@/components/finance/BottomNav';

<BottomNav />

Responsive Behavior

Breakpoints

Using Tailwind's default breakpoints:

const breakpoints = {
mobile: '< 768px',
tablet: '768px - 1023px',
desktop: '≥ 1024px',
};

Layout Changes

Mobile (<768px):

.sidebar { display: none; }
.bottom-nav { display: flex; }
.top-bar { padding: 1rem; }

Tablet (768px-1023px):

.sidebar {
display: flex;
width: 240px;
/* Can collapse to icon-only */
}
.bottom-nav { display: none; }

Desktop (≥1024px):

.sidebar {
display: flex;
width: 280px;
/* Always expanded */
}
.bottom-nav { display: none; }

Collapsed State:

  • Width: 64px
  • Hide text labels
  • Show icons only
  • Tooltip on hover
const [isCollapsed, setIsCollapsed] = useState(false);

<aside className={cn(
'transition-all duration-300',
isCollapsed ? 'w-16' : 'w-64'
)}>
{/* Navigation items */}
</aside>

Implemented in Phase 6 for better UX during page transitions.

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

export function NavLink({ href, children, icon: Icon }) {
const router = useRouter();
const [isNavigating, setIsNavigating] = useState(false);

const handleClick = (e) => {
e.preventDefault();
setIsNavigating(true);
router.push(href);
};

return (
<Link
href={href}
onClick={handleClick}
className="relative"
>
{isNavigating && (
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<Loader2 className="w-4 h-4 animate-spin" />
</div>
)}
<Icon className="w-5 h-5" />
<span>{children}</span>
</Link>
);
}

Top Bar Loading Indicator

Progress bar at top of page during navigation:

'use client';

import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';

export function NavigationProgress() {
const pathname = usePathname();
const [loading, setLoading] = useState(false);

useEffect(() => {
setLoading(true);
const timeout = setTimeout(() => setLoading(false), 300);
return () => clearTimeout(timeout);
}, [pathname]);

if (!loading) return null;

return (
<div className="fixed top-0 left-0 right-0 h-1 bg-[#10b981] z-50 animate-pulse" />
);
}

User Menu

Desktop User Menu (Sidebar)

<div className="border-t border-[#2a2a2a] p-4">
<div className="flex items-center gap-3 mb-3">
<Avatar>{user.name[0]}</Avatar>
<div>
<p className="font-medium text-white">{user.name}</p>
<p className="text-sm text-gray-400">{user.email}</p>
</div>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 px-3 py-2 text-gray-300 hover:bg-[#1a1a1a] rounded-lg"
>
<LogOut className="w-4 h-4" />
<span>Logout</span>
</button>
</div>

Mobile User Menu (TopBar Dropdown)

<DropdownMenu>
<DropdownMenuTrigger>
<Avatar />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

For nested pages (e.g., Edit Budget):

import { Breadcrumb } from '@/components/ui/Breadcrumb';

<Breadcrumb>
<BreadcrumbItem href="/finance">Home</BreadcrumbItem>
<BreadcrumbItem href="/finance/budgets">Budgets</BreadcrumbItem>
<BreadcrumbItem active>Edit Budget</BreadcrumbItem>
</Breadcrumb>

Renders as:

Home > Budgets > Edit Budget

Keyboard Navigation

Keyboard Shortcuts

ShortcutAction
g dGo to Dashboard
g aGo to Accounts
g tGo to Transactions
g bGo to Budgets
g sGo to Settings
/Focus search
EscClose modals/menus

Implementation:

'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export function useKeyboardShortcuts() {
const router = useRouter();

useEffect(() => {
let gPressed = false;

const handleKeyDown = (e: KeyboardEvent) => {
// g key pressed
if (e.key === 'g' && !gPressed) {
gPressed = true;
setTimeout(() => { gPressed = false; }, 1000);
return;
}

// Second key after g
if (gPressed) {
switch (e.key) {
case 'd': router.push('/finance'); break;
case 'a': router.push('/finance/accounts'); break;
case 't': router.push('/finance/transactions'); break;
case 'b': router.push('/finance/budgets'); break;
case 's': router.push('/finance/settings'); break;
}
gPressed = false;
}

// Focus search
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
document.getElementById('search-input')?.focus();
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [router]);
}

Usage in Layout:

export default function FinanceLayout({ children }) {
useKeyboardShortcuts();

return <>{children}</>;
}

Deep Linking

Support for deep links to specific resources:

URLs:

  • /finance/accounts/[accountId] - View account details
  • /finance/transactions/[transactionId] - View transaction details
  • /finance/budgets/edit/[budgetId] - Edit budget

Back Button:

import { useRouter } from 'next/navigation';

function BackButton() {
const router = useRouter();

return (
<button onClick={() => router.back()}>
<ArrowLeft className="w-5 h-5" />
Back
</button>
);
}

Protected Routes

Redirect unauthenticated users to login:

'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';

export function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const router = useRouter();

useEffect(() => {
if (!loading && !user) {
router.push('/finance/login');
}
}, [user, loading, router]);

if (loading) return <LoadingSpinner />;
if (!user) return null;

return children;
}

Admin-Only Routes

export function AdminRoute({ children }) {
const { user, loading } = useAuth();

if (loading) return <LoadingSpinner />;
if (!user?.is_admin) return <Forbidden />;

return children;
}

Mobile Gestures

Swipe Navigation (Future Enhancement)

Swipe gestures for mobile navigation:

import { useSwipeable } from 'react-swipeable';

export function SwipeableLayout({ children }) {
const router = useRouter();

const handlers = useSwipeable({
onSwipedLeft: () => router.push('/finance/accounts'),
onSwipedRight: () => router.push('/finance/dashboard'),
trackMouse: false,
trackTouch: true,
});

return <div {...handlers}>{children}</div>;
}

Accessibility

ARIA Labels

<nav aria-label="Main navigation">
<Link href="/finance" aria-current={isActive ? 'page' : undefined}>
Dashboard
</Link>
</nav>

Allow keyboard users to skip to main content:

<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>

<main id="main-content">
{/* Page content */}
</main>

Focus Management

Trap focus in mobile menu when open:

import { Dialog } from '@headlessui/react';

<Dialog open={isMobileMenuOpen} onClose={closeMobileMenu}>
<Dialog.Panel>
{/* Mobile menu content */}
</Dialog.Panel>
</Dialog>

Performance Optimization

Next.js prefetches links in viewport:

<Link href="/finance/budgets" prefetch={true}>
Budgets
</Link>

Code Splitting

Lazy load navigation components:

const BottomNav = dynamic(
() => import('@/components/finance/BottomNav'),
{ ssr: false } // Only load on client
);

Testing Navigation

Unit Tests

import { render, screen } from '@testing-library/react';
import { Sidebar } from './Sidebar';

test('highlights active link', () => {
render(<Sidebar currentPath="/finance/budgets" />);
const budgetsLink = screen.getByText('Budgets');
expect(budgetsLink).toHaveClass('bg-[#10b981]');
});

E2E Tests

test('user can navigate to budgets page', async ({ page }) => {
await page.goto('/finance');
await page.click('text=Budgets');
await expect(page).toHaveURL('/finance/budgets');
await expect(page.locator('h1')).toContainText('Budgets');
});

Future Enhancements

  • Command palette (Cmd+K) for quick navigation
  • Recent pages/searches in menu
  • Customizable sidebar (reorder links)
  • Contextual actions in top bar
  • Persistent sidebar state (localStorage)
  • Notification center in top bar
  • Global search across all data
  • Tour/onboarding for new users