From 0700592ebe27a0c79cb9d614996a98209fb9f558 Mon Sep 17 00:00:00 2001 From: Citali Date: Tue, 24 Jun 2025 23:44:25 +0200 Subject: [PATCH] Add CRUD for galleries --- package-lock.json | 163 ++++++++++++++- package.json | 8 +- .../20250624201728_init_gallery/migration.sql | 14 ++ .../20250624212636_add_albums/migration.sql | 15 ++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 34 ++++ src/actions/galleries/createGallery.ts | 15 ++ src/actions/galleries/deleteGallery.ts | 7 + src/actions/galleries/updateGallery.ts | 21 ++ src/app/galleries/edit/[id]/page.tsx | 22 +++ src/app/galleries/new/page.tsx | 10 + src/app/galleries/page.tsx | 20 ++ .../galleries/edit/EditGalleryForm.tsx | 152 ++++++++++++++ .../galleries/list/ListGalleries.tsx | 24 +++ .../galleries/new/CreateGalleryForm.tsx | 88 +++++++++ src/components/global/TopNav.tsx | 2 +- src/components/ui/card.tsx | 92 +++++++++ src/components/ui/form.tsx | 167 ++++++++++++++++ src/components/ui/input.tsx | 21 ++ src/components/ui/label.tsx | 24 +++ src/components/ui/select.tsx | 185 ++++++++++++++++++ src/lib/prisma.ts | 14 ++ src/schemas/galleries/gallerySchema.ts | 7 + 23 files changed, 1096 insertions(+), 12 deletions(-) create mode 100644 prisma/migrations/20250624201728_init_gallery/migration.sql create mode 100644 prisma/migrations/20250624212636_add_albums/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/actions/galleries/createGallery.ts create mode 100644 src/actions/galleries/deleteGallery.ts create mode 100644 src/actions/galleries/updateGallery.ts create mode 100644 src/app/galleries/edit/[id]/page.tsx create mode 100644 src/app/galleries/new/page.tsx create mode 100644 src/app/galleries/page.tsx create mode 100644 src/components/galleries/edit/EditGalleryForm.tsx create mode 100644 src/components/galleries/list/ListGalleries.tsx create mode 100644 src/components/galleries/new/CreateGalleryForm.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/lib/prisma.ts create mode 100644 src/schemas/galleries/gallerySchema.ts diff --git a/package-lock.json b/package-lock.json index a5df52a..40a78f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "admin.fellies.art", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@prisma/client": "^6.10.1", "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -18,8 +22,10 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.58.1", "sonner": "^2.0.5", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -289,6 +295,18 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", + "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1035,11 +1053,33 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@prisma/client": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz", + "integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/@prisma/config": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz", "integrity": "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "jiti": "2.4.2" @@ -1049,14 +1089,14 @@ "version": "6.10.1", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz", "integrity": "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz", "integrity": "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1070,14 +1110,14 @@ "version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz", "integrity": "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz", "integrity": "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.10.1", @@ -1089,12 +1129,18 @@ "version": "6.10.1", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz", "integrity": "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.10.1" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", @@ -1309,6 +1355,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", @@ -1519,6 +1588,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1716,6 +1828,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -4951,7 +5069,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -5934,7 +6052,7 @@ "version": "6.10.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz", "integrity": "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -6020,6 +6138,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.58.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz", + "integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6979,7 +7113,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7240,6 +7374,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 57aaf70..feb697f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@prisma/client": "^6.10.1", "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -19,8 +23,10 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.58.1", "sonner": "^2.0.5", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/prisma/migrations/20250624201728_init_gallery/migration.sql b/prisma/migrations/20250624201728_init_gallery/migration.sql new file mode 100644 index 0000000..f0964c5 --- /dev/null +++ b/prisma/migrations/20250624201728_init_gallery/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Gallery" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + + CONSTRAINT "Gallery_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Gallery_slug_key" ON "Gallery"("slug"); diff --git a/prisma/migrations/20250624212636_add_albums/migration.sql b/prisma/migrations/20250624212636_add_albums/migration.sql new file mode 100644 index 0000000..35d5aa8 --- /dev/null +++ b/prisma/migrations/20250624212636_add_albums/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "Album" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "galleryId" TEXT, + + CONSTRAINT "Album_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Album" ADD CONSTRAINT "Album_galleryId_fkey" FOREIGN KEY ("galleryId") REFERENCES "Gallery"("id") ON DELETE SET NULL ON UPDATE CASCADE; 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 aaeee1b..3370cfa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,3 +13,37 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } + +model Gallery { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String + slug String @unique + + description String? + + // coverImageId String? + // coverImage Image? @relation("GalleryCoverImage", fields: [coverImageId], references: [id]) + + albums Album[] +} + +model Album { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String + slug String + + description String? + + // coverImageId String? + galleryId String? + // coverImage Image? @relation("AlbumCoverImage", fields: [coverImageId], references: [id]) + gallery Gallery? @relation(fields: [galleryId], references: [id]) + + // images Image[] +} diff --git a/src/actions/galleries/createGallery.ts b/src/actions/galleries/createGallery.ts new file mode 100644 index 0000000..28663d1 --- /dev/null +++ b/src/actions/galleries/createGallery.ts @@ -0,0 +1,15 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { gallerySchema } from "@/schemas/galleries/gallerySchema"; +import * as z from "zod/v4"; + +export async function createGallery(values: z.infer) { + return await prisma.gallery.create({ + data: { + name: values.name, + slug: values.slug, + description: values.description, + } + }) +} \ No newline at end of file diff --git a/src/actions/galleries/deleteGallery.ts b/src/actions/galleries/deleteGallery.ts new file mode 100644 index 0000000..ae72d09 --- /dev/null +++ b/src/actions/galleries/deleteGallery.ts @@ -0,0 +1,7 @@ +"use server"; + +import prisma from "@/lib/prisma"; + +export async function deleteGallery(id: string) { + await prisma.gallery.delete({ where: { id } }); +} \ No newline at end of file diff --git a/src/actions/galleries/updateGallery.ts b/src/actions/galleries/updateGallery.ts new file mode 100644 index 0000000..17cc2c7 --- /dev/null +++ b/src/actions/galleries/updateGallery.ts @@ -0,0 +1,21 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { gallerySchema } from "@/schemas/galleries/gallerySchema"; +import * as z from "zod/v4"; + +export async function updateGallery( + values: z.infer, + id: string +) { + return await prisma.gallery.update({ + where: { + id: id + }, + data: { + name: values.name, + slug: values.slug, + description: values.description, + } + }) +} \ No newline at end of file diff --git a/src/app/galleries/edit/[id]/page.tsx b/src/app/galleries/edit/[id]/page.tsx new file mode 100644 index 0000000..2c0305d --- /dev/null +++ b/src/app/galleries/edit/[id]/page.tsx @@ -0,0 +1,22 @@ +import EditGalleryForm from "@/components/galleries/edit/EditGalleryForm"; +import prisma from "@/lib/prisma"; + +export default async function GalleriesEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + + const gallery = await prisma.gallery.findUnique({ + where: { + id, + }, + include: { + albums: true + } + }); + + return ( +
+

