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"
|
"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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<div>Footer</div>
|
<div>
|
||||||
|
© 2025 Fellies Art
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user