From 92081828f041970e7d193fa9f6f4fb64cdc0a2a5 Mon Sep 17 00:00:00 2001 From: Citali Date: Sun, 20 Jul 2025 10:07:09 +0200 Subject: [PATCH] Add art types and categories --- .../migration.sql | 22 + .../migration.sql | 2 + .../migration.sql | 9 + .../migration.sql | 28 ++ prisma/schema.prisma | 27 +- .../portfolio/arttypes/createArtType.ts | 23 ++ src/app/portfolio/arttypes/edit/page.tsx | 5 + src/app/portfolio/arttypes/new/page.tsx | 12 + src/app/portfolio/arttypes/page.tsx | 27 ++ src/app/portfolio/categories/page.tsx | 5 + src/app/portfolio/edit/[id]/page.tsx | 3 +- src/components/global/TopNav.tsx | 40 +- .../portfolio/arttypes/EditArtTypeForm.tsx | 384 ++++++++++++++++++ .../portfolio/arttypes/ListItems.tsx | 55 +++ .../portfolio/arttypes/NewArtTypeForm.tsx | 91 +++++ .../portfolio/arttypes/SortableItem.tsx | 76 ++++ .../portfolio/edit/EditImageForm.tsx | 260 +++++++----- src/components/portfolio/tags/page.tsx | 5 + src/schemas/artTypeSchema.ts | 9 + src/schemas/imageSchema.ts | 9 + 20 files changed, 989 insertions(+), 103 deletions(-) create mode 100644 prisma/migrations/20250713200703_set_as_header_toggle/migration.sql create mode 100644 prisma/migrations/20250713200736_set_as_header_toggle/migration.sql create mode 100644 prisma/migrations/20250714174114_add_type_fields/migration.sql create mode 100644 prisma/migrations/20250714182458_add_type_fields/migration.sql create mode 100644 src/actions/portfolio/arttypes/createArtType.ts create mode 100644 src/app/portfolio/arttypes/edit/page.tsx create mode 100644 src/app/portfolio/arttypes/new/page.tsx create mode 100644 src/app/portfolio/arttypes/page.tsx create mode 100644 src/app/portfolio/categories/page.tsx create mode 100644 src/components/portfolio/arttypes/EditArtTypeForm.tsx create mode 100644 src/components/portfolio/arttypes/ListItems.tsx create mode 100644 src/components/portfolio/arttypes/NewArtTypeForm.tsx create mode 100644 src/components/portfolio/arttypes/SortableItem.tsx create mode 100644 src/components/portfolio/tags/page.tsx create mode 100644 src/schemas/artTypeSchema.ts diff --git a/prisma/migrations/20250713200703_set_as_header_toggle/migration.sql b/prisma/migrations/20250713200703_set_as_header_toggle/migration.sql new file mode 100644 index 0000000..78b6995 --- /dev/null +++ b/prisma/migrations/20250713200703_set_as_header_toggle/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name]` on the table `PortfolioCategory` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[name]` on the table `PortfolioTag` will be added. If there are existing duplicate values, this will fail. + - Made the column `name` on table `PortfolioCategory` required. This step will fail if there are existing NULL values in that column. + - Made the column `name` on table `PortfolioTag` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "PortfolioCategory" ADD COLUMN "description" TEXT, +ALTER COLUMN "name" SET NOT NULL; + +-- AlterTable +ALTER TABLE "PortfolioTag" ADD COLUMN "description" TEXT, +ALTER COLUMN "name" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioCategory_name_key" ON "PortfolioCategory"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioTag_name_key" ON "PortfolioTag"("name"); diff --git a/prisma/migrations/20250713200736_set_as_header_toggle/migration.sql b/prisma/migrations/20250713200736_set_as_header_toggle/migration.sql new file mode 100644 index 0000000..c2a122e --- /dev/null +++ b/prisma/migrations/20250713200736_set_as_header_toggle/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PortfolioImage" ADD COLUMN "setAsHeader" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20250714174114_add_type_fields/migration.sql b/prisma/migrations/20250714174114_add_type_fields/migration.sql new file mode 100644 index 0000000..58061df --- /dev/null +++ b/prisma/migrations/20250714174114_add_type_fields/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "PortfolioImage" ADD COLUMN "artType" TEXT, +ADD COLUMN "group" TEXT, +ADD COLUMN "kind" TEXT, +ADD COLUMN "layoutGroup" TEXT, +ADD COLUMN "layoutOrder" INTEGER, +ADD COLUMN "month" INTEGER, +ADD COLUMN "series" TEXT, +ADD COLUMN "year" INTEGER; diff --git a/prisma/migrations/20250714182458_add_type_fields/migration.sql b/prisma/migrations/20250714182458_add_type_fields/migration.sql new file mode 100644 index 0000000..54fd5c8 --- /dev/null +++ b/prisma/migrations/20250714182458_add_type_fields/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the column `artType` on the `PortfolioImage` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "PortfolioImage" DROP COLUMN "artType", +ADD COLUMN "artTypeId" TEXT; + +-- CreateTable +CREATE TABLE "PortfolioArtType" ( + "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, + "description" TEXT, + + CONSTRAINT "PortfolioArtType_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PortfolioArtType_name_key" ON "PortfolioArtType"("name"); + +-- AddForeignKey +ALTER TABLE "PortfolioImage" ADD CONSTRAINT "PortfolioImage_artTypeId_fkey" FOREIGN KEY ("artTypeId") REFERENCES "PortfolioArtType"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5029940..30e695c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -160,24 +160,47 @@ model PortfolioImage { 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? - metadata ImageMetadata? - + 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()) diff --git a/src/actions/portfolio/arttypes/createArtType.ts b/src/actions/portfolio/arttypes/createArtType.ts new file mode 100644 index 0000000..c42ac25 --- /dev/null +++ b/src/actions/portfolio/arttypes/createArtType.ts @@ -0,0 +1,23 @@ +import prisma from '@/lib/prisma'; +import { artTypeSchema } from '@/schemas/artTypeSchema'; + +export async function createArtType(formData: artTypeSchema) { + const parsed = artTypeSchema.safeParse(formData) + + if (!parsed.success) { + console.error("Validation failed", parsed.error) + throw new Error("Invalid input") + } + + const data = parsed.data + + const created = await prisma.portfolioArtType.create({ + data: { + name: data.name, + slug: data.slug, + description: data.description + }, + }) + + return created +} \ No newline at end of file diff --git a/src/app/portfolio/arttypes/edit/page.tsx b/src/app/portfolio/arttypes/edit/page.tsx new file mode 100644 index 0000000..a7f05c2 --- /dev/null +++ b/src/app/portfolio/arttypes/edit/page.tsx @@ -0,0 +1,5 @@ +export default function ArtTypesEditPage() { + return ( +
ArtTypesEditPage
+ ); +} \ No newline at end of file diff --git a/src/app/portfolio/arttypes/new/page.tsx b/src/app/portfolio/arttypes/new/page.tsx new file mode 100644 index 0000000..6d7399c --- /dev/null +++ b/src/app/portfolio/arttypes/new/page.tsx @@ -0,0 +1,12 @@ +import NewArtTypeForm from "@/components/portfolio/arttypes/NewArtTypeForm"; + +export default function ArtTypesNewPage() { + return ( +
+
+

New Art Type

+
+ +
+ ); +} \ No newline at end of file diff --git a/src/app/portfolio/arttypes/page.tsx b/src/app/portfolio/arttypes/page.tsx new file mode 100644 index 0000000..01b4261 --- /dev/null +++ b/src/app/portfolio/arttypes/page.tsx @@ -0,0 +1,27 @@ +import ListItems from "@/components/portfolio/arttypes/ListItems"; +import prisma from "@/lib/prisma"; +import { PlusCircleIcon } from "lucide-react"; +import Link from "next/link"; + +export default async function ArtTypesPage() { + const items = await prisma.portfolioArtType.findMany( + { + orderBy: [ + { sortIndex: 'asc' }, + { name: 'asc' } + ] + } + ); + + return ( +
+
+

Art Types

+ + Add new type + +
+ {items.length > 0 ? :

No items found.

} +
+ ); +} \ No newline at end of file diff --git a/src/app/portfolio/categories/page.tsx b/src/app/portfolio/categories/page.tsx new file mode 100644 index 0000000..adcc484 --- /dev/null +++ b/src/app/portfolio/categories/page.tsx @@ -0,0 +1,5 @@ +export default function CategoriesPage() { + return ( +
CategoriesPage
+ ); +} \ No newline at end of file diff --git a/src/app/portfolio/edit/[id]/page.tsx b/src/app/portfolio/edit/[id]/page.tsx index 273451e..3b42107 100644 --- a/src/app/portfolio/edit/[id]/page.tsx +++ b/src/app/portfolio/edit/[id]/page.tsx @@ -20,13 +20,14 @@ export default async function PortfolioEditPage({ params }: { params: { id: stri const categories = await prisma.portfolioCategory.findMany({ orderBy: { name: "asc" } }); const tags = await prisma.portfolioTag.findMany({ orderBy: { name: "asc" } }); + const artTypes = await prisma.portfolioArtType.findMany({ orderBy: { name: "asc" } }); return (

Edit image

- {image ? : 'Image not found...'} + {image ? : 'Image not found...'}
{image && }
diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index 29ed23a..debbe3a 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -1,6 +1,6 @@ "use client" -import { NavigationMenu, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; +import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import Link from "next/link"; export default function TopNav() { @@ -12,11 +12,43 @@ export default function TopNav() { Home + - - Portfolio - + Portfolio + +
    +
  • + + + All Portfolio Images + + +
  • +
  • + + + Art Types + + +
  • +
  • + + + Categories + + +
  • +
  • + + + Tags + + +
  • +
+
+ CommissionTypes diff --git a/src/components/portfolio/arttypes/EditArtTypeForm.tsx b/src/components/portfolio/arttypes/EditArtTypeForm.tsx new file mode 100644 index 0000000..88c3613 --- /dev/null +++ b/src/components/portfolio/arttypes/EditArtTypeForm.tsx @@ -0,0 +1,384 @@ +"use client" + +import { updateImage } from "@/actions/portfolio/edit/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, PortfolioArtType, PortfolioCategory, PortfolioImage, PortfolioTag } from "@/generated/prisma"; +import { cn } from "@/lib/utils"; +import { imageSchema } from "@/schemas/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 * as z from "zod/v4"; + +type ImageWithItems = PortfolioImage & { + metadata: ImageMetadata | null, + colors: ( + ImageColor & { + color: Color + } + )[], + variants: ImageVariant[], + categories: PortfolioCategory[], + tags: PortfolioTag[], + artTypes: PortfolioArtType[], +}; + + +export default function EditImageForm({ image, categories, tags, artTypes }: + { + image: ImageWithItems, + categories: PortfolioCategory[] + tags: PortfolioTag[], + artTypes: PortfolioArtType[] + }) { + 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, + + altText: image.altText || "", + description: image.description || "", + fileType: image.fileType || "", + group: image.group || "", + kind: image.kind || "", + layoutGroup: image.layoutGroup || "", + name: image.name || "", + series: image.series || "", + slug: image.slug || "", + type: image.type || "", + fileSize: image.fileSize || undefined, + layoutOrder: image.layoutOrder || undefined, + month: image.month || undefined, + year: image.year || undefined, + creationDate: image.creationDate ? new Date(image.creationDate) : 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 + +