feat: added breadcrumb

This commit is contained in:
Yehoshua Sandler 2025-05-01 12:18:57 -05:00
parent a1ba745b0d
commit 1ebbf524be
12 changed files with 781 additions and 22 deletions

View File

@ -5,6 +5,14 @@ const nextConfig = {
images: { images: {
domains: ['covers.openlibrary.org', 'cdn.beitzah.net'], domains: ['covers.openlibrary.org', 'cdn.beitzah.net'],
}, },
async rewrites() {
return [
{
source: '/search',
destination: '/books',
},
]
},
} }
export default withPayload(nextConfig, { devBundleServerPackages: false }) export default withPayload(nextConfig, { devBundleServerPackages: false })

235
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",
"@payloadcms/richtext-lexical": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0",
"@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.11", "@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-select": "^2.2.2", "@radix-ui/react-select": "^2.2.2",
@ -4352,6 +4353,77 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.12.tgz",
"integrity": "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-menu": "2.1.12",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": { "node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
@ -4479,6 +4551,169 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.12.tgz",
"integrity": "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.7",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.4",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.4",
"@radix-ui/react-portal": "1.1.6",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-roving-focus": "1.1.7",
"@radix-ui/react-slot": "1.2.0",
"@radix-ui/react-use-callback-ref": "1.1.1",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz",
"integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz",
"integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": { "node_modules/@radix-ui/react-popover": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz",

View File

@ -24,6 +24,7 @@
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",
"@payloadcms/richtext-lexical": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0",
"@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.11", "@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-select": "^2.2.2", "@radix-ui/react-select": "^2.2.2",

View File

@ -2,6 +2,18 @@ import { Book, Copy, Repository } from '@/payload-types'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import configPromise from '@payload-config' import configPromise from '@payload-config'
import BookByIdPageClient from './page.client' import BookByIdPageClient from './page.client'
import { PageBreadCrumb, Route } from '@/components/PageBreadCrumb'
const staticBreadcrumRoutes: Route[] = [
{
label: 'Home',
href: '/',
},
{
label: 'Search',
href: '/search',
},
]
type Params = Promise<{ bookId: string }> type Params = Promise<{ bookId: string }>
type SearchParams = Promise<{ [key: string]: string | undefined }> type SearchParams = Promise<{ [key: string]: string | undefined }>
@ -64,7 +76,26 @@ const BookByIdPage = async (props: Props) => {
}, },
})) as PaginatedDocs<Repository> })) as PaginatedDocs<Repository>
return <BookByIdPageClient book={foundBook} repositories={repositories} /> const createBreadcrumbRoutes =() => {
let title
if (!foundBook?.title) title = 'Book Not Found'
else title = foundBook.title.length > 25 ? `${foundBook.title.slice(0, 25)}...` : foundBook.title
return [...staticBreadcrumRoutes, {
label: title,
href: `/books/${bookId}`
}]
}
console.log(createBreadcrumbRoutes())
return (
<>
<PageBreadCrumb routes={createBreadcrumbRoutes()} />
<BookByIdPageClient book={foundBook} repositories={repositories} />
</>
)
} }
export default BookByIdPage export default BookByIdPage

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import BookList from '@/components/BookList' import SearchBooks from '@/components/Search/SearchBooks'
import { Book } from '@/payload-types' import { Book } from '@/payload-types'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import { useMemo } from 'react' import { useMemo } from 'react'
@ -14,9 +14,9 @@ const BooksPageClient = (props: Props) => {
}, [props.initialBooks]) }, [props.initialBooks])
return ( return (
<div> <section>
<BookList books={initialBooks} /> <SearchBooks initBrowseBooks={initialBooks} />
</div> </section>
) )
} }

View File

@ -2,6 +2,18 @@ import { Book } from '@/payload-types'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import configPromise from '@payload-config' import configPromise from '@payload-config'
import BooksPageClient from './page.client' import BooksPageClient from './page.client'
import { PageBreadCrumb, Route } from '@/components/PageBreadCrumb'
const breadcrumRoutes: Route[] = [
{
label: 'Home',
href: '/',
},
{
label: 'Search',
href: '/search',
},
]
type Params = Promise<{ slug: string }> type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | undefined }> type SearchParams = Promise<{ [key: string]: string | undefined }>
@ -38,7 +50,12 @@ const BooksPage = async (props: Props) => {
}, },
})) as PaginatedDocs<Book> })) as PaginatedDocs<Book>
return <BooksPageClient initialBooks={initialBooks} /> return (
<>
<PageBreadCrumb routes={breadcrumRoutes} />
<BooksPageClient initialBooks={initialBooks} />
</>
)
} }
export default BooksPage export default BooksPage

View File

@ -90,7 +90,7 @@ export default async function HomePage() {
return ( return (
<div className="home"> <div className="home">
<HomeHero user={user} /> <HomeHero user={user} />
<div id="homeContent" className='flex flex-col justify-around min-h-[90vh]'>
{user ? ( {user ? (
<Tabs id="tabs" defaultValue="feed" className="p-4"> <Tabs id="tabs" defaultValue="feed" className="p-4">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
@ -117,5 +117,6 @@ export default async function HomePage() {
</div> </div>
)} )}
</div> </div>
</div>
) )
} }

