feat: working like button
This commit is contained in:
parent
bc7460bd71
commit
e458d4459f
4 changed files with 105 additions and 13 deletions
61
src/app/api/like/route.ts
Normal file
61
src/app/api/like/route.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
@ -6,26 +6,32 @@ import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
likes: number;
|
likes: number;
|
||||||
|
miiId: number | undefined;
|
||||||
|
isLiked: boolean;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
big?: boolean;
|
big?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LikeButton({ likes, isLoggedIn, big }: Props) {
|
export default function LikeButton({ likes, isLiked, miiId, isLoggedIn, big }: Props) {
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
const [isLikedState, setIsLikedState] = useState(isLiked);
|
||||||
const [likesState, setLikesState] = useState(likes);
|
const [likesState, setLikesState] = useState(likes);
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = async () => {
|
||||||
if (!isLoggedIn) redirect("/login");
|
if (!isLoggedIn) redirect("/login");
|
||||||
|
|
||||||
setIsLiked((prev) => !prev);
|
setIsLikedState((prev) => !prev);
|
||||||
setLikesState((prev) => (isLiked ? prev - 1 : prev + 1));
|
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 (
|
return (
|
||||||
<button onClick={onClick} className={`flex items-center gap-2 text-red-400 cursor-pointer ${big ? "text-3xl" : "text-xl"}`}>
|
<button onClick={onClick} className={`flex items-center gap-2 text-red-400 cursor-pointer ${big ? "text-3xl" : "text-xl"}`}>
|
||||||
<Icon icon={isLiked ? "icon-park-solid:like" : "icon-park-outline:like"} />
|
<Icon icon={isLikedState ? "icon-park-solid:like" : "icon-park-outline:like"} />
|
||||||
<span>{likesState}</span>
|
<span>{likesState}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Link from "next/link";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
|
// for use on profiles
|
||||||
userId?: number;
|
userId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ export default async function MiiList({ searchParams, userId }: Props) {
|
||||||
const whereTags = tagFilter.length > 0 ? { tags: { hasSome: tagFilter } } : undefined;
|
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
|
// If the mii list is on a user's profile, don't query for the username
|
||||||
const include =
|
const userInclude =
|
||||||
userId == null
|
userId == null
|
||||||
? {
|
? {
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -63,9 +64,24 @@ export default async function MiiList({ searchParams, userId }: Props) {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
orderBy,
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between items-end mb-2">
|
<div className="flex justify-between items-end mb-2">
|
||||||
|
|
@ -99,13 +115,13 @@ export default async function MiiList({ searchParams, userId }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
|
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
|
||||||
{miis.map((mii) => (
|
{formattedMiis.map((mii) => (
|
||||||
<div
|
<div
|
||||||
key={mii.id}
|
key={mii.id}
|
||||||
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
|
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
|
||||||
>
|
>
|
||||||
<Link href={`/mii/${mii.id}`}>
|
<Link href={`/mii/${mii.id}`}>
|
||||||
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl" />
|
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl border-2 border-zinc-300" />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="p-4 flex flex-col gap-1 h-full">
|
<div className="p-4 flex flex-col gap-1 h-full">
|
||||||
<Link href={`/mii/${mii.id}`} className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
|
<Link href={`/mii/${mii.id}`} className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
|
||||||
|
|
@ -120,7 +136,7 @@ export default async function MiiList({ searchParams, userId }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto grid grid-cols-2 items-center">
|
<div className="mt-auto grid grid-cols-2 items-center">
|
||||||
<LikeButton likes={mii.likes} isLoggedIn={session?.user != null} />
|
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLikedByUser} isLoggedIn={session?.user != null} />
|
||||||
|
|
||||||
{userId == null && (
|
{userId == null && (
|
||||||
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
|
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,18 @@ export default async function ProfilePage({ params }: Props) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isLiked = await prisma.like.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_miiId: {
|
||||||
|
userId: Number(session?.user.id),
|
||||||
|
miiId: Number(slug),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 max-sm:flex-col">
|
<div className="flex gap-2 max-sm:flex-col">
|
||||||
<img src="https://placehold.co/400x300" alt="mii" className="rounded-xl" />
|
<img src="https://placehold.co/400x300" alt="mii" className="rounded-xl border-2 border-zinc-300 shadow-lg" />
|
||||||
<div className="flex flex-col gap-1 p-4">
|
<div className="flex flex-col gap-1 p-4">
|
||||||
<h1 className="text-5xl font-extrabold break-words">{mii?.name}</h1>
|
<h1 className="text-5xl font-extrabold break-words">{mii?.name}</h1>
|
||||||
<div id="tags" className="flex gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
<div id="tags" className="flex gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
||||||
|
|
@ -50,7 +59,7 @@ export default async function ProfilePage({ params }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<LikeButton likes={mii?.likes ?? 0} isLoggedIn={session?.user != null} big />
|
<LikeButton likes={mii?.likes ?? 0} miiId={mii?.id} isLiked={isLiked != null} isLoggedIn={session?.user != null} big />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue