diff --git a/prisma/migrations/20250721221415_add_mosaic_sorting/migration.sql b/prisma/migrations/20250721221415_add_mosaic_sorting/migration.sql new file mode 100644 index 0000000..27bd729 --- /dev/null +++ b/prisma/migrations/20250721221415_add_mosaic_sorting/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 315cfee..3cbb6dc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 { diff --git a/src/actions/portfolio/images/updateImage.ts b/src/actions/portfolio/images/updateImage.ts index 1af6dca..2eb32cc 100644 --- a/src/actions/portfolio/images/updateImage.ts +++ b/src/actions/portfolio/images/updateImage.ts @@ -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, diff --git a/src/app/portfolio/images/page.tsx b/src/app/portfolio/images/page.tsx index 70aec72..2183e11 100644 --- a/src/app/portfolio/images/page.tsx +++ b/src/app/portfolio/images/page.tsx @@ -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 (
@@ -46,7 +80,15 @@ export default async function PortfolioImagesPage(
- +
{images && images.length > 0 ? :

There are no images yet. Consider adding some!

}
diff --git a/src/components/portfolio/images/FilterBar.tsx b/src/components/portfolio/images/FilterBar.tsx index e9008a2..7935a64 100644 --- a/src/components/portfolio/images/FilterBar.tsx +++ b/src/components/portfolio/images/FilterBar.tsx @@ -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 (
+ {/* GroupBy Toggle */} +
+ Group by: + setFilter("groupBy", "year")} + /> + setFilter("groupBy", "album")} + /> +
+ + {/* Subnavigation */} +
+ Filter: + setFilter(groupBy, "all")} + /> + {groupBy === "year" && + years.map((year) => ( + setFilter("year", String(year))} + /> + ))} + {groupBy === "album" && + albums.map((album) => ( + setFilter("album", album.id)} + /> + ))} +
+ {/* Type Filter */}
Type: