diff --git a/src/app/api/mii/[id]/delete/route.ts b/src/app/api/mii/[id]/delete/route.ts new file mode 100644 index 0000000..a9cdd98 --- /dev/null +++ b/src/app/api/mii/[id]/delete/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import fs from "fs/promises"; +import path from "path"; + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +const uploadsDirectory = path.join(process.cwd(), "public", "mii"); + +const slugSchema = z.coerce + .number({ message: "Mii ID must be a number" }) + .int({ message: "Mii ID must be an integer" }) + .positive({ message: "Mii ID must be valid" }); + +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { id: slugId } = await params; + const parsed = slugSchema.safeParse(slugId); + + if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); + const miiId = parsed.data; + + const miiUploadsDirectory = path.join(uploadsDirectory, miiId.toString()); + + try { + await prisma.mii.delete({ + where: { id: miiId }, + }); + } catch (error) { + console.error("Failed to delete Mii from database:", error); + return NextResponse.json({ error: "Failed to delete Mii" }, { status: 500 }); + } + + try { + await fs.rm(miiUploadsDirectory, { recursive: true, force: true }); + } catch (error) { + console.warn("Failed to delete Mii image files:", error); + } + + return NextResponse.json({ success: true }); +} diff --git a/src/app/components/delete-mii.tsx b/src/app/components/delete-mii.tsx new file mode 100644 index 0000000..5ddb2c5 --- /dev/null +++ b/src/app/components/delete-mii.tsx @@ -0,0 +1,105 @@ +"use client"; + +import Image from "next/image"; + +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { Icon } from "@iconify/react"; + +import LikeButton from "./like-button"; + +interface Props { + miiId: number; + miiName: string; + likes: number; +} + +export default function DeleteMiiButton({ miiId, miiName, likes }: Props) { + const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const [error, setError] = useState(undefined); + + const submit = async () => { + const response = await fetch(`/api/mii/${miiId}/delete`, { method: "DELETE" }); + if (!response.ok) { + const { error } = await response.json(); + setError(error); + return; + } + + close(); + window.location.reload(); // I would use router.refresh() here but the API data fetching breaks + }; + + const close = () => { + setIsVisible(false); + setTimeout(() => { + setIsOpen(false); + }, 300); + }; + + useEffect(() => { + if (isOpen) { + // slight delay to trigger animation + setTimeout(() => setIsVisible(true), 10); + } + }, [isOpen]); + + return ( + <> + + + {isOpen && + createPortal( +
+
+ +
+
+

Delete Mii

+ +
+ +

Are you sure? This will delete your Mii permanently. This action cannot be undone.

+ +
+ mii image +
+

+ {miiName} +

+ +
+
+ + {error && Error: {error}} + +
+ + +
+
+
, + document.body + )} + + ); +} diff --git a/src/app/components/mii-list/index.tsx b/src/app/components/mii-list/index.tsx index d786e40..f7438dd 100644 --- a/src/app/components/mii-list/index.tsx +++ b/src/app/components/mii-list/index.tsx @@ -4,12 +4,15 @@ import { useSearchParams } from "next/navigation"; import Link from "next/link"; import useSWR from "swr"; +import { Icon } from "@iconify/react"; + +import Skeleton from "./skeleton"; +import FilterSelect from "./filter-select"; import SortSelect from "./sort-select"; import Carousel from "../carousel"; import LikeButton from "../like-button"; -import FilterSelect from "./filter-select"; +import DeleteMiiButton from "../delete-mii"; import Pagination from "./pagination"; -import Skeleton from "./skeleton"; interface Props { isLoggedIn: boolean; @@ -86,7 +89,7 @@ export default function MiiList({ isLoggedIn, userId }: Props) { />
- + {mii.name}
@@ -100,10 +103,17 @@ export default function MiiList({ isLoggedIn, userId }: Props) {
- {userId == null && ( + {!userId ? ( @{mii.user?.username} + ) : ( +
+ + + + +
)}
diff --git a/src/app/mii/[slug]/page.tsx b/src/app/mii/[slug]/page.tsx index 784f7cd..d59c33f 100644 --- a/src/app/mii/[slug]/page.tsx +++ b/src/app/mii/[slug]/page.tsx @@ -1,12 +1,15 @@ import Link from "next/link"; import { redirect } from "next/navigation"; +import { Icon } from "@iconify/react"; + import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import Carousel from "@/app/components/carousel"; import LikeButton from "@/app/components/like-button"; import ImageViewer from "@/app/components/image-viewer"; +import DeleteMiiButton from "@/app/components/delete-mii"; interface Props { params: Promise<{ slug: string }>; @@ -86,23 +89,32 @@ export default async function MiiPage({ params }: Props) {
-
- Mii Info - -
+
+
+ Mii Info +
    +
  • + Name:{" "} + + {mii.firstName} {mii.lastName} + +
  • +
  • + From: {mii.islandName} Island +
  • +
  • + Copying: +
  • +
+
+ +
+ + + + +
+