243 lines
6.8 KiB
TypeScript
243 lines
6.8 KiB
TypeScript
import React from 'react';
|
||
import Link from 'next/link';
|
||
import { cn } from '../../lib/utils';
|
||
import { Container } from '../ui/Container';
|
||
|
||
interface BreadcrumbItem {
|
||
label: string;
|
||
href?: string;
|
||
icon?: React.ReactNode;
|
||
}
|
||
|
||
interface BreadcrumbsProps {
|
||
items: BreadcrumbItem[];
|
||
separator?: React.ReactNode;
|
||
className?: string;
|
||
showHome?: boolean;
|
||
homeLabel?: string;
|
||
homeHref?: string;
|
||
collapseMobile?: boolean;
|
||
}
|
||
|
||
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
||
items,
|
||
separator = '/',
|
||
className = '',
|
||
showHome = true,
|
||
homeLabel = 'Home',
|
||
homeHref = '/',
|
||
collapseMobile = true,
|
||
}) => {
|
||
// Generate schema.org structured data
|
||
const generateSchema = () => {
|
||
const breadcrumbs = [
|
||
...(showHome ? [{ label: homeLabel, href: homeHref }] : []),
|
||
...items,
|
||
].map((item, index) => ({
|
||
'@type': 'ListItem',
|
||
position: index + 1,
|
||
name: item.label,
|
||
...(item.href && { item: item.href }),
|
||
}));
|
||
|
||
return {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'BreadcrumbList',
|
||
itemListElement: breadcrumbs,
|
||
};
|
||
};
|
||
|
||
// Render individual breadcrumb item
|
||
const renderItem = (item: BreadcrumbItem, index: number, isLast: boolean) => {
|
||
const isHome = showHome && index === 0 && item.href === homeHref;
|
||
|
||
const content = (
|
||
<span className="flex items-center gap-2">
|
||
{isHome && (
|
||
<span className="inline-flex items-center justify-center w-4 h-4">
|
||
<svg
|
||
className="w-4 h-4"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<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>
|
||
</span>
|
||
)}
|
||
{item.icon && <span className="inline-flex items-center">{item.icon}</span>}
|
||
<span className={cn(
|
||
'transition-colors duration-200',
|
||
isLast ? 'font-semibold text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
||
)}>
|
||
{item.label}
|
||
</span>
|
||
</span>
|
||
);
|
||
|
||
if (!isLast && item.href) {
|
||
return (
|
||
<Link
|
||
href={item.href}
|
||
className={cn(
|
||
'inline-flex items-center gap-2',
|
||
'transition-colors duration-200',
|
||
'hover:text-primary'
|
||
)}
|
||
>
|
||
{content}
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
return content;
|
||
};
|
||
|
||
const allItems = [
|
||
...(showHome ? [{ label: homeLabel, href: homeHref }] : []),
|
||
...items,
|
||
];
|
||
|
||
// Schema.org JSON-LD
|
||
const schema = generateSchema();
|
||
|
||
return (
|
||
<>
|
||
{/* Schema.org structured data */}
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||
/>
|
||
|
||
{/* Breadcrumbs navigation */}
|
||
<nav
|
||
aria-label="Breadcrumb"
|
||
className={cn('w-full bg-gray-50 py-3', className)}
|
||
>
|
||
<Container maxWidth="6xl" padding="md">
|
||
<ol className={cn(
|
||
'flex items-center flex-wrap gap-2 text-sm',
|
||
// Mobile: show only current and parent, hide others
|
||
collapseMobile && 'max-w-full overflow-hidden'
|
||
)}>
|
||
{allItems.map((item, index) => {
|
||
const isLast = index === allItems.length - 1;
|
||
const isFirst = index === 0;
|
||
|
||
return (
|
||
<li
|
||
key={index}
|
||
className={cn(
|
||
'flex items-center gap-2',
|
||
// On mobile, hide intermediate items but keep structure
|
||
collapseMobile && !isFirst && !isLast && 'hidden sm:flex'
|
||
)}
|
||
>
|
||
{renderItem(item, index, isLast)}
|
||
|
||
{!isLast && (
|
||
<span className={cn(
|
||
'text-gray-400 select-none',
|
||
// Use different separator for mobile vs desktop
|
||
'hidden sm:inline'
|
||
)}>
|
||
{separator}
|
||
</span>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ol>
|
||
</Container>
|
||
</nav>
|
||
</>
|
||
);
|
||
};
|
||
|
||
// Sub-components for variations
|
||
export const BreadcrumbsCompact: React.FC<Omit<BreadcrumbsProps, 'collapseMobile'>> = ({
|
||
items,
|
||
separator = '›',
|
||
className = '',
|
||
showHome = true,
|
||
homeLabel = 'Home',
|
||
homeHref = '/',
|
||
}) => {
|
||
const allItems = [
|
||
...(showHome ? [{ label: homeLabel, href: homeHref }] : []),
|
||
...items,
|
||
];
|
||
|
||
return (
|
||
<nav aria-label="Breadcrumb" className={cn('w-full', className)}>
|
||
<Container maxWidth="6xl" padding="md">
|
||
<ol className="flex items-center gap-2 text-xs md:text-sm">
|
||
{allItems.map((item, index) => {
|
||
const isLast = index === allItems.length - 1;
|
||
|
||
return (
|
||
<li key={index} className="flex items-center gap-2">
|
||
{item.href && !isLast ? (
|
||
<Link
|
||
href={item.href}
|
||
className="text-gray-500 hover:text-gray-900 transition-colors"
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
) : (
|
||
<span className={cn(
|
||
isLast ? 'font-medium text-gray-900' : 'text-gray-500'
|
||
)}>
|
||
{item.label}
|
||
</span>
|
||
)}
|
||
|
||
{!isLast && (
|
||
<span className="text-gray-400">{separator}</span>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ol>
|
||
</Container>
|
||
</nav>
|
||
);
|
||
};
|
||
|
||
export const BreadcrumbsSimple: React.FC<{
|
||
items: BreadcrumbItem[];
|
||
className?: string;
|
||
}> = ({ items, className = '' }) => {
|
||
return (
|
||
<nav aria-label="Breadcrumb" className={cn('w-full', className)}>
|
||
<ol className="flex items-center gap-2 text-sm">
|
||
{items.map((item, index) => {
|
||
const isLast = index === items.length - 1;
|
||
|
||
return (
|
||
<li key={index} className="flex items-center gap-2">
|
||
{item.href && !isLast ? (
|
||
<Link
|
||
href={item.href}
|
||
className="text-blue-600 hover:text-blue-800 hover:underline transition-colors"
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
) : (
|
||
<span className={cn(
|
||
isLast ? 'font-semibold text-gray-900' : 'text-gray-600'
|
||
)}>
|
||
{item.label}
|
||
</span>
|
||
)}
|
||
|
||
{!isLast && <span className="text-gray-400">/</span>}
|
||
</li>
|
||
);
|
||
})}
|
||
</ol>
|
||
</nav>
|
||
);
|
||
};
|
||
|
||
export default Breadcrumbs; |