Compare commits

...

7 commits

Author SHA1 Message Date
c1f3eacd03 fix: unoptimize images
ITS SLOWING DOWN THE SITE SO MUCH IT'S OFFLINE
2026-03-30 19:46:28 +01:00
b01dd799dc refactor: rename makeup to face paint 2026-03-30 19:27:13 +01:00
c4d01fa8ee feat: show controversial miis on profiles by default 2026-03-30 18:30:03 +01:00
1805d21b12 fix: temp fix for c1ce38f5 2026-03-30 14:13:11 +01:00
c1ce38f594 fix: images breaking? 2026-03-30 14:07:49 +01:00
e47914f873 fix: resize custom images 2026-03-30 13:54:20 +01:00
e078d59812 fix: admins can't see author buttons 2026-03-30 13:38:34 +01:00
13 changed files with 54 additions and 79 deletions

View file

@ -4,34 +4,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
images: { images: {
localPatterns: [ unoptimized: true,
{
pathname: "/mii/*/image",
},
{
pathname: "/profile/*/picture",
},
{
pathname: "/tutorial/**",
},
{
pathname: "/guest.png",
},
],
remotePatterns: [
{
hostname: "avatars.githubusercontent.com",
},
{
hostname: "cdn.discordapp.com",
},
{
hostname: "studio.mii.nintendo.com",
},
{
hostname: "*.googleusercontent.com",
},
],
}, },
}; };

View file

@ -163,7 +163,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
await Promise.all( await Promise.all(
images.map(async (image, index) => { images.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer(); const pngBuffer = await sharp(buffer).resize({ height: 800, fit: "inside", withoutEnlargement: true }).png({ quality: 85 }).toBuffer();
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`); const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
await fs.writeFile(fileLocation, pngBuffer); await fs.writeFile(fileLocation, pngBuffer);

View file

@ -313,7 +313,7 @@ export async function POST(request: NextRequest) {
await Promise.all( await Promise.all(
customImages.map(async (image, index) => { customImages.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer(); const pngBuffer = await sharp(buffer).resize({ height: 800, fit: "inside", withoutEnlargement: true }).png({ quality: 85 }).toBuffer();
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`); const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
await fs.writeFile(fileLocation, pngBuffer); await fs.writeFile(fileLocation, pngBuffer);

View file

@ -19,7 +19,7 @@ const searchParamsSchema = z.object({
}); });
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const rateLimit = new RateLimit(request, 200, "/mii/image"); const rateLimit = new RateLimit(request, 20000, "/mii/image");
const check = await rateLimit.handle(); const check = await rateLimit.handle();
if (check) return check; if (check) return check;
@ -110,6 +110,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return rateLimit.sendResponse(buffer, 200, { return rateLimit.sendResponse(buffer, 200, {
"Content-Type": "image/png", "Content-Type": "image/png",
"X-Robots-Tag": "noindex, noimageindex, nofollow", "X-Robots-Tag": "noindex, noimageindex, nofollow",
"Cache-Control": "no-store", "Cache-Control": "public, max-age=31536000, immutable",
}); });
} }

View file

@ -22,7 +22,7 @@ interface Props {
export default function AuthorButtons({ mii }: Props) { export default function AuthorButtons({ mii }: Props) {
const session = useSession(); const session = useSession();
if (!session.data || Number(session.data.user?.id) !== mii.userId || Number(session.data.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) if (!session.data || (Number(session.data.user?.id) !== mii.userId && Number(session.data.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)))
return null; return null;
return ( return (

View file

@ -121,18 +121,13 @@ export default function FilterMenu() {
<> <>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Makeup</span> <span>Face Paint</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<MakeupSelect /> <MakeupSelect />
</> </>
)} )}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
<hr className="grow border-zinc-300" />
<span>Other</span>
<hr className="grow border-zinc-300" />
</div>
<OtherFilters /> <OtherFilters />
</div> </div>
)} )}

View file

