From 312b2c2f9485fc21d2ee2878a057183d2ed23e96 Mon Sep 17 00:00:00 2001 From: Citali Date: Sun, 20 Jul 2025 12:49:47 +0200 Subject: [PATCH] Refactor images --- package.json | 2 +- .../migration.sql | 371 +++++++++++ .../migration.sql | 2 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 315 ++++----- .../commissions/tos/getTos.ts | 0 .../commissions/tos/saveTosAction.ts | 0 .../commissions/types/deleteType.ts | 0 .../commissions/types/newType.ts | 2 +- .../types/updateCommissionTypeSortOrder.ts | 0 .../commissions/types/updateType.ts | 2 +- .../arttypes/createArtType.ts | 0 .../arttypes/updateArtType.ts | 0 .../arttypes/updateArtTypeSortOrder.ts | 0 .../edit/deleteImage.ts | 0 .../edit/generateImageColors.ts | 0 .../edit/updateImage.ts | 0 .../updateImageSortOrder.ts | 0 .../upload/uploadImage.ts | 0 src/actions/portfolio/images/createImage.ts | 189 ++++++ src/actions/portfolio/images/deleteImage.ts | 67 ++ .../portfolio/images/generateImageColors.ts | 66 ++ src/actions/portfolio/images/sortImages.ts | 15 + src/actions/portfolio/images/updateImage.ts | 70 ++ src/app/portfolio/images/[id]/page.tsx | 47 +- src/app/portfolio/images/new/page.tsx | 7 +- src/app/portfolio/images/page.tsx | 23 +- .../portfolio/images/DeleteImageButton.tsx | 26 + .../portfolio/images/EditImageForm.tsx | 380 +++++++++++ .../portfolio/images/ImageColors.tsx | 50 ++ src/components/portfolio/images/ImageList.tsx | 57 ++ .../portfolio/images/ImageVariants.tsx | 21 + .../portfolio/images/UploadImageForm.tsx | 93 +++ src/components/portfolio/images/page.tsx | 45 ++ src/components/sort/items/SortableItem.tsx | 85 +++ src/components/sort/lists/SortableList.tsx | 71 ++ src/components/ui/multiselect.tsx | 608 ++++++++++++++++++ .../{ => commissions}/commissionType.ts | 0 src/schemas/portfolio/categorySchema.ts | 9 + src/schemas/{ => portfolio}/imageSchema.ts | 18 +- .../tagSchema.ts} | 7 +- src/schemas/portfolio/typeSchema.ts | 9 + src/types/SortableItem.ts | 3 +- 43 files changed, 2486 insertions(+), 177 deletions(-) create mode 100644 prisma/migrations/20250720102612_change_images_back/migration.sql create mode 100644 prisma/migrations/20250720103246_change_images_back/migration.sql create mode 100644 prisma/migrations/migration_lock.toml rename src/actions/{items => _items}/commissions/tos/getTos.ts (100%) rename src/actions/{items => _items}/commissions/tos/saveTosAction.ts (100%) rename src/actions/{items => _items}/commissions/types/deleteType.ts (100%) rename src/actions/{items => _items}/commissions/types/newType.ts (96%) rename src/actions/{items => _items}/commissions/types/updateCommissionTypeSortOrder.ts (100%) rename src/actions/{items => _items}/commissions/types/updateType.ts (95%) rename src/actions/{portfolio => _portfolio}/arttypes/createArtType.ts (100%) rename src/actions/{portfolio => _portfolio}/arttypes/updateArtType.ts (100%) rename src/actions/{portfolio => _portfolio}/arttypes/updateArtTypeSortOrder.ts (100%) rename src/actions/{portfolio => _portfolio}/edit/deleteImage.ts (100%) rename src/actions/{portfolio => _portfolio}/edit/generateImageColors.ts (100%) rename src/actions/{portfolio => _portfolio}/edit/updateImage.ts (100%) rename src/actions/{portfolio => _portfolio}/updateImageSortOrder.ts (100%) rename src/actions/{portfolio => _portfolio}/upload/uploadImage.ts (100%) create mode 100644 src/actions/portfolio/images/createImage.ts create mode 100644 src/actions/portfolio/images/deleteImage.ts create mode 100644 src/actions/portfolio/images/generateImageColors.ts create mode 100644 src/actions/portfolio/images/sortImages.ts create mode 100644 src/actions/portfolio/images/updateImage.ts create mode 100644 src/components/portfolio/images/DeleteImageButton.tsx create mode 100644 src/components/portfolio/images/EditImageForm.tsx create mode 100644 src/components/portfolio/images/ImageColors.tsx create mode 100644 src/components/portfolio/images/ImageList.tsx create mode 100644 src/components/portfolio/images/ImageVariants.tsx create mode 100644 src/components/portfolio/images/UploadImageForm.tsx create mode 100644 src/components/portfolio/images/page.tsx create mode 100644 src/components/sort/items/SortableItem.tsx create mode 100644 src/components/sort/lists/SortableList.tsx create mode 100644 src/components/ui/multiselect.tsx rename src/schemas/{ => commissions}/commissionType.ts (100%) create mode 100644 src/schemas/portfolio/categorySchema.ts rename src/schemas/{ => portfolio}/imageSchema.ts (72%) rename src/schemas/{artTypeSchema.ts => portfolio/tagSchema.ts} (67%) create mode 100644 src/schemas/portfolio/typeSchema.ts diff --git a/package.json b/package.json index d3cdecb..2f3f3e8 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "PORT=3001 NODE_OPTIONS='--max-old-space-size=4096' next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/prisma/migrations/20250720102612_change_images_back/migration.sql b/prisma/migrations/20250720102612_change_images_back/migration.sql new file mode 100644 index 0000000..4a61ff9 --- /dev/null +++ b/prisma/migrations/20250720102612_change_images_back/migration.sql @@ -0,0 +1,371 @@ +-- CreateTable +CREATE TABLE "PortfolioImage" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "fileKey" TEXT NOT NULL, + "originalFile" TEXT NOT NULL, + "name" TEXT NOT NULL, + "nsfw" BOOLEAN NOT NULL DEFAULT false, + "published" BOOLEAN NOT NULL DEFAULT true, + "setAsHeader" BOOLEAN NOT NULL DEFAULT false, + "altText" TEXT, + "description" TEXT, + "fileType" TEXT, + "layoutGroup" TEXT, + "layoutOrder" INTEGER, + "month" INTEGER, + "year" INTEGER, + "creationDate" TIMESTAMP(3), + "typeId" TEXT, + + CONSTRAINT "PortfolioImage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PortfolioType" ( + "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 "PortfolioType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PortfolioCategory" ( + "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 "PortfolioCategory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PortfolioTag" ( + "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 "PortfolioTag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Color" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "hex" TEXT, + "blue" INTEGER, + "green" INTEGER, + "red" INTEGER, + + CONSTRAINT "Color_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ImageColor" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "imageId" TEXT NOT NULL, + "colorId" TEXT NOT NULL, + "type" TEXT NOT NULL, + + CONSTRAINT "ImageColor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ImageMetadata" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "imageId" TEXT NOT NULL, + "depth" TEXT NOT NULL, + "format" TEXT NOT NULL, + "space" TEXT NOT NULL, + "channels" INTEGER NOT NULL, + "height" INTEGER NOT NULL, + "width" INTEGER NOT NULL, + "autoOrientH" INTEGER, + "autoOrientW" INTEGER, + "bitsPerSample" INTEGER, + "density" INTEGER, + "hasAlpha" BOOLEAN, + "hasProfile" BOOLEAN, + "isPalette" BOOLEAN, + "isProgressive" BOOLEAN, + + CONSTRAINT "ImageMetadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ImageVariant" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "imageId" TEXT NOT NULL, + "s3Key" TEXT NOT NULL, + "type" TEXT NOT NULL, + "height" INTEGER NOT NULL, + "width" INTEGER NOT NULL, + "fileExtension" TEXT, + "mimeType" TEXT, + "url" TEXT, + "sizeBytes" INTEGER, + + CONSTRAINT "ImageVariant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionType" ( + "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, + "description" TEXT, + + CONSTRAINT "CommissionType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionOption" ( + "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, + "description" TEXT, + + CONSTRAINT "CommissionOption_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionExtra" ( + "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, + "description" TEXT, + + CONSTRAINT "CommissionExtra_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionCustomInput" ( + "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, + "fieldId" TEXT NOT NULL, + + CONSTRAINT "CommissionCustomInput_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionTypeOption" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "typeId" TEXT NOT NULL, + "optionId" TEXT NOT NULL, + "priceRange" TEXT, + "pricePercent" DOUBLE PRECISION, + "price" DOUBLE PRECISION, + + CONSTRAINT "CommissionTypeOption_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionTypeExtra" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "typeId" TEXT NOT NULL, + "extraId" TEXT NOT NULL, + "priceRange" TEXT, + "pricePercent" DOUBLE PRECISION, + "price" DOUBLE PRECISION, + + CONSTRAINT "CommissionTypeExtra_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionTypeCustomInput" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "typeId" TEXT NOT NULL, + "customInputId" TEXT NOT NULL, + "inputType" TEXT NOT NULL, + "label" TEXT NOT NULL, + "required" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "CommissionTypeCustomInput_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TermsOfService" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "version" SERIAL NOT NULL, + "markdown" TEXT NOT NULL, + + CONSTRAINT "TermsOfService_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CommissionRequest" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "customerName" TEXT NOT NULL, + "customerEmail" TEXT NOT NULL, + "message" TEXT NOT NULL, + "optionId" TEXT, + "typeId" TEXT, + + CONSTRAINT "CommissionRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_PortfolioImageToPortfolioTag" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_PortfolioImageToPortfolioTag_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_PortfolioCategoryToPortfolioImage" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_PortfolioCategoryToPortfolioImage_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioImage_fileKey_key" ON "PortfolioImage"("fileKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioImage_originalFile_key" ON "PortfolioImage"("originalFile"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioType_name_key" ON "PortfolioType"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioType_slug_key" ON "PortfolioType"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioCategory_name_key" ON "PortfolioCategory"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioCategory_slug_key" ON "PortfolioCategory"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioTag_name_key" ON "PortfolioTag"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioTag_slug_key" ON "PortfolioTag"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Color_name_key" ON "Color"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "ImageColor_imageId_type_key" ON "ImageColor"("imageId", "type"); + +-- CreateIndex +CREATE UNIQUE INDEX "ImageMetadata_imageId_key" ON "ImageMetadata"("imageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ImageVariant_imageId_type_key" ON "ImageVariant"("imageId", "type"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommissionCustomInput_name_key" ON "CommissionCustomInput"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommissionTypeOption_typeId_optionId_key" ON "CommissionTypeOption"("typeId", "optionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommissionTypeExtra_typeId_extraId_key" ON "CommissionTypeExtra"("typeId", "extraId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommissionTypeCustomInput_typeId_customInputId_key" ON "CommissionTypeCustomInput"("typeId", "customInputId"); + +-- CreateIndex +CREATE INDEX "_PortfolioImageToPortfolioTag_B_index" ON "_PortfolioImageToPortfolioTag"("B"); + +-- CreateIndex +CREATE INDEX "_PortfolioCategoryToPortfolioImage_B_index" ON "_PortfolioCategoryToPortfolioImage"("B"); + +-- AddForeignKey +ALTER TABLE "PortfolioImage" ADD CONSTRAINT "PortfolioImage_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "PortfolioType"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ImageColor" ADD CONSTRAINT "ImageColor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "PortfolioImage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ImageColor" ADD CONSTRAINT "ImageColor_colorId_fkey" FOREIGN KEY ("colorId") REFERENCES "Color"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ImageMetadata" ADD CONSTRAINT "ImageMetadata_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "PortfolioImage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ImageVariant" ADD CONSTRAINT "ImageVariant_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "PortfolioImage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CommissionOption"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_extraId_fkey" FOREIGN KEY ("extraId") REFERENCES "CommissionExtra"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeCustomInput" ADD CONSTRAINT "CommissionTypeCustomInput_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionTypeCustomInput" ADD CONSTRAINT "CommissionTypeCustomInput_customInputId_fkey" FOREIGN KEY ("customInputId") REFERENCES "CommissionCustomInput"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionRequest" ADD CONSTRAINT "CommissionRequest_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CommissionOption"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CommissionRequest" ADD CONSTRAINT "CommissionRequest_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PortfolioImageToPortfolioTag" ADD CONSTRAINT "_PortfolioImageToPortfolioTag_A_fkey" FOREIGN KEY ("A") REFERENCES "PortfolioImage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PortfolioImageToPortfolioTag" ADD CONSTRAINT "_PortfolioImageToPortfolioTag_B_fkey" FOREIGN KEY ("B") REFERENCES "PortfolioTag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PortfolioCategoryToPortfolioImage" ADD CONSTRAINT "_PortfolioCategoryToPortfolioImage_A_fkey" FOREIGN KEY ("A") REFERENCES "PortfolioCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PortfolioCategoryToPortfolioImage" ADD CONSTRAINT "_PortfolioCategoryToPortfolioImage_B_fkey" FOREIGN KEY ("B") REFERENCES "PortfolioImage"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250720103246_change_images_back/migration.sql b/prisma/migrations/20250720103246_change_images_back/migration.sql new file mode 100644 index 0000000..ea19639 --- /dev/null +++ b/prisma/migrations/20250720103246_change_images_back/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PortfolioImage" ADD COLUMN "fileSize" INTEGER; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 30e695c..315cfee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,6 +14,165 @@ datasource db { url = env("DATABASE_URL") } +// Portfolio +model PortfolioImage { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortIndex Int @default(0) + + fileKey String @unique + originalFile String @unique + name String + nsfw Boolean @default(false) + published Boolean @default(true) + setAsHeader 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? + + typeId String? + type PortfolioType? @relation(fields: [typeId], references: [id]) + + metadata ImageMetadata? + + categories PortfolioCategory[] + colors ImageColor[] + tags PortfolioTag[] + variants ImageVariant[] +} + +model PortfolioType { + 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 PortfolioCategory { + 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 PortfolioTag { + 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 Color { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String @unique + type String + + hex String? + blue Int? + green Int? + red Int? + + images ImageColor[] +} + +model ImageColor { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + imageId String + colorId String + type String + + image PortfolioImage @relation(fields: [imageId], references: [id]) + color Color @relation(fields: [colorId], references: [id]) + + @@unique([imageId, type]) +} + +model ImageMetadata { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + imageId String @unique + depth String + format String + space String + channels Int + height Int + width Int + + autoOrientH Int? + autoOrientW Int? + bitsPerSample Int? + density Int? + hasAlpha Boolean? + hasProfile Boolean? + isPalette Boolean? + isProgressive Boolean? + + image PortfolioImage @relation(fields: [imageId], references: [id]) +} + +model ImageVariant { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + imageId String + s3Key String + type String + height Int + width Int + + fileExtension String? + mimeType String? + url String? + sizeBytes Int? + + image PortfolioImage @relation(fields: [imageId], references: [id]) + + @@unique([imageId, type]) +} + model CommissionType { id String @id @default(cuid()) createdAt DateTime @default(now()) @@ -149,159 +308,3 @@ model CommissionRequest { option CommissionOption? @relation(fields: [optionId], references: [id]) type CommissionType? @relation(fields: [typeId], references: [id]) } - -model PortfolioImage { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - fileKey String @unique - originalFile String @unique - nsfw Boolean @default(false) - published Boolean @default(false) - setAsHeader Boolean @default(false) - - altText String? - description String? - fileType String? - group String? - kind String? - layoutGroup String? - name String? - series String? - slug String? - type String? - fileSize Int? - layoutOrder Int? - month Int? - year Int? - creationDate DateTime? - - artTypeId String? - artType PortfolioArtType? @relation(fields: [artTypeId], references: [id]) - metadata ImageMetadata? - categories PortfolioCategory[] - colors ImageColor[] - tags PortfolioTag[] - variants ImageVariant[] -} - -model PortfolioArtType { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - name String @unique - - slug String? - description String? - - images PortfolioImage[] -} - -model PortfolioCategory { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - name String @unique - - slug String? - description String? - - images PortfolioImage[] -} - -model PortfolioTag { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - name String @unique - - slug String? - description String? - - images PortfolioImage[] -} - -model Color { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - name String @unique - type String - - hex String? - blue Int? - green Int? - red Int? - - images ImageColor[] -} - -model ImageColor { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - imageId String - colorId String - type String - - image PortfolioImage @relation(fields: [imageId], references: [id]) - color Color @relation(fields: [colorId], references: [id]) - - @@unique([imageId, type]) -} - -model ImageMetadata { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - imageId String @unique - depth String - format String - space String - channels Int - height Int - width Int - - autoOrientH Int? - autoOrientW Int? - bitsPerSample Int? - density Int? - hasAlpha Boolean? - hasProfile Boolean? - isPalette Boolean? - isProgressive Boolean? - - image PortfolioImage @relation(fields: [imageId], references: [id]) -} - -model ImageVariant { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - imageId String - s3Key String - type String - height Int - width Int - - fileExtension String? - mimeType String? - url String? - sizeBytes Int? - - image PortfolioImage @relation(fields: [imageId], references: [id]) - - @@unique([imageId, type]) -} diff --git a/src/actions/items/commissions/tos/getTos.ts b/src/actions/_items/commissions/tos/getTos.ts similarity index 100% rename from src/actions/items/commissions/tos/getTos.ts rename to src/actions/_items/commissions/tos/getTos.ts diff --git a/src/actions/items/commissions/tos/saveTosAction.ts b/src/actions/_items/commissions/tos/saveTosAction.ts similarity index 100% rename from src/actions/items/commissions/tos/saveTosAction.ts rename to src/actions/_items/commissions/tos/saveTosAction.ts diff --git a/src/actions/items/commissions/types/deleteType.ts b/src/actions/_items/commissions/types/deleteType.ts similarity index 100% rename from src/actions/items/commissions/types/deleteType.ts rename to src/actions/_items/commissions/types/deleteType.ts diff --git a/src/actions/items/commissions/types/newType.ts b/src/actions/_items/commissions/types/newType.ts similarity index 96% rename from src/actions/items/commissions/types/newType.ts rename to src/actions/_items/commissions/types/newType.ts index 2777265..9b5b92d 100644 --- a/src/actions/items/commissions/types/newType.ts +++ b/src/actions/_items/commissions/types/newType.ts @@ -1,7 +1,7 @@ "use server" import prisma from "@/lib/prisma" -import { commissionTypeSchema } from "@/schemas/commissionType" +import { commissionTypeSchema } from "@/schemas/commissions/commissionType" export async function createCommissionOption(data: { name: string }) { return await prisma.commissionOption.create({ diff --git a/src/actions/items/commissions/types/updateCommissionTypeSortOrder.ts b/src/actions/_items/commissions/types/updateCommissionTypeSortOrder.ts similarity index 100% rename from src/actions/items/commissions/types/updateCommissionTypeSortOrder.ts rename to src/actions/_items/commissions/types/updateCommissionTypeSortOrder.ts diff --git a/src/actions/items/commissions/types/updateType.ts b/src/actions/_items/commissions/types/updateType.ts similarity index 95% rename from src/actions/items/commissions/types/updateType.ts rename to src/actions/_items/commissions/types/updateType.ts index ce73d2c..9348ab5 100644 --- a/src/actions/items/commissions/types/updateType.ts +++ b/src/actions/_items/commissions/types/updateType.ts @@ -1,7 +1,7 @@ "use server" import prisma from "@/lib/prisma" -import { commissionTypeSchema } from "@/schemas/commissionType" +import { commissionTypeSchema } from "@/schemas/commissions/commissionType" import * as z from "zod/v4" export async function updateCommissionType( diff --git a/src/actions/portfolio/arttypes/createArtType.ts b/src/actions/_portfolio/arttypes/createArtType.ts similarity index 100% rename from src/actions/portfolio/arttypes/createArtType.ts rename to src/actions/_portfolio/arttypes/createArtType.ts diff --git a/src/actions/portfolio/arttypes/updateArtType.ts b/src/actions/_portfolio/arttypes/updateArtType.ts similarity index 100% rename from src/actions/portfolio/arttypes/updateArtType.ts rename to src/actions/_portfolio/arttypes/updateArtType.ts diff --git a/src/actions/portfolio/arttypes/updateArtTypeSortOrder.ts b/src/actions/_portfolio/arttypes/updateArtTypeSortOrder.ts similarity index 100% rename from src/actions/portfolio/arttypes/updateArtTypeSortOrder.ts rename to src/actions/_portfolio/arttypes/updateArtTypeSortOrder.ts diff --git a/src/actions/portfolio/edit/deleteImage.ts b/src/actions/_portfolio/edit/deleteImage.ts similarity index 100% rename from src/actions/portfolio/edit/deleteImage.ts rename to src/actions/_portfolio/edit/deleteImage.ts diff --git a/src/actions/portfolio/edit/generateImageColors.ts b/src/actions/_portfolio/edit/generateImageColors.ts similarity index 100% rename from src/actions/portfolio/edit/generateImageColors.ts rename to src/actions/_portfolio/edit/generateImageColors.ts diff --git a/src/actions/portfolio/edit/updateImage.ts b/src/actions/_portfolio/edit/updateImage.ts similarity index 100% rename from src/actions/portfolio/edit/updateImage.ts rename to src/actions/_portfolio/edit/updateImage.ts diff --git a/src/actions/portfolio/updateImageSortOrder.ts b/src/actions/_portfolio/updateImageSortOrder.ts similarity index 100% rename from src/actions/portfolio/updateImageSortOrder.ts rename to src/actions/_portfolio/updateImageSortOrder.ts diff --git a/src/actions/portfolio/upload/uploadImage.ts b/src/actions/_portfolio/upload/uploadImage.ts similarity index 100% rename from src/actions/portfolio/upload/uploadImage.ts rename to src/actions/_portfolio/upload/uploadImage.ts diff --git a/src/actions/portfolio/images/createImage.ts b/src/actions/portfolio/images/createImage.ts new file mode 100644 index 0000000..60e3f9f --- /dev/null +++ b/src/actions/portfolio/images/createImage.ts @@ -0,0 +1,189 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { s3 } from "@/lib/s3"; +import { imageUploadSchema } from "@/schemas/portfolio/imageSchema"; +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import sharp from "sharp"; +import { v4 as uuidv4 } from 'uuid'; +import { z } from "zod/v4"; + +export async function createImage(values: z.infer) { + const imageFile = values.file[0]; + + if (!(imageFile instanceof File)) { + console.log("No image or invalid type"); + return null; + } + + const fileName = imageFile.name; + const fileType = imageFile.type; + const fileSize = imageFile.size; + const lastModified = new Date(imageFile.lastModified); + + const fileKey = uuidv4(); + + const arrayBuffer = await imageFile.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + const realFileType = fileType.split("/")[1]; + const originalKey = `original/${fileKey}.${realFileType}`; + const modifiedKey = `modified/${fileKey}.webp`; + const resizedKey = `resized/${fileKey}.webp`; + const thumbnailKey = `thumbnail/${fileKey}.webp`; + + const sharpData = sharp(buffer); + const metadata = await sharpData.metadata(); + + //--- Original file + await s3.send( + new PutObjectCommand({ + Bucket: "gaertan", + Key: originalKey, + Body: buffer, + ContentType: "image/" + metadata.format, + }) + ); + //--- Modified file + const modifiedBuffer = await sharp(buffer) + .toFormat('webp') + .toBuffer() + const modifiedMetadata = await sharp(modifiedBuffer).metadata(); + await s3.send( + new PutObjectCommand({ + Bucket: "gaertan", + Key: modifiedKey, + Body: modifiedBuffer, + ContentType: "image/" + modifiedMetadata.format, + }) + ); + //--- Resized file + const { width, height } = modifiedMetadata; + const targetSize = 400; + let resizeOptions; + if (width && height) { + if (height < width) { + resizeOptions = { height: targetSize }; + } else { + resizeOptions = { width: targetSize }; + } + } else { + resizeOptions = { height: targetSize }; + } + const resizedBuffer = await sharp(modifiedBuffer) + .resize({ ...resizeOptions, withoutEnlargement: true }) + .toFormat('webp') + .toBuffer(); + const resizedMetadata = await sharp(resizedBuffer).metadata(); + await s3.send( + new PutObjectCommand({ + Bucket: "gaertan", + Key: resizedKey, + Body: resizedBuffer, + ContentType: "image/" + resizedMetadata.format, + }) + ); + //--- Thumbnail file + const thumbnailTargetSize = 160; + let thumbnailOptions; + if (width && height) { + if (height < width) { + thumbnailOptions = { height: thumbnailTargetSize }; + } else { + thumbnailOptions = { width: thumbnailTargetSize }; + } + } else { + thumbnailOptions = { height: thumbnailTargetSize }; + } + const thumbnailBuffer = await sharp(modifiedBuffer) + .resize({ ...thumbnailOptions, withoutEnlargement: true }) + .toFormat('webp') + .toBuffer(); + const thumbnailMetadata = await sharp(thumbnailBuffer).metadata(); + await s3.send( + new PutObjectCommand({ + Bucket: "gaertan", + Key: thumbnailKey, + Body: thumbnailBuffer, + ContentType: "image/" + thumbnailMetadata.format, + }) + ); + + const image = await prisma.portfolioImage.create({ + data: { + name: fileName, + fileKey, + originalFile: fileName, + creationDate: lastModified, + fileType: realFileType, + fileSize: fileSize, + }, + }); + + await prisma.imageMetadata.create({ + data: { + imageId: image.id, + format: metadata.format || "unknown", + width: metadata.width || 0, + height: metadata.height || 0, + space: metadata.space || "unknown", + channels: metadata.channels || 0, + depth: metadata.depth || "unknown", + density: metadata.density ?? undefined, + bitsPerSample: metadata.bitsPerSample ?? undefined, + isProgressive: metadata.isProgressive ?? undefined, + isPalette: metadata.isPalette ?? undefined, + hasProfile: metadata.hasProfile ?? undefined, + hasAlpha: metadata.hasAlpha ?? undefined, + autoOrientW: metadata.autoOrient?.width ?? undefined, + autoOrientH: metadata.autoOrient?.height ?? undefined, + }, + }); + + await prisma.imageVariant.createMany({ + data: [ + { + s3Key: originalKey, + type: "original", + height: metadata.height, + width: metadata.width, + fileExtension: metadata.format, + mimeType: "image/" + metadata.format, + sizeBytes: metadata.size, + imageId: image.id + }, + { + s3Key: modifiedKey, + type: "modified", + height: modifiedMetadata.height, + width: modifiedMetadata.width, + fileExtension: modifiedMetadata.format, + mimeType: "image/" + modifiedMetadata.format, + sizeBytes: modifiedMetadata.size, + imageId: image.id + }, + { + s3Key: resizedKey, + type: "resized", + height: resizedMetadata.height, + width: resizedMetadata.width, + fileExtension: resizedMetadata.format, + mimeType: "image/" + resizedMetadata.format, + sizeBytes: resizedMetadata.size, + imageId: image.id + }, + { + s3Key: thumbnailKey, + type: "thumbnail", + height: thumbnailMetadata.height, + width: thumbnailMetadata.width, + fileExtension: thumbnailMetadata.format, + mimeType: "image/" + thumbnailMetadata.format, + sizeBytes: thumbnailMetadata.size, + imageId: image.id + } + ], + }); + + return image +} \ No newline at end of file diff --git a/src/actions/portfolio/images/deleteImage.ts b/src/actions/portfolio/images/deleteImage.ts new file mode 100644 index 0000000..6512ca4 --- /dev/null +++ b/src/actions/portfolio/images/deleteImage.ts @@ -0,0 +1,67 @@ +"use server"; + +import prisma from "@/lib/prisma"; +import { s3 } from "@/lib/s3"; +import { DeleteObjectCommand } from "@aws-sdk/client-s3"; + +export async function deleteImage(imageId: string) { + const image = await prisma.portfolioImage.findUnique({ + where: { id: imageId }, + include: { + variants: true, + colors: true, + metadata: true, + tags: true, + categories: true, + }, + }); + + if (!image) throw new Error("Image not found"); + + // Delete S3 objects + for (const variant of image.variants) { + try { + await s3.send( + new DeleteObjectCommand({ + Bucket: "gaertan", + Key: variant.s3Key, + }) + ); + } catch (err) { + console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err); + } + } + + // Step 1: Delete join entries + await prisma.imageColor.deleteMany({ where: { imageId } }); + + // Colors + for (const color of image.colors) { + const count = await prisma.imageColor.count({ + where: { colorId: color.colorId }, + }); + if (count === 0) { + await prisma.color.delete({ where: { id: color.colorId } }); + } + } + + // Delete variants + await prisma.imageVariant.deleteMany({ where: { imageId } }); + + // Delete metadata + await prisma.imageMetadata.deleteMany({ where: { imageId } }); + + // Clean many-to-many tag/category joins + await prisma.portfolioImage.update({ + where: { id: imageId }, + data: { + tags: { set: [] }, + categories: { set: [] }, + }, + }); + + // Finally delete the image + await prisma.portfolioImage.delete({ where: { id: imageId } }); + + return { success: true }; +} \ No newline at end of file diff --git a/src/actions/portfolio/images/generateImageColors.ts b/src/actions/portfolio/images/generateImageColors.ts new file mode 100644 index 0000000..edbe0e8 --- /dev/null +++ b/src/actions/portfolio/images/generateImageColors.ts @@ -0,0 +1,66 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { VibrantSwatch } from "@/types/VibrantSwatch"; +import { getImageBufferFromS3 } from "@/utils/getImageBufferFromS3"; +import { generateColorName, rgbToHex } from "@/utils/uploadHelper"; +import { Vibrant } from "node-vibrant/node"; + +export async function generateImageColors(imageId: string, fileKey: string, fileType?: string) { + const buffer = await getImageBufferFromS3(fileKey, fileType); + const palette = await Vibrant.from(buffer).getPalette(); + + const vibrantHexes = Object.entries(palette).map(([key, swatch]) => { + const castSwatch = swatch as VibrantSwatch | null; + const rgb = castSwatch?._rgb; + const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined); + return { type: key, hex }; + }); + + for (const { type, hex } of vibrantHexes) { + if (!hex) continue; + + const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16)); + const name = generateColorName(hex); + + const color = await prisma.color.upsert({ + where: { name }, + create: { + name, + type, + hex, + red: r, + green: g, + blue: b, + }, + update: { + hex, + red: r, + green: g, + blue: b, + }, + }); + + await prisma.imageColor.upsert({ + where: { + imageId_type: { + imageId, + type, + }, + }, + create: { + imageId, + colorId: color.id, + type, + }, + update: { + colorId: color.id, + }, + }); + } + + return await prisma.imageColor.findMany({ + where: { imageId }, + include: { color: true }, + }); +} \ No newline at end of file diff --git a/src/actions/portfolio/images/sortImages.ts b/src/actions/portfolio/images/sortImages.ts new file mode 100644 index 0000000..0d8ea9c --- /dev/null +++ b/src/actions/portfolio/images/sortImages.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function sortImages(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.portfolioImage.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/actions/portfolio/images/updateImage.ts b/src/actions/portfolio/images/updateImage.ts new file mode 100644 index 0000000..50f92bc --- /dev/null +++ b/src/actions/portfolio/images/updateImage.ts @@ -0,0 +1,70 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { imageSchema } from "@/schemas/portfolio/imageSchema"; +import { z } from "zod/v4"; + +export async function updateImage( + values: z.infer, + id: string +) { + const validated = imageSchema.safeParse(values); + if (!validated.success) { + throw new Error("Invalid image data"); + } + + const { + fileKey, + originalFile, + nsfw, + published, + altText, + description, + fileType, + name, + fileSize, + creationDate, + tagIds, + categoryIds + } = validated.data; + + const updatedImage = await prisma.portfolioImage.update({ + where: { id: id }, + data: { + fileKey, + originalFile, + nsfw, + published, + altText, + description, + fileType, + name, + fileSize, + creationDate, + } + }); + + if (tagIds) { + await prisma.portfolioImage.update({ + where: { id: id }, + data: { + tags: { + set: tagIds.map(id => ({ id })) + } + } + }); + } + + if (categoryIds) { + await prisma.portfolioImage.update({ + where: { id: id }, + data: { + categories: { + set: categoryIds.map(id => ({ id })) + } + } + }); + } + + return updatedImage +} \ No newline at end of file diff --git a/src/app/portfolio/images/[id]/page.tsx b/src/app/portfolio/images/[id]/page.tsx index 3fdf030..7d170ba 100644 --- a/src/app/portfolio/images/[id]/page.tsx +++ b/src/app/portfolio/images/[id]/page.tsx @@ -1,5 +1,48 @@ -export default function PortfolioImagesEditPage() { +import DeleteImageButton from "@/components/portfolio/images/DeleteImageButton"; +import EditImageForm from "@/components/portfolio/images/EditImageForm"; +import ImageColors from "@/components/portfolio/images/ImageColors"; +import ImageVariants from "@/components/portfolio/images/ImageVariants"; +import prisma from "@/lib/prisma"; + +export default async function PortfolioImagesEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + const image = await prisma.portfolioImage.findUnique({ + where: { id }, + include: { + type: true, + metadata: true, + categories: true, + colors: { include: { color: true } }, + tags: true, + variants: true + } + }) + + 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" } }); + + if (!image) return
Image not found
+ return ( -
PortfolioImagesEditPage
+
+

Edit image

+
+
+ {image ? : 'Image not found...'} +
+ {image && } +
+
+ {image && } +
+
+
+
+ {image && } +
+
+
+
); } \ No newline at end of file diff --git a/src/app/portfolio/images/new/page.tsx b/src/app/portfolio/images/new/page.tsx index b7dff62..33c5a57 100644 --- a/src/app/portfolio/images/new/page.tsx +++ b/src/app/portfolio/images/new/page.tsx @@ -1,5 +1,10 @@ +import UploadImageForm from "@/components/portfolio/images/UploadImageForm"; + export default function PortfolioImagesNewPage() { return ( -
PortfolioImagesNewPage
+
+

Upload image

+ +
); } \ No newline at end of file diff --git a/src/app/portfolio/images/page.tsx b/src/app/portfolio/images/page.tsx index 730f7f0..24ac32b 100644 --- a/src/app/portfolio/images/page.tsx +++ b/src/app/portfolio/images/page.tsx @@ -1,5 +1,24 @@ -export default function PortfolioImagesPage() { +import ImageList from "@/components/portfolio/images/ImageList"; +import prisma from "@/lib/prisma"; +import { PlusCircleIcon } from "lucide-react"; +import Link from "next/link"; + +export default async function PortfolioImagesPage() { + const images = await prisma.portfolioImage.findMany( + { + orderBy: [{ sortIndex: 'asc' }] + } + ) + return ( -
PortfolioImagesPage
+
+
+

Images

+ + Upload new image + +
+ {images && images.length > 0 ? :

There are no images yet. Consider adding some!

} +
); } \ No newline at end of file diff --git a/src/components/portfolio/images/DeleteImageButton.tsx b/src/components/portfolio/images/DeleteImageButton.tsx new file mode 100644 index 0000000..7d52b7d --- /dev/null +++ b/src/components/portfolio/images/DeleteImageButton.tsx @@ -0,0 +1,26 @@ +"use client" + +import { deleteImage } from "@/actions/portfolio/images/deleteImage"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; + +export default function DeleteImageButton({ imageId }: { imageId: string }) { + const router = useRouter(); + + async function handleDelete() { + if (confirm("Are you sure you want to delete this image? This action is irreversible.")) { + const result = await deleteImage(imageId); + if (result?.success) { + router.push("/portfolio/images"); + } else { + alert("Failed to delete image."); + } + } + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/portfolio/images/EditImageForm.tsx b/src/components/portfolio/images/EditImageForm.tsx new file mode 100644 index 0000000..533c45b --- /dev/null +++ b/src/components/portfolio/images/EditImageForm.tsx @@ -0,0 +1,380 @@ +"use client" + +import { updateImage } from "@/actions/portfolio/images/updateImage"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import MultipleSelector from "@/components/ui/multiselect"; +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 { cn } from "@/lib/utils"; +import { imageSchema } from "@/schemas/portfolio/imageSchema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod/v4"; + +type ImageWithItems = PortfolioImage & { + metadata: ImageMetadata | null, + colors: ( + ImageColor & { + color: Color + } + )[], + variants: ImageVariant[], + categories: PortfolioCategory[], + tags: PortfolioTag[], + type: PortfolioType | null, +}; + + +export default function EditImageForm({ image, categories, tags, types }: + { + image: ImageWithItems, + categories: PortfolioCategory[] + tags: PortfolioTag[], + types: PortfolioType[] + }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(imageSchema), + defaultValues: { + fileKey: image.fileKey, + originalFile: image.originalFile, + nsfw: image.nsfw ?? false, + published: image.nsfw ?? false, + setAsHeader: image.setAsHeader ?? false, + name: image.name, + + 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, + + typeId: image.typeId ?? undefined, + tagIds: image.tags?.map(tag => tag.id) ?? [], + categoryIds: image.categories?.map(cat => cat.id) ?? [], + } + }) + + async function onSubmit(values: z.infer) { + const updatedImage = await updateImage(values, image.id) + if (updatedImage) { + toast.success("Image updated") + router.push(`/portfolio`) + } + } + + return ( +
+
+ + {/* String */} + ( + + Image name + + + + + + )} + /> + ( + + Alt Text + + + + + + )} + /> + ( + + Description + +