Refactor mosaic

This commit is contained in:
2025-07-22 00:33:32 +02:00
parent a8d5dbaa09
commit 9962bb1476
5 changed files with 191 additions and 27 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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,

View File

@ -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>

View File

@ -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>