From 27535e45f0bf5c9665ae21eff1d9fcc44037d1fd Mon Sep 17 00:00:00 2001 From: Citali Date: Wed, 25 Jun 2025 00:28:32 +0200 Subject: [PATCH] Add CRUD for albums --- src/actions/albums/createAlbum.ts | 16 +++ src/actions/albums/deleteAlbum.ts | 7 + src/actions/albums/updateAlbum.ts | 22 ++++ src/app/albums/edit/[id]/page.tsx | 21 +++ src/app/albums/new/page.tsx | 13 ++ src/app/albums/page.tsx | 25 ++++ src/components/albums/edit/EditAlbumForm.tsx | 120 ++++++++++++++++++ src/components/albums/list/ListAlbums.tsx | 42 ++++++ src/components/albums/new/CreateAlbumForm.tsx | 118 +++++++++++++++++ .../galleries/new/CreateGalleryForm.tsx | 5 +- src/components/global/TopNav.tsx | 5 + src/schemas/albums/albumSchema.ts | 9 ++ 12 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 src/actions/albums/createAlbum.ts create mode 100644 src/actions/albums/deleteAlbum.ts create mode 100644 src/actions/albums/updateAlbum.ts create mode 100644 src/app/albums/edit/[id]/page.tsx create mode 100644 src/app/albums/new/page.tsx create mode 100644 src/app/albums/page.tsx create mode 100644 src/components/albums/edit/EditAlbumForm.tsx create mode 100644 src/components/albums/list/ListAlbums.tsx create mode 100644 src/components/albums/new/CreateAlbumForm.tsx create mode 100644 src/schemas/albums/albumSchema.ts diff --git a/src/actions/albums/createAlbum.ts b/src/actions/albums/createAlbum.ts new file mode 100644 index 0000000..c45cd37 --- /dev/null +++ b/src/actions/albums/createAlbum.ts @@ -0,0 +1,16 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { albumSchema } from "@/schemas/albums/albumSchema"; +import * as z from "zod/v4"; + +export async function createAlbum(values: z.infer) { + return await prisma.album.create({ + data: { + name: values.name, + slug: values.slug, + description: values.description, + galleryId: values.galleryId + } + }) +} \ No newline at end of file diff --git a/src/actions/albums/deleteAlbum.ts b/src/actions/albums/deleteAlbum.ts new file mode 100644 index 0000000..6414777 --- /dev/null +++ b/src/actions/albums/deleteAlbum.ts @@ -0,0 +1,7 @@ +"use server"; + +import prisma from "@/lib/prisma"; + +export async function deleteAlbum(id: string) { + await prisma.gallery.delete({ where: { id } }); +} \ No newline at end of file diff --git a/src/actions/albums/updateAlbum.ts b/src/actions/albums/updateAlbum.ts new file mode 100644 index 0000000..d3fc4d4 --- /dev/null +++ b/src/actions/albums/updateAlbum.ts @@ -0,0 +1,22 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { albumSchema } from "@/schemas/albums/albumSchema"; +import * as z from "zod/v4"; + +export async function updateAlbum( + values: z.infer, + id: string +) { + return await prisma.album.update({ + where: { + id: id + }, + data: { + name: values.name, + slug: values.slug, + description: values.description, + galleryId: values.galleryId + } + }) +} \ No newline at end of file diff --git a/src/app/albums/edit/[id]/page.tsx b/src/app/albums/edit/[id]/page.tsx new file mode 100644 index 0000000..5e21f6b --- /dev/null +++ b/src/app/albums/edit/[id]/page.tsx @@ -0,0 +1,21 @@ +import EditAlbumForm from "@/components/albums/edit/EditAlbumForm"; +import prisma from "@/lib/prisma"; + +export default async function AlbumsEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + + const album = await prisma.album.findUnique({ + where: { + id, + } + }); + + const galleries = await prisma.gallery.findMany({}); + + return ( +
+

Edit album

+ {album ? : 'Album not found...'} +
+ ); +} \ No newline at end of file diff --git a/src/app/albums/new/page.tsx b/src/app/albums/new/page.tsx new file mode 100644 index 0000000..8296d9a --- /dev/null +++ b/src/app/albums/new/page.tsx @@ -0,0 +1,13 @@ +import CreateAlbumForm from "@/components/albums/new/CreateAlbumForm"; +import prisma from "@/lib/prisma"; + +export default async function AlbumsNewPage() { + const galleries = await prisma.gallery.findMany({}); + + return ( +
+

New album

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

Albums

+ + Add new Album + +
+ {albums.length > 0 ? :

No albums found.

} +
+ ); +} \ No newline at end of file diff --git a/src/components/albums/edit/EditAlbumForm.tsx b/src/components/albums/edit/EditAlbumForm.tsx new file mode 100644 index 0000000..68b8f08 --- /dev/null +++ b/src/components/albums/edit/EditAlbumForm.tsx @@ -0,0 +1,120 @@ +"use client" + +import { updateAlbum } from "@/actions/albums/updateAlbum"; +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Album, Gallery } from "@/generated/prisma"; +import { albumSchema } from "@/schemas/albums/albumSchema"; +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 EditAlbumForm({ album, galleries }: { album: Album, galleries: Gallery[] }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(albumSchema), + defaultValues: { + name: album.name, + slug: album.slug, + description: album.description || "", + galleryId: album.galleryId || "", + }, + }) + + async function onSubmit(values: z.infer) { + var updatedAlbum = await updateAlbum(values, album.id) + if (updatedAlbum) { + toast.success("Album updated") + router.push(`/albums`) + } + } + + return ( +
+
+ + ( + + Album name + + + + + This is your public display name. + + + + )} + /> + ( + + Album slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Album description (optional) + + + + + Description of the Album. + + + + )} + /> + ( + + Gallery + + + + )} + /> +
+ + +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/albums/list/ListAlbums.tsx b/src/components/albums/list/ListAlbums.tsx new file mode 100644 index 0000000..e00f6ac --- /dev/null +++ b/src/components/albums/list/ListAlbums.tsx @@ -0,0 +1,42 @@ +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Album, Gallery } from "@/generated/prisma"; +import { TriangleAlert } from "lucide-react"; +import Link from "next/link"; + +type AlbumWithGallery = Album & { + gallery: Gallery | null; +}; + +export default function ListAlbums({ albums }: { albums: AlbumWithGallery[] }) { + return ( +
+ {albums.map((album) => ( + + + + {album.name} + + + {album.description &&

{album.description}

} +
+ +
+ {album.gallery ? ( + <>Gallery: {album.gallery.name} + ) : ( +
+ + No gallery +
+ )} +

+ Total images in this album: 0 +

+
+
+
+ + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/albums/new/CreateAlbumForm.tsx b/src/components/albums/new/CreateAlbumForm.tsx new file mode 100644 index 0000000..abf5425 --- /dev/null +++ b/src/components/albums/new/CreateAlbumForm.tsx @@ -0,0 +1,118 @@ +"use client" + +import { createAlbum } from "@/actions/albums/createAlbum"; +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Gallery } from "@/generated/prisma"; +import { albumSchema } from "@/schemas/albums/albumSchema"; +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 CreateAlbumForm({ galleries }: { galleries: Gallery[] }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(albumSchema), + defaultValues: { + name: "", + slug: "", + description: "", + galleryId: "", + }, + }) + + async function onSubmit(values: z.infer) { + var album = await createAlbum(values) + if (album) { + toast.success("Album created") + router.push(`/albums`) + } + } + + return ( +
+ + ( + + Album name + + + + + This is your public display name. + + + + )} + /> + ( + + Album slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Album description + + + + + Description of the album. + + + + )} + /> + ( + + Gallery + + + + )} + /> +
+ + +
+ + + ); +} \ No newline at end of file diff --git a/src/components/galleries/new/CreateGalleryForm.tsx b/src/components/galleries/new/CreateGalleryForm.tsx index 180ac0f..f9ae1c6 100644 --- a/src/components/galleries/new/CreateGalleryForm.tsx +++ b/src/components/galleries/new/CreateGalleryForm.tsx @@ -81,7 +81,10 @@ export default function CreateGalleryForm() { )} /> - +
+ + +
); diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index f16e41a..3c5bccc 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -17,6 +17,11 @@ export default function TopNav() { Galleries + + + Albums + + ); diff --git a/src/schemas/albums/albumSchema.ts b/src/schemas/albums/albumSchema.ts new file mode 100644 index 0000000..1d1f0d5 --- /dev/null +++ b/src/schemas/albums/albumSchema.ts @@ -0,0 +1,9 @@ +import * as z from "zod/v4"; + +export const albumSchema = z.object({ + name: z.string().min(3, "Name is required. Min 3 characters."), + slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"), + galleryId: z.string().min(1, "Please select a gallery"), + description: z.string().optional(), +}) +