Edit gallery

+ {gallery ? : 'Gallery not found...'} +
+ ); +} \ No newline at end of file diff --git a/src/app/galleries/new/page.tsx b/src/app/galleries/new/page.tsx new file mode 100644 index 0000000..10933d4 --- /dev/null +++ b/src/app/galleries/new/page.tsx @@ -0,0 +1,10 @@ +import CreateGalleryForm from "@/components/galleries/new/CreateGalleryForm"; + +export default function GalleriesNewPage() { + return ( +
+

New gallery

+ +
+ ); +} \ No newline at end of file diff --git a/src/app/galleries/page.tsx b/src/app/galleries/page.tsx new file mode 100644 index 0000000..441bdde --- /dev/null +++ b/src/app/galleries/page.tsx @@ -0,0 +1,20 @@ +import ListGalleries from "@/components/galleries/list/ListGalleries"; +import prisma from "@/lib/prisma"; +import { PlusCircleIcon } from "lucide-react"; +import Link from "next/link"; + +export default async function GalleriesPage() { + const galleries = await prisma.gallery.findMany({ orderBy: { createdAt: "asc" } }); + + return ( +
+
+

Galleries

+ + Add new gallery + +
+ {galleries.length > 0 ? :

No galleries found.

} +
+ ); +} \ No newline at end of file diff --git a/src/components/galleries/edit/EditGalleryForm.tsx b/src/components/galleries/edit/EditGalleryForm.tsx new file mode 100644 index 0000000..8fef47b --- /dev/null +++ b/src/components/galleries/edit/EditGalleryForm.tsx @@ -0,0 +1,152 @@ +"use client" + +import { deleteGallery } from "@/actions/galleries/deleteGallery"; +import { updateGallery } from "@/actions/galleries/updateGallery"; +import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Album, Gallery } from "@/generated/prisma"; +import { gallerySchema } from "@/schemas/galleries/gallerySchema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod/v4"; + +export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albums: Album[] } }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(gallerySchema), + defaultValues: { + name: gallery.name, + slug: gallery.slug, + description: gallery.description || "", + }, + }) + + async function onSubmit(values: z.infer) { + var updatedGallery = await updateGallery(values, gallery.id) + if (updatedGallery) { + toast.success("Gallery updated") + router.push(`/galleries`) + } + } + + return ( +
+
+ + ( + + Gallery name + + + + + This is your public display name. + + + + )} + /> + ( + + Gallery slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Gallery description + + + + + Description of the gallery. + + + + )} + /> +
+ + +
+ + +
+

