From c7a9c68605d79a22fa9b63eac4cf92bb1ec6b029 Mon Sep 17 00:00:00 2001 From: Citali Date: Fri, 27 Jun 2025 22:45:43 +0200 Subject: [PATCH] Add image edit function --- package-lock.json | 83 ++++ package.json | 3 + prisma/schema.prisma | 4 - src/actions/images/updateImage.ts | 27 +- src/app/api/image/[...key]/route.ts | 33 ++ src/app/images/edit/[id]/page.tsx | 46 ++- .../galleries/edit/EditGalleryForm.tsx | 2 +- .../images/edit/EditGalleryForm.tsx | 152 -------- .../images/edit/EditImageColors.tsx | 20 + src/components/images/edit/EditImageForm.tsx | 354 ++++++++++++++++++ .../images/edit/EditImagePalettes.tsx | 34 ++ .../images/edit/EditImageVariants.tsx | 20 + src/components/images/list/ListImages.tsx | 2 +- src/components/ui/calendar.tsx | 210 +++++++++++ src/components/ui/popover.tsx | 48 +++ src/components/ui/textarea.tsx | 18 + src/schemas/images/imageSchema.ts | 13 +- 17 files changed, 889 insertions(+), 180 deletions(-) create mode 100644 src/app/api/image/[...key]/route.ts delete mode 100644 src/components/images/edit/EditGalleryForm.tsx create mode 100644 src/components/images/edit/EditImageColors.tsx create mode 100644 src/components/images/edit/EditImageForm.tsx create mode 100644 src/components/images/edit/EditImagePalettes.tsx create mode 100644 src/components/images/edit/EditImageVariants.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/textarea.tsx diff --git a/package-lock.json b/package-lock.json index 594fbad..56343ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "@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-popover": "^1.1.14", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "extract-colors": "^4.2.0", "get-pixels": "^3.3.3", "lucide-react": "^0.523.0", @@ -28,6 +30,7 @@ "next-themes": "^0.4.6", "node-vibrant": "^4.0.3", "react": "^19.0.0", + "react-day-picker": "^9.7.0", "react-dom": "^19.0.0", "react-hook-form": "^7.58.1", "react-icons": "^5.5.0", @@ -986,6 +989,12 @@ "node": ">=18.0.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -2513,6 +2522,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@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-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "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-popper": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", @@ -5491,6 +5537,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -8770,6 +8832,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz", + "integrity": "sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index f7eb8e0..39fdadb 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "@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-popover": "^1.1.14", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "extract-colors": "^4.2.0", "get-pixels": "^3.3.3", "lucide-react": "^0.523.0", @@ -29,6 +31,7 @@ "next-themes": "^0.4.6", "node-vibrant": "^4.0.3", "react": "^19.0.0", + "react-day-picker": "^9.7.0", "react-dom": "^19.0.0", "react-hook-form": "^7.58.1", "react-icons": "^5.5.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29e5384..3321be7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -110,8 +110,6 @@ model Image { stats ImageStats[] theme ThemeSeed[] variants ImageVariant[] - // colors ImageColor[] - // extractColors ExtractColor[] // // albumCover Album[] @relation("AlbumCoverImage") // categories Category[] @relation("ImageCategories") @@ -214,12 +212,10 @@ model ExtractColor { blue Int green Int red Int - // isLight Boolean area Float? hue Float? saturation Float? - // value Float? image Image @relation(fields: [imageId], references: [id]) } diff --git a/src/actions/images/updateImage.ts b/src/actions/images/updateImage.ts index ae61e7c..9a871cc 100644 --- a/src/actions/images/updateImage.ts +++ b/src/actions/images/updateImage.ts @@ -1,21 +1,22 @@ "use server" -import prisma from "@/lib/prisma"; -import { gallerySchema } from "@/schemas/galleries/gallerySchema"; +import { imageSchema } from "@/schemas/images/imageSchema"; import * as z from "zod/v4"; export async function updateImage( - values: z.infer, + values: z.infer, id: string ) { - return await prisma.gallery.update({ - where: { - id: id - }, - data: { - name: values.name, - slug: values.slug, - description: values.description, - } - }) + console.log(values, id) + // return await prisma.image.update({ + // where: { + // id: id + // }, + // data: { + // name: values.name, + // slug: values.slug, + // description: values.description, + // } + // }) + return null } \ No newline at end of file diff --git a/src/app/api/image/[...key]/route.ts b/src/app/api/image/[...key]/route.ts new file mode 100644 index 0000000..73a7718 --- /dev/null +++ b/src/app/api/image/[...key]/route.ts @@ -0,0 +1,33 @@ +import { s3 } from "@/lib/s3"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; + +export async function GET(req: Request, { params }: { params: { key: string[] } }) { + const { key } = await params; + const s3Key = key.join("/"); + + try { + const command = new GetObjectCommand({ + Bucket: "felliesartapp", + Key: s3Key, + }); + + const response = await s3.send(command); + + if (!response.Body) { + return new Response("No body", { status: 500 }); + } + + const contentType = response.ContentType ?? "application/octet-stream"; + + return new Response(response.Body as ReadableStream, { + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=3600", + "Content-Disposition": "inline", // use 'attachment' to force download + }, + }); + } catch (err) { + console.log(err) + return new Response("Image not found", { status: 404 }); + } +} \ No newline at end of file diff --git a/src/app/images/edit/[id]/page.tsx b/src/app/images/edit/[id]/page.tsx index 2c0305d..ab401d4 100644 --- a/src/app/images/edit/[id]/page.tsx +++ b/src/app/images/edit/[id]/page.tsx @@ -1,22 +1,56 @@ -import EditGalleryForm from "@/components/galleries/edit/EditGalleryForm"; +import EditImageColors from "@/components/images/edit/EditImageColors"; +import EditImageForm from "@/components/images/edit/EditImageForm"; +import EditImagePalettes from "@/components/images/edit/EditImagePalettes"; +import EditImageVariants from "@/components/images/edit/EditImageVariants"; import prisma from "@/lib/prisma"; -export default async function GalleriesEditPage({ params }: { params: { id: string } }) { +export default async function ImagesEditPage({ params }: { params: { id: string } }) { const { id } = await params; - const gallery = await prisma.gallery.findUnique({ + const image = await prisma.image.findUnique({ where: { id, }, include: { - albums: true + album: true, + artist: true, + colors: true, + extractColors: true, + metadata: true, + pixels: true, + stats: true, + theme: true, + variants: true, + palettes: { + include: { + items: true + } + } } }); + const artists = await prisma.artist.findMany({ orderBy: { createdAt: "asc" } }); + const albums = await prisma.album.findMany({ orderBy: { createdAt: "asc" } }); + return (
-