View File

@ -11,9 +11,15 @@ type Props = {
} }
const HomeHero = (props: Props) => { const HomeHero = (props: Props) => {
const { user } = props const { user } = props
const onClickEnter = () => {
const tabs = document.getElementById('homeContent')
tabs?.scrollIntoView({ behavior: 'smooth' })
}
return ( return (
<div> <div className="flex flex-col justify-around min-h-[90vh]">
<div className="relative isolate overflow-hidden py-24 sm:py-32 rounded-md"> <div className="relative isolate overflow-hidden py-24 sm:py-32 rounded-md mb-auto">
<img <img
alt="" alt=""
src="/api/media/file/geniza1.jpg" src="/api/media/file/geniza1.jpg"
@ -73,15 +79,12 @@ const HomeHero = (props: Props) => {
</div> </div>
</div> </div>
<div className="flex items-center justify-center h-32"> <div className="flex items-center justify-center h-32 mb-10">
<Magnetic intensity={0.2} springOptions={{ bounce: 0.1 }} actionArea="global" range={200}> <Magnetic intensity={0.2} springOptions={{ bounce: 0.1 }} actionArea="global" range={200}>
<Button <Button
type="button" type="button"
className="animate-pulse animate-bounce mx-auto block items-center rounded-lg border-zinc-100/10 bg-transparent text-foreground px-8 py-2 text-sm transition-all duration-200 h-fit" className="animate-pulse animate-bounce mx-auto block items-center rounded-lg border-zinc-100/10 bg-transparent text-foreground px-8 py-2 text-sm transition-all duration-200 h-fit"
onClick={() => { onClick={onClickEnter}
const tabs = document.getElementById('tabs')
tabs?.scrollIntoView({ behavior: 'smooth' })
}}
> >
<Image <Image
src="/images/down.svg" src="/images/down.svg"

View File

@ -0,0 +1,98 @@
import Link from 'next/link'
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from './ui/breadcrumb'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './ui/dropdown-menu'
import { Fragment } from 'react'
export type Route = {
href: string
label: string
}
type RouteGroups = {
mostRecent: Route[]
inBetween: Route[]
root: Route
}
const groupBreadCrumbHistory = (routes: Route[]) => {
const mostRecent =
routes.length >= 3
? [routes[routes.length - 2], routes[routes.length - 1]]
: [routes[routes.length - 1]]
const inBetween = routes.length >= 3 ? routes.splice(1, routes.length - 3) : []
const groups: RouteGroups = {
root: routes[0],
mostRecent,
inBetween,
}
return groups
}
type Props = {
routes: Route[]
}
export function PageBreadCrumb(props: Props) {
const { routes } = props
const groups = groupBreadCrumbHistory(routes)
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href={groups.root.href}>{groups.root.label}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
{!!groups.inBetween.length && (
<>
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
<BreadcrumbEllipsis className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{groups.inBetween.map((r) => (
<DropdownMenuItem key={r.label + r.href}>
<Link href={r.href}>{r.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
)}
{groups.mostRecent.map((r, index) => {
if (index != groups.mostRecent.length - 1)
return (
<Fragment key={r.label + r.href}>
<BreadcrumbItem>
<BreadcrumbLink href={r.href}>{r.label}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</Fragment>
)
else
return (
<BreadcrumbItem key={r.label + r.href}>
<BreadcrumbPage>{r.label}</BreadcrumbPage>
</BreadcrumbItem>
)
})}
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@ -31,12 +31,13 @@ import { useGlobal } from '@/providers/GlobalProvider'
import { ArrowRightStartOnRectangleIcon, LightBulbIcon, UserIcon } from '@heroicons/react/16/solid' import { ArrowRightStartOnRectangleIcon, LightBulbIcon, UserIcon } from '@heroicons/react/16/solid'
import { MagnifyingGlassIcon, SunIcon, MoonIcon } from '@heroicons/react/20/solid' import { MagnifyingGlassIcon, SunIcon, MoonIcon } from '@heroicons/react/20/solid'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import Link from 'next/link'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
const navItems = [ const navItems = [
{ label: 'Home', url: '/' }, { label: 'Home', url: '/' },
{ label: 'Browse', url: '/books' },
{ label: 'Events', url: '/events' }, { label: 'Events', url: '/events' },
{ label: 'Explore', url: '/browse' },
{ label: 'Settings', url: '/settings' }, { label: 'Settings', url: '/settings' },
] ]
@ -135,13 +136,11 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
<Sidebar> <Sidebar>
<SidebarHeader> <SidebarHeader>
<Dropdown> <Dropdown>
<DropdownButton <DropdownButton as={SidebarItem} className="lg:mb-2.5">
as={SidebarItem} <Link href="/" className="flex items-center justify-around gap-3">
className="lg:mb-2.5" <Avatar src="/api/media/file/bethel-logo.jpg" className="size-8" />
onClick={() => (window.location.href = '/')}
>
<Avatar src="/api/media/file/bethel-logo.jpg" />
<SidebarLabel>Midrashim</SidebarLabel> <SidebarLabel>Midrashim</SidebarLabel>
</Link>
</DropdownButton> </DropdownButton>
</Dropdown> </Dropdown>
</SidebarHeader> </SidebarHeader>

View File

@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}