@ -1,7 +1,4 @@
import Link from "next/link";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { Icon } from "@iconify/react";
import crypto from "crypto"; import crypto from "crypto";
import seedrandom from "seedrandom"; import seedrandom from "seedrandom";
@ -11,9 +8,6 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import SortSelect from "./sort-select"; import SortSelect from "./sort-select";
import Carousel from "../../carousel";
import LikeButton from "../../like-button";
import DeleteMiiButton from "../delete-mii-button";
import Pagination from "./pagination"; import Pagination from "./pagination";
import FilterMenu from "./filter-menu"; import FilterMenu from "./filter-menu";
import MiiGrid from "./mii-grid"; import MiiGrid from "./mii-grid";
@ -61,7 +55,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
// Makeup // Makeup
...(makeup && { makeup: { equals: makeup } }), ...(makeup && { makeup: { equals: makeup } }),
// Quarantined // Quarantined
...(!quarantined && { quarantined: false }), ...(!quarantined && !userId && { quarantined: false }),
// Profiles // Profiles
...(userId && { userId }), ...(userId && { userId }),
}; };
@ -189,7 +183,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
</div> </div>
</div> </div>
<MiiGrid miis={miis} /> <MiiGrid miis={miis} userId={userId} />
<Pagination lastPage={lastPage} /> <Pagination lastPage={lastPage} />
</div> </div>
); );

View file

@ -35,39 +35,39 @@ export default function MakeupSelect() {
{/* Full Makeup */} {/* Full Makeup */}
<button <button
onClick={() => handleClick("FULL")} onClick={() => handleClick("FULL")}
aria-label="Filter for Full Makeup" aria-label="Filter for Full Face Paint"
data-tooltip-span data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" selected === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
> >
<div className="tooltip bg-pink-400! border-pink-400! before:border-b-pink-400!">Full Makeup</div> <div className="tooltip bg-pink-400! border-pink-400! before:border-b-pink-400!">Full Face Paint</div>
<Icon icon="mdi:palette" className="text-pink-400" /> <Icon icon="mdi:palette" className="text-pink-400" />
</button> </button>
{/* Partial Makeup */} {/* Partial Makeup */}
<button <button
onClick={() => handleClick("PARTIAL")} onClick={() => handleClick("PARTIAL")}
aria-label="Filter for Partial Makeup" aria-label="Filter for Partial Face Paint"
data-tooltip-span data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" selected === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
> >
<div className="tooltip bg-purple-400! border-purple-400! before:border-b-purple-400!">Partial Makeup</div> <div className="tooltip bg-purple-400! border-purple-400! before:border-b-purple-400!">Partial Face Paint</div>
<Icon icon="mdi:lipstick" className="text-purple-400" /> <Icon icon="mdi:lipstick" className="text-purple-400" />
</button> </button>
{/* No Makeup */} {/* No Makeup */}
<button <button
onClick={() => handleClick("NONE")} onClick={() => handleClick("NONE")}
aria-label="Filter for No Makeup" aria-label="Filter for No Face Paint"
data-tooltip-span data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" selected === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
> >
<div className="tooltip bg-gray-400! border-gray-400! before:border-b-gray-400!">No Makeup</div> <div className="tooltip bg-gray-400! border-gray-400! before:border-b-gray-400!">No Face Paint</div>
<Icon icon="codex:cross" className="text-gray-400" /> <Icon icon="codex:cross" className="text-gray-400" />
</button> </button>
</div> </div>

View file

@ -1,13 +1,13 @@
"use client"; "use client";
import { Icon } from "@iconify/react";
import { MiiPlatform } from "@prisma/client"; import { MiiPlatform } from "@prisma/client";
import { useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { ChangeEvent, useState, useTransition } from "react"; import { ChangeEvent, useState, useTransition } from "react";
export default function OtherFilters() { export default function OtherFilters() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const pathname = usePathname();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const platform = (searchParams.get("platform") as MiiPlatform) || undefined; const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
@ -48,22 +48,35 @@ export default function OtherFilters() {
}); });
}; };
const showAllowCopying = platform !== "SWITCH";
const showQuarantined = !pathname.startsWith("/profile");
if (!showAllowCopying && !showQuarantined) return null;
return ( return (
<> <>
{platform === "THREE_DS" && ( <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
<div className="flex justify-between items-center w-full"> <hr className="grow border-zinc-300" />
<span>Other</span>
<hr className="grow border-zinc-300" />
</div>
{showAllowCopying && (
<div className="flex justify-between items-center w-full mb-1">
<label htmlFor="allowCopying" className="text-sm"> <label htmlFor="allowCopying" className="text-sm">
Allow Copying Allow Copying
</label> </label>
<input type="checkbox" id="allowCopying" className="checkbox-alt" checked={allowCopying} onChange={handleChangeAllowCopying} /> <input type="checkbox" id="allowCopying" className="checkbox-alt" checked={allowCopying} onChange={handleChangeAllowCopying} />
</div> </div>
)} )}
<div className="flex justify-between items-center w-full"> {showQuarantined && (
<label htmlFor="quarantined" className="text-sm"> <div className="flex justify-between items-center w-full">
Show Controversial Miis <label htmlFor="quarantined" className="text-sm">
</label> Show Controversial Miis
<input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={handleChangeQuarantined} /> </label>
</div> <input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={handleChangeQuarantined} />
</div>
)}
</> </>
); );
} }

