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 LikeButton from "@/components/like-button";
|
||||||
import ImageViewer from "@/components/image-viewer";
|
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 ShareMiiButton from "@/components/mii/share-mii-button";
|
||||||
import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan";
|
import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan";
|
||||||
import SwitchScanTutorialButton from "@/components/tutorial/switch-add-mii";
|
import SwitchScanTutorialButton from "@/components/tutorial/switch-add-mii";
|
||||||
|
|
@ -359,15 +359,7 @@ export default async function MiiPage({ params }: Props) {
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* 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">
|
<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)) && (
|
<AuthorButtons mii={mii} />
|
||||||
<>
|
|
||||||
<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 />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ShareMiiButton miiId={mii.id} />
|
<ShareMiiButton miiId={mii.id} />
|
||||||
<Link aria-label="Report Mii" href={`/report/mii/${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"]);
|
loadIcons(["icon-park-solid:like", "icon-park-outline:like"]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLikedState(isLiked);
|
||||||
|
}, [isLiked]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
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 DeleteMiiButton from "../delete-mii-button";
|
||||||
import Pagination from "./pagination";
|
import Pagination from "./pagination";
|
||||||
import FilterMenu from "./filter-menu";
|
import FilterMenu from "./filter-menu";
|
||||||
|
import MiiGrid from "./mii-grid";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
|
|
@ -102,7 +103,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
|
|
||||||
let totalCount: number;
|
let totalCount: number;
|
||||||
let filteredCount: number;
|
let filteredCount: number;
|
||||||
let list: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
let miis: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
||||||
|
|
||||||
if (sort === "random") {
|
if (sort === "random") {
|
||||||
// Get all IDs that match the where conditions
|
// 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
|
// Convert to number[] array
|
||||||
const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id);
|
const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id);
|
||||||
|
|
||||||
list = await prisma.mii.findMany({
|
miis = await prisma.mii.findMany({
|
||||||
where: {
|
where: {
|
||||||
id: { in: selectedIds },
|
id: { in: selectedIds },
|
||||||
},
|
},
|
||||||
|
|
@ -148,7 +149,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
orderBy = [{ createdAt: "desc" }, { name: "asc" }];
|
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: { ...where, userId } }),
|
||||||
prisma.mii.count({ where, skip, take: limit }),
|
prisma.mii.count({ where, skip, take: limit }),
|
||||||
prisma.mii.findMany({
|
prisma.mii.findMany({
|
||||||
|
|
@ -162,11 +163,6 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastPage = Math.ceil(totalCount / limit);
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
@ -193,64 +189,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<MiiGrid miis={miis} />
|
||||||
{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>
|
|
||||||
|
|
||||||
<Pagination lastPage={lastPage} />
|
<Pagination lastPage={lastPage} />
|
||||||
</div>
|
</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