fix: better like count

This commit is contained in:
trafficlunar 2026-04-19 18:47:11 +01:00
parent 79b19f4807
commit 28bbaa52f1
9 changed files with 70 additions and 62 deletions

View file

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "miis" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0;
-- CreateIndex
CREATE INDEX "miis_likeCount_idx" ON "miis"("likeCount" DESC);
UPDATE miis SET like_count = (SELECT COUNT(*) FROM likes WHERE likes."miiId" = miis.id);

View file

@ -91,13 +91,15 @@ model Mii {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
likedBy Like[] likeCount Int @default(0)
punishmentId Int? punishmentId Int?
punishments MiiPunishment[] punishments MiiPunishment[]
likedBy Like[]
@@index([tags], type: Gin) @@index([tags], type: Gin)
@@index([createdAt]) @@index([createdAt])
@@index([likeCount(sort: Desc)])
@@index([quarantined, createdAt(sort: Desc)]) @@index([quarantined, createdAt(sort: Desc)])
@@index([platform, createdAt(sort: Desc)]) @@index([platform, createdAt(sort: Desc)])
@@index([userId, createdAt(sort: Desc)]) @@index([userId, createdAt(sort: Desc)])

View file

@ -5,55 +5,57 @@ import { prisma } from "@/lib/prisma";
import { idSchema } from "@/lib/schemas"; import { idSchema } from "@/lib/schemas";
import { RateLimit } from "@/lib/rate-limit"; import { RateLimit } from "@/lib/rate-limit";
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
// const session = await auth(); const session = await auth();
// if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// const rateLimit = new RateLimit(request, 100, "/api/mii/like"); const rateLimit = new RateLimit(request, 100, "/api/mii/like");
// const check = await rateLimit.handle(); const check = await rateLimit.handle();
// if (check) return check; if (check) return check;
// const { id: slugId } = await params; const { id: slugId } = await params;
// const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
// if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
// const miiId = parsed.data; const miiId = parsed.data;
// const result = await prisma.$transaction(async (tx) => { const result = await prisma.$transaction(async (tx) => {
// const existingLike = await tx.like.findUnique({ const existingLike = await tx.like.findUnique({
// where: { where: {
// userId_miiId: { userId_miiId: {
// userId: Number(session.user?.id), userId: Number(session.user?.id),
// miiId, miiId,
// }, },
// }, },
// }); });
// if (existingLike) { if (existingLike) {
// // Remove the like if it exists await tx.like.delete({
// await tx.like.delete({ where: {
// where: { userId_miiId: {
// userId_miiId: { userId: Number(session.user?.id),
// userId: Number(session.user?.id), miiId,
// miiId, },
// }, },
// }, });
// }); await tx.mii.update({
// } else { where: { id: miiId },
// // Add a like if it doesn't exist data: { likeCount: { decrement: 1 } },
// await tx.like.create({ });
// data: { } else {
// userId: Number(session.user?.id), await tx.like.create({
// miiId, data: {
// }, userId: Number(session.user?.id),
// }); miiId,
// } },
});
// const likeCount = await tx.like.count({ await tx.mii.update({
// where: { miiId }, where: { id: miiId },
// }); data: { likeCount: { increment: 1 } },
});
// return { liked: !existingLike, count: likeCount }; }
// });
return { liked: !existingLike };
return NextResponse.json({ success: false }); });
return rateLimit.sendResponse({ success: true, liked: result.liked });
} }

View file

@ -46,5 +46,5 @@ export default async function MiiPage({ params }: Props) {
// Check ownership // Check ownership
if (!mii || (Number(session?.user?.id) !== mii.userId && Number(session?.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) redirect("/404"); if (!mii || (Number(session?.user?.id) !== mii.userId && Number(session?.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) redirect("/404");
return <EditForm mii={mii} likes={mii._count.likedBy} />; return <EditForm mii={mii} likes={mii.likeCount} />;
} }

View file

@ -51,13 +51,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
return { return {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!), metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
title: `${mii.name} - TomodachiShare`, title: `${mii.name} - TomodachiShare`,
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`, description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
keywords: ["mii", "tomodachi life", "nintendo", "tomodachishare", "tomodachi-share", "mii creator", "mii collection", ...mii.tags], keywords: ["mii", "tomodachi life", "nintendo", "tomodachishare", "tomodachi-share", "mii creator", "mii collection", ...mii.tags],
creator: name, creator: name,
openGraph: { openGraph: {
type: "article", type: "article",
title: `${mii.name} - TomodachiShare`, title: `${mii.name} - TomodachiShare`,
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`, description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
images: [ images: [
{ {
url: metadataImageUrl, url: metadataImageUrl,
@ -70,7 +70,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: `${mii.name} - TomodachiShare`, title: `${mii.name} - TomodachiShare`,
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`, description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
images: [ images: [
{ {
url: metadataImageUrl, url: metadataImageUrl,
@ -306,7 +306,7 @@ export default async function MiiPage({ params }: Props) {
{/* Submission name */} {/* Submission name */}
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1> <h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
{/* Like button */} {/* Like button */}
<LikeButton likes={mii._count.likedBy ?? 0} miiId={mii.id} isLiked={false} big /> <LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={false} big />
</div> </div>
{/* Tags */} {/* Tags */}
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs"> <div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">

View file

@ -41,7 +41,7 @@ export default async function ReportMiiPage({ params }: Props) {
return ( return (
<div className="flex justify-center w-full"> <div className="flex justify-center w-full">
<ReportMiiForm mii={mii} likes={mii._count.likedBy} /> <ReportMiiForm mii={mii} likes={mii.likeCount} />
</div> </div>
); );
} }

View file

@ -31,7 +31,7 @@ export default function AuthorButtons({ mii }: Props) {
<Icon icon="mdi:pencil" /> <Icon icon="mdi:pencil" />
<span>Edit</span> <span>Edit</span>
</Link> </Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount ?? 0} inMiiPage />
</> </>
); );
} }

View file

@ -89,6 +89,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
allowedCopying: true, allowedCopying: true,
quarantined: true, quarantined: true,
in_queue: true, in_queue: true,
likeCount: true,
// Mii liked check // Mii liked check
...(session?.user?.id && { ...(session?.user?.id && {
likedBy: { likedBy: {
@ -96,10 +97,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
select: { userId: true }, select: { userId: true },
}, },
}), }),
// Like count
_count: {
select: { likedBy: true },
},
}; };
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
@ -111,7 +108,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
let orderBy: Prisma.MiiOrderByWithRelationInput[]; let orderBy: Prisma.MiiOrderByWithRelationInput[];
if (sort === "likes") { if (sort === "likes") {
orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }]; orderBy = [{ likeCount: "desc" }, { name: "asc" }];
} else if (sort === "oldest") { } else if (sort === "oldest") {
orderBy = [{ createdAt: "asc" }, { name: "asc" }]; orderBy = [{ createdAt: "asc" }, { name: "asc" }];
} else { } else {

View file

@ -74,7 +74,7 @@ export default function MiiGrid({ miis, userId, parentPage }: 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._count.likedBy} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate /> <LikeButton likes={mii.likeCount} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
{!userId && ( {!userId && (
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap"> <Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
@ -87,7 +87,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit"> <Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
<Icon icon="mdi:pencil" /> <Icon icon="mdi:pencil" />
</Link> </Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
</div> </div>
)} )}
@ -105,7 +105,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
<Icon icon="material-symbols:check-rounded" /> <Icon icon="material-symbols:check-rounded" />
</button> </button>
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center"> <div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
</div> </div>
</div> </div>