Refactor mosaic
This commit is contained in:
		@ -0,0 +1,32 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "PortfolioImage" ADD COLUMN     "albumId" TEXT,
 | 
			
		||||
ADD COLUMN     "needsWork" BOOLEAN NOT NULL DEFAULT false,
 | 
			
		||||
ALTER COLUMN "published" SET DEFAULT false;
 | 
			
		||||
 | 
			
		||||
-- CreateTable
 | 
			
		||||
CREATE TABLE "PortfolioAlbum" (
 | 
			
		||||
    "id" TEXT NOT NULL,
 | 
			
		||||
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    "updatedAt" TIMESTAMP(3) NOT NULL,
 | 
			
		||||
    "sortIndex" INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
    "name" TEXT NOT NULL,
 | 
			
		||||
    "slug" TEXT NOT NULL,
 | 
			
		||||
    "description" TEXT,
 | 
			
		||||
 | 
			
		||||
    CONSTRAINT "PortfolioAlbum_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE UNIQUE INDEX "PortfolioAlbum_name_key" ON "PortfolioAlbum"("name");
 | 
			
		||||
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE UNIQUE INDEX "PortfolioAlbum_slug_key" ON "PortfolioAlbum"("slug");
 | 
			
		||||
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE INDEX "PortfolioImage_typeId_year_layoutGroup_layoutOrder_idx" ON "PortfolioImage"("typeId", "year", "layoutGroup", "layoutOrder");
 | 
			
		||||
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE INDEX "PortfolioImage_albumId_layoutGroup_layoutOrder_idx" ON "PortfolioImage"("albumId", "layoutGroup", "layoutOrder");
 | 
			
		||||
 | 
			
		||||
-- AddForeignKey
 | 
			
		||||
ALTER TABLE "PortfolioImage" ADD CONSTRAINT "PortfolioImage_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "PortfolioAlbum"("id") ON DELETE SET NULL ON UPDATE CASCADE;
 | 
			
		||||
@ -25,8 +25,9 @@ model PortfolioImage {
 | 
			
		||||
  originalFile String  @unique
 | 
			
		||||
  name         String
 | 
			
		||||
  nsfw         Boolean @default(false)
 | 
			
		||||
  published    Boolean @default(true)
 | 
			
		||||
  published    Boolean @default(false)
 | 
			
		||||
  setAsHeader  Boolean @default(false)
 | 
			
		||||
  needsWork    Boolean @default(false)
 | 
			
		||||
 | 
			
		||||
  altText      String?
 | 
			
		||||
  description  String?
 | 
			
		||||
@ -43,8 +44,10 @@ model PortfolioImage {
 | 
			
		||||
  // slug         String?
 | 
			
		||||
  // fileSize     Int?
 | 
			
		||||
 | 
			
		||||
  typeId String?
 | 
			
		||||
  type   PortfolioType? @relation(fields: [typeId], references: [id])
 | 
			
		||||
  albumId String?
 | 
			
		||||
  typeId  String?
 | 
			
		||||
  album   PortfolioAlbum? @relation(fields: [albumId], references: [id])
 | 
			
		||||
  type    PortfolioType?  @relation(fields: [typeId], references: [id])
 | 
			
		||||
 | 
			
		||||
  metadata ImageMetadata?
 | 
			
		||||
 | 
			
		||||
@ -52,6 +55,23 @@ model PortfolioImage {
 | 
			
		||||
  colors     ImageColor[]
 | 
			
		||||
  tags       PortfolioTag[]
 | 
			
		||||
  variants   ImageVariant[]
 | 
			
		||||
 | 
			
		||||
  @@index([typeId, year, layoutGroup, layoutOrder])
 | 
			
		||||
  @@index([albumId, layoutGroup, layoutOrder])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model PortfolioAlbum {
 | 
			
		||||
  id        String   @id @default(cuid())
 | 
			
		||||
  createdAt DateTime @default(now())
 | 
			
		||||
  updatedAt DateTime @updatedAt
 | 
			
		||||
  sortIndex Int      @default(0)
 | 
			
		||||
 | 
			
		||||
  name String @unique
 | 
			
		||||
  slug String @unique
 | 
			
		||||
 | 
			
		||||
  description String?
 | 
			
		||||
 | 
			
		||||
  images PortfolioImage[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model PortfolioType {
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ export async function updateImage(
 | 
			
		||||
    originalFile,
 | 
			
		||||
    nsfw,
 | 
			
		||||
    published,
 | 
			
		||||
    setAsHeader,
 | 
			
		||||
    altText,
 | 
			
		||||
    description,
 | 
			
		||||
    fileType,
 | 
			
		||||
@ -29,6 +30,14 @@ export async function updateImage(
 | 
			
		||||
    categoryIds
 | 
			
		||||
  } = validated.data;
 | 
			
		||||
 | 
			
		||||
  if(setAsHeader) {
 | 
			
		||||
    await prisma.portfolioImage.updateMany({
 | 
			
		||||
      where: { setAsHeader: true },
 | 
			
		||||
      data: { setAsHeader: false },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const updatedImage = await prisma.portfolioImage.update({
 | 
			
		||||
    where: { id: id },
 | 
			
		||||
    data: {
 | 
			
		||||
@ -36,6 +45,7 @@ export async function updateImage(
 | 
			
		||||
      originalFile,
 | 
			
		||||
      nsfw,
 | 
			
		||||
      published,
 | 
			
		||||
      setAsHeader,
 | 
			
		||||
      altText,
 | 
			
		||||
      description,
 | 
			
		||||
      fileType,
 | 
			
		||||
 | 
			
		||||
@ -5,31 +5,50 @@ import prisma from "@/lib/prisma";
 | 
			
		||||
import { PlusCircleIcon } from "lucide-react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export default async function PortfolioImagesPage(
 | 
			
		||||
  { searchParams }:
 | 
			
		||||
    { searchParams: { type: string, published: string } }
 | 
			
		||||
export default async function PortfolioImagesPage({
 | 
			
		||||
  searchParams
 | 
			
		||||
}: {
 | 
			
		||||
  searchParams?: {
 | 
			
		||||
    type?: string;
 | 
			
		||||
    published?: string;
 | 
			
		||||
    groupBy?: string;
 | 
			
		||||
    year?: string;
 | 
			
		||||
    album?: string;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
) {
 | 
			
		||||
  const { type, published } = await searchParams;
 | 
			
		||||
  const {
 | 
			
		||||
    type = "all",
 | 
			
		||||
    published = "all",
 | 
			
		||||
    groupBy = "year",
 | 
			
		||||
    year,
 | 
			
		||||
    album,
 | 
			
		||||
  } = searchParams ?? {};
 | 
			
		||||
 | 
			
		||||
  const types = await prisma.portfolioType.findMany({
 | 
			
		||||
    orderBy: { sortIndex: "asc" },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const typeFilter = type ?? "all";
 | 
			
		||||
  const publishedFilter = published ?? "all";
 | 
			
		||||
  const groupMode = groupBy === "album" ? "album" : "year";
 | 
			
		||||
  const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
 | 
			
		||||
 | 
			
		||||
  const where: Prisma.PortfolioImageWhereInput = {};
 | 
			
		||||
 | 
			
		||||
  if (typeFilter !== "all") {
 | 
			
		||||
    where.typeId = typeFilter === "none" ? null : typeFilter;
 | 
			
		||||
  // Filter by type
 | 
			
		||||
  if (type !== "all") {
 | 
			
		||||
    where.typeId = type === "none" ? null : type;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (publishedFilter === "published") {
 | 
			
		||||
  // Filter by published status
 | 
			
		||||
  if (published === "published") {
 | 
			
		||||
    where.published = true;
 | 
			
		||||
  } else if (publishedFilter === "unpublished") {
 | 
			
		||||
  } else if (published === "unpublished") {
 | 
			
		||||
    where.published = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Filter by group (year or album)
 | 
			
		||||
  if (groupMode === "year" && groupId !== "all") {
 | 
			
		||||
    where.year = parseInt(groupId);
 | 
			
		||||
  } else if (groupMode === "album" && groupId !== "all") {
 | 
			
		||||
    where.albumId = groupId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const images = await prisma.portfolioImage.findMany(
 | 
			
		||||
    {
 | 
			
		||||
      where,
 | 
			
		||||
@ -37,6 +56,21 @@ export default async function PortfolioImagesPage(
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const [types, albums, yearsRaw] = await Promise.all([
 | 
			
		||||
    prisma.portfolioType.findMany({ orderBy: { sortIndex: "asc" } }),
 | 
			
		||||
    prisma.portfolioAlbum.findMany({ orderBy: { sortIndex: "asc" } }),
 | 
			
		||||
    prisma.portfolioImage.findMany({
 | 
			
		||||
      where: {},
 | 
			
		||||
      distinct: ['year'],
 | 
			
		||||
      select: { year: true },
 | 
			
		||||
      orderBy: { year: 'desc' },
 | 
			
		||||
    }),
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const years = yearsRaw
 | 
			
		||||
    .map((y) => y.year)
 | 
			
		||||
    .filter((y): y is number => y !== null && y !== undefined);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex justify-between pb-4 items-end">
 | 
			
		||||
@ -46,7 +80,15 @@ export default async function PortfolioImagesPage(
 | 
			
		||||
        </Link>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <FilterBar types={types} currentType={typeFilter} currentPublished={publishedFilter} />
 | 
			
		||||
      <FilterBar
 | 
			
		||||
        types={types}
 | 
			
		||||
        albums={albums}
 | 
			
		||||
        years={years}
 | 
			
		||||
        currentType={type}
 | 
			
		||||
        currentPublished={published}
 | 
			
		||||
        groupBy={groupMode}
 | 
			
		||||
        groupId={groupId}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="mt-6">
 | 
			
		||||
        {images && images.length > 0 ? <ImageList images={images} /> : <p>There are no images yet. Consider adding some!</p>}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,93 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { PortfolioType } from "@/generated/prisma";
 | 
			
		||||
import { PortfolioAlbum, PortfolioType } from "@/generated/prisma";
 | 
			
		||||
import { usePathname, useRouter } from "next/navigation";
 | 
			
		||||
 | 
			
		||||
type FilterBarProps = {
 | 
			
		||||
  types: PortfolioType[];
 | 
			
		||||
  currentType: string;
 | 
			
		||||
  currentPublished: string;
 | 
			
		||||
  groupBy: "year" | "album";
 | 
			
		||||
  groupId: string;
 | 
			
		||||
  years: number[];
 | 
			
		||||
  albums: PortfolioAlbum[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function FilterBar({
 | 
			
		||||
  types,
 | 
			
		||||
  currentType,
 | 
			
		||||
  currentPublished,
 | 
			
		||||
}: {
 | 
			
		||||
  types: PortfolioType[];
 | 
			
		||||
  currentType: string;
 | 
			
		||||
  currentPublished: string;
 | 
			
		||||
}) {
 | 
			
		||||
  groupBy,
 | 
			
		||||
  groupId,
 | 
			
		||||
  years,
 | 
			
		||||
  albums
 | 
			
		||||
}: FilterBarProps) {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const pathname = usePathname();
 | 
			
		||||
  const searchParams = new URLSearchParams();
 | 
			
		||||
 | 
			
		||||
  const setFilter = (key: string, value: string) => {
 | 
			
		||||
    const params = new URLSearchParams(window.location.search);
 | 
			
		||||
 | 
			
		||||
    if (value !== "all") {
 | 
			
		||||
      searchParams.set(key, value);
 | 
			
		||||
      params.set(key, value);
 | 
			
		||||
    } else {
 | 
			
		||||
      searchParams.delete(key);
 | 
			
		||||
      params.delete(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    router.push(`${pathname}?${searchParams.toString()}`);
 | 
			
		||||
    if (key === "groupBy") {
 | 
			
		||||
      params.delete("year");
 | 
			
		||||
      params.delete("album");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    router.push(`${pathname}?${params.toString()}`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-wrap gap-4 border-b pb-4">
 | 
			
		||||
      {/* GroupBy Toggle */}
 | 
			
		||||
      <div className="flex gap-2 items-center">
 | 
			
		||||
        <span className="text-sm font-medium text-muted-foreground">Group by:</span>
 | 
			
		||||
        <FilterButton
 | 
			
		||||
          active={groupBy === "year"}
 | 
			
		||||
          label="Year"
 | 
			
		||||
          onClick={() => setFilter("groupBy", "year")}
 | 
			
		||||
        />
 | 
			
		||||
        <FilterButton
 | 
			
		||||
          active={groupBy === "album"}
 | 
			
		||||
          label="Album"
 | 
			
		||||
          onClick={() => setFilter("groupBy", "album")}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Subnavigation */}
 | 
			
		||||
      <div className="flex gap-2 items-center flex-wrap">
 | 
			
		||||
        <span className="text-sm font-medium text-muted-foreground">Filter:</span>
 | 
			
		||||
        <FilterButton
 | 
			
		||||
          active={groupId === "all"}
 | 
			
		||||
          label="All"
 | 
			
		||||
          onClick={() => setFilter(groupBy, "all")}
 | 
			
		||||
        />
 | 
			
		||||
        {groupBy === "year" &&
 | 
			
		||||
          years.map((year) => (
 | 
			
		||||
            <FilterButton
 | 
			
		||||
              key={year}
 | 
			
		||||
              active={groupId === String(year)}
 | 
			
		||||
              label={String(year)}
 | 
			
		||||
              onClick={() => setFilter("year", String(year))}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
        {groupBy === "album" &&
 | 
			
		||||
          albums.map((album) => (
 | 
			
		||||
            <FilterButton
 | 
			
		||||
              key={album.id}
 | 
			
		||||
              active={groupId === album.id}
 | 
			
		||||
              label={album.name}
 | 
			
		||||
              onClick={() => setFilter("album", album.id)}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Type Filter */}
 | 
			
		||||
      <div className="flex gap-2 items-center flex-wrap">
 | 
			
		||||
        <span className="text-sm font-medium text-muted-foreground">Type:</span>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user