Albums in this Gallery

+ {gallery.albums.length === 0 ? ( +

No albums yet.

+ ) : ( +
    + {gallery.albums.map((album) => ( +
  • +
    +
    {album.name}
    +
    Slug: {album.slug}
    +
    +
    + {/* Replace this with actual image count later */} + Images: 0 +
    +
  • + ))} +
+ )} +
+

+ Total images in this gallery: 0 +

+
+ {gallery.albums.length === 0 ? ( + + ) : ( + <> + +

+ You must remove all albums before deleting this gallery. +

+ + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/galleries/list/ListGalleries.tsx b/src/components/galleries/list/ListGalleries.tsx new file mode 100644 index 0000000..6828304 --- /dev/null +++ b/src/components/galleries/list/ListGalleries.tsx @@ -0,0 +1,24 @@ +// "use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Gallery } from "@/generated/prisma"; +import Link from "next/link"; + +export default function ListGalleries({ galleries }: { galleries: Gallery[] }) { + return ( +
+ {galleries.map((gallery) => ( + + + + {gallery.name} + + + {gallery.description &&

{gallery.description}

} +
+
+ + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/galleries/new/CreateGalleryForm.tsx b/src/components/galleries/new/CreateGalleryForm.tsx new file mode 100644 index 0000000..180ac0f --- /dev/null +++ b/src/components/galleries/new/CreateGalleryForm.tsx @@ -0,0 +1,88 @@ +"use client" + +import { createGallery } from "@/actions/galleries/createGallery"; +import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { gallerySchema } from "@/schemas/galleries/gallerySchema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod/v4"; + +export default function CreateGalleryForm() { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(gallerySchema), + defaultValues: { + name: "", + slug: "", + description: "", + }, + }) + + async function onSubmit(values: z.infer) { + var gallery = await createGallery(values) + if (gallery) { + toast.success("Gallery created") + router.push(`/galleries`) + } + } + + return ( +
+ + ( + + Gallery name + + + + + This is your public display name. + + + + )} + /> + ( + + Gallery slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Gallery description + + + + + Description of the gallery. + + + + )} + /> + + + + ); +} \ No newline at end of file diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index aac0316..f16e41a 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -14,7 +14,7 @@ export default function TopNav() { - Uploads + Galleries diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +