From ef281ef70f2b48c9996379c521d085e81334941b Mon Sep 17 00:00:00 2001 From: Citali Date: Sat, 26 Jul 2025 19:00:19 +0200 Subject: [PATCH] Working sorting kinda? --- .../migration.sql | 20 ++ .../migration.sql | 32 ++++ .../migration.sql | 30 +++ .../migration.sql | 20 ++ .../20250726125324_change_bool/migration.sql | 2 + prisma/schema.prisma | 42 +++-- src/actions/portfolio/deleteItem.ts | 3 + src/actions/portfolio/images/getImageSort.ts | 89 +++++++++ src/actions/portfolio/images/saveImageSort.ts | 41 +++++ .../images/saveImageSortForSubset.ts | 35 ++++ src/actions/portfolio/images/saveSortOrder.ts | 40 ++++ src/actions/portfolio/images/updateImage.ts | 13 +- src/app/portfolio/images/[id]/page.tsx | 5 +- src/app/portfolio/images/page.tsx | 2 + src/app/portfolio/images/sort/page.tsx | 54 +----- .../portfolio/images/EditImageForm.tsx | 69 ++++++- src/components/portfolio/images/FilterBar.tsx | 5 + .../portfolio/images/ImageSortGallery.tsx | 173 +++++++++--------- .../portfolio/images/SortableImageItem.tsx | 48 +++++ src/schemas/portfolio/imageSchema.ts | 19 +- src/utils/getSortKey.ts | 13 ++ 21 files changed, 586 insertions(+), 169 deletions(-) create mode 100644 prisma/migrations/20250726103307_add_sort_context/migration.sql create mode 100644 prisma/migrations/20250726103332_change_sort_context/migration.sql create mode 100644 prisma/migrations/20250726105552_change_sort_context/migration.sql create mode 100644 prisma/migrations/20250726124727_remove_unneeded_fields/migration.sql create mode 100644 prisma/migrations/20250726125324_change_bool/migration.sql create mode 100644 src/actions/portfolio/images/getImageSort.ts create mode 100644 src/actions/portfolio/images/saveImageSort.ts create mode 100644 src/actions/portfolio/images/saveImageSortForSubset.ts create mode 100644 src/actions/portfolio/images/saveSortOrder.ts create mode 100644 src/components/portfolio/images/SortableImageItem.tsx create mode 100644 src/utils/getSortKey.ts diff --git a/prisma/migrations/20250726103307_add_sort_context/migration.sql b/prisma/migrations/20250726103307_add_sort_context/migration.sql new file mode 100644 index 0000000..9f1a1a7 --- /dev/null +++ b/prisma/migrations/20250726103307_add_sort_context/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "ImageSortContext" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "key" TEXT NOT NULL, + "index" INTEGER NOT NULL, + "imageId" TEXT NOT NULL, + + CONSTRAINT "ImageSortContext_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "ImageSortContext_key_index_idx" ON "ImageSortContext"("key", "index"); + +-- CreateIndex +CREATE UNIQUE INDEX "ImageSortContext_key_imageId_key" ON "ImageSortContext"("key", "imageId"); + +-- AddForeignKey +ALTER TABLE "ImageSortContext" ADD CONSTRAINT "ImageSortContext_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "PortfolioImage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250726103332_change_sort_context/migration.sql b/prisma/migrations/20250726103332_change_sort_context/migration.sql new file mode 100644 index 0000000..dd05e62 --- /dev/null +++ b/prisma/migrations/20250726103332_change_sort_context/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the `ImageSortContext` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "ImageSortContext" DROP CONSTRAINT "ImageSortContext_imageId_fkey"; + +-- DropTable +DROP TABLE "ImageSortContext"; + +-- CreateTable +CREATE TABLE "PortfolioSortContext" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "key" TEXT NOT NULL, + "index" INTEGER NOT NULL, + "imageId" TEXT NOT NULL, + + CONSTRAINT "PortfolioSortContext_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "PortfolioSortContext_key_index_idx" ON "PortfolioSortContext"("key", "index"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioSortContext_key_imageId_key" ON "PortfolioSortContext"("key", "imageId"); + +-- AddForeignKey +ALTER TABLE "PortfolioSortContext" ADD CONSTRAINT "PortfolioSortContext_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "PortfolioImage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250726105552_change_sort_context/migration.sql b/prisma/migrations/20250726105552_change_sort_context/migration.sql new file mode 100644 index 0000000..c877731 --- /dev/null +++ b/prisma/migrations/20250726105552_change_sort_context/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `index` on the `PortfolioSortContext` table. All the data in the column will be lost. + - You are about to drop the column `key` on the `PortfolioSortContext` table. All the data in the column will be lost. + - A unique constraint covering the columns `[imageId,year,albumId,type,group]` on the table `PortfolioSortContext` will be added. If there are existing duplicate values, this will fail. + - Added the required column `albumId` to the `PortfolioSortContext` table without a default value. This is not possible if the table is not empty. + - Added the required column `group` to the `PortfolioSortContext` table without a default value. This is not possible if the table is not empty. + - Added the required column `sortOrder` to the `PortfolioSortContext` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `PortfolioSortContext` table without a default value. This is not possible if the table is not empty. + - Added the required column `year` to the `PortfolioSortContext` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "PortfolioSortContext_key_imageId_key"; + +-- DropIndex +DROP INDEX "PortfolioSortContext_key_index_idx"; + +-- AlterTable +ALTER TABLE "PortfolioSortContext" DROP COLUMN "index", +DROP COLUMN "key", +ADD COLUMN "albumId" TEXT NOT NULL, +ADD COLUMN "group" TEXT NOT NULL, +ADD COLUMN "sortOrder" INTEGER NOT NULL, +ADD COLUMN "type" TEXT NOT NULL, +ADD COLUMN "year" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioSortContext_imageId_year_albumId_type_group_key" ON "PortfolioSortContext"("imageId", "year", "albumId", "type", "group"); diff --git a/prisma/migrations/20250726124727_remove_unneeded_fields/migration.sql b/prisma/migrations/20250726124727_remove_unneeded_fields/migration.sql new file mode 100644 index 0000000..663fbea --- /dev/null +++ b/prisma/migrations/20250726124727_remove_unneeded_fields/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the column `layoutGroup` on the `PortfolioImage` table. All the data in the column will be lost. + - You are about to drop the column `layoutOrder` on the `PortfolioImage` table. All the data in the column will be lost. + - Made the column `fileType` on table `PortfolioImage` required. This step will fail if there are existing NULL values in that column. + - Made the column `fileSize` on table `PortfolioImage` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropIndex +DROP INDEX "PortfolioImage_albumId_layoutGroup_layoutOrder_idx"; + +-- DropIndex +DROP INDEX "PortfolioImage_typeId_year_layoutGroup_layoutOrder_idx"; + +-- AlterTable +ALTER TABLE "PortfolioImage" DROP COLUMN "layoutGroup", +DROP COLUMN "layoutOrder", +ALTER COLUMN "fileType" SET NOT NULL, +ALTER COLUMN "fileSize" SET NOT NULL; diff --git a/prisma/migrations/20250726125324_change_bool/migration.sql b/prisma/migrations/20250726125324_change_bool/migration.sql new file mode 100644 index 0000000..547dbdd --- /dev/null +++ b/prisma/migrations/20250726125324_change_bool/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PortfolioImage" ALTER COLUMN "needsWork" SET DEFAULT true; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3cbb6dc..9cafa8a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,26 +23,19 @@ model PortfolioImage { fileKey String @unique originalFile String @unique + fileType String name String + fileSize Int + needsWork Boolean @default(true) nsfw Boolean @default(false) published Boolean @default(false) setAsHeader Boolean @default(false) - needsWork Boolean @default(false) altText String? description String? - fileType String? - layoutGroup String? - fileSize Int? - layoutOrder Int? month Int? year Int? creationDate DateTime? - // group String? - // kind String? - // series String? - // slug String? - // fileSize Int? albumId String? typeId String? @@ -51,13 +44,11 @@ model PortfolioImage { metadata ImageMetadata? - categories PortfolioCategory[] - colors ImageColor[] - tags PortfolioTag[] - variants ImageVariant[] - - @@index([typeId, year, layoutGroup, layoutOrder]) - @@index([albumId, layoutGroup, layoutOrder]) + categories PortfolioCategory[] + colors ImageColor[] + sortContexts PortfolioSortContext[] + tags PortfolioTag[] + variants ImageVariant[] } model PortfolioAlbum { @@ -116,6 +107,23 @@ model PortfolioTag { images PortfolioImage[] } +model PortfolioSortContext { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + year String + albumId String + type String + group String + sortOrder Int + + imageId String + image PortfolioImage @relation(fields: [imageId], references: [id]) + + @@unique([imageId, year, albumId, type, group]) +} + model Color { id String @id @default(cuid()) createdAt DateTime @default(now()) diff --git a/src/actions/portfolio/deleteItem.ts b/src/actions/portfolio/deleteItem.ts index 0d5d297..015ab15 100644 --- a/src/actions/portfolio/deleteItem.ts +++ b/src/actions/portfolio/deleteItem.ts @@ -14,6 +14,9 @@ export async function deleteItems(itemId: string, type: string) { case "types": await prisma.portfolioType.delete({ where: { id: itemId } }); break; + case "albums": + await prisma.portfolioAlbum.delete({ where: { id: itemId } }); + break; } return { success: true }; diff --git a/src/actions/portfolio/images/getImageSort.ts b/src/actions/portfolio/images/getImageSort.ts new file mode 100644 index 0000000..281145f --- /dev/null +++ b/src/actions/portfolio/images/getImageSort.ts @@ -0,0 +1,89 @@ +'use server' + +import { Prisma } from "@/generated/prisma" +import prisma from "@/lib/prisma" + +type LayoutGroup = "highlighted" | "featured" | "default" + +type GroupedImages = Record + +type ImageWithSortContext = Prisma.PortfolioImageGetPayload<{ + include: { sortContexts: true } +}> + +const isValidGroup = (group: string): group is LayoutGroup => + ["highlighted", "featured", "default"].includes(group) + +export async function getImageSort({ + type, + published, + groupMode, + groupId, +}: { + type: string + published: string + groupMode: "year" | "album" + groupId?: string +}): Promise { + const where: Prisma.PortfolioImageWhereInput = {} + + // fallback-safe values + const resolvedGroupId = groupId ?? "all" + const resolvedYear = groupMode === "year" ? resolvedGroupId : "all" + const resolvedAlbumId = groupMode === "album" ? resolvedGroupId : "all" + const resolvedType = type ?? "all" + + if (type !== "all") { + where.typeId = type === "none" ? null : type + } + + if (published === "published") { + where.published = true + } else if (published === "unpublished") { + where.published = false + } + + if (groupMode === "year" && resolvedGroupId !== "all") { + where.year = parseInt(resolvedGroupId) + } else if (groupMode === "album" && resolvedGroupId !== "all") { + where.albumId = resolvedGroupId + } + + const images = await prisma.portfolioImage.findMany({ + where, + include: { + sortContexts: true, + }, + }) + + const groups: GroupedImages = { + highlighted: [], + featured: [], + default: [], + } + + for (const image of images) { + const context = image.sortContexts.find((ctx) => + ctx.year === resolvedYear && + ctx.albumId === resolvedAlbumId && + ctx.type === resolvedType && + isValidGroup(ctx.group) + ) + + const group: LayoutGroup = context?.group && isValidGroup(context.group) + ? context.group + : "default" + + groups[group].push(image) + } + + for (const group of Object.keys(groups) as LayoutGroup[]) { + groups[group].sort((a, b) => { + const aOrder = a.sortContexts.find((ctx) => ctx.group === group)?.sortOrder ?? 0 + const bOrder = b.sortContexts.find((ctx) => ctx.group === group)?.sortOrder ?? 0 + return aOrder - bOrder + }) + } + + return groups +} diff --git a/src/actions/portfolio/images/saveImageSort.ts b/src/actions/portfolio/images/saveImageSort.ts new file mode 100644 index 0000000..1a5af7f --- /dev/null +++ b/src/actions/portfolio/images/saveImageSort.ts @@ -0,0 +1,41 @@ +'use server' + +import prisma from "@/lib/prisma" + +type SortPayload = { + imageId: string + group: string + sortOrder: number + year?: string + albumId?: string + type?: string +} + +export async function saveImageSort( + updates: SortPayload[] +) { + for (const { imageId, group, sortOrder, year = "all", albumId = "all", type = "all" } of updates) { + await prisma.portfolioSortContext.upsert({ + where: { + imageId_year_albumId_type_group: { + imageId, + year, + albumId, + type, + group, + }, + }, + create: { + imageId, + year, + albumId, + type, + group, + sortOrder, + }, + update: { + sortOrder, + }, + }) + } +} diff --git a/src/actions/portfolio/images/saveImageSortForSubset.ts b/src/actions/portfolio/images/saveImageSortForSubset.ts new file mode 100644 index 0000000..9df7218 --- /dev/null +++ b/src/actions/portfolio/images/saveImageSortForSubset.ts @@ -0,0 +1,35 @@ +import prisma from "@/lib/prisma" + +interface SaveSortParams { + year: string + albumId: string + type: string + groups: { + group: "highlighted" | "featured" | "default" + imageIds: string[] + }[] +} + +export async function saveImageSortForSubset({ + year, + albumId, + type, + groups, +}: SaveSortParams) { + await prisma.portfolioSortContext.deleteMany({ + where: { year, albumId, type }, + }) + + const data = groups.flatMap(({ group, imageIds }) => + imageIds.map((id, index) => ({ + year, + albumId, + type, + group, + imageId: id, + sortOrder: index, + })) + ) + + await prisma.portfolioSortContext.createMany({ data }) +} diff --git a/src/actions/portfolio/images/saveSortOrder.ts b/src/actions/portfolio/images/saveSortOrder.ts new file mode 100644 index 0000000..07cf2dd --- /dev/null +++ b/src/actions/portfolio/images/saveSortOrder.ts @@ -0,0 +1,40 @@ +"use server"; + +import prisma from "@/lib/prisma"; + +interface SaveSortOrderParams { + year: string; + albumId: string; + type: string; + group: "highlighted" | "featured" | "default" | string; + imageOrder: { imageId: string; sortOrder: number }[]; +} + +export async function saveSortOrder({ + year, + albumId, + type, + group, + imageOrder, +}: SaveSortOrderParams) { + const contextFilter = { + year, + albumId, + type, + group, + }; + + // Delete previous context entries for this group/subset + await prisma.portfolioSortContext.deleteMany({ + where: contextFilter, + }); + + // Recreate with new order + await prisma.portfolioSortContext.createMany({ + data: imageOrder.map(({ imageId, sortOrder }) => ({ + ...contextFilter, + imageId, + sortOrder, + })), + }); +} diff --git a/src/actions/portfolio/images/updateImage.ts b/src/actions/portfolio/images/updateImage.ts index af49ff9..69df563 100644 --- a/src/actions/portfolio/images/updateImage.ts +++ b/src/actions/portfolio/images/updateImage.ts @@ -9,6 +9,7 @@ export async function updateImage( id: string ) { const validated = imageSchema.safeParse(values); + // console.log(validated) if (!validated.success) { throw new Error("Invalid image data"); } @@ -16,17 +17,19 @@ export async function updateImage( const { fileKey, originalFile, + fileType, name, + fileSize, + needsWork, nsfw, published, setAsHeader, altText, description, - fileType, - fileSize, month, year, creationDate, + albumId, typeId, tagIds, categoryIds @@ -45,17 +48,19 @@ export async function updateImage( data: { fileKey, originalFile, + fileType, name, + fileSize, + needsWork, nsfw, published, setAsHeader, altText, description, - fileType, - fileSize, month, year, creationDate, + albumId, typeId } }); diff --git a/src/app/portfolio/images/[id]/page.tsx b/src/app/portfolio/images/[id]/page.tsx index 7d170ba..2203568 100644 --- a/src/app/portfolio/images/[id]/page.tsx +++ b/src/app/portfolio/images/[id]/page.tsx @@ -9,15 +9,18 @@ export default async function PortfolioImagesEditPage({ params }: { params: { id const image = await prisma.portfolioImage.findUnique({ where: { id }, include: { + album: true, type: true, metadata: true, categories: true, colors: { include: { color: true } }, + sortContexts: true, tags: true, variants: true } }) + const albums = await prisma.portfolioAlbum.findMany({ orderBy: { sortIndex: "asc" } }); const categories = await prisma.portfolioCategory.findMany({ orderBy: { sortIndex: "asc" } }); const tags = await prisma.portfolioTag.findMany({ orderBy: { sortIndex: "asc" } }); const types = await prisma.portfolioType.findMany({ orderBy: { sortIndex: "asc" } }); @@ -29,7 +32,7 @@ export default async function PortfolioImagesEditPage({ params }: { params: { id

Edit image

- {image ? : 'Image not found...'} + {image ? : 'Image not found...'}
{image && }
diff --git a/src/app/portfolio/images/page.tsx b/src/app/portfolio/images/page.tsx index 9f7632e..285396a 100644 --- a/src/app/portfolio/images/page.tsx +++ b/src/app/portfolio/images/page.tsx @@ -40,6 +40,8 @@ export default async function PortfolioImagesPage({ where.published = true; } else if (published === "unpublished") { where.published = false; + } else if (published === "needsWork") { + where.needsWork = true; } // Filter by group (year or album) diff --git a/src/app/portfolio/images/sort/page.tsx b/src/app/portfolio/images/sort/page.tsx index 6c8a563..5aa0187 100644 --- a/src/app/portfolio/images/sort/page.tsx +++ b/src/app/portfolio/images/sort/page.tsx @@ -1,6 +1,5 @@ +import { getImageSort } from "@/actions/portfolio/images/getImageSort"; import ImageSortGallery from "@/components/portfolio/images/ImageSortGallery"; -import { Prisma } from "@/generated/prisma"; -import prisma from "@/lib/prisma"; export default async function PortfolioImagesSortPage({ searchParams @@ -24,50 +23,15 @@ export default async function PortfolioImagesSortPage({ const groupMode = groupBy === "album" ? "album" : "year"; const groupId = groupMode === "album" ? album ?? "all" : year ?? "all"; - const where: Prisma.PortfolioImageWhereInput = {}; - - // Filter by type - if (type !== "all") { - where.typeId = type === "none" ? null : type; - } - - // Filter by published status - if (published === "published") { - where.published = true; - } 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, - orderBy: [{ sortIndex: 'asc' }], - } - ) + const imageGroups = await getImageSort({ type, published, groupMode, groupId }); return ( -
-
- {/* {images && images.length > 0 ? ({ - ...img, - width: 400, - height: 300, - }))} - /> :

No images found.

} */} - {images && images.length > 0 ? - - : -

No images found.

- } -
+
+ {Object.values(imageGroups).flat().length > 0 ? ( + + ) : ( +

No images found.

+ )}
); -} \ No newline at end of file +} diff --git a/src/components/portfolio/images/EditImageForm.tsx b/src/components/portfolio/images/EditImageForm.tsx index 18b36f6..b9ba697 100644 --- a/src/components/portfolio/images/EditImageForm.tsx +++ b/src/components/portfolio/images/EditImageForm.tsx @@ -10,7 +10,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioCategory, PortfolioImage, PortfolioTag, PortfolioType } from "@/generated/prisma"; +import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioAlbum, PortfolioCategory, PortfolioImage, PortfolioSortContext, PortfolioTag, PortfolioType } from "@/generated/prisma"; import { cn } from "@/lib/utils"; import { imageSchema } from "@/schemas/portfolio/imageSchema"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -21,22 +21,25 @@ import { toast } from "sonner"; import { z } from "zod/v4"; type ImageWithItems = PortfolioImage & { + album: PortfolioAlbum | null, + type: PortfolioType | null, metadata: ImageMetadata | null, + categories: PortfolioCategory[], colors: ( ImageColor & { color: Color } )[], - variants: ImageVariant[], - categories: PortfolioCategory[], + sortContexts: PortfolioSortContext[], tags: PortfolioTag[], - type: PortfolioType | null, + variants: ImageVariant[], }; -export default function EditImageForm({ image, categories, tags, types }: +export default function EditImageForm({ image, albums, categories, tags, types }: { image: ImageWithItems, + albums: PortfolioAlbum[], categories: PortfolioCategory[] tags: PortfolioTag[], types: PortfolioType[] @@ -47,24 +50,28 @@ export default function EditImageForm({ image, categories, tags, types }: defaultValues: { fileKey: image.fileKey, originalFile: image.originalFile, + fileType: image.fileType, name: image.name, + fileSize: image.fileSize, + needsWork: image.needsWork ?? true, nsfw: image.nsfw ?? false, published: image.published ?? false, setAsHeader: image.setAsHeader ?? false, altText: image.altText || "", description: image.description || "", - fileType: image.fileType || "", - layoutGroup: image.layoutGroup || "", - fileSize: image.fileSize || undefined, - layoutOrder: image.layoutOrder || undefined, month: image.month || undefined, year: image.year || undefined, creationDate: image.creationDate ? new Date(image.creationDate) : undefined, + albumId: image.albumId ?? undefined, typeId: image.typeId ?? undefined, - tagIds: image.tags?.map(tag => tag.id) ?? [], + metadataId: image.metadata?.id ?? undefined, categoryIds: image.categories?.map(cat => cat.id) ?? [], + colorIds: image.colors?.map(color => color.id) ?? [], + sortContextIds: image.sortContexts?.map(sortContext => sortContext.id) ?? [], + tagIds: image.tags?.map(tag => tag.id) ?? [], + variantIds: image.variants?.map(variant => variant.id) ?? [], } }) @@ -203,6 +210,33 @@ export default function EditImageForm({ image, categories, tags, types }: )} /> {/* Select */} + ( + + Album + + + + )} + /> {/* Boolean */} + ( + +
+ Needs some work + +
+ + + +
+ )} + /> setFilter("published", "unpublished")} /> + setFilter("published", "needsWork")} + />
diff --git a/src/components/portfolio/images/ImageSortGallery.tsx b/src/components/portfolio/images/ImageSortGallery.tsx index 614c7a2..7d8a256 100644 --- a/src/components/portfolio/images/ImageSortGallery.tsx +++ b/src/components/portfolio/images/ImageSortGallery.tsx @@ -1,5 +1,6 @@ "use client" +import { saveImageSort } from "@/actions/portfolio/images/saveImageSort" import { PortfolioImage } from "@/generated/prisma" import { closestCenter, @@ -14,120 +15,101 @@ import { useSortable } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" +import { ImageIcon, Sparkles, Star } from "lucide-react" import Image from "next/image" import React, { useEffect, useState } from "react" type LayoutGroup = "highlighted" | "featured" | "default" - type GroupedImages = Record -export default function ImageSortGallery({ images }: { images: PortfolioImage[] }) { - const [items, setItems] = useState({ - highlighted: [], - featured: [], - default: [], - }) +export default function ImageSortGallery({ images }: { images: GroupedImages }) { + const [items, setItems] = useState(images) + const [mounted, setMounted] = useState(false) useEffect(() => { - setItems({ - highlighted: images - .filter((img) => img.layoutGroup === "highlighted") - .sort((a, b) => a.sortIndex - b.sortIndex), - featured: images - .filter((img) => img.layoutGroup === "featured") - .sort((a, b) => a.sortIndex - b.sortIndex), - default: images - .filter((img) => !img.layoutGroup || img.layoutGroup === "default") - .sort((a, b) => a.sortIndex - b.sortIndex), - }) - }, [images]) + setMounted(true) + }, []) - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event; - if (!over) return; + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + if (!over) return - const activeId = active.id as string; - const overId = over.id as string; + const activeId = active.id as string + const overId = over.id as string - // Find source group (where the item is coming from) - const sourceGroup = findGroupOfItem(activeId); - if (!sourceGroup) return; + const sourceGroup = findGroupOfItem(activeId) + const targetGroup = findGroupOfItem(overId) ?? (over.id as LayoutGroup) + if (!sourceGroup || !targetGroup) return - // Determine target group (where the item is going to) - let targetGroup: LayoutGroup; - - // Check if we're dropping onto an item (then use its group) - const overGroup = findGroupOfItem(overId); - if (overGroup) { - targetGroup = overGroup; - } else { - // Otherwise, we're dropping onto a zone (use the zone's id) - targetGroup = overId as LayoutGroup; - } - - // If dropping onto the same item, do nothing - if (sourceGroup === targetGroup && activeId === overId) return; - - // Find the active item - const activeItem = items[sourceGroup].find((i) => i.id === activeId); - if (!activeItem) return; + const activeItem = items[sourceGroup].find((i) => i.id === activeId) + if (!activeItem) return if (sourceGroup === targetGroup) { - // Intra-group movement - const oldIndex = items[sourceGroup].findIndex((i) => i.id === activeId); - const newIndex = items[targetGroup].findIndex((i) => i.id === overId); - if (oldIndex === -1 || newIndex === -1) return; + const oldIndex = items[sourceGroup].findIndex((i) => i.id === activeId) + const newIndex = items[targetGroup].findIndex((i) => i.id === overId) + if (oldIndex === -1 || newIndex === -1) return setItems((prev) => ({ ...prev, [sourceGroup]: arrayMove(prev[sourceGroup], oldIndex, newIndex), - })); + })) } else { - // Inter-group movement setItems((prev) => { - // Remove from source group - const updatedSource = prev[sourceGroup].filter((i) => i.id !== activeId); - - // Add to target group at the end (or you could insert at a specific position) - const updatedTarget = [...prev[targetGroup], { - ...activeItem, - layoutGroup: targetGroup, - sortIndex: prev[targetGroup].length // Set new sort index - }]; - + const updatedSource = prev[sourceGroup].filter((i) => i.id !== activeId) + const updatedTarget = [...prev[targetGroup], activeItem] return { ...prev, [sourceGroup]: updatedSource, [targetGroup]: updatedTarget, - }; - }); + } + }) } } const findGroupOfItem = (id: string): LayoutGroup | undefined => { - for (const group of ['highlighted', 'featured', 'default'] as LayoutGroup[]) { - if (items[group].some((img) => img.id === id)) { - return group; - } - } - return undefined; - }; + return (["highlighted", "featured", "default"] as LayoutGroup[]).find( + (group) => items[group].some((img) => img.id === id) + ) + } const savePositions = async () => { - await fetch("/api/images", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(items), - }) - alert("Positions saved successfully!") + const allUpdates = (["highlighted", "featured", "default"] as LayoutGroup[]).flatMap((group) => + items[group].map((img, index) => ({ + imageId: img.id, + group, + sortOrder: index, + year: img.year?.toString() ?? "all", + albumId: img.albumId ?? "all", + type: img.typeId ?? "all", + })) + ) + + await saveImageSort(allUpdates) + alert("Positions saved.") } + const groupColors = { + highlighted: "text-pink-500", + featured: "text-yellow-500", + default: "text-gray-500", + } + + const groupIcons = { + highlighted: , + featured: , + default: , + } + + if (!mounted) return null + return (
{(["highlighted", "featured", "default"] as LayoutGroup[]).map((group) => (
-

{group}

+

+ {groupIcons[group]} {group} +

i.id)} strategy={rectSortingStrategy} @@ -141,7 +123,6 @@ export default function ImageSortGallery({ images }: { images: PortfolioImage[]
))}
-
+ ); +} diff --git a/src/schemas/portfolio/imageSchema.ts b/src/schemas/portfolio/imageSchema.ts index 631be26..387ef43 100644 --- a/src/schemas/portfolio/imageSchema.ts +++ b/src/schemas/portfolio/imageSchema.ts @@ -11,29 +11,28 @@ export const imageUploadSchema = z.object({ export const imageSchema = z.object({ fileKey: z.string().min(1, "File key is required"), originalFile: z.string().min(1, "Original file is required"), + fileType: z.string().min(1, "File type is required"), name: z.string().min(1, "Name is required"), + fileSize: z.number().min(1, "File size is required"), + needsWork: z.boolean(), nsfw: z.boolean(), published: z.boolean(), setAsHeader: z.boolean(), altText: z.string().optional(), description: z.string().optional(), - fileType: z.string().optional(), - layoutGroup: z.string().optional(), - fileSize: z.number().optional(), - layoutOrder: z.number().optional(), month: z.number().optional(), year: z.number().optional(), creationDate: z.date().optional(), - // group: z.string().optional(), - // kind: z.string().optional(), - // series: z.string().optional(), - // slug: z.string().optional(), - // fileSize: z.number().optional(), + albumId: z.string().optional(), typeId: z.string().optional(), - colorIds: z.array(z.string()).optional(), + metadataId: z.string().optional(), + categoryIds: z.array(z.string()).optional(), + colorIds: z.array(z.string()).optional(), + sortContextIds: z.array(z.string()).optional(), tagIds: z.array(z.string()).optional(), + variantIds: z.array(z.string()).optional(), }) \ No newline at end of file diff --git a/src/utils/getSortKey.ts b/src/utils/getSortKey.ts new file mode 100644 index 0000000..3b44ec9 --- /dev/null +++ b/src/utils/getSortKey.ts @@ -0,0 +1,13 @@ +export function getSortKey({ + type, + published, + groupBy, + groupId, +}: { + type: string + published: string + groupBy: "year" | "album" + groupId: string +}) { + return `groupBy:${groupBy}|group:${groupId}|type:${type}|published:${published}`; +}