Working sorting kinda?
This commit is contained in:
@ -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;
|
@ -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;
|
@ -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");
|
@ -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;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PortfolioImage" ALTER COLUMN "needsWork" SET DEFAULT true;
|
@ -23,26 +23,19 @@ model PortfolioImage {
|
|||||||
|
|
||||||
fileKey String @unique
|
fileKey String @unique
|
||||||
originalFile String @unique
|
originalFile String @unique
|
||||||
|
fileType String
|
||||||
name String
|
name String
|
||||||
|
fileSize Int
|
||||||
|
needsWork Boolean @default(true)
|
||||||
nsfw Boolean @default(false)
|
nsfw Boolean @default(false)
|
||||||
published Boolean @default(false)
|
published Boolean @default(false)
|
||||||
setAsHeader Boolean @default(false)
|
setAsHeader Boolean @default(false)
|
||||||
needsWork Boolean @default(false)
|
|
||||||
|
|
||||||
altText String?
|
altText String?
|
||||||
description String?
|
description String?
|
||||||
fileType String?
|
|
||||||
layoutGroup String?
|
|
||||||
fileSize Int?
|
|
||||||
layoutOrder Int?
|
|
||||||
month Int?
|
month Int?
|
||||||
year Int?
|
year Int?
|
||||||
creationDate DateTime?
|
creationDate DateTime?
|
||||||
// group String?
|
|
||||||
// kind String?
|
|
||||||
// series String?
|
|
||||||
// slug String?
|
|
||||||
// fileSize Int?
|
|
||||||
|
|
||||||
albumId String?
|
albumId String?
|
||||||
typeId String?
|
typeId String?
|
||||||
@ -51,13 +44,11 @@ model PortfolioImage {
|
|||||||
|
|
||||||
metadata ImageMetadata?
|
metadata ImageMetadata?
|
||||||
|
|
||||||
categories PortfolioCategory[]
|
categories PortfolioCategory[]
|
||||||
colors ImageColor[]
|
colors ImageColor[]
|
||||||
tags PortfolioTag[]
|
sortContexts PortfolioSortContext[]
|
||||||
variants ImageVariant[]
|
tags PortfolioTag[]
|
||||||
|
variants ImageVariant[]
|
||||||
@@index([typeId, year, layoutGroup, layoutOrder])
|
|
||||||
@@index([albumId, layoutGroup, layoutOrder])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model PortfolioAlbum {
|
model PortfolioAlbum {
|
||||||
@ -116,6 +107,23 @@ model PortfolioTag {
|
|||||||
images PortfolioImage[]
|
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 {
|
model Color {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@ -14,6 +14,9 @@ export async function deleteItems(itemId: string, type: string) {
|
|||||||
case "types":
|
case "types":
|
||||||
await prisma.portfolioType.delete({ where: { id: itemId } });
|
await prisma.portfolioType.delete({ where: { id: itemId } });
|
||||||
break;
|
break;
|
||||||
|
case "albums":
|
||||||
|
await prisma.portfolioAlbum.delete({ where: { id: itemId } });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
89
src/actions/portfolio/images/getImageSort.ts
Normal file
89
src/actions/portfolio/images/getImageSort.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { Prisma } from "@/generated/prisma"
|
||||||
|
import prisma from "@/lib/prisma"
|
||||||
|
|
||||||
|
type LayoutGroup = "highlighted" | "featured" | "default"
|
||||||
|
|
||||||
|
type GroupedImages = Record<LayoutGroup, ImageWithSortContext[]>
|
||||||
|
|
||||||
|
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<GroupedImages> {
|
||||||
|
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
|
||||||
|
}
|
41
src/actions/portfolio/images/saveImageSort.ts
Normal file
41
src/actions/portfolio/images/saveImageSort.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
35
src/actions/portfolio/images/saveImageSortForSubset.ts
Normal file
35
src/actions/portfolio/images/saveImageSortForSubset.ts
Normal file
@ -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 })
|
||||||
|
}
|
40
src/actions/portfolio/images/saveSortOrder.ts
Normal file
40
src/actions/portfolio/images/saveSortOrder.ts
Normal file
@ -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,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
@ -9,6 +9,7 @@ export async function updateImage(
|
|||||||
id: string
|
id: string
|
||||||
) {
|
) {
|
||||||
const validated = imageSchema.safeParse(values);
|
const validated = imageSchema.safeParse(values);
|
||||||
|
// console.log(validated)
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
throw new Error("Invalid image data");
|
throw new Error("Invalid image data");
|
||||||
}
|
}
|
||||||
@ -16,17 +17,19 @@ export async function updateImage(
|
|||||||
const {
|
const {
|
||||||
fileKey,
|
fileKey,
|
||||||
originalFile,
|
originalFile,
|
||||||
|
fileType,
|
||||||
name,
|
name,
|
||||||
|
fileSize,
|
||||||
|
needsWork,
|
||||||
nsfw,
|
nsfw,
|
||||||
published,
|
published,
|
||||||
setAsHeader,
|
setAsHeader,
|
||||||
altText,
|
altText,
|
||||||
description,
|
description,
|
||||||
fileType,
|
|
||||||
fileSize,
|
|
||||||
month,
|
month,
|
||||||
year,
|
year,
|
||||||
creationDate,
|
creationDate,
|
||||||
|
albumId,
|
||||||
typeId,
|
typeId,
|
||||||
tagIds,
|
tagIds,
|
||||||
categoryIds
|
categoryIds
|
||||||
@ -45,17 +48,19 @@ export async function updateImage(
|
|||||||
data: {
|
data: {
|
||||||
fileKey,
|
fileKey,
|
||||||
originalFile,
|
originalFile,
|
||||||
|
fileType,
|
||||||
name,
|
name,
|
||||||
|
fileSize,
|
||||||
|
needsWork,
|
||||||
nsfw,
|
nsfw,
|
||||||
published,
|
published,
|
||||||
setAsHeader,
|
setAsHeader,
|
||||||
altText,
|
altText,
|
||||||
description,
|
description,
|
||||||
fileType,
|
|
||||||
fileSize,
|
|
||||||
month,
|
month,
|
||||||
year,
|
year,
|
||||||
creationDate,
|
creationDate,
|
||||||
|
albumId,
|
||||||
typeId
|
typeId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9,15 +9,18 @@ export default async function PortfolioImagesEditPage({ params }: { params: { id
|
|||||||
const image = await prisma.portfolioImage.findUnique({
|
const image = await prisma.portfolioImage.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
|
album: true,
|
||||||
type: true,
|
type: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
colors: { include: { color: true } },
|
colors: { include: { color: true } },
|
||||||
|
sortContexts: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
variants: true
|
variants: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const albums = await prisma.portfolioAlbum.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
const categories = await prisma.portfolioCategory.findMany({ orderBy: { sortIndex: "asc" } });
|
const categories = await prisma.portfolioCategory.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
const tags = await prisma.portfolioTag.findMany({ orderBy: { sortIndex: "asc" } });
|
const tags = await prisma.portfolioTag.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
const types = await prisma.portfolioType.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
|
|||||||
<h1 className="text-2xl font-bold mb-4">Edit image</h1>
|
<h1 className="text-2xl font-bold mb-4">Edit image</h1>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<div>
|
<div>
|
||||||
{image ? <EditImageForm image={image} tags={tags} categories={categories} types={types} /> : 'Image not found...'}
|
{image ? <EditImageForm image={image} albums={albums} tags={tags} categories={categories} types={types} /> : 'Image not found...'}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{image && <DeleteImageButton imageId={image.id} />}
|
{image && <DeleteImageButton imageId={image.id} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,8 @@ export default async function PortfolioImagesPage({
|
|||||||
where.published = true;
|
where.published = true;
|
||||||
} else if (published === "unpublished") {
|
} else if (published === "unpublished") {
|
||||||
where.published = false;
|
where.published = false;
|
||||||
|
} else if (published === "needsWork") {
|
||||||
|
where.needsWork = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by group (year or album)
|
// Filter by group (year or album)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
import { getImageSort } from "@/actions/portfolio/images/getImageSort";
|
||||||
import ImageSortGallery from "@/components/portfolio/images/ImageSortGallery";
|
import ImageSortGallery from "@/components/portfolio/images/ImageSortGallery";
|
||||||
import { Prisma } from "@/generated/prisma";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function PortfolioImagesSortPage({
|
export default async function PortfolioImagesSortPage({
|
||||||
searchParams
|
searchParams
|
||||||
@ -24,50 +23,15 @@ export default async function PortfolioImagesSortPage({
|
|||||||
const groupMode = groupBy === "album" ? "album" : "year";
|
const groupMode = groupBy === "album" ? "album" : "year";
|
||||||
const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
|
const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
|
||||||
|
|
||||||
const where: Prisma.PortfolioImageWhereInput = {};
|
const imageGroups = await getImageSort({ type, published, groupMode, groupId });
|
||||||
|
|
||||||
// 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' }],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mt-6">
|
||||||
<div className="mt-6">
|
{Object.values(imageGroups).flat().length > 0 ? (
|
||||||
{/* {images && images.length > 0 ? <MosaicGallery
|
<ImageSortGallery images={imageGroups} />
|
||||||
images={images.map((img) => ({
|
) : (
|
||||||
...img,
|
<p className="text-muted-foreground italic">No images found.</p>
|
||||||
width: 400,
|
)}
|
||||||
height: 300,
|
|
||||||
}))}
|
|
||||||
/> : <p className="text-muted-foreground italic">No images found.</p>} */}
|
|
||||||
{images && images.length > 0 ?
|
|
||||||
<ImageSortGallery images={images} />
|
|
||||||
:
|
|
||||||
<p className="text-muted-foreground italic">No images found.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 { cn } from "@/lib/utils";
|
||||||
import { imageSchema } from "@/schemas/portfolio/imageSchema";
|
import { imageSchema } from "@/schemas/portfolio/imageSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -21,22 +21,25 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
type ImageWithItems = PortfolioImage & {
|
type ImageWithItems = PortfolioImage & {
|
||||||
|
album: PortfolioAlbum | null,
|
||||||
|
type: PortfolioType | null,
|
||||||
metadata: ImageMetadata | null,
|
metadata: ImageMetadata | null,
|
||||||
|
categories: PortfolioCategory[],
|
||||||
colors: (
|
colors: (
|
||||||
ImageColor & {
|
ImageColor & {
|
||||||
color: Color
|
color: Color
|
||||||
}
|
}
|
||||||
)[],
|
)[],
|
||||||
variants: ImageVariant[],
|
sortContexts: PortfolioSortContext[],
|
||||||
categories: PortfolioCategory[],
|
|
||||||
tags: PortfolioTag[],
|
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,
|
image: ImageWithItems,
|
||||||
|
albums: PortfolioAlbum[],
|
||||||
categories: PortfolioCategory[]
|
categories: PortfolioCategory[]
|
||||||
tags: PortfolioTag[],
|
tags: PortfolioTag[],
|
||||||
types: PortfolioType[]
|
types: PortfolioType[]
|
||||||
@ -47,24 +50,28 @@ export default function EditImageForm({ image, categories, tags, types }:
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
fileKey: image.fileKey,
|
fileKey: image.fileKey,
|
||||||
originalFile: image.originalFile,
|
originalFile: image.originalFile,
|
||||||
|
fileType: image.fileType,
|
||||||
name: image.name,
|
name: image.name,
|
||||||
|
fileSize: image.fileSize,
|
||||||
|
needsWork: image.needsWork ?? true,
|
||||||
nsfw: image.nsfw ?? false,
|
nsfw: image.nsfw ?? false,
|
||||||
published: image.published ?? false,
|
published: image.published ?? false,
|
||||||
setAsHeader: image.setAsHeader ?? false,
|
setAsHeader: image.setAsHeader ?? false,
|
||||||
|
|
||||||
altText: image.altText || "",
|
altText: image.altText || "",
|
||||||
description: image.description || "",
|
description: image.description || "",
|
||||||
fileType: image.fileType || "",
|
|
||||||
layoutGroup: image.layoutGroup || "",
|
|
||||||
fileSize: image.fileSize || undefined,
|
|
||||||
layoutOrder: image.layoutOrder || undefined,
|
|
||||||
month: image.month || undefined,
|
month: image.month || undefined,
|
||||||
year: image.year || undefined,
|
year: image.year || undefined,
|
||||||
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
||||||
|
|
||||||
|
albumId: image.albumId ?? undefined,
|
||||||
typeId: image.typeId ?? undefined,
|
typeId: image.typeId ?? undefined,
|
||||||
tagIds: image.tags?.map(tag => tag.id) ?? [],
|
metadataId: image.metadata?.id ?? undefined,
|
||||||
categoryIds: image.categories?.map(cat => cat.id) ?? [],
|
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 */}
|
{/* Select */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="albumId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Album</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(value === "" ? undefined : value)}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an album" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{albums.map((album) => (
|
||||||
|
<SelectItem key={album.id} value={album.id}>
|
||||||
|
{album.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="typeId"
|
name="typeId"
|
||||||
@ -293,6 +327,21 @@ export default function EditImageForm({ image, categories, tags, types }:
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Boolean */}
|
{/* Boolean */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="needsWork"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Needs some work</FormLabel>
|
||||||
|
<FormDescription></FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="nsfw"
|
name="nsfw"
|
||||||
|
@ -112,6 +112,11 @@ export default function FilterBar({
|
|||||||
label="Unpublished"
|
label="Unpublished"
|
||||||
onClick={() => setFilter("published", "unpublished")}
|
onClick={() => setFilter("published", "unpublished")}
|
||||||
/>
|
/>
|
||||||
|
<FilterButton
|
||||||
|
active={currentPublished === "needsWork"}
|
||||||
|
label="Needs work"
|
||||||
|
onClick={() => setFilter("published", "needsWork")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 border-b pb-6">
|
<div className="flex gap-6 border-b pb-6">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { saveImageSort } from "@/actions/portfolio/images/saveImageSort"
|
||||||
import { PortfolioImage } from "@/generated/prisma"
|
import { PortfolioImage } from "@/generated/prisma"
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@ -14,120 +15,101 @@ import {
|
|||||||
useSortable
|
useSortable
|
||||||
} from "@dnd-kit/sortable"
|
} from "@dnd-kit/sortable"
|
||||||
import { CSS } from "@dnd-kit/utilities"
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { ImageIcon, Sparkles, Star } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
|
|
||||||
type LayoutGroup = "highlighted" | "featured" | "default"
|
type LayoutGroup = "highlighted" | "featured" | "default"
|
||||||
|
|
||||||
type GroupedImages = Record<LayoutGroup, PortfolioImage[]>
|
type GroupedImages = Record<LayoutGroup, PortfolioImage[]>
|
||||||
|
|
||||||
export default function ImageSortGallery({ images }: { images: PortfolioImage[] }) {
|
export default function ImageSortGallery({ images }: { images: GroupedImages }) {
|
||||||
const [items, setItems] = useState<GroupedImages>({
|
const [items, setItems] = useState<GroupedImages>(images)
|
||||||
highlighted: [],
|
const [mounted, setMounted] = useState(false)
|
||||||
featured: [],
|
|
||||||
default: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setItems({
|
setMounted(true)
|
||||||
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])
|
|
||||||
|
|
||||||
function handleDragEnd(event: DragEndEvent) {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
if (!over) return;
|
if (!over) return
|
||||||
|
|
||||||
const activeId = active.id as string;
|
const activeId = active.id as string
|
||||||
const overId = over.id as string;
|
const overId = over.id as string
|
||||||
|
|
||||||
// Find source group (where the item is coming from)
|
const sourceGroup = findGroupOfItem(activeId)
|
||||||
const sourceGroup = findGroupOfItem(activeId);
|
const targetGroup = findGroupOfItem(overId) ?? (over.id as LayoutGroup)
|
||||||
if (!sourceGroup) return;
|
if (!sourceGroup || !targetGroup) return
|
||||||
|
|
||||||
// Determine target group (where the item is going to)
|
const activeItem = items[sourceGroup].find((i) => i.id === activeId)
|
||||||
let targetGroup: LayoutGroup;
|
if (!activeItem) return
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
if (sourceGroup === targetGroup) {
|
if (sourceGroup === targetGroup) {
|
||||||
// Intra-group movement
|
const oldIndex = items[sourceGroup].findIndex((i) => i.id === activeId)
|
||||||
const oldIndex = items[sourceGroup].findIndex((i) => i.id === activeId);
|
const newIndex = items[targetGroup].findIndex((i) => i.id === overId)
|
||||||
const newIndex = items[targetGroup].findIndex((i) => i.id === overId);
|
if (oldIndex === -1 || newIndex === -1) return
|
||||||
if (oldIndex === -1 || newIndex === -1) return;
|
|
||||||
|
|
||||||
setItems((prev) => ({
|
setItems((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[sourceGroup]: arrayMove(prev[sourceGroup], oldIndex, newIndex),
|
[sourceGroup]: arrayMove(prev[sourceGroup], oldIndex, newIndex),
|
||||||
}));
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Inter-group movement
|
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
// Remove from source group
|
const updatedSource = prev[sourceGroup].filter((i) => i.id !== activeId)
|
||||||
const updatedSource = prev[sourceGroup].filter((i) => i.id !== activeId);
|
const updatedTarget = [...prev[targetGroup], activeItem]
|
||||||
|
|
||||||
// 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
|
|
||||||
}];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[sourceGroup]: updatedSource,
|
[sourceGroup]: updatedSource,
|
||||||
[targetGroup]: updatedTarget,
|
[targetGroup]: updatedTarget,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const findGroupOfItem = (id: string): LayoutGroup | undefined => {
|
const findGroupOfItem = (id: string): LayoutGroup | undefined => {
|
||||||
for (const group of ['highlighted', 'featured', 'default'] as LayoutGroup[]) {
|
return (["highlighted", "featured", "default"] as LayoutGroup[]).find(
|
||||||
if (items[group].some((img) => img.id === id)) {
|
(group) => items[group].some((img) => img.id === id)
|
||||||
return group;
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const savePositions = async () => {
|
const savePositions = async () => {
|
||||||
await fetch("/api/images", {
|
const allUpdates = (["highlighted", "featured", "default"] as LayoutGroup[]).flatMap((group) =>
|
||||||
method: "POST",
|
items[group].map((img, index) => ({
|
||||||
headers: { "Content-Type": "application/json" },
|
imageId: img.id,
|
||||||
body: JSON.stringify(items),
|
group,
|
||||||
})
|
sortOrder: index,
|
||||||
alert("Positions saved successfully!")
|
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: <Sparkles className="inline-block w-4 h-4 text-pink-500 mr-1" />,
|
||||||
|
featured: <Star className="inline-block w-4 h-4 text-yellow-500 mr-1" />,
|
||||||
|
default: <ImageIcon className="inline-block w-4 h-4 text-gray-500 mr-1" />,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
{(["highlighted", "featured", "default"] as LayoutGroup[]).map((group) => (
|
{(["highlighted", "featured", "default"] as LayoutGroup[]).map((group) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<h2 className="text-xl font-bold capitalize mb-2">{group}</h2>
|
<h2 className={`text-lg font-semibold tracking-tight mb-2 capitalize ${groupColors[group]}`}>
|
||||||
|
{groupIcons[group]} {group}
|
||||||
|
</h2>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={items[group].map((i) => i.id)}
|
items={items[group].map((i) => i.id)}
|
||||||
strategy={rectSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
@ -141,7 +123,6 @@ export default function ImageSortGallery({ images }: { images: PortfolioImage[]
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={savePositions}
|
onClick={savePositions}
|
||||||
className="mt-4 px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700"
|
className="mt-4 px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||||
@ -152,17 +133,32 @@ export default function ImageSortGallery({ images }: { images: PortfolioImage[]
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function DroplayoutGroup({ id, children }: { id: string; children: React.ReactNode }) {
|
function DroplayoutGroup({ id, children }: { id: string; children: React.ReactNode }) {
|
||||||
const { setNodeRef, isOver } = useDroppable({ id });
|
const { setNodeRef, isOver } = useDroppable({ id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`min-h-[200px] border-2 border-dashed rounded p-4 flex flex-wrap gap-4 transition-colors ${isOver ? 'bg-blue-100 border-blue-500' : 'bg-gray-50'
|
className={`
|
||||||
} ${React.Children.count(children) === 0 ? 'items-center justify-center' : ''}`}
|
min-h-[200px]
|
||||||
|
rounded-xl
|
||||||
|
p-4
|
||||||
|
flex flex-wrap gap-4
|
||||||
|
border border-muted
|
||||||
|
shadow-sm
|
||||||
|
transition-colors
|
||||||
|
duration-200
|
||||||
|
bg-background
|
||||||
|
${isOver
|
||||||
|
? 'ring-2 ring-ring ring-offset-2 ring-offset-background'
|
||||||
|
: 'hover:ring-1 hover:ring-muted-foreground/40'
|
||||||
|
}
|
||||||
|
${React.Children.count(children) === 0 ? 'items-center justify-center' : ''}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{React.Children.count(children) === 0 ? (
|
{React.Children.count(children) === 0 ? (
|
||||||
<p className="text-gray-400">Drop images here</p>
|
<p className="text-muted-foreground text-sm">Drop images here</p>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
@ -170,6 +166,7 @@ function DroplayoutGroup({ id, children }: { id: string; children: React.ReactNo
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function DraggableImage({ id, fileKey }: { id: string; fileKey: string }) {
|
function DraggableImage({ id, fileKey }: { id: string; fileKey: string }) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@ -192,7 +189,19 @@ function DraggableImage({ id, fileKey }: { id: string; fileKey: string }) {
|
|||||||
style={style}
|
style={style}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
className="w-[100px] h-[100px] border rounded overflow-hidden"
|
className={`
|
||||||
|
w-[100px] h-[100px]
|
||||||
|
rounded-lg
|
||||||
|
overflow-hidden
|
||||||
|
border
|
||||||
|
bg-muted
|
||||||
|
transition
|
||||||
|
duration-200
|
||||||
|
shadow-sm
|
||||||
|
hover:shadow-md
|
||||||
|
hover:ring-2 hover:ring-ring
|
||||||
|
${isDragging ? 'opacity-50' : ''}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={`/api/image/thumbnail/${fileKey}.webp`}
|
src={`/api/image/thumbnail/${fileKey}.webp`}
|
||||||
|
48
src/components/portfolio/images/SortableImageItem.tsx
Normal file
48
src/components/portfolio/images/SortableImageItem.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// src/components/portfolio/SortableImageItem.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PortfolioImage } from "@/generated/prisma";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
image: PortfolioImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SortableImageItem({ id, image }: Props) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.4 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={style}
|
||||||
|
className="bg-white rounded border overflow-hidden shadow hover:shadow-md transition"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={`/api/image/thumbnail/${image.fileKey}.wepb`}
|
||||||
|
alt={image.altText ?? image.name}
|
||||||
|
width={300}
|
||||||
|
height={200}
|
||||||
|
className="object-cover w-full h-48"
|
||||||
|
/>
|
||||||
|
<div className="px-2 py-1 text-sm text-center truncate">{image.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -11,29 +11,28 @@ export const imageUploadSchema = z.object({
|
|||||||
export const imageSchema = z.object({
|
export const imageSchema = z.object({
|
||||||
fileKey: z.string().min(1, "File key is required"),
|
fileKey: z.string().min(1, "File key is required"),
|
||||||
originalFile: z.string().min(1, "Original file 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"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
fileSize: z.number().min(1, "File size is required"),
|
||||||
|
needsWork: z.boolean(),
|
||||||
nsfw: z.boolean(),
|
nsfw: z.boolean(),
|
||||||
published: z.boolean(),
|
published: z.boolean(),
|
||||||
setAsHeader: z.boolean(),
|
setAsHeader: z.boolean(),
|
||||||
|
|
||||||
altText: z.string().optional(),
|
altText: z.string().optional(),
|
||||||
description: 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(),
|
month: z.number().optional(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
creationDate: z.date().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(),
|
typeId: z.string().optional(),
|
||||||
|
|
||||||
colorIds: z.array(z.string()).optional(),
|
metadataId: z.string().optional(),
|
||||||
|
|
||||||
categoryIds: z.array(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(),
|
tagIds: z.array(z.string()).optional(),
|
||||||
|
variantIds: z.array(z.string()).optional(),
|
||||||
})
|
})
|
13
src/utils/getSortKey.ts
Normal file
13
src/utils/getSortKey.ts
Normal file
@ -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}`;
|
||||||
|
}
|
Reference in New Issue
Block a user