diff --git a/src/app/api/mii/[id]/delete/route.ts b/src/app/api/mii/[id]/delete/route.ts index d9233f2..4cf0978 100644 --- a/src/app/api/mii/[id]/delete/route.ts +++ b/src/app/api/mii/[id]/delete/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; import fs from "fs/promises"; import path from "path"; diff --git a/src/app/api/mii/[id]/edit/route.ts b/src/app/api/mii/[id]/edit/route.ts new file mode 100644 index 0000000..9720525 --- /dev/null +++ b/src/app/api/mii/[id]/edit/route.ts @@ -0,0 +1,124 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { Mii } from "@prisma/client"; + +import fs from "fs/promises"; +import path from "path"; +import sharp from "sharp"; + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { idSchema, nameSchema, tagsSchema } from "@/lib/schemas"; + +import { validateImage } from "@/lib/images"; + +const uploadsDirectory = path.join(process.cwd(), "public", "mii"); + +const editSchema = z.object({ + name: nameSchema.optional(), + tags: tagsSchema.optional(), + image1: z.union([z.instanceof(File), z.any()]).optional(), + image2: z.union([z.instanceof(File), z.any()]).optional(), + image3: z.union([z.instanceof(File), z.any()]).optional(), +}); + +export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + // Get Mii ID + const { id: slugId } = await params; + const parsedId = idSchema.safeParse(slugId); + + if (!parsedId.success) return NextResponse.json({ error: parsedId.error.errors[0].message }, { status: 400 }); + const miiId = parsedId.data; + + // Check ownership of Mii + const mii = await prisma.mii.findUnique({ + where: { + id: miiId, + }, + }); + + if (!mii) return NextResponse.json({ error: "Mii not found" }, { status: 404 }); + if (Number(session.user.id) !== mii.userId) return NextResponse.json({ error: "You don't have ownership of that Mii" }, { status: 403 }); + + // Parse form data + const formData = await request.formData(); + + let rawTags: string[] | undefined = undefined; + try { + const value = formData.get("tags"); + if (value) rawTags = JSON.parse(value as string); + } catch { + return NextResponse.json({ error: "Invalid JSON in tags" }, { status: 400 }); + } + + const parsed = editSchema.safeParse({ + name: formData.get("name") ?? undefined, + tags: rawTags, + image1: formData.get("image1"), + image2: formData.get("image2"), + image3: formData.get("image3"), + }); + + if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); + const { name, tags, image1, image2, image3 } = parsed.data; + + // Validate image files + const images: File[] = []; + + for (const img of [image1, image2, image3]) { + if (!img) continue; + + const imageValidation = await validateImage(img); + if (imageValidation.valid) { + images.push(img); + } else { + return NextResponse.json({ error: imageValidation.error }, { status: imageValidation.status ?? 400 }); + } + } + + // Edit Mii in database + const updateData: Partial = {}; + if (name !== undefined) updateData.name = name; + if (tags !== undefined) updateData.tags = tags; + if (images.length > 0) updateData.imageCount = images.length; + + if (Object.keys(updateData).length == 0) return NextResponse.json({ error: "Nothing was changed" }, { status: 400 }); + await prisma.mii.update({ + where: { + id: miiId, + }, + data: updateData, + }); + + // Only touch files if new images were uploaded + if (images.length > 0) { + // Ensure directories exist + const miiUploadsDirectory = path.join(uploadsDirectory, miiId.toString()); + await fs.mkdir(miiUploadsDirectory, { recursive: true }); + + // Delete all custom images + const files = await fs.readdir(miiUploadsDirectory); + await Promise.all(files.filter((file) => file.startsWith("image")).map((file) => fs.unlink(path.join(miiUploadsDirectory, file)))); + + // Compress and upload new images + try { + await Promise.all( + images.map(async (image, index) => { + const buffer = Buffer.from(await image.arrayBuffer()); + const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer(); + const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`); + + await fs.writeFile(fileLocation, webpBuffer); + }) + ); + } catch (error) { + console.error("Error uploading user images:", error); + return NextResponse.json({ error: "Failed to store user images" }, { status: 500 }); + } + } + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/mii/[id]/like/route.ts b/src/app/api/mii/[id]/like/route.ts index d13acf9..3a8a868 100644 --- a/src/app/api/mii/[id]/like/route.ts +++ b/src/app/api/mii/[id]/like/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; diff --git a/src/app/edit/[slug]/page.tsx b/src/app/edit/[slug]/page.tsx new file mode 100644 index 0000000..f7c0f78 --- /dev/null +++ b/src/app/edit/[slug]/page.tsx @@ -0,0 +1,30 @@ +import { redirect } from "next/navigation"; + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import EditForm from "@/components/submit-form/edit-form"; + +interface Props { + params: Promise<{ slug: string }>; +} + +export default async function MiiPage({ params }: Props) { + const { slug } = await params; + const session = await auth(); + + const mii = await prisma.mii.findUnique({ + where: { + id: Number(slug), + }, + include: { + _count: { + select: { likedBy: true }, // Get total like count + }, + }, + }); + + // Check ownership + if (!mii || Number(session?.user.id) !== mii.userId) redirect("/404"); + + return ; +} diff --git a/src/app/mii/[slug]/page.tsx b/src/app/mii/[slug]/page.tsx index 81ace57..4c94a17 100644 --- a/src/app/mii/[slug]/page.tsx +++ b/src/app/mii/[slug]/page.tsx @@ -19,7 +19,7 @@ export default async function MiiPage({ params }: Props) { const { slug } = await params; const session = await auth(); - const mii = await prisma.mii.findFirst({ + const mii = await prisma.mii.findUnique({ where: { id: Number(slug), }, @@ -55,10 +55,12 @@ export default async function MiiPage({ params }: Props) { return (
+ {/* Carousel */}
+ {/* Information */}

{mii.name}

@@ -89,10 +91,11 @@ export default async function MiiPage({ params }: Props) {
+ {/* Extra information */}
Mii Info -
    +
    • Name:{" "} @@ -103,7 +106,7 @@ export default async function MiiPage({ params }: Props) { From: {mii.islandName} Island
    • - Copying: + Copying:
@@ -117,6 +120,7 @@ export default async function MiiPage({ params }: Props) {
+ {/* Images */}
{images.map((src, index) => ( diff --git a/src/app/profile/[slug]/page.tsx b/src/app/profile/[slug]/page.tsx index b1bff5c..61b4fa8 100644 --- a/src/app/profile/[slug]/page.tsx +++ b/src/app/profile/[slug]/page.tsx @@ -17,7 +17,7 @@ export default async function ProfilePage({ params }: Props) { const session = await auth(); const { slug } = await params; - const user = await prisma.user.findFirst({ + const user = await prisma.user.findUnique({ where: { id: Number(slug), }, diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index 5bcabec..aba8377 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -61,6 +61,7 @@ export default function Carousel({ images, className }: Props) { {images.length > 1 && ( <>
@@ -131,6 +131,7 @@ export default function ImageViewer({ src, alt, width, height, className, images }`} > +
+
+ + ); +}