Compare commits

..

No commits in common. "e1885fd8fed2d963729d20f5a42ea71fcfd944b6" and "79b19f4807795fb0de7c2ee850d43adb43ba6e89" have entirely different histories.

9 changed files with 62 additions and 70 deletions

View file

@ -1,7 +0,0 @@
-- 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 "likeCount" = (SELECT COUNT(*) FROM likes WHERE likes."miiId" = miis.id);

View file

@ -90,16 +90,14 @@ 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)
likeCount Int @default(0) likedBy Like[]
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,57 +5,55 @@ 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 POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PATCH(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) {
await tx.like.delete({ // // Remove the like if it exists
where: { // await tx.like.delete({
userId_miiId: { // where: {
userId: Number(session.user?.id), // userId_miiId: {
miiId, // userId: Number(session.user?.id),
}, // miiId,
}, // },
}); // },
await tx.mii.update({ // });
where: { id: miiId }, // } else {
data: { likeCount: { decrement: 1 } }, // // Add a like if it doesn't exist
}); // await tx.like.create({
} else { // data: {
await tx.like.create({ // userId: Number(session.user?.id),
data: { // miiId,
userId: Number(session.user?.id), // },
miiId, // });
}, // }
});
await tx.mii.update({
where: { id: miiId },
data: { likeCount: { increment: 1 } },
});
}
return { liked: !existingLike }; // const likeCount = await tx.like.count({
}); // where: { miiId },
// });
return rateLimit.sendResponse({ success: true, liked: result.liked }); // return { liked: !existingLike, count: likeCount };
// });
return NextResponse.json({ success: false });
} }

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.likeCount} />; return <EditForm mii={mii} likes={mii._count.likedBy} />;
} }

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.likeCount} 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._count.likedBy} 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.likeCount} 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._count.likedBy} 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.likeCount} 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._count.likedBy} 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.likeCount ?? 0} miiId={mii.id} isLiked={false} big /> <LikeButton likes={mii._count.likedBy ?? 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.likeCount} /> <ReportMiiForm mii={mii} likes={mii._count.likedBy} />
</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.likeCount ?? 0} inMiiPage /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
</> </>
); );
} }

View file

@ -89,7 +89,6 @@ 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: {
@ -97,6 +96,10 @@ 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;
@ -108,7 +111,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 = [{ likeCount: "desc" }, { name: "asc" }]; orderBy = [{ likedBy: { _count: "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.likeCount} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate /> <LikeButton likes={mii._count.likedBy} 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.likeCount} /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
</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.likeCount} /> <DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
</div> </div>
</div> </div>