Edit gallery

- {gallery ? : 'Gallery not found...'} +

Edit image

+
+
+ {image ? : 'Image not found...'} +
+
+
+ {image && } +
+
+ {image && } +
+
+ {image && } +
+
+
); } \ No newline at end of file diff --git a/src/components/galleries/edit/EditGalleryForm.tsx b/src/components/galleries/edit/EditGalleryForm.tsx index 8fef47b..8571ef9 100644 --- a/src/components/galleries/edit/EditGalleryForm.tsx +++ b/src/components/galleries/edit/EditGalleryForm.tsx @@ -25,7 +25,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu }) async function onSubmit(values: z.infer) { - var updatedGallery = await updateGallery(values, gallery.id) + const updatedGallery = await updateGallery(values, gallery.id) if (updatedGallery) { toast.success("Gallery updated") router.push(`/galleries`) diff --git a/src/components/images/edit/EditGalleryForm.tsx b/src/components/images/edit/EditGalleryForm.tsx deleted file mode 100644 index 8fef47b..0000000 --- a/src/components/images/edit/EditGalleryForm.tsx +++ /dev/null @@ -1,152 +0,0 @@ -"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/images/edit/EditImageColors.tsx b/src/components/images/edit/EditImageColors.tsx new file mode 100644 index 0000000..526ae9a --- /dev/null +++ b/src/components/images/edit/EditImageColors.tsx @@ -0,0 +1,20 @@ +import { ExtractColor, ImageColor } from "@/generated/prisma"; + +export default function EditImageColors({ extractColors, colors }: { extractColors: ExtractColor[], colors: ImageColor[] }) { + return ( + <> +

Extracted Colors

+
+ {extractColors.map((color, index) => ( +
+ ))} +
+

Image Colors

+
+ {colors.map((color, index) => ( +
+ ))} +
+ + ); +} \ No newline at end of file diff --git a/src/components/images/edit/EditImageForm.tsx b/src/components/images/edit/EditImageForm.tsx new file mode 100644 index 0000000..6cd101b --- /dev/null +++ b/src/components/images/edit/EditImageForm.tsx @@ -0,0 +1,354 @@ +"use client" + +import { updateImage } from "@/actions/images/updateImage"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Album, Artist, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageMetadata, ImageStats, ImageVariant, PixelSummary, ThemeSeed } from "@/generated/prisma"; +import { cn } from "@/lib/utils"; +import { imageSchema } from "@/schemas/images/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 = Image & { + album: Album, + artist: Artist, + colors: ImageColor[], + extractColors: ExtractColor[], + metadata: ImageMetadata[], + pixels: PixelSummary[], + stats: ImageStats[], + theme: ThemeSeed[], + variants: ImageVariant[], + palettes: ColorPalette[] & { + items: ColorPaletteItem[] + } +}; + +export default function EditImageForm({ image, artists, albums }: { image: ImageWithItems, artists: Artist[], albums: Album[] }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(imageSchema), + defaultValues: { + fileKey: image.fileKey, + imageName: image.imageName, + originalFile: image.originalFile, + uploadDate: image.uploadDate, + + altText: image.altText || "", + description: image.description || "", + fileType: image.fileType || "", + imageData: image.imageData || "", + creationMonth: image.creationMonth || undefined, + creationYear: image.creationYear || undefined, + fileSize: image.fileSize || undefined, + creationDate: image.creationDate ? new Date(image.creationDate) : undefined, + + artistId: image.artist?.id || undefined, + albumId: image.album?.id || undefined, + }, + }) + + // const watchCreationDate = form.watch("creationDate"); + // console.log("Watched creationDate:", watchCreationDate); + + async function onSubmit(values: z.infer) { + const updatedImage = await updateImage(values, image.id) + if (updatedImage) { + toast.success("Image updated") + + router.push(`/images`) + } + } + + return ( +
+
+ + ( + + Image Key + + + + )} + /> + ( + + Image Name + + + + )} + /> + ( + + Original file + + + + )} + /> + ( + + Upload Date + + + + + + + + + + + + + )} + /> + + ( + + Alt Text + + + + )} + /> + ( + + Description +