Skip to content

Commit 42e9d96

Browse files
committed
feat: add shadcn Breadcrumb component
1 parent 59d3677 commit 42e9d96

File tree

2 files changed

+156
-57
lines changed

2 files changed

+156
-57
lines changed

src/components/Breadcrumbs/index.tsx

Lines changed: 34 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import React from "react"
21
import { useRouter } from "next/router"
32
import { useTranslation } from "next-i18next"
43

54
import type { Lang } from "@/lib/types"
65

7-
import { cn } from "@/lib/utils/cn"
86
import { isLangRightToLeft } from "@/lib/utils/translations"
97

10-
import { BaseLink } from "../ui/Link"
8+
import {
9+
Breadcrumb,
10+
BreadcrumbItem,
11+
BreadcrumbLink,
12+
BreadcrumbList,
13+
BreadcrumbPage,
14+
BreadcrumbProps,
15+
BreadcrumbSeparator,
16+
} from "../ui/breadcrumb"
1117

12-
// Ref: https://ui.shadcn.com/docs/components/breadcrumb
13-
type RootBreadcrumbProps = React.ComponentPropsWithoutRef<"nav">
14-
15-
export type BreadcrumbsProps = RootBreadcrumbProps & {
18+
export type BreadcrumbsProps = BreadcrumbProps & {
1619
slug: string
1720
startDepth?: number
1821
}
@@ -22,11 +25,6 @@ type Crumb = {
2225
text: string
2326
}
2427

25-
const Breadcrumb = React.forwardRef<HTMLElement, RootBreadcrumbProps>(
26-
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
27-
)
28-
Breadcrumb.displayName = "Breadcrumb"
29-
3028
// Generate crumbs from slug
3129
// e.g. "/en/eth2/proof-of-stake/" will generate:
3230
// [
@@ -40,13 +38,7 @@ Breadcrumb.displayName = "Breadcrumb"
4038
// { fullPath: "/en/eth2/", text: "ETH2" },
4139
// { fullPath: "/en/eth2/proof-of-stake/", text: "PROOF OF STAKE" },
4240
// ]
43-
44-
const Breadcrumbs = ({
45-
slug,
46-
startDepth = 0,
47-
className = "",
48-
...props
49-
}: BreadcrumbsProps) => {
41+
const Breadcrumbs = ({ slug, startDepth = 0, ...props }: BreadcrumbsProps) => {
5042
const { t } = useTranslation("common")
5143
const { locale, asPath } = useRouter()
5244
const dir = isLangRightToLeft(locale! as Lang) ? "rtl" : "ltr"
@@ -74,44 +66,29 @@ const Breadcrumbs = ({
7466
.slice(startDepth)
7567

7668
return (
77-
<Breadcrumb
78-
className={cn("flex flex-wrap items-center justify-start", className)}
79-
{...props}
80-
>
81-
{crumbs.map(({ fullPath, text }) => {
82-
const normalizePath = (path) => path.replace(/\/$/, "") // Remove trailing slash
83-
const isCurrentPage = normalizePath(slug) === normalizePath(fullPath)
84-
return (
85-
<div
86-
key={fullPath}
87-
className={cn(
88-
"inline-flex items-center tracking-wider",
89-
dir === "rtl" ? "flex-row-reverse" : ""
90-
)}
91-
>
92-
{!isCurrentPage && dir === "rtl" && (
93-
<span className="me-[0.625rem] ms-[0.625rem] text-gray-400">
94-
/
95-
</span>
96-
)}
97-
{isCurrentPage ? (
98-
<span className="uppercase text-primary">{text}</span>
99-
) : (
100-
<BaseLink
101-
href={fullPath}
102-
className="uppercase !text-body-medium no-underline hover:!text-primary"
103-
>
104-
{text}
105-
</BaseLink>
106-
)}
107-
{!isCurrentPage && dir === "ltr" && (
108-
<span className="me-[0.625rem] ms-[0.625rem] text-gray-400">
109-
/
110-
</span>
111-
)}
112-
</div>
113-
)
114-
})}
69+
<Breadcrumb {...props} dir={dir}>
70+
<BreadcrumbList>
71+
{crumbs.map(({ fullPath, text }) => {
72+
const normalizePath = (path) => path.replace(/\/$/, "") // Remove trailing slash
73+
const isCurrentPage = normalizePath(slug) === normalizePath(fullPath)
74+
return (
75+
<>
76+
<BreadcrumbItem key={fullPath}>
77+
{isCurrentPage ? (
78+
<BreadcrumbPage>{text}</BreadcrumbPage>
79+
) : (
80+
<BreadcrumbLink href={fullPath}>{text}</BreadcrumbLink>
81+
)}
82+
</BreadcrumbItem>
83+
{!isCurrentPage && (
84+
<BreadcrumbSeparator className="me-[0.625rem] ms-[0.625rem] text-gray-400">
85+
/
86+
</BreadcrumbSeparator>
87+
)}
88+
</>
89+
)
90+
})}
91+
</BreadcrumbList>
11592
</Breadcrumb>
11693
)
11794
}

src/components/ui/breadcrumb.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as React from "react"
2+
import { LuChevronRight, LuMoreHorizontal } from "react-icons/lu"
3+
import { Slot } from "@radix-ui/react-slot"
4+
5+
import { cn } from "@/lib/utils/cn"
6+
7+
interface BreadcrumbProps extends React.ComponentPropsWithoutRef<"nav"> {
8+
separator?: React.ReactNode
9+
}
10+
const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(
11+
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
12+
)
13+
Breadcrumb.displayName = "Breadcrumb"
14+
15+
const BreadcrumbList = React.forwardRef<
16+
HTMLOListElement,
17+
React.ComponentPropsWithoutRef<"ol">
18+
>(({ className, ...props }, ref) => (
19+
<ol
20+
ref={ref}
21+
className={cn(
22+
"m-0 flex list-none flex-wrap items-center tracking-wider",
23+
className
24+
)}
25+
{...props}
26+
/>
27+
))
28+
BreadcrumbList.displayName = "BreadcrumbList"
29+
30+
const BreadcrumbItem = React.forwardRef<
31+
HTMLLIElement,
32+
React.ComponentPropsWithoutRef<"li">
33+
>(({ className, ...props }, ref) => (
34+
<li
35+
ref={ref}
36+
className={cn(
37+
"m-0 inline-flex items-center gap-1.5 tracking-wider",
38+
className
39+
)}
40+
{...props}
41+
/>
42+
))
43+
BreadcrumbItem.displayName = "BreadcrumbItem"
44+
45+
const BreadcrumbLink = React.forwardRef<
46+
HTMLAnchorElement,
47+
React.ComponentPropsWithoutRef<"a"> & {
48+
asChild?: boolean
49+
}
50+
>(({ asChild, className, ...props }, ref) => {
51+
const Comp = asChild ? Slot : "a"
52+
53+
return (
54+
<Comp
55+
ref={ref}
56+
className={cn(
57+
"uppercase !text-body-medium no-underline transition-colors hover:!text-primary",
58+
className
59+
)}
60+
{...props}
61+
/>
62+
)
63+
})
64+
BreadcrumbLink.displayName = "BreadcrumbLink"
65+
66+
const BreadcrumbPage = React.forwardRef<
67+
HTMLSpanElement,
68+
React.ComponentPropsWithoutRef<"span">
69+
>(({ className, ...props }, ref) => (
70+
<span
71+
ref={ref}
72+
role="link"
73+
aria-disabled="true"
74+
aria-current="page"
75+
className={cn("uppercase text-primary", className)}
76+
{...props}
77+
/>
78+
))
79+
BreadcrumbPage.displayName = "BreadcrumbPage"
80+
81+
const BreadcrumbSeparator = ({
82+
children,
83+
className,
84+
...props
85+
}: React.ComponentProps<"li">) => (
86+
<li
87+
role="presentation"
88+
aria-hidden="true"
89+
className={cn("m-0", className)}
90+
{...props}
91+
>
92+
{children ?? <LuChevronRight />}
93+
</li>
94+
)
95+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
96+
97+
const BreadcrumbEllipsis = ({
98+
className,
99+
...props
100+
}: React.ComponentProps<"span">) => (
101+
<span
102+
role="presentation"
103+
aria-hidden="true"
104+
className={cn("flex h-9 w-9 items-center justify-center", className)}
105+
{...props}
106+
>
107+
<LuMoreHorizontal className="h-4 w-4" />
108+
<span className="sr-only">More</span>
109+
</span>
110+
)
111+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
112+
113+
export {
114+
Breadcrumb,
115+
BreadcrumbEllipsis,
116+
BreadcrumbItem,
117+
BreadcrumbLink,
118+
BreadcrumbList,
119+
BreadcrumbPage,
120+
type BreadcrumbProps,
121+
BreadcrumbSeparator,
122+
}

0 commit comments

Comments
 (0)