Implemented responsive layout system with: - Header: Top navigation with profile menu and mobile hamburger - Sidebar: Desktop-only navigation sidebar (lg and above) - Breadcrumbs: Auto-generated navigation breadcrumbs - Layout: Main wrapper component with configurable options Features: - Responsive design (mobile, tablet, desktop) - Active route highlighting - User profile integration via auth store - Click-outside dropdown closing - Comprehensive test coverage (49 tests passing) Updated (main) layout to use new Layout component system. Updated dashboard page to work with new layout structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
4.0 KiB
TypeScript
145 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useMemo } from 'react';
|
|
|
|
/**
|
|
* Breadcrumb item type
|
|
*/
|
|
interface BreadcrumbItem {
|
|
label: string;
|
|
href?: string;
|
|
}
|
|
|
|
/**
|
|
* Route label mappings
|
|
* Maps route segments to human-readable labels
|
|
*/
|
|
const routeLabels: Record<string, string> = {
|
|
dashboard: 'Dashboard',
|
|
beneficiaries: 'Loved Ones',
|
|
sensors: 'Sensors',
|
|
settings: 'Settings',
|
|
profile: 'Profile',
|
|
'add-sensor': 'Add Sensor',
|
|
subscription: 'Subscription',
|
|
equipment: 'Equipment',
|
|
share: 'Share Access',
|
|
};
|
|
|
|
/**
|
|
* Breadcrumbs Component
|
|
*
|
|
* Displays navigation breadcrumbs based on current route.
|
|
* Automatically generates breadcrumb trail from pathname.
|
|
*
|
|
* Features:
|
|
* - Auto-generated from URL path
|
|
* - Clickable navigation links
|
|
* - Current page highlighted
|
|
* - Responsive (hidden on mobile)
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <Breadcrumbs />
|
|
* ```
|
|
*
|
|
* Route examples:
|
|
* - /dashboard -> Dashboard
|
|
* - /beneficiaries/123 -> Loved Ones / John Doe
|
|
* - /beneficiaries/123/sensors -> Loved Ones / John Doe / Sensors
|
|
*/
|
|
export function Breadcrumbs() {
|
|
const pathname = usePathname();
|
|
|
|
const breadcrumbs = useMemo(() => {
|
|
// Don't show breadcrumbs on auth pages
|
|
if (pathname.startsWith('/login') || pathname.startsWith('/verify-otp')) {
|
|
return [];
|
|
}
|
|
|
|
const segments = pathname.split('/').filter(Boolean);
|
|
const items: BreadcrumbItem[] = [];
|
|
|
|
// Build breadcrumb trail
|
|
let currentPath = '';
|
|
segments.forEach((segment, index) => {
|
|
currentPath += `/${segment}`;
|
|
|
|
// Check if segment is a dynamic ID (numeric)
|
|
const isId = /^\d+$/.test(segment);
|
|
|
|
if (isId) {
|
|
// For IDs, use a placeholder or fetch the actual name
|
|
// In production, you would fetch the beneficiary name here
|
|
items.push({
|
|
label: `#${segment}`,
|
|
href: currentPath,
|
|
});
|
|
} else {
|
|
// Use mapped label or capitalize segment
|
|
const label = routeLabels[segment] || segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
items.push({
|
|
label,
|
|
href: currentPath,
|
|
});
|
|
}
|
|
});
|
|
|
|
return items;
|
|
}, [pathname]);
|
|
|
|
// Don't render if no breadcrumbs
|
|
if (breadcrumbs.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<nav aria-label="Breadcrumb" className="hidden md:block">
|
|
<ol className="flex items-center gap-2 text-sm">
|
|
{/* Home icon */}
|
|
<li>
|
|
<Link
|
|
href="/dashboard"
|
|
className="text-slate-500 hover:text-slate-700"
|
|
aria-label="Home"
|
|
>
|
|
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
|
</svg>
|
|
</Link>
|
|
</li>
|
|
|
|
{/* Breadcrumb items */}
|
|
{breadcrumbs.map((item, index) => {
|
|
const isLast = index === breadcrumbs.length - 1;
|
|
|
|
return (
|
|
<li key={item.href} className="flex items-center gap-2">
|
|
{/* Separator */}
|
|
<svg className="h-4 w-4 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
|
|
{/* Link or current page */}
|
|
{isLast ? (
|
|
<span className="font-medium text-slate-900" aria-current="page">
|
|
{item.label}
|
|
</span>
|
|
) : (
|
|
<Link
|
|
href={item.href || '#'}
|
|
className="text-slate-500 hover:text-slate-700"
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</nav>
|
|
);
|
|
}
|