diff --git a/prisma/migrations/20250625102620_artist_social/migration.sql b/prisma/migrations/20250625102620_artist_social/migration.sql new file mode 100644 index 0000000..3a4d564 --- /dev/null +++ b/prisma/migrations/20250625102620_artist_social/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "Artist" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "displayName" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "nickname" TEXT, + + CONSTRAINT "Artist_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Social" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" TEXT, + "handle" TEXT, + "url" TEXT, + "type" TEXT, + "icon" TEXT, + "artistId" TEXT, + + CONSTRAINT "Social_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Artist_slug_key" ON "Artist"("slug"); + +-- AddForeignKey +ALTER TABLE "Social" ADD CONSTRAINT "Social_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3370cfa..14bce66 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,3 +47,35 @@ model Album { // images Image[] } + +model Artist { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + displayName String + slug String @unique + + nickname String? + // mentionUrl String? + // contact String? + // urls String[] + + socials Social[] + // images Image[] +} + +model Social { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String? + handle String? + url String? + type String? + icon String? + + artistId String? + artist Artist? @relation(fields: [artistId], references: [id]) +} diff --git a/src/actions/albums/deleteAlbum.ts b/src/actions/albums/deleteAlbum.ts index 6414777..0c9166f 100644 --- a/src/actions/albums/deleteAlbum.ts +++ b/src/actions/albums/deleteAlbum.ts @@ -3,5 +3,5 @@ import prisma from "@/lib/prisma"; export async function deleteAlbum(id: string) { - await prisma.gallery.delete({ where: { id } }); + await prisma.album.delete({ where: { id } }); } \ No newline at end of file diff --git a/src/actions/artists/createArtist.ts b/src/actions/artists/createArtist.ts new file mode 100644 index 0000000..f6e4d03 --- /dev/null +++ b/src/actions/artists/createArtist.ts @@ -0,0 +1,15 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { artistSchema } from "@/schemas/artists/artistSchema"; +import * as z from "zod/v4"; + +export async function createArtist(values: z.infer) { + return await prisma.artist.create({ + data: { + displayName: values.displayName, + slug: values.slug, + nickname: values.nickname + } + }) +} \ No newline at end of file diff --git a/src/actions/artists/deleteArtist.ts b/src/actions/artists/deleteArtist.ts new file mode 100644 index 0000000..203407e --- /dev/null +++ b/src/actions/artists/deleteArtist.ts @@ -0,0 +1,7 @@ +"use server"; + +import prisma from "@/lib/prisma"; + +export async function deleteArtist(id: string) { + await prisma.artist.delete({ where: { id } }); +} \ No newline at end of file diff --git a/src/actions/artists/updateArtist.ts b/src/actions/artists/updateArtist.ts new file mode 100644 index 0000000..fcf9c1f --- /dev/null +++ b/src/actions/artists/updateArtist.ts @@ -0,0 +1,21 @@ +"use server" + +import prisma from "@/lib/prisma"; +import { artistSchema } from "@/schemas/artists/artistSchema"; +import * as z from "zod/v4"; + +export async function updateArtist( + values: z.infer, + id: string +) { + return await prisma.artist.update({ + where: { + id: id + }, + data: { + displayName: values.displayName, + slug: values.slug, + nickname: values.nickname + } + }) +} \ No newline at end of file diff --git a/src/app/artists/edit/[id]/page.tsx b/src/app/artists/edit/[id]/page.tsx new file mode 100644 index 0000000..2cf888d --- /dev/null +++ b/src/app/artists/edit/[id]/page.tsx @@ -0,0 +1,19 @@ +import EditArtistForm from "@/components/artists/edit/EditArtistForm"; +import prisma from "@/lib/prisma"; + +export default async function ArtistsEditPage({ params }: { params: { id: string } }) { + const { id } = await params; + + const artist = await prisma.artist.findUnique({ + where: { + id, + } + }); + + return ( +
+

Edit album

+ {artist ? : 'Artist not found...'} +
+ ); +} \ No newline at end of file diff --git a/src/app/artists/new/page.tsx b/src/app/artists/new/page.tsx new file mode 100644 index 0000000..10f3596 --- /dev/null +++ b/src/app/artists/new/page.tsx @@ -0,0 +1,10 @@ +import CreateArtistForm from "@/components/artists/new/CreateArtistForm"; + +export default function ArtistsNewPage() { + return ( +
+

New artist

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

Artists

+ + Add new Artist + +
+ {artists.length > 0 ? :

No artists found.

} +
+ ); +} \ No newline at end of file diff --git a/src/components/albums/edit/EditAlbumForm.tsx b/src/components/albums/edit/EditAlbumForm.tsx index 68b8f08..49ddfda 100644 --- a/src/components/albums/edit/EditAlbumForm.tsx +++ b/src/components/albums/edit/EditAlbumForm.tsx @@ -26,7 +26,7 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall }) async function onSubmit(values: z.infer) { - var updatedAlbum = await updateAlbum(values, album.id) + const updatedAlbum = await updateAlbum(values, album.id) if (updatedAlbum) { toast.success("Album updated") router.push(`/albums`) diff --git a/src/components/albums/new/CreateAlbumForm.tsx b/src/components/albums/new/CreateAlbumForm.tsx index abf5425..cf2c36d 100644 --- a/src/components/albums/new/CreateAlbumForm.tsx +++ b/src/components/albums/new/CreateAlbumForm.tsx @@ -26,7 +26,7 @@ export default function CreateAlbumForm({ galleries }: { galleries: Gallery[] }) }) async function onSubmit(values: z.infer) { - var album = await createAlbum(values) + const album = await createAlbum(values) if (album) { toast.success("Album created") router.push(`/albums`) diff --git a/src/components/artists/edit/EditArtistForm.tsx b/src/components/artists/edit/EditArtistForm.tsx new file mode 100644 index 0000000..5e27349 --- /dev/null +++ b/src/components/artists/edit/EditArtistForm.tsx @@ -0,0 +1,94 @@ +"use client" + +import { updateArtist } from "@/actions/artists/updateArtist"; +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 { Artist } from "@/generated/prisma"; +import { artistSchema } from "@/schemas/artists/artistSchema"; +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({ artist }: { artist: Artist }) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(artistSchema), + defaultValues: { + displayName: artist.displayName, + slug: artist.slug, + nickname: artist.nickname || "", + }, + }) + + async function onSubmit(values: z.infer) { + const updatedArtist = await updateArtist(values, artist.id) + if (updatedArtist) { + toast.success("Artist updated") + router.push(`/artists`) + } + } + + return ( +
+
+ + ( + + Artist name + + + + + This is your public display name. + + + + )} + /> + ( + + Artist slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Artist nickname (optional) + + + + + Nickname of the artist. + + + + )} + /> +
+ + +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/artists/list/ListArtists.tsx b/src/components/artists/list/ListArtists.tsx new file mode 100644 index 0000000..9ba5ec2 --- /dev/null +++ b/src/components/artists/list/ListArtists.tsx @@ -0,0 +1,23 @@ +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Artist } from "@/generated/prisma"; +import Link from "next/link"; + +export default function ListArtists({ artists }: { artists: Artist[] }) { + return ( +
+ {artists.map((artist) => ( + + + + {artist.displayName} + + + + + + + + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/artists/new/CreateArtistForm.tsx b/src/components/artists/new/CreateArtistForm.tsx new file mode 100644 index 0000000..167081a --- /dev/null +++ b/src/components/artists/new/CreateArtistForm.tsx @@ -0,0 +1,91 @@ +"use client" + +import { createArtist } from "@/actions/artists/createArtist"; +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 { artistSchema } from "@/schemas/artists/artistSchema"; +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 CreateArtistForm() { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(artistSchema), + defaultValues: { + displayName: "", + slug: "", + nickname: "", + }, + }) + + async function onSubmit(values: z.infer) { + const artist = await createArtist(values) + if (artist) { + toast.success("Artist created") + router.push(`/artists`) + } + } + + return ( +
+ + ( + + Artist name + + + + + This is your public display name. + + + + )} + /> + ( + + Artist slug + + + + + Will be used for the navigation. + + + + )} + /> + ( + + Artist nickname + + + + + Nickname of the artist. + + + + )} + /> +
+ + +
+ + + ); +} \ No newline at end of file diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index 3c5bccc..447ea81 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -22,6 +22,11 @@ export default function TopNav() { Albums + + + Artists + + ); diff --git a/src/schemas/artists/artistSchema.ts b/src/schemas/artists/artistSchema.ts new file mode 100644 index 0000000..7759c51 --- /dev/null +++ b/src/schemas/artists/artistSchema.ts @@ -0,0 +1,8 @@ +import * as z from "zod/v4"; + +export const artistSchema = z.object({ + displayName: 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)"), + nickname: z.string().optional(), +}) +