mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
fix: likes not working with cache rules (#18)
This commit is contained in:
parent
3163fac2eb
commit
1bf83e4cae
6 changed files with 164 additions and 76 deletions
28
src/app/api/mii/has-liked/route.ts
Normal file
28
src/app/api/mii/has-liked/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const rateLimit = new RateLimit(request, 50, "/api/mii/like_get");
|
||||
const check = await rateLimit.handle();
|
||||
if (check) return check;
|
||||
|
||||
const idsParam = new URL(request.url).searchParams.get("ids");
|
||||
if (!idsParam) return NextResponse.json({ error: "Missing IDs parameter" }, { status: 400 });
|
||||
|
||||
const ids = idsParam.split(",").map(Number).filter(Boolean);
|
||||
if (!ids.length) return NextResponse.json({ error: "No valid IDs provided" }, { status: 400 });
|
||||
if (ids.length > 100) return NextResponse.json({ error: "Too many IDs, maximum is 100" }, { status: 400 });
|
||||
|
||||
const liked = await prisma.like.findMany({
|
||||
where: { userId: Number(session.user?.id), miiId: { in: ids } },
|
||||
select: { miiId: true },
|
||||
});
|
||||
|
||||
// Return only Miis that are liked
|
||||
return NextResponse.json(liked.map((l) => l.miiId));
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import { MiiPlatform } from "@prisma/client";
|
|||
|
||||
import LikeButton from "@/components/like-button";
|
||||
import ImageViewer from "@/components/image-viewer";
|
||||
import DeleteMiiButton from "@/components/mii/delete-mii-button";
|
||||
import AuthorButtons from "@/components/mii/author-buttons";
|
||||
import ShareMiiButton from "@/components/mii/share-mii-button";
|
||||
import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan";
|
||||
import SwitchScanTutorialButton from "@/components/tutorial/switch-add-mii";
|
||||
|
|
@ -359,15 +359,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 w-fit bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 text-3xl text-orange-400 max-md:place-self-center *:size-12 *:flex *:flex-col *:items-center *:gap-1 **:transition-discrete **:duration-150 *:hover:brightness-75 *:hover:scale-[1.08] *:[&_span]:text-xs">
|
||||
{session && (Number(session.user?.id) === mii.userId || Number(session.user?.id) === Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) && (
|
||||
<>
|
||||
<Link aria-label="Edit Mii" href={`/edit/${mii.id}`}>
|
||||
<Icon icon="mdi:pencil" />
|
||||
<span>Edit</span>
|
||||
</Link>
|
||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
|
||||
</>
|
||||
)}
|
||||
<AuthorButtons mii={mii} />
|
||||
|
||||
<ShareMiiButton miiId={mii.id} />
|
||||
<Link aria-label="Report Mii" href={`/report/mii/${mii.id}`}>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate
|
|||
loadIcons(["icon-park-solid:like", "icon-park-outline:like"]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLikedState(isLiked);
|
||||
}, [isLiked]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
|
|
|||
37
src/components/mii/author-buttons.tsx
Normal file
37
src/components/mii/author-buttons.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import DeleteMiiButton from "./delete-mii-button";
|
||||
|
||||
interface Props {
|
||||
mii: Prisma.MiiGetPayload<{
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
likedBy: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function AuthorButtons({ mii }: Props) {
|
||||
const session = useSession();
|
||||
|
||||
if (!session.data || Number(session.data.user?.id) !== mii.userId || Number(session.data.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link aria-label="Edit Mii" href={`/edit/${mii.id}`}>
|
||||
<Icon icon="mdi:pencil" />
|
||||
<span>Edit</span>
|
||||
</Link>
|
||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import LikeButton from "../../like-button";
|
|||
import DeleteMiiButton from "../delete-mii-button";
|
||||
import Pagination from "./pagination";
|
||||
import FilterMenu from "./filter-menu";
|
||||
import MiiGrid from "./mii-grid";
|
||||
|
||||
interface Props {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
|
|
@ -102,7 +103,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
|
||||
let totalCount: number;
|
||||
let filteredCount: number;
|
||||
let list: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
||||
let miis: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
||||
|
||||
if (sort === "random") {
|
||||
// Get all IDs that match the where conditions
|
||||
|
|
@ -129,7 +130,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
// Convert to number[] array
|
||||
const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id);
|
||||
|
||||
list = await prisma.mii.findMany({
|
||||
miis = await prisma.mii.findMany({
|
||||
where: {
|
||||
id: { in: selectedIds },
|
||||
},
|
||||
|
|
@ -148,7 +149,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
orderBy = [{ createdAt: "desc" }, { name: "asc" }];
|
||||
}
|
||||
|
||||
[totalCount, filteredCount, list] = await Promise.all([
|
||||
[totalCount, filteredCount, miis] = await Promise.all([
|
||||
prisma.mii.count({ where: { ...where, userId } }),
|
||||
prisma.mii.count({ where, skip, take: limit }),
|
||||
prisma.mii.findMany({
|
||||
|
|
@ -162,11 +163,6 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
}
|
||||
|
||||
const lastPage = Math.ceil(totalCount / limit);
|
||||
const miis = list.map(({ _count, likedBy, ...rest }) => ({
|
||||
...rest,
|
||||
likes: _count.likedBy,
|
||||
isLiked: session?.user?.id ? likedBy.length > 0 : false,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
|
@ -193,64 +189,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
||||
{miis.map((mii) => (
|
||||
<div
|
||||
key={mii.id}
|
||||
className={`flex flex-col relative bg-zinc-50 rounded-3xl border-2 shadow-lg p-[0.8rem] transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600 ${mii.quarantined ? "border-red-300" : "border-zinc-300"}`}
|
||||
>
|
||||
<Carousel
|
||||
images={[
|
||||
`/mii/${mii.id}/image?type=mii`,
|
||||
...(mii.platform === "THREE_DS" ? [`/mii/${mii.id}/image?type=qr-code`] : [`/mii/${mii.id}/image?type=features`]),
|
||||
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="p-4 flex flex-col gap-1 h-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<Link href={`/mii/${mii.id}`} className="relative font-bold text-2xl line-clamp-1 w-full text-ellipsis wrap-break-word" title={mii.name}>
|
||||
{mii.name}
|
||||
</Link>
|
||||
<div title={mii.platform === "SWITCH" ? "Switch" : "3DS"} className="-mr-3 text-[1.25rem] opacity-25">
|
||||
{mii.platform === "SWITCH" ? (
|
||||
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||
) : (
|
||||
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="tags" className="flex flex-wrap gap-1">
|
||||
{mii.tags.map((tag) => (
|
||||
<Link href={{ query: { tags: tag } }} key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto grid grid-cols-2 items-center">
|
||||
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLiked} abbreviate />
|
||||
|
||||
{!userId && (
|
||||
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
@{mii.user?.name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{userId && Number(session?.user?.id) == userId && (
|
||||
<div className="flex gap-1 text-2xl justify-end text-zinc-400">
|
||||
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
||||
<Icon icon="mdi:pencil" />
|
||||
</Link>
|
||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likes} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<MiiGrid miis={miis} />
|
||||
<Pagination lastPage={lastPage} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
88
src/components/mii/list/mii-grid.tsx
Normal file
88
src/components/mii/list/mii-grid.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import LikeButton from "@/components/like-button";
|
||||
import DeleteMiiButton from "../delete-mii-button";
|
||||
import Carousel from "@/components/carousel";
|
||||
|
||||
interface Props {
|
||||
miis: Prisma.MiiGetPayload<{ include: { user: { select: { id: true; name: true } }; _count: { select: { likedBy: true } } } }>[];
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export default function MiiGrid({ miis, userId }: Props) {
|
||||
const session = useSession();
|
||||
const ids = miis.map((m) => m.id).join(",");
|
||||
const { data } = useSWR<number[]>(session.data?.user && miis.length > 0 ? `/api/mii/has-liked?ids=${ids}` : null, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
});
|
||||
const likedIds = new Set(data ?? []);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
||||
{miis.map((mii) => (
|
||||
<div
|
||||
key={mii.id}
|
||||
className={`flex flex-col relative bg-zinc-50 rounded-3xl border-2 shadow-lg p-[0.8rem] transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600 ${mii.quarantined ? "border-red-300" : "border-zinc-300"}`}
|
||||
>
|
||||
<Carousel
|
||||
images={[
|
||||
`/mii/${mii.id}/image?type=mii`,
|
||||
...(mii.platform === "THREE_DS" ? [`/mii/${mii.id}/image?type=qr-code`] : [`/mii/${mii.id}/image?type=features`]),
|
||||
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="p-4 flex flex-col gap-1 h-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<Link href={`/mii/${mii.id}`} className="relative font-bold text-2xl line-clamp-1 w-full text-ellipsis wrap-break-word" title={mii.name}>
|
||||
{mii.name}
|
||||
</Link>
|
||||
<div title={mii.platform === "SWITCH" ? "Switch" : "3DS"} className="-mr-3 text-[1.25rem] opacity-25">
|
||||
{mii.platform === "SWITCH" ? (
|
||||
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||
) : (
|
||||
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="tags" className="flex flex-wrap gap-1">
|
||||
{mii.tags.map((tag) => (
|
||||
<Link href={{ query: { tags: tag } }} key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto grid grid-cols-2 items-center">
|
||||
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
|
||||
|
||||
{!userId && (
|
||||
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
@{mii.user?.name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{userId && Number(session.data?.user?.id) == userId && (
|
||||
<div className="flex gap-1 text-2xl justify-end text-zinc-400">
|
||||
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
||||
<Icon icon="mdi:pencil" />
|
||||
</Link>
|
||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue