From 8615a4d8641fb6fb45d4d4ee41c7c765a3db0b3f Mon Sep 17 00:00:00 2001 From: AlexHelo Date: Thu, 16 Apr 2026 17:28:37 -0600 Subject: [PATCH 1/7] Fix Cloudflare RSC caching bug and reduce database load --- next.config.ts | 11 +++++++++++ prisma/schema.prisma | 1 + src/components/mii/list/index.tsx | 22 ++++------------------ src/lib/rate-limit.ts | 3 ++- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/next.config.ts b/next.config.ts index f7158ac..8114235 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,6 +5,17 @@ const nextConfig: NextConfig = { images: { unoptimized: true, }, + async headers() { + return [ + { + // Prevent Cloudflare from serving cached HTML for RSC navigation requests + source: "/:path*", + headers: [ + { key: "Vary", value: "RSC, Next-Router-State-Tree, Next-Router-Prefetch" }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cfc142b..c3cc1cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -104,6 +104,7 @@ model Mii { @@index([gender]) @@index([makeup]) @@index([quarantined, id]) + @@index([in_queue, quarantined, createdAt(sort: Desc)]) @@map("miis") } diff --git a/src/components/mii/list/index.tsx b/src/components/mii/list/index.tsx index 6d1485f..8fb5afa 100644 --- a/src/components/mii/list/index.tsx +++ b/src/components/mii/list/index.tsx @@ -108,7 +108,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop const skip = (page - 1) * limit; let totalCount: number; - let filteredCount: number; let miis: Prisma.MiiGetPayload<{ select: typeof select }>[]; if (sort === "random") { @@ -119,7 +118,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop }); totalCount = matchingIds.length; - filteredCount = Math.max(0, Math.min(limit, totalCount - skip)); if (matchingIds.length === 0) return; @@ -155,14 +153,13 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop orderBy = [{ createdAt: "desc" }, { name: "asc" }]; } - [totalCount, filteredCount, miis] = await Promise.all([ + [totalCount, miis] = await Promise.all([ prisma.mii.count({ where: { ...where, userId } }), - prisma.mii.count({ where, skip, take: limit }), prisma.mii.findMany({ where, orderBy, select, - skip: (page - 1) * limit, + skip, take: limit, }), ]); @@ -174,19 +171,8 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
- {totalCount == filteredCount ? ( - <> - {totalCount} - {totalCount === 1 ? "Mii" : "Miis"} - - ) : ( - <> - {filteredCount} - of - {totalCount} - Miis - - )} + {totalCount} + {totalCount === 1 ? "Mii" : "Miis"}
diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts index 6788127..b8a53c4 100644 --- a/src/lib/rate-limit.ts +++ b/src/lib/rate-limit.ts @@ -68,9 +68,10 @@ export class RateLimit { return { success, limit: this.maxRequests, remaining, expires: expireAt }; } catch (error) { + // Fail open — don't block users when Redis is unreachable console.error("Rate limit check failed", error); return { - success: false, + success: true, limit: this.maxRequests, remaining: this.maxRequests, expires: expireAt, From 8ebc4802330f8d80205e138305bd4515392a166a Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 17 Apr 2026 00:44:26 +0100 Subject: [PATCH 2/7] feat: prisma migration --- prisma/migrations/20260416234406_pr_28/migration.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 prisma/migrations/20260416234406_pr_28/migration.sql diff --git a/prisma/migrations/20260416234406_pr_28/migration.sql b/prisma/migrations/20260416234406_pr_28/migration.sql new file mode 100644 index 0000000..da78f81 --- /dev/null +++ b/prisma/migrations/20260416234406_pr_28/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "miis_in_queue_quarantined_createdAt_idx" ON "miis"("in_queue", "quarantined", "createdAt" DESC); From c72dab19629ea411851379ba103aeef7b0331075 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 17 Apr 2026 00:54:13 +0100 Subject: [PATCH 3/7] refactor: remove random sort --- package.json | 2 - pnpm-lock.yaml | 16 ------ src/components/mii/list/index.tsx | 73 +++++++------------------ src/components/mii/list/sort-select.tsx | 8 +-- src/lib/schemas.ts | 4 +- 5 files changed, 23 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 16787e4..05c95cf 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "react-image-crop": "^11.0.10", "redis": "^5.11.0", "satori": "^0.26.0", - "seedrandom": "^3.0.5", "sharp": "^0.34.5", "sjcl-with-all": "1.0.8", "swr": "^2.4.1", @@ -46,7 +45,6 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/seedrandom": "^3.0.8", "@types/sjcl": "^1.0.34", "eslint": "^10.2.0", "eslint-config-next": "16.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13b6320..26a202f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: satori: specifier: ^0.26.0 version: 0.26.0 - seedrandom: - specifier: ^3.0.5 - version: 3.0.5 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -108,9 +105,6 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) - '@types/seedrandom': - specifier: ^3.0.8 - version: 3.0.8 '@types/sjcl': specifier: ^1.0.34 version: 1.0.34 @@ -797,9 +791,6 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@types/seedrandom@3.0.8': - resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} - '@types/sjcl@1.0.34': resolution: {integrity: sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==} @@ -2252,9 +2243,6 @@ packages: schema-dts@2.0.0: resolution: {integrity: sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==} - seedrandom@3.0.5: - resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3111,8 +3099,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/seedrandom@3.0.8': {} - '@types/sjcl@1.0.34': {} '@types/use-sync-external-store@0.0.6': {} @@ -4701,8 +4687,6 @@ snapshots: transitivePeerDependencies: - typescript - seedrandom@3.0.5: {} - semver@6.3.1: {} semver@7.7.4: {} diff --git a/src/components/mii/list/index.tsx b/src/components/mii/list/index.tsx index 8fb5afa..b4c2dd8 100644 --- a/src/components/mii/list/index.tsx +++ b/src/components/mii/list/index.tsx @@ -1,7 +1,6 @@ import { Prisma } from "@prisma/client"; import crypto from "crypto"; -import seedrandom from "seedrandom"; import { searchSchema } from "@/lib/schemas"; import { auth } from "@/lib/auth"; @@ -23,7 +22,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop const parsed = searchSchema.safeParse(searchParams); if (!parsed.success) return

{parsed.error.issues[0].message}

; - const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, seed } = parsed.data; + const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24 } = parsed.data; // My Likes page let miiIdsLiked: number[] | undefined = undefined; @@ -110,61 +109,29 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop let totalCount: number; let miis: Prisma.MiiGetPayload<{ select: typeof select }>[]; - if (sort === "random") { - // Get all IDs that match the where conditions - const matchingIds = await prisma.mii.findMany({ - where, - select: { id: true }, - }); + // Sorting by likes, newest, or oldest + let orderBy: Prisma.MiiOrderByWithRelationInput[]; - totalCount = matchingIds.length; - - if (matchingIds.length === 0) return; - - // Use seed for consistent random results - const randomSeed = seed || crypto.randomInt(0, 1_000_000_000); - const rng = seedrandom(randomSeed.toString()); - - // Randomize all IDs using the Durstenfeld algorithm - for (let i = matchingIds.length - 1; i > 0; i--) { - const j = Math.floor(rng() * (i + 1)); - [matchingIds[i], matchingIds[j]] = [matchingIds[j], matchingIds[i]]; - } - - // Convert to number[] array - const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id); - - miis = await prisma.mii.findMany({ - where: { - id: { in: selectedIds }, - }, - select, - }); + if (sort === "likes") { + orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }]; + } else if (sort === "oldest") { + orderBy = [{ createdAt: "asc" }, { name: "asc" }]; } else { - // Sorting by likes, newest, or oldest - let orderBy: Prisma.MiiOrderByWithRelationInput[]; - - if (sort === "likes") { - orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }]; - } else if (sort === "oldest") { - orderBy = [{ createdAt: "asc" }, { name: "asc" }]; - } else { - // default to newest - orderBy = [{ createdAt: "desc" }, { name: "asc" }]; - } - - [totalCount, miis] = await Promise.all([ - prisma.mii.count({ where: { ...where, userId } }), - prisma.mii.findMany({ - where, - orderBy, - select, - skip, - take: limit, - }), - ]); + // default to newest + orderBy = [{ createdAt: "desc" }, { name: "asc" }]; } + [totalCount, miis] = await Promise.all([ + prisma.mii.count({ where: { ...where, userId } }), + prisma.mii.findMany({ + where, + orderBy, + select, + skip, + take: limit, + }), + ]); + const lastPage = Math.ceil(totalCount / limit); return ( diff --git a/src/components/mii/list/sort-select.tsx b/src/components/mii/list/sort-select.tsx index e62ed01..654e796 100644 --- a/src/components/mii/list/sort-select.tsx +++ b/src/components/mii/list/sort-select.tsx @@ -5,9 +5,9 @@ import { useTransition } from "react"; import { useSelect } from "downshift"; import { Icon } from "@iconify/react"; -type Sort = "likes" | "newest" | "oldest" | "random"; +type Sort = "likes" | "newest" | "oldest"; -const items = ["likes", "newest", "oldest", "random"]; +const items = ["likes", "newest", "oldest"]; export default function SortSelect() { const router = useRouter(); @@ -26,10 +26,6 @@ export default function SortSelect() { params.set("page", "1"); params.set("sort", selectedItem); - if (selectedItem == "random") { - params.set("seed", Math.floor(Math.random() * 1_000_000_000).toString()); - } - startTransition(() => { router.push(`?${params.toString()}`, { scroll: false }); }); diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 5e4ae4e..490cc5f 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -39,7 +39,7 @@ export const idSchema = z.coerce.number({ error: "ID must be a number" }).int({ export const searchSchema = z.object({ q: querySchema.optional(), - sort: z.enum(["likes", "newest", "oldest", "random"], { error: "Sort must be either 'likes', 'newest', 'oldest', or 'random'" }).default("newest"), + sort: z.enum(["likes", "newest", "oldest"], { error: "Sort must be either 'likes', 'newest', or 'oldest'" }).default("newest"), tags: z .string() .optional() @@ -72,8 +72,6 @@ export const searchSchema = z.object({ .max(100, { error: "Limit cannot be more than 100" }) .optional(), page: z.coerce.number({ error: "Page must be a number" }).int({ error: "Page must be an integer" }).min(1, { error: "Page must be at least 1" }).optional(), - // Random sort - seed: z.coerce.number({ error: "Seed must be a number" }).int({ error: "Seed must be an integer" }).optional(), }); export const userNameSchema = z From b66fbd305a98004736679bd27597ee0045542531 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 17 Apr 2026 01:11:49 +0100 Subject: [PATCH 4/7] temp disable likes --- src/app/api/mii/[id]/like/route.ts | 86 ++++++++++++++-------------- src/app/api/mii/has-liked/route.ts | 33 +++++------ src/components/like-button.tsx | 48 ++++++++-------- src/components/mii/list/index.tsx | 2 - src/components/mii/list/mii-grid.tsx | 12 ++-- 5 files changed, 90 insertions(+), 91 deletions(-) diff --git a/src/app/api/mii/[id]/like/route.ts b/src/app/api/mii/[id]/like/route.ts index 3a1e213..3dbea23 100644 --- a/src/app/api/mii/[id]/like/route.ts +++ b/src/app/api/mii/[id]/like/route.ts @@ -6,54 +6,54 @@ import { idSchema } from "@/lib/schemas"; import { RateLimit } from "@/lib/rate-limit"; 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 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) { - // 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, - }, - }); - } + // 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, + // }, + // }); + // } - const likeCount = await tx.like.count({ - where: { miiId }, - }); + // const likeCount = await tx.like.count({ + // where: { miiId }, + // }); - return { liked: !existingLike, count: likeCount }; - }); + // return { liked: !existingLike, count: likeCount }; + // }); - return rateLimit.sendResponse({ success: true, liked: result.liked, count: result.count }); + return rateLimit.sendResponse({ success: false }); } diff --git a/src/app/api/mii/has-liked/route.ts b/src/app/api/mii/has-liked/route.ts index ecb0e87..f064c4d 100644 --- a/src/app/api/mii/has-liked/route.ts +++ b/src/app/api/mii/has-liked/route.ts @@ -4,25 +4,26 @@ 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 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 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 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 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 }, - }); + // 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)); + // // Return only Miis that are liked + // return NextResponse.json(liked.map((l) => l.miiId)); + return NextResponse.json({ success: false }, { status: 500 }); } diff --git a/src/components/like-button.tsx b/src/components/like-button.tsx index 7628beb..470864e 100644 --- a/src/components/like-button.tsx +++ b/src/components/like-button.tsx @@ -24,31 +24,31 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate const [isAnimating, setIsAnimating] = useState(false); const onClick = async () => { - if (disabled) return; - if (!session.data?.user) { - router.push("/login"); - return; - } + // if (disabled) return; + // if (!session.data?.user) { + // router.push("/login"); + // return; + // } - setIsLikedState(!isLikedState); - setLikesState(isLikedState ? likesState - 1 : likesState + 1); + // setIsLikedState(!isLikedState); + // setLikesState(isLikedState ? likesState - 1 : likesState + 1); - // Trigger animation - if (!isLikedState) { - setIsAnimating(true); - setTimeout(() => setIsAnimating(false), 1000); // match animation duration - } + // // Trigger animation + // if (!isLikedState) { + // setIsAnimating(true); + // setTimeout(() => setIsAnimating(false), 1000); // match animation duration + // } - const response = await fetch(`/api/mii/${miiId}/like`, { method: "PATCH" }); + // const response = await fetch(`/api/mii/${miiId}/like`, { method: "PATCH" }); - if (response.ok) { - const { liked, count } = await response.json(); - setIsLikedState(liked); - setLikesState(count); - } else { - setIsLikedState(isLikedState); - setLikesState(likesState); - } + // if (response.ok) { + // const { liked, count } = await response.json(); + // setIsLikedState(liked); + // setLikesState(count); + // } else { + // setIsLikedState(isLikedState); + // setLikesState(likesState); + // } }; // Preload like button icons @@ -56,9 +56,9 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate loadIcons(["icon-park-solid:like", "icon-park-outline:like"]); }, []); - useEffect(() => { - setIsLikedState(isLiked); - }, [isLiked]); + // useEffect(() => { + // setIsLikedState(isLiked); + // }, [isLiked]); return (