View file

@ -268,7 +268,7 @@ export default function EditForm({ mii, likes }: Props) {
<> <>
<div className="w-full grid grid-cols-3 items-start"> <div className="w-full grid grid-cols-3 items-start">
<label htmlFor="makeup" className="font-semibold py-2"> <label htmlFor="makeup" className="font-semibold py-2">
Makeup Face Paint
</label> </label>
<div className="col-span-2 flex gap-1"> <div className="col-span-2 flex gap-1">
@ -276,8 +276,8 @@ export default function EditForm({ mii, likes }: Props) {
<button <button
type="button" type="button"
onClick={() => setMakeup("FULL")} onClick={() => setMakeup("FULL")}
aria-label="Full makeup" aria-label="Full Face Paint"
data-tooltip="Full Makeup" data-tooltip="Full Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
@ -289,8 +289,8 @@ export default function EditForm({ mii, likes }: Props) {
<button <button
type="button" type="button"
onClick={() => setMakeup("PARTIAL")} onClick={() => setMakeup("PARTIAL")}
aria-label="Partial makeup" aria-label="Partial Face Paint"
data-tooltip="Partial Makeup" data-tooltip="Partial Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
@ -302,8 +302,8 @@ export default function EditForm({ mii, likes }: Props) {
<button <button
type="button" type="button"
onClick={() => setMakeup("NONE")} onClick={() => setMakeup("NONE")}
aria-label="No makeup" aria-label="No Face Paint"
data-tooltip="No Makeup" data-tooltip="No Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${
makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}

View file

@ -332,7 +332,7 @@ export default function SubmitForm() {
{/* Makeup (switch only) */} {/* Makeup (switch only) */}
<div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}>
<label htmlFor="makeup" className="font-semibold py-2"> <label htmlFor="makeup" className="font-semibold py-2">
Makeup Face Paint
</label> </label>
<div className="col-span-2 flex gap-1"> <div className="col-span-2 flex gap-1">
@ -340,8 +340,8 @@ export default function SubmitForm() {
<button <button
type="button" type="button"
onClick={() => setMakeup("FULL")} onClick={() => setMakeup("FULL")}
aria-label="Full makeup" aria-label="Full Face Paint"
data-tooltip="Full Makeup" data-tooltip="Full Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
@ -353,8 +353,8 @@ export default function SubmitForm() {
<button <button
type="button" type="button"
onClick={() => setMakeup("PARTIAL")} onClick={() => setMakeup("PARTIAL")}
aria-label="Partial makeup" aria-label="Partial Face Paint"
data-tooltip="Partial Makeup" data-tooltip="Partial Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}
@ -366,8 +366,8 @@ export default function SubmitForm() {
<button <button
type="button" type="button"
onClick={() => setMakeup("NONE")} onClick={() => setMakeup("NONE")}
aria-label="No makeup" aria-label="No Face Paint"
data-tooltip="No Makeup" data-tooltip="No Face Paint"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${
makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`} }`}

View file

@ -181,7 +181,7 @@ export default function HeadTab({ instructions }: Props) {
type="number" type="number"
id="age" id="age"
min={1} min={1}
max={100} max={1000}
className="pill input text-sm py-1! px-3! w-full" className="pill input text-sm py-1! px-3! w-full"
value={birthday.age ?? undefined} value={birthday.age ?? undefined}
onChange={(e) => { onChange={(e) => {

View file

@ -288,7 +288,7 @@ export const switchMiiInstructionsSchema = z
.object({ .object({
day: z.number().int().min(1).max(31).optional(), day: z.number().int().min(1).max(31).optional(),
month: z.number().int().min(1).max(12).optional(), month: z.number().int().min(1).max(12).optional(),
age: z.number().int().min(1).max(100).optional(), age: z.number().int().min(1).max(1000).optional(),
dontAge: z.boolean().optional(), dontAge: z.boolean().optional(),
}) })
.optional(), .optional(),