mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 21:27:46 +00:00
Compare commits
7 commits
d208565a61
...
7f52773bd9
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f52773bd9 | |||
| 0c2dcf3192 | |||
| b66fbd305a | |||
| c72dab1962 | |||
| 8ebc480233 | |||
| b00ce4dc3b | |||
|
|
8615a4d864 |
13 changed files with 131 additions and 188 deletions
|
|
@ -5,6 +5,17 @@ const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
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;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.0.10",
|
||||||
"redis": "^5.11.0",
|
"redis": "^5.11.0",
|
||||||
"satori": "^0.26.0",
|
"satori": "^0.26.0",
|
||||||
"seedrandom": "^3.0.5",
|
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"sjcl-with-all": "1.0.8",
|
"sjcl-with-all": "1.0.8",
|
||||||
"swr": "^2.4.1",
|
"swr": "^2.4.1",
|
||||||
|
|
@ -46,7 +45,6 @@
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/seedrandom": "^3.0.8",
|
|
||||||
"@types/sjcl": "^1.0.34",
|
"@types/sjcl": "^1.0.34",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.2.0",
|
||||||
"eslint-config-next": "16.2.3",
|
"eslint-config-next": "16.2.3",
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,6 @@ importers:
|
||||||
satori:
|
satori:
|
||||||
specifier: ^0.26.0
|
specifier: ^0.26.0
|
||||||
version: 0.26.0
|
version: 0.26.0
|
||||||
seedrandom:
|
|
||||||
specifier: ^3.0.5
|
|
||||||
version: 3.0.5
|
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.34.5
|
specifier: ^0.34.5
|
||||||
version: 0.34.5
|
version: 0.34.5
|
||||||
|
|
@ -108,9 +105,6 @@ importers:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.2.3
|
specifier: ^19.2.3
|
||||||
version: 19.2.3(@types/react@19.2.14)
|
version: 19.2.3(@types/react@19.2.14)
|
||||||
'@types/seedrandom':
|
|
||||||
specifier: ^3.0.8
|
|
||||||
version: 3.0.8
|
|
||||||
'@types/sjcl':
|
'@types/sjcl':
|
||||||
specifier: ^1.0.34
|
specifier: ^1.0.34
|
||||||
version: 1.0.34
|
version: 1.0.34
|
||||||
|
|
@ -797,9 +791,6 @@ packages:
|
||||||
'@types/react@19.2.14':
|
'@types/react@19.2.14':
|
||||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
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':
|
'@types/sjcl@1.0.34':
|
||||||
resolution: {integrity: sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==}
|
resolution: {integrity: sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==}
|
||||||
|
|
||||||
|
|
@ -2252,9 +2243,6 @@ packages:
|
||||||
schema-dts@2.0.0:
|
schema-dts@2.0.0:
|
||||||
resolution: {integrity: sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==}
|
resolution: {integrity: sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==}
|
||||||
|
|
||||||
seedrandom@3.0.5:
|
|
||||||
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
|
|
||||||
|
|
||||||
semver@6.3.1:
|
semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -3111,8 +3099,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
'@types/seedrandom@3.0.8': {}
|
|
||||||
|
|
||||||
'@types/sjcl@1.0.34': {}
|
'@types/sjcl@1.0.34': {}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
@ -4701,8 +4687,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
seedrandom@3.0.5: {}
|
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.4: {}
|
semver@7.7.4: {}
|
||||||
|
|
|
||||||
2
prisma/migrations/20260416234406_pr_28/migration.sql
Normal file
2
prisma/migrations/20260416234406_pr_28/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "miis_in_queue_quarantined_createdAt_idx" ON "miis"("in_queue", "quarantined", "createdAt" DESC);
|
||||||
|
|
@ -104,6 +104,7 @@ model Mii {
|
||||||
@@index([gender])
|
@@index([gender])
|
||||||
@@index([makeup])
|
@@index([makeup])
|
||||||
@@index([quarantined, id])
|
@@index([quarantined, id])
|
||||||
|
@@index([in_queue, quarantined, createdAt(sort: Desc)])
|
||||||
@@map("miis")
|
@@map("miis")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,54 +6,54 @@ 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 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) {
|
||||||
// Remove the like if it exists
|
// // 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,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
// Add a like if it doesn't exist
|
// // Add a like if it doesn't exist
|
||||||
await tx.like.create({
|
// await tx.like.create({
|
||||||
data: {
|
// data: {
|
||||||
userId: Number(session.user?.id),
|
// userId: Number(session.user?.id),
|
||||||
miiId,
|
// miiId,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const likeCount = await tx.like.count({
|
// const likeCount = await tx.like.count({
|
||||||
where: { miiId },
|
// where: { miiId },
|
||||||
});
|
// });
|
||||||
|
|
||||||
return { liked: !existingLike, count: likeCount };
|
// return { liked: !existingLike, count: likeCount };
|
||||||
});
|
// });
|
||||||
|
|
||||||
return rateLimit.sendResponse({ success: true, liked: result.liked, count: result.count });
|
return NextResponse.json({ success: false });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,26 @@ import { prisma } from "@/lib/prisma";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
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, 50, "/api/mii/like_get");
|
// const rateLimit = new RateLimit(request, 50, "/api/mii/like_get");
|
||||||
const check = await rateLimit.handle();
|
// const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
// if (check) return check;
|
||||||
|
|
||||||
const idsParam = new URL(request.url).searchParams.get("ids");
|
// const idsParam = new URL(request.url).searchParams.get("ids");
|
||||||
if (!idsParam) return NextResponse.json({ error: "Missing IDs parameter" }, { status: 400 });
|
// if (!idsParam) return NextResponse.json({ error: "Missing IDs parameter" }, { status: 400 });
|
||||||
|
|
||||||
const ids = idsParam.split(",").map(Number).filter(Boolean);
|
// const ids = idsParam.split(",").map(Number).filter(Boolean);
|
||||||
if (!ids.length) return NextResponse.json({ error: "No valid IDs provided" }, { status: 400 });
|
// 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 });
|
// if (ids.length > 100) return NextResponse.json({ error: "Too many IDs, maximum is 100" }, { status: 400 });
|
||||||
|
|
||||||
const liked = await prisma.like.findMany({
|
// const liked = await prisma.like.findMany({
|
||||||
where: { userId: Number(session.user?.id), miiId: { in: ids } },
|
// where: { userId: Number(session.user?.id), miiId: { in: ids } },
|
||||||
select: { miiId: true },
|
// select: { miiId: true },
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Return only Miis that are liked
|
// // Return only Miis that are liked
|
||||||
return NextResponse.json(liked.map((l) => l.miiId));
|
// return NextResponse.json(liked.map((l) => l.miiId));
|
||||||
|
return NextResponse.json({ success: false }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,31 +24,31 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
if (disabled) return;
|
// if (disabled) return;
|
||||||
if (!session.data?.user) {
|
// if (!session.data?.user) {
|
||||||
router.push("/login");
|
// router.push("/login");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
setIsLikedState(!isLikedState);
|
// setIsLikedState(!isLikedState);
|
||||||
setLikesState(isLikedState ? likesState - 1 : likesState + 1);
|
// setLikesState(isLikedState ? likesState - 1 : likesState + 1);
|
||||||
|
|
||||||
// Trigger animation
|
// // Trigger animation
|
||||||
if (!isLikedState) {
|
// if (!isLikedState) {
|
||||||
setIsAnimating(true);
|
// setIsAnimating(true);
|
||||||
setTimeout(() => setIsAnimating(false), 1000); // match animation duration
|
// 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) {
|
// if (response.ok) {
|
||||||
const { liked, count } = await response.json();
|
// const { liked, count } = await response.json();
|
||||||
setIsLikedState(liked);
|
// setIsLikedState(liked);
|
||||||
setLikesState(count);
|
// setLikesState(count);
|
||||||
} else {
|
// } else {
|
||||||
setIsLikedState(isLikedState);
|
// setIsLikedState(isLikedState);
|
||||||
setLikesState(likesState);
|
// setLikesState(likesState);
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preload like button icons
|
// 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"]);
|
loadIcons(["icon-park-solid:like", "icon-park-outline:like"]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
setIsLikedState(isLiked);
|
// setIsLikedState(isLiked);
|
||||||
}, [isLiked]);
|
// }, [isLiked]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import crypto from "crypto";
|
|
||||||
import seedrandom from "seedrandom";
|
|
||||||
|
|
||||||
import { searchSchema } from "@/lib/schemas";
|
import { searchSchema } from "@/lib/schemas";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
@ -23,7 +20,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
const parsed = searchSchema.safeParse(searchParams);
|
const parsed = searchSchema.safeParse(searchParams);
|
||||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
||||||
|
|
||||||
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
|
// My Likes page
|
||||||
let miiIdsLiked: number[] | undefined = undefined;
|
let miiIdsLiked: number[] | undefined = undefined;
|
||||||
|
|
@ -108,41 +105,8 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
let totalCount: number;
|
let totalCount: number;
|
||||||
let filteredCount: number;
|
|
||||||
let miis: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
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 },
|
|
||||||
});
|
|
||||||
|
|
||||||
totalCount = matchingIds.length;
|
|
||||||
filteredCount = Math.max(0, Math.min(limit, totalCount - skip));
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Sorting by likes, newest, or oldest
|
// Sorting by likes, newest, or oldest
|
||||||
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
||||||
|
|
||||||
|
|
@ -155,18 +119,16 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
orderBy = [{ createdAt: "desc" }, { name: "asc" }];
|
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: { ...where, userId } }),
|
||||||
prisma.mii.count({ where, skip, take: limit }),
|
|
||||||
prisma.mii.findMany({
|
prisma.mii.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
select,
|
select,
|
||||||
skip: (page - 1) * limit,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
const lastPage = Math.ceil(totalCount / limit);
|
const lastPage = Math.ceil(totalCount / limit);
|
||||||
|
|
||||||
|
|
@ -174,19 +136,8 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{totalCount == filteredCount ? (
|
|
||||||
<>
|
|
||||||
<span className="text-2xl font-bold text-amber-900">{totalCount}</span>
|
<span className="text-2xl font-bold text-amber-900">{totalCount}</span>
|
||||||
<span className="text-lg text-amber-700">{totalCount === 1 ? "Mii" : "Miis"}</span>
|
<span className="text-lg text-amber-700">{totalCount === 1 ? "Mii" : "Miis"}</span>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="text-2xl font-bold text-amber-900">{filteredCount}</span>
|
|
||||||
<span className="text-sm text-amber-700">of</span>
|
|
||||||
<span className="text-lg font-semibold text-amber-800">{totalCount}</span>
|
|
||||||
<span className="text-lg text-amber-700">Miis</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,12 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const ids = miis.map((m) => m.id).join(",");
|
// 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, {
|
// const { data } = useSWR<number[]>(session.data?.user && miis.length > 0 ? `/api/mii/has-liked?ids=${ids}` : null, fetcher, {
|
||||||
revalidateOnFocus: false,
|
// revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
// revalidateOnReconnect: false,
|
||||||
});
|
// });
|
||||||
const likedIds = new Set(data ?? []);
|
const likedIds = new Set([0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { useTransition } from "react";
|
||||||
import { useSelect } from "downshift";
|
import { useSelect } from "downshift";
|
||||||
import { Icon } from "@iconify/react";
|
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() {
|
export default function SortSelect() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -26,10 +26,6 @@ export default function SortSelect() {
|
||||||
params.set("page", "1");
|
params.set("page", "1");
|
||||||
params.set("sort", selectedItem);
|
params.set("sort", selectedItem);
|
||||||
|
|
||||||
if (selectedItem == "random") {
|
|
||||||
params.set("seed", Math.floor(Math.random() * 1_000_000_000).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
router.push(`?${params.toString()}`, { scroll: false });
|
router.push(`?${params.toString()}`, { scroll: false });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,10 @@ export class RateLimit {
|
||||||
|
|
||||||
return { success, limit: this.maxRequests, remaining, expires: expireAt };
|
return { success, limit: this.maxRequests, remaining, expires: expireAt };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Fail open — don't block users when Redis is unreachable
|
||||||
console.error("Rate limit check failed", error);
|
console.error("Rate limit check failed", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
limit: this.maxRequests,
|
limit: this.maxRequests,
|
||||||
remaining: this.maxRequests,
|
remaining: this.maxRequests,
|
||||||
expires: expireAt,
|
expires: expireAt,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const idSchema = z.coerce.number({ error: "ID must be a number" }).int({
|
||||||
|
|
||||||
export const searchSchema = z.object({
|
export const searchSchema = z.object({
|
||||||
q: querySchema.optional(),
|
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
|
tags: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|
@ -72,8 +72,6 @@ export const searchSchema = z.object({
|
||||||
.max(100, { error: "Limit cannot be more than 100" })
|
.max(100, { error: "Limit cannot be more than 100" })
|
||||||
.optional(),
|
.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(),
|
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
|
export const userNameSchema = z
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue