Implemented breadcrumbs

This commit is contained in:
2025-07-03 21:44:02 +02:00
parent 287ef67c37
commit 51ff6c1ade
6 changed files with 187 additions and 61 deletions

130
src/actions/breadcrumbs.ts Normal file
View 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
}

View File

@ -1,75 +1,69 @@
"use client" "use client"
import { import { getBreadcrumbLabels } from "@/actions/breadcrumbs"
FolderIcon, import { cn } from "@/lib/utils"
FolderOpenIcon, import { GalleryHorizontalEndIcon, HomeIcon, ImageIcon, ImagesIcon, InfoIcon, LayersIcon, TagIcon, UserIcon } from "lucide-react"
HomeIcon,
ImageIcon,
InfoIcon,
LayersIcon,
TagIcon,
UserIcon,
} from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { Fragment, JSX } from "react" import { JSX, useEffect, useState } from "react"
const iconMap: Record<string, JSX.Element> = { const iconMap: Record<string, JSX.Element> = {
"about": <InfoIcon className="w-4 h-4" />, home: <HomeIcon className="w-4 h-4" />,
"artists": <UserIcon className="w-4 h-4" />, gallery: <GalleryHorizontalEndIcon className="w-4 h-4" />,
"categories": <LayersIcon className="w-4 h-4" />, album: <ImagesIcon className="w-4 h-4" />,
"tags": <TagIcon className="w-4 h-4" />, category: <LayersIcon className="w-4 h-4" />,
"gallery": <FolderIcon className="w-4 h-4" />, image: <ImageIcon className="w-4 h-4" />,
"album": <FolderOpenIcon className="w-4 h-4" />, tag: <TagIcon className="w-4 h-4" />,
"image": <ImageIcon 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() { export default function Breadcrumbs() {
const pathname = usePathname() const pathname = usePathname()
const rawSegments = pathname.split("/").filter(Boolean) const [labels, setLabels] = useState<Crumb[]>([])
// If path is just "/", return nothing useEffect(() => {
if (rawSegments.length === 0) return null async function fetchLabels() {
const data = await getBreadcrumbLabels(pathname)
setLabels(data)
}
fetchLabels()
}, [pathname])
// Special handling: remove "galleries" from breadcrumb trail if (!labels.length) return null
const segments = rawSegments.filter((seg, index) =>
!(seg === "galleries" && index === 0)
)
return ( return (
<div className="text-sm px-1 text-muted-foreground flex items-center flex-wrap gap-1"> <nav className="flex items-center flex-wrap gap-1 text-muted-foreground">
<Link href="/" className="inline-flex items-center gap-1 hover:underline font-medium text-foreground"> {labels.map((crumb, index) => {
<HomeIcon className="w-4 h-4" /> const isLast = index === labels.length - 1
Home
</Link>
{segments.length > 0 && <span>{">"}</span>} const item = (
<span
{segments.map((seg, i) => { className={cn(
const href = "/" + rawSegments.slice(0, rawSegments.indexOf(seg) + 1).join("/") "inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
const isLast = i === segments.length - 1 isLast
const icon = iconMap[rawSegments[0]] || null ? "bg-muted pointer-events-none"
: "bg-muted/50 hover:bg-muted text-foreground"
)}
>
{iconMap[crumb.icon]}
{crumb.label}
</span>
)
return ( return (
<Fragment key={href}> <div key={index} className="flex items-center gap-1">
{i > 0 && <span>{">"}</span>} {isLast ? item : <Link href={crumb.href} className="no-underline">{item}</Link>}
{isLast ? ( {index < labels.length - 1 && <span className="mx-1 text-muted-foreground">{">"}</span>}
<span className="inline-flex items-center gap-1 font-medium text-foreground"> </div>
{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> </nav>
) )
} }

View File

@ -1,5 +1,7 @@
export default function Footer() { export default function Footer() {
return ( return (
<div>Footer</div> <div>
&copy; 2025 Fellies Art
</div>
); );
} }

View File

@ -8,14 +8,14 @@ export default function Header() {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<TopNav /> <Breadcrumbs />
<div className="flex gap-4"> <div className="flex gap-4">
<TopNav />
<AnimateToggle /> <AnimateToggle />
<NSFWToggle /> <NSFWToggle />
<ModeToggle /> <ModeToggle />
</div> </div>
</div> </div>
<Breadcrumbs />
</div> </div>
); );
} }

View File

@ -7,11 +7,11 @@ export default function TopNav() {
return ( return (
<NavigationMenu viewport={false}> <NavigationMenu viewport={false}>
<NavigationMenuList> <NavigationMenuList>
<NavigationMenuItem> {/* <NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}> <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/">Home</Link> <Link href="/">Home</Link>
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem> */}
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}> <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/about">About</Link> <Link href="/about">About</Link>

View File

@ -1,7 +1,7 @@
"use client" "use client"
import { Album, Category, Gallery, Tag } from "@/generated/prisma" 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" import Link from "next/link"
type Props = { type Props = {
@ -62,7 +62,7 @@ export default function ImageMetadataBox({
{album && ( {album && (
<div className="flex items-center gap-2 flex-wrap"> <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"> <Link href={`/galleries/${album.gallery?.slug}/${album.slug}`} className="text-sm underline">
{album.name} {album.name}
</Link> </Link>