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