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
|
originalFile String @unique
|
||||||
name String
|
name String
|
||||||
nsfw Boolean @default(false)
|
nsfw Boolean @default(false)
|
||||||
published Boolean @default(true)
|
published Boolean @default(false)
|
||||||
setAsHeader Boolean @default(false)
|
setAsHeader Boolean @default(false)
|
||||||
|
needsWork Boolean @default(false)
|
||||||
|
|
||||||
altText String?
|
altText String?
|
||||||
description String?
|
description String?
|
||||||
@ -43,8 +44,10 @@ model PortfolioImage {
|
|||||||
// slug String?
|
// slug String?
|
||||||
// fileSize Int?
|
// fileSize Int?
|
||||||
|
|
||||||
typeId String?
|
albumId String?
|
||||||
type PortfolioType? @relation(fields: [typeId], references: [id])
|
typeId String?
|
||||||
|
album PortfolioAlbum? @relation(fields: [albumId], references: [id])
|
||||||
|
type PortfolioType? @relation(fields: [typeId], references: [id])
|
||||||
|
|
||||||
metadata ImageMetadata?
|
metadata ImageMetadata?
|
||||||
|
|
||||||
@ -52,6 +55,23 @@ model PortfolioImage {
|
|||||||
colors ImageColor[]
|
colors ImageColor[]
|
||||||
tags PortfolioTag[]
|
tags PortfolioTag[]
|
||||||
variants ImageVariant[]
|
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 {
|
model PortfolioType {
|
||||||
|
@ -18,6 +18,7 @@ export async function updateImage(
|
|||||||
originalFile,
|
originalFile,
|
||||||
nsfw,
|
nsfw,
|
||||||
published,
|
published,
|
||||||
|
setAsHeader,
|
||||||
altText,
|
altText,
|
||||||
description,
|
description,
|
||||||
fileType,
|
fileType,
|
||||||
@ -29,6 +30,14 @@ export async function updateImage(
|
|||||||
categoryIds
|
categoryIds
|
||||||
} = validated.data;
|
} = validated.data;
|
||||||
|
|
||||||
|
if(setAsHeader) {
|
||||||
|
await prisma.portfolioImage.updateMany({
|
||||||
|
where: { setAsHeader: true },
|
||||||
|
data: { setAsHeader: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const updatedImage = await prisma.portfolioImage.update({
|
const updatedImage = await prisma.portfolioImage.update({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
data: {
|
data: {
|
||||||
@ -36,6 +45,7 @@ export async function updateImage(
|
|||||||
originalFile,
|
originalFile,
|
||||||
nsfw,
|
nsfw,
|
||||||
published,
|
published,
|
||||||
|
setAsHeader,
|
||||||
altText,
|
altText,
|
||||||
description,
|
description,
|
||||||
fileType,
|
fileType,
|
||||||
|
@ -5,31 +5,50 @@ import prisma from "@/lib/prisma";
|
|||||||
import { PlusCircleIcon } from "lucide-react";
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function PortfolioImagesPage(
|
export default async function PortfolioImagesPage({
|
||||||
{ searchParams }:
|
searchParams
|
||||||
{ searchParams: { type: string, published: string } }
|
}: {
|
||||||
|
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({
|
const groupMode = groupBy === "album" ? "album" : "year";
|
||||||
orderBy: { sortIndex: "asc" },
|
const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
|
||||||
});
|
|
||||||
|
|
||||||
const typeFilter = type ?? "all";
|
|
||||||
const publishedFilter = published ?? "all";
|
|
||||||
|
|
||||||
const where: Prisma.PortfolioImageWhereInput = {};
|
const where: Prisma.PortfolioImageWhereInput = {};
|
||||||
|
|
||||||
if (typeFilter !== "all") {
|
// Filter by type
|
||||||
where.typeId = typeFilter === "none" ? null : typeFilter;
|
if (type !== "all") {
|
||||||
|
where.typeId = type === "none" ? null : type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publishedFilter === "published") {
|
// Filter by published status
|
||||||
|
if (published === "published") {
|
||||||
where.published = true;
|
where.published = true;
|
||||||
} else if (publishedFilter === "unpublished") {
|
} else if (published === "unpublished") {
|
||||||
where.published = false;
|
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(
|
const images = await prisma.portfolioImage.findMany(
|
||||||
{
|
{
|
||||||
where,
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between pb-4 items-end">
|
<div className="flex justify-between pb-4 items-end">
|
||||||
@ -46,7 +80,15 @@ export default async function PortfolioImagesPage(
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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">
|
<div className="mt-6">
|
||||||
{images && images.length > 0 ? <ImageList images={images} /> : <p>There are no images yet. Consider adding some!</p>}
|
{images && images.length > 0 ? <ImageList images={images} /> : <p>There are no images yet. Consider adding some!</p>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,33 +1,93 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { PortfolioType } from "@/generated/prisma";
|
import { PortfolioAlbum, PortfolioType } from "@/generated/prisma";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
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({
|
export default function FilterBar({
|
||||||
types,
|
types,
|
||||||
currentType,
|
currentType,
|
||||||
currentPublished,
|
currentPublished,
|
||||||
}: {
|
groupBy,
|
||||||
types: PortfolioType[];
|
groupId,
|
||||||
currentType: string;
|
years,
|
||||||
currentPublished: string;
|
albums
|
||||||
}) {
|
}: FilterBarProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
const setFilter = (key: string, value: string) => {
|
const setFilter = (key: string, value: string) => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
if (value !== "all") {
|
if (value !== "all") {
|
||||||
searchParams.set(key, value);
|
params.set(key, value);
|
||||||
} else {
|
} 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 (
|
return (
|
||||||
<div className="flex flex-wrap gap-4 border-b pb-4">
|
<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 */}
|
{/* Type Filter */}
|
||||||
<div className="flex gap-2 items-center flex-wrap">
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Type:</span>
|
<span className="text-sm font-medium text-muted-foreground">Type:</span>
|
||||||
|
Reference in New Issue
Block a user