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