Implemented breadcrumbs
This commit is contained in:
130
src/actions/breadcrumbs.ts
Normal file
130
src/actions/breadcrumbs.ts
Normal file
@ -0,0 +1,130 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function getBreadcrumbLabels(path: string) {
|
||||
const segments = path.split("/").filter(Boolean)
|
||||
|
||||
const breadcrumbs: { label: string; href: string; icon: string }[] = [
|
||||
{ label: "Home", href: "/", icon: "home" }
|
||||
]
|
||||
|
||||
let base = ""
|
||||
let i = 0
|
||||
|
||||
while (i < segments.length) {
|
||||
const segment = segments[i]
|
||||
|
||||
switch (segment) {
|
||||
case "galleries": {
|
||||
const gallerySlug = segments[i + 1]
|
||||
const gallery = await prisma.gallery.findUnique({ where: { slug: gallerySlug } })
|
||||
|
||||
if (gallery) {
|
||||
base += `/galleries/${gallerySlug}`
|
||||
breadcrumbs.push({
|
||||
label: gallery.name,
|
||||
href: base,
|
||||
icon: "gallery"
|
||||
})
|
||||
|
||||
// Check if there's an album
|
||||
const albumSlug = segments[i + 2]
|
||||
if (albumSlug) {
|
||||
const album = await prisma.album.findFirst({
|
||||
where: { slug: albumSlug, galleryId: gallery.id }
|
||||
})
|
||||
if (album) {
|
||||
base += `/${albumSlug}`
|
||||
breadcrumbs.push({
|
||||
label: album.name,
|
||||
href: base,
|
||||
icon: "album"
|
||||
})
|
||||
|
||||
// Check for image ID
|
||||
const imageId = segments[i + 3]
|
||||
if (imageId) {
|
||||
const image = await prisma.image.findUnique({
|
||||
where: { id: imageId }
|
||||
})
|
||||
if (image) {
|
||||
base += `/${imageId}`
|
||||
breadcrumbs.push({
|
||||
label: image.imageName,
|
||||
href: base,
|
||||
icon: "image"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 4
|
||||
break
|
||||
}
|
||||
|
||||
case "artists": {
|
||||
const slug = segments[i + 1]
|
||||
const artist = await prisma.artist.findUnique({ where: { slug } })
|
||||
if (artist) {
|
||||
base += `/artists/${slug}`
|
||||
breadcrumbs.push({
|
||||
label: artist.displayName,
|
||||
href: base,
|
||||
icon: "artist"
|
||||
})
|
||||
}
|
||||
i += 2
|
||||
break
|
||||
}
|
||||
|
||||
case "categories": {
|
||||
const id = segments[i + 1]
|
||||
const category = await prisma.category.findUnique({ where: { id } })
|
||||
if (category) {
|
||||
base += `/categories/${id}`
|
||||
breadcrumbs.push({
|
||||
label: category.name,
|
||||
href: base,
|
||||
icon: "category"
|
||||
})
|
||||
}
|
||||
i += 2
|
||||
break
|
||||
}
|
||||
|
||||
case "tags": {
|
||||
const id = segments[i + 1]
|
||||
const tag = await prisma.tag.findUnique({ where: { id } })
|
||||
if (tag) {
|
||||
base += `/tags/${id}`
|
||||
breadcrumbs.push({
|
||||
label: tag.name,
|
||||
href: base,
|
||||
icon: "tag"
|
||||
})
|
||||
}
|
||||
i += 2
|
||||
break
|
||||
}
|
||||
|
||||
case "about": {
|
||||
base += `/about`
|
||||
breadcrumbs.push({
|
||||
label: "About",
|
||||
href: base,
|
||||
icon: "about"
|
||||
})
|
||||
i++
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
i++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
@ -1,75 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
FolderIcon,
|
||||
FolderOpenIcon,
|
||||
HomeIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
LayersIcon,
|
||||
TagIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react"
|
||||
import { getBreadcrumbLabels } from "@/actions/breadcrumbs"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { GalleryHorizontalEndIcon, HomeIcon, ImageIcon, ImagesIcon, InfoIcon, LayersIcon, TagIcon, UserIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Fragment, JSX } from "react"
|
||||
import { JSX, useEffect, useState } from "react"
|
||||
|
||||
const iconMap: Record<string, JSX.Element> = {
|
||||
"about": <InfoIcon className="w-4 h-4" />,
|
||||
"artists": <UserIcon className="w-4 h-4" />,
|
||||
"categories": <LayersIcon className="w-4 h-4" />,
|
||||
"tags": <TagIcon className="w-4 h-4" />,
|
||||
"gallery": <FolderIcon className="w-4 h-4" />,
|
||||
"album": <FolderOpenIcon className="w-4 h-4" />,
|
||||
"image": <ImageIcon className="w-4 h-4" />,
|
||||
home: <HomeIcon className="w-4 h-4" />,
|
||||
gallery: <GalleryHorizontalEndIcon className="w-4 h-4" />,
|
||||
album: <ImagesIcon className="w-4 h-4" />,
|
||||
category: <LayersIcon className="w-4 h-4" />,
|
||||
image: <ImageIcon className="w-4 h-4" />,
|
||||
tag: <TagIcon className="w-4 h-4" />,
|
||||
artist: <UserIcon className="w-4 h-4" />,
|
||||
about: <InfoIcon className="w-4 h-4" />,
|
||||
}
|
||||
|
||||
type Crumb = {
|
||||
label: string
|
||||
href: string
|
||||
icon: keyof typeof iconMap
|
||||
}
|
||||
|
||||
export default function Breadcrumbs() {
|
||||
const pathname = usePathname()
|
||||
const rawSegments = pathname.split("/").filter(Boolean)
|
||||
const [labels, setLabels] = useState<Crumb[]>([])
|
||||
|
||||
// If path is just "/", return nothing
|
||||
if (rawSegments.length === 0) return null
|
||||
useEffect(() => {
|
||||
async function fetchLabels() {
|
||||
const data = await getBreadcrumbLabels(pathname)
|
||||
setLabels(data)
|
||||
}
|
||||
fetchLabels()
|
||||
}, [pathname])
|
||||
|
||||
// Special handling: remove "galleries" from breadcrumb trail
|
||||
const segments = rawSegments.filter((seg, index) =>
|
||||
!(seg === "galleries" && index === 0)
|
||||
)
|
||||
if (!labels.length) return null
|
||||
|
||||
return (
|
||||
<div className="text-sm px-1 text-muted-foreground flex items-center flex-wrap gap-1">
|
||||
<Link href="/" className="inline-flex items-center gap-1 hover:underline font-medium text-foreground">
|
||||
<HomeIcon className="w-4 h-4" />
|
||||
Home
|
||||
</Link>
|
||||
<nav className="flex items-center flex-wrap gap-1 text-muted-foreground">
|
||||
{labels.map((crumb, index) => {
|
||||
const isLast = index === labels.length - 1
|
||||
|
||||
{segments.length > 0 && <span>{">"}</span>}
|
||||
|
||||
{segments.map((seg, i) => {
|
||||
const href = "/" + rawSegments.slice(0, rawSegments.indexOf(seg) + 1).join("/")
|
||||
const isLast = i === segments.length - 1
|
||||
const icon = iconMap[rawSegments[0]] || null
|
||||
const item = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
isLast
|
||||
? "bg-muted pointer-events-none"
|
||||
: "bg-muted/50 hover:bg-muted text-foreground"
|
||||
)}
|
||||
>
|
||||
{iconMap[crumb.icon]}
|
||||
{crumb.label}
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment key={href}>
|
||||
{i > 0 && <span>{">"}</span>}
|
||||
{isLast ? (
|
||||
<span className="inline-flex items-center gap-1 font-medium text-foreground">
|
||||
{i === 0 && icon}
|
||||
{decodeURIComponent(seg)}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{i === 0 && icon}
|
||||
{decodeURIComponent(seg)}
|
||||
</Link>
|
||||
)}
|
||||
</Fragment>
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
{isLast ? item : <Link href={crumb.href} className="no-underline">{item}</Link>}
|
||||
{index < labels.length - 1 && <span className="mx-1 text-muted-foreground">{">"}</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div>Footer</div>
|
||||
<div>
|
||||
© 2025 Fellies Art
|
||||
</div>
|
||||
);
|
||||
}
|
@ -8,14 +8,14 @@ export default function Header() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<TopNav />
|
||||
<Breadcrumbs />
|
||||
<div className="flex gap-4">
|
||||
<TopNav />
|
||||
<AnimateToggle />
|
||||
<NSFWToggle />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -7,11 +7,11 @@ export default function TopNav() {
|
||||
return (
|
||||
<NavigationMenu viewport={false}>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
{/* <NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/">Home</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuItem> */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/about">About</Link>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Album, Category, Gallery, Tag } from "@/generated/prisma"
|
||||
import { CalendarDaysIcon, FolderIcon, LayersIcon, QuoteIcon, TagIcon } from "lucide-react"
|
||||
import { CalendarDaysIcon, ImagesIcon, LayersIcon, QuoteIcon, TagIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
type Props = {
|
||||
@ -62,7 +62,7 @@ export default function ImageMetadataBox({
|
||||
|
||||
{album && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<FolderIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<ImagesIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<Link href={`/galleries/${album.gallery?.slug}/${album.slug}`} className="text-sm underline">
|
||||
{album.name}
|
||||
</Link>
|
||||
|
Reference in New Issue
Block a user