import Link from "next/link"; import { MiiGender, MiiPlatform, Prisma } from "@prisma/client"; import { Icon } from "@iconify/react"; import { z } from "zod"; import seedrandom from "seedrandom"; import { querySchema } from "@/lib/schemas"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import FilterMenu from "./filter-menu"; import SortSelect from "./sort-select"; import Carousel from "../carousel"; import LikeButton from "../like-button"; import DeleteMiiButton from "../delete-mii"; import Pagination from "./pagination"; interface Props { searchParams: { [key: string]: string | string[] | undefined }; userId?: number; // Profiles inLikesPage?: boolean; // Self-explanatory } 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"), tags: z .string() .optional() .transform((value) => value ?.split(",") .map((tag) => tag.trim()) .filter((tag) => tag.length > 0) ), platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(), gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(), // todo: incorporate tagsSchema // Pages limit: z.coerce .number({ error: "Limit must be a number" }) .int({ error: "Limit must be an integer" }) .min(1, { error: "Limit must be at least 1" }) .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 default async function MiiList({ searchParams, userId, inLikesPage }: Props) { const session = await auth(); const parsed = searchSchema.safeParse(searchParams); if (!parsed.success) return

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

; const { q: query, sort, tags, platform, gender, page = 1, limit = 24, seed } = parsed.data; // My Likes page let miiIdsLiked: number[] | undefined = undefined; if (inLikesPage && session?.user.id) { const likedMiis = await prisma.like.findMany({ where: { userId: Number(session.user.id) }, select: { miiId: true }, }); miiIdsLiked = likedMiis.map((like) => like.miiId); } const where: Prisma.MiiWhereInput = { // Only show liked miis on likes page ...(inLikesPage && miiIdsLiked && { id: { in: miiIdsLiked } }), // Searching ...(query && { OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }, { description: { contains: query, mode: "insensitive" } }], }), // Tag filtering ...(tags && tags.length > 0 && { tags: { hasEvery: tags } }), // Platform ...(platform && { platform: { equals: platform } }), // Gender ...(gender && { gender: { equals: gender } }), // Profiles ...(userId && { userId }), }; const select: Prisma.MiiSelect = { id: true, // Don't show when userId is specified ...(!userId && { user: { select: { id: true, username: true, }, }, }), platform: true, name: true, imageCount: true, tags: true, createdAt: true, gender: true, // Mii liked check ...(session?.user?.id && { likedBy: { where: { userId: Number(session.user.id) }, select: { userId: true }, }, }), // Like count _count: { select: { likedBy: true }, }, }; const skip = (page - 1) * limit; let totalCount: number; let filteredCount: number; let list: Prisma.MiiGetPayload<{ select: typeof select }>[]; if (sort === "random") { // Use seed for consistent random results const randomSeed = seed || Math.floor(Math.random() * 1_000_000_000); // Get all IDs that match the where conditions const matchingIds = await prisma.mii.findMany({ where, select: { id: true }, }); totalCount = matchingIds.length; filteredCount = Math.min(matchingIds.length, limit); if (matchingIds.length === 0) return; 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(0, limit).map((i) => i.id); list = await prisma.mii.findMany({ where: { id: { in: selectedIds }, }, select, }); } 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, filteredCount, list] = 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, take: limit }), ]); } const lastPage = Math.ceil(totalCount / limit); const miis = list.map(({ _count, likedBy, ...rest }) => ({ ...rest, likes: _count.likedBy, isLiked: session?.user?.id ? likedBy.length > 0 : false, })); return (
{totalCount == filteredCount ? ( <> {totalCount} {totalCount === 1 ? "Mii" : "Miis"} ) : ( <> {filteredCount} of {totalCount} Miis )}
{miis.map((mii) => (
`/mii/${mii.id}/image?type=image${index}`), ]} />
{mii.name}
{mii.tags.map((tag) => ( {tag} ))}
{!userId && ( @{mii.user?.username} )} {userId && Number(session?.user.id) == userId && (
)}
))}
); }