Compare commits

..

1 commit

Author SHA1 Message Date
Alex Helo
5f45f205f4
Merge 8df69bcd79 into 79b19f4807 2026-04-17 10:03:53 -04:00
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 like_count = (SELECT COUNT(*) FROM likes WHERE likes."miiId" = miis.id);

View file

@ -90,16 +90,14 @@ model Mii {
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
likeCount Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
likedBy Like[]
punishmentId Int?
punishments MiiPunishment[]
likedBy Like[]
@@index([tags], type: Gin)
@@index([createdAt])
@@index([likeCount(sort: Desc)])
@@index([quarantined, createdAt(sort: Desc)])
@@index([platform, 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 { RateLimit } from "@/lib/rate-limit";
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
// const session = await auth();
// if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 100, "/api/mii/like");
const check = await rateLimit.handle();
if (check) return check;
// const rateLimit = new RateLimit(request, 100, "/api/mii/like");
// const check = await rateLimit.handle();
// if (check) return check;
const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data;
// const { id: slugId } = await params;
// const parsed = idSchema.safeParse(slugId);
// if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
// const miiId = parsed.data;
const result = await prisma.$transaction(async (tx) => {
const existingLike = await tx.like.findUnique({
where: {
userId_miiId: {
userId: Number(session.user?.id),
miiId,
},
},
});
// const result = await prisma.$transaction(async (tx) => {
// const existingLike = await tx.like.findUnique({
// where: {
// userId_miiId: {
// userId: Number(session.user?.id),
// miiId,
// },
// },
// });
if (existingLike) {
await tx.like.delete({
where: {
userId_miiId: {
userId: Number(session.user?.id),
miiId,
},
},
});
await tx.mii.update({
where: { id: miiId },
data: { likeCount: { decrement: 1 } },
});
} else {
await tx.like.create({
data: {
userId: Number(session.user?.id),
miiId,
},
});
await tx.mii.update({
where: { id: miiId },
data: { likeCount: { increment: 1 } },
});
}
// if (existingLike) {
// // Remove the like if it exists
// await tx.like.delete({
// where: {
// userId_miiId: {
// userId: Number(session.user?.id),
// miiId,
// },
// },
// });
// } else {
// // Add a like if it doesn't exist
// await tx.like.create({
// data: {
// userId: Number(session.user?.id),
// miiId,
// },
// });
// }
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
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 {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
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],
creator: name,
openGraph: {
type: "article",
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: [
{
url: metadataImageUrl,
@ -70,7 +70,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
twitter: {
card: "summary_large_image",
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: [
{
url: metadataImageUrl,
@ -306,7 +306,7 @@ export default async function MiiPage({ params }: Props) {
{/* 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>
{/* 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>
{/* Tags */}
<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 (
<div className="flex justify-center w-full">
<ReportMiiForm mii={mii} likes={mii.likeCount} />
<ReportMiiForm mii={mii} likes={mii._count.likedBy} />
</div>
);
}

View file

@ -31,7 +31,7 @@ export default function AuthorButtons({ mii }: Props) {
<Icon icon="mdi:pencil" />
<span>Edit</span>
</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

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

View file

@ -74,7 +74,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
</div>
<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 && (
<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">
<Icon icon="mdi:pencil" />
</Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
</div>
)}
@ -105,7 +105,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
<Icon icon="material-symbols:check-rounded" />
</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">
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
</div>
</div>