🪛 04 breadcrumbs

This commit is contained in:
RJ
2025-11-10 09:49:07 +02:00
parent 05390016b2
commit b28b9bd137
12 changed files with 319 additions and 2 deletions

View File

@@ -0,0 +1,15 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export default function AboutBreadcrumb() {
return (
<Breadcrumbs
items={[
{
label: 'Despre',
href: '/about',
current: true,
},
]}
/>
);
}

View File

@@ -0,0 +1,53 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { getPostBySlug } from '@/lib/markdown';
interface BreadcrumbItem {
label: string;
href: string;
current?: boolean;
}
function formatDirectoryName(name: string): string {
const directoryNames: { [key: string]: string } = {
tech: 'Tehnologie',
design: 'Design',
tutorial: 'Tutoriale',
};
return directoryNames[name] || name.charAt(0).toUpperCase() + name.slice(1);
}
export default async function BlogPostBreadcrumb({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
const slugPath = slug.join('/');
const post = getPostBySlug(slugPath);
const items: BreadcrumbItem[] = [
{
label: 'Blog',
href: '/blog',
},
];
if (slug.length > 1) {
for (let i = 0; i < slug.length - 1; i++) {
const segmentPath = slug.slice(0, i + 1).join('/');
items.push({
label: formatDirectoryName(slug[i]),
href: `/blog/${segmentPath}`,
});
}
}
items.push({
label: post ? post.frontmatter.title : slug[slug.length - 1],
href: `/blog/${slugPath}`,
current: true,
});
return <Breadcrumbs items={items} />;
}

View File

@@ -0,0 +1,15 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export default function BlogBreadcrumb() {
return (
<Breadcrumbs
items={[
{
label: 'Blog',
href: '/blog',
current: true,
},
]}
/>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export default function DefaultBreadcrumb() {
return <Breadcrumbs />;
}

View File

@@ -0,0 +1,29 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export default async function TagBreadcrumb({
params,
}: {
params: Promise<{ tag: string }>;
}) {
const { tag } = await params;
const tagName = tag
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return (
<Breadcrumbs
items={[
{
label: 'Tag-uri',
href: '/tags',
},
{
label: tagName,
href: `/tags/${tag}`,
current: true,
},
]}
/>
);
}

View File

@@ -0,0 +1,15 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export default function TagsBreadcrumb() {
return (
<Breadcrumbs
items={[
{
label: 'Tag-uri',
href: '/tags',
current: true,
},
]}
/>
);
}

View File

@@ -1 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@layer utilities {
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}

View File

@@ -27,8 +27,10 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
breadcrumbs,
}: { }: {
children: React.ReactNode children: React.ReactNode
breadcrumbs: React.ReactNode
}) { }) {
return ( return (
<html lang="ro"> <html lang="ro">
@@ -48,6 +50,7 @@ export default function RootLayout({
</div> </div>
</nav> </nav>
</header> </header>
{breadcrumbs}
<main className="container mx-auto px-4 py-8"> <main className="container mx-auto px-4 py-8">
{children} {children}
</main> </main>

View File

@@ -0,0 +1,145 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Fragment } from 'react';
import { BreadcrumbsSchema } from './BreadcrumbsSchema';
interface BreadcrumbItem {
label: string;
href: string;
current?: boolean;
}
function HomeIcon({ className }: { className?: string }) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
);
}
function ChevronIcon({ className }: { className?: string }) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
);
}
function formatSegmentLabel(segment: string): string {
const specialCases: { [key: string]: string } = {
blog: 'Blog',
tags: 'Tag-uri',
about: 'Despre',
};
if (specialCases[segment]) {
return specialCases[segment];
}
return segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
const pathname = usePathname();
let breadcrumbs: BreadcrumbItem[] = items || [];
if (!items) {
const segments = pathname.split('/').filter(Boolean);
breadcrumbs = segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/');
const label = formatSegmentLabel(segment);
const current = index === segments.length - 1;
return { label, href, current };
});
}
if (pathname === '/') {
return null;
}
const schemaItems = [
{ position: 1, name: 'Acasă', item: '/' },
...breadcrumbs.map((item, index) => ({
position: index + 2,
name: item.label,
item: item.href,
})),
];
return (
<>
<nav
aria-label="Breadcrumb"
className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
>
<div className="container mx-auto px-4 py-3">
<ol className="flex items-center space-x-2 text-sm overflow-x-auto scrollbar-hide">
<li className="flex-shrink-0">
<Link
href="/"
className="flex items-center text-gray-500 hover:text-primary-600 transition"
aria-label="Acasă"
>
<HomeIcon className="w-4 h-4" />
</Link>
</li>
{breadcrumbs.map((item) => (
<Fragment key={item.href}>
<li className="text-gray-400 flex-shrink-0">
<ChevronIcon className="w-4 h-4" />
</li>
<li className="flex-shrink-0">
{item.current ? (
<span
className="font-medium text-gray-700 dark:text-gray-300 truncate max-w-[150px] sm:max-w-none block"
aria-current="page"
>
{item.label}
</span>
) : (
<Link
href={item.href}
className="text-gray-500 hover:text-primary-600 transition truncate max-w-[150px] sm:max-w-none block"
>
{item.label}
</Link>
)}
</li>
</Fragment>
))}
</ol>
</div>
</nav>
<BreadcrumbsSchema items={schemaItems} />
</>
);
}

View File

@@ -0,0 +1,25 @@
interface BreadcrumbSchemaItem {
position: number;
name: string;
item: string;
}
export function BreadcrumbsSchema({ items }: { items: BreadcrumbSchemaItem[] }) {
const structuredData = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item) => ({
'@type': 'ListItem',
position: item.position,
name: item.name,
item: `http://localhost:3000${item.item}`,
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
);
}

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev -p 3030",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",