migration wip
This commit is contained in:
243
components/content/Breadcrumbs.tsx
Normal file
243
components/content/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user