diff --git a/src/app/api/like/route.ts b/src/app/api/like/route.ts new file mode 100644 index 0000000..47dbf95 --- /dev/null +++ b/src/app/api/like/route.ts @@ -0,0 +1,61 @@ +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function PATCH(request: Request) { + // todo: rate limit + + const session = await auth(); + if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const { miiId } = await request.json(); + if (!miiId) return Response.json({ error: "Mii ID is required" }, { status: 400 }); + + const result = await prisma.$transaction(async (tx) => { + const existingLike = await tx.like.findUnique({ + where: { + userId_miiId: { + userId: Number(session.user.id), + miiId, + }, + }, + }); + + if (existingLike) { + // Delete the like if it exists + await tx.like.delete({ + where: { + userId_miiId: { + userId: Number(session.user.id), + miiId, + }, + }, + }); + + const updatedMii = await tx.mii.update({ + where: { id: miiId }, + data: { likes: { decrement: 1 } }, + select: { likes: true }, + }); + + return { liked: false, count: updatedMii.likes }; + } else { + // Create a new like if it doesn't exist + await tx.like.create({ + data: { + userId: Number(session.user.id), + miiId, + }, + }); + + const updatedMii = await tx.mii.update({ + where: { id: miiId }, + data: { likes: { increment: 1 } }, + select: { likes: true }, + }); + + return { liked: true, count: updatedMii.likes }; + } + }); + + return Response.json({ success: true, liked: result.liked, count: result.count }); +} diff --git a/src/app/components/like-button.tsx b/src/app/components/like-button.tsx index 09ae5a4..a3d598e 100644 --- a/src/app/components/like-button.tsx +++ b/src/app/components/like-button.tsx @@ -6,26 +6,32 @@ import { Icon } from "@iconify/react"; interface Props { likes: number; + miiId: number | undefined; + isLiked: boolean; isLoggedIn: boolean; big?: boolean; } -export default function LikeButton({ likes, isLoggedIn, big }: Props) { - const [isLiked, setIsLiked] = useState(false); +export default function LikeButton({ likes, isLiked, miiId, isLoggedIn, big }: Props) { + const [isLikedState, setIsLikedState] = useState(isLiked); const [likesState, setLikesState] = useState(likes); - const onClick = () => { + const onClick = async () => { if (!isLoggedIn) redirect("/login"); - setIsLiked((prev) => !prev); + setIsLikedState((prev) => !prev); setLikesState((prev) => (isLiked ? prev - 1 : prev + 1)); - // todo: update database + const response = await fetch("/api/like", { method: "PATCH", body: JSON.stringify({ miiId }) }); + const { liked, count } = await response.json(); + + setIsLikedState(liked); + setLikesState(count); }; return ( ); diff --git a/src/app/components/mii-list.tsx b/src/app/components/mii-list.tsx index b8afb20..a89690c 100644 --- a/src/app/components/mii-list.tsx +++ b/src/app/components/mii-list.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; interface Props { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + // for use on profiles userId?: number; } @@ -38,7 +39,7 @@ export default async function MiiList({ searchParams, userId }: Props) { const whereTags = tagFilter.length > 0 ? { tags: { hasSome: tagFilter } } : undefined; // If the mii list is on a user's profile, don't query for the username - const include = + const userInclude = userId == null ? { user: { @@ -63,9 +64,24 @@ export default async function MiiList({ searchParams, userId }: Props) { userId, }, orderBy, - include, + include: { + ...userInclude, + likedBy: { + where: { + userId: Number(session?.user.id), + }, + select: { + userId: true, + }, + }, + }, }); + const formattedMiis = miis.map((mii) => ({ + ...mii, + isLikedByUser: mii.likedBy.length > 0, // True if the user has liked the Mii + })); + return (