feat: nonbinary miis

This commit is contained in:
trafficlunar 2026-02-24 17:16:34 +00:00
parent 0b1516e930
commit d45eb07879
10 changed files with 117 additions and 62 deletions

View file

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "MiiGender" ADD VALUE 'NONBINARY';

View file

@ -164,6 +164,7 @@ enum MiiPlatform {
enum MiiGender {
MALE
FEMALE
NONBINARY
}
enum ReportType {

View file

@ -67,7 +67,6 @@ export async function POST(request: NextRequest) {
const check = await rateLimit.handle();
if (check) return check;
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
const { value } = await response.json();
if (!value) return rateLimit.sendResponse({ error: "Submissions are temporarily disabled" }, 503);
@ -239,6 +238,14 @@ export async function POST(request: NextRequest) {
return rateLimit.sendResponse({ error: "Failed to process and store Mii files" }, 500);
}
try {
await generateMetadataImage(miiRecord, session.user.name!);
} catch (error) {
console.error(error);
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "metadata-image" } });
return rateLimit.sendResponse({ error: `Failed to generate 'metadata' type image for mii ${miiRecord.id}` }, 500);
}
// Compress and store user images
try {
await Promise.all(

View file

@ -178,7 +178,7 @@ export default async function MiiPage({ params }: Props) {
</div>
<div
className={`rounded-xl flex justify-center items-center size-16 text-4xl border-2 shadow-sm ${
className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${
mii.platform === "THREE_DS" ? "bg-sky-100 border-sky-400" : "bg-white border-gray-300"
}`}
>
@ -186,7 +186,7 @@ export default async function MiiPage({ params }: Props) {
</div>
<div
className={`rounded-xl flex justify-center items-center size-16 text-4xl border-2 shadow-sm ${
className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${
mii.platform === "SWITCH" ? "bg-red-100 border-red-400" : "bg-white border-gray-300"
}`}
>
@ -201,17 +201,21 @@ export default async function MiiPage({ params }: Props) {
<hr className="grow border-zinc-300" />
</div>
<div data-tooltip-span title={mii.gender ?? "NULL"} className="grid grid-cols-2 gap-2">
<div data-tooltip-span title={mii.gender ?? "NULL"} className="flex gap-1">
<div
className={`tooltip mt-1! ${
mii.gender === "MALE" ? "bg-blue-400! border-blue-400! before:border-b-blue-400!" : "bg-pink-400! border-pink-400! before:border-b-pink-400!"
mii.gender === "MALE"
? "bg-blue-400! border-blue-400! before:border-b-blue-400!"
: mii.gender === "FEMALE"
? "bg-pink-400! border-pink-400! before:border-b-pink-400!"
: "bg-purple-400! border-purple-400! before:border-b-purple-400!"
}`}
>
{mii.gender === "MALE" ? "Male" : "Female"}
{mii.gender === "MALE" ? "Male" : mii.gender === "FEMALE" ? "Female" : "Nonbinary"}
</div>
<div
className={`rounded-xl flex justify-center items-center size-16 text-5xl border-2 shadow-sm ${
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "MALE" ? "bg-blue-100 border-blue-400" : "bg-white border-gray-300"
}`}
>
@ -219,12 +223,20 @@ export default async function MiiPage({ params }: Props) {
</div>
<div
className={`rounded-xl flex justify-center items-center size-16 text-5xl border-2 shadow-sm ${
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "FEMALE" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300"
}`}
>
<Icon icon="foundation:female" className="text-pink-400" />
</div>
<div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "NONBINARY" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300"
}`}
>
<Icon icon="mdi:gender-non-binary" className="text-purple-400" />
</div>
</div>
</div>

View file

@ -19,26 +19,26 @@ export const metadata: Metadata = {
};
export default async function SubmitPage() {
// const session = await auth();
const session = await auth();
// if (!session) redirect("/login");
// if (!session.user.username) redirect("/create-username");
// const activePunishment = await prisma.punishment.findFirst({
// where: {
// userId: Number(session?.user.id),
// returned: false,
// },
// });
// if (activePunishment) redirect("/off-the-island");
if (!session) redirect("/login");
if (!session.user.username) redirect("/create-username");
const activePunishment = await prisma.punishment.findFirst({
where: {
userId: Number(session?.user.id),
returned: false,
},
});
if (activePunishment) redirect("/off-the-island");
// Check if submissions are disabled
let value: boolean | null = true;
// try {
// const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
// value = await response.json();
// } catch (error) {
// return <p>An error occurred!</p>;
// }
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
value = await response.json();
} catch (error) {
return <p>An error occurred!</p>;
}
if (!value)
return (

View file

@ -4,8 +4,9 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { Icon } from "@iconify/react";
import { MiiGender } from "@prisma/client";
import { MiiGender, MiiPlatform } from "@prisma/client";
import PlatformSelect from "./platform-select";
import TagFilter from "./tag-filter";
import GenderSelect from "./gender-select";
import OtherFilters from "./other-filters";
@ -16,9 +17,10 @@ export default function FilterMenu() {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
const gender = (searchParams.get("gender") as MiiGender) || undefined;
const rawTags = searchParams.get("tags") || "";
const rawExclude = searchParams.get("exclude") || "";
const gender = (searchParams.get("gender") as MiiGender) || undefined;
const allowCopying = (searchParams.get("allowCopying") as unknown as boolean) || false;
const tags = useMemo(
@ -61,11 +63,12 @@ export default function FilterMenu() {
// Count all active filters
useEffect(() => {
let count = tags.length + exclude.length;
if (platform) count++;
if (gender) count++;
if (allowCopying) count++;
setFilterCount(count);
}, [tags, exclude, gender, allowCopying]);
}, [tags, exclude, platform, gender, allowCopying]);
return (
<div className="relative">
@ -84,6 +87,20 @@ export default function FilterMenu() {
<div className="absolute bottom-full left-1/6 -translate-x-1/2 size-0 border-8 border-transparent border-b-amber-500"></div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mb-2">
<hr className="grow border-zinc-300" />
<span>Platform</span>
<hr className="grow border-zinc-300" />
</div>
<PlatformSelect />
<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>Gender</span>
<hr className="grow border-zinc-300" />
</div>
<GenderSelect />
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-2">
<hr className="grow border-zinc-300" />
<span>Tags Include</span>
<hr className="grow border-zinc-300" />
@ -97,19 +114,16 @@ export default function FilterMenu() {
</div>
<TagFilter isExclude />
<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>Gender</span>
<hr className="grow border-zinc-300" />
</div>
<GenderSelect />
<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 />
{platform !== "SWITCH" && (
<>
<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 />
</>
)}
</div>
)}
</div>

View file

@ -3,16 +3,15 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useTransition } from "react";
import { Icon } from "@iconify/react";
import { MiiGender } from "@prisma/client";
import { MiiGender, MiiPlatform } from "@prisma/client";
export default function GenderSelect() {
const router = useRouter();
const searchParams = useSearchParams();
const [, startTransition] = useTransition();
const [selected, setSelected] = useState<MiiGender | null>(
(searchParams.get("gender") as MiiGender) ?? null
);
const [selected, setSelected] = useState<MiiGender | null>((searchParams.get("gender") as MiiGender) ?? null);
const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
const handleClick = (gender: MiiGender) => {
const filter = selected === gender ? null : gender;
@ -33,20 +32,16 @@ export default function GenderSelect() {
};
return (
<div className="grid grid-cols-2 gap-0.5 w-fit">
<div className="flex gap-0.5 w-fit">
<button
onClick={() => handleClick("MALE")}
aria-label="Filter for Male Miis"
data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "MALE"
? "bg-blue-100 border-blue-400 shadow-md"
: "bg-white border-gray-300 hover:border-gray-400"
selected === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
<div className="tooltip bg-blue-400! border-blue-400! before:border-b-blue-400!">
Male
</div>
<div className="tooltip bg-blue-400! border-blue-400! before:border-b-blue-400!">Male</div>
<Icon icon="foundation:male" className="text-blue-400" />
</button>
@ -55,16 +50,26 @@ export default function GenderSelect() {
aria-label="Filter for Female Miis"
data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "FEMALE"
? "bg-pink-100 border-pink-400 shadow-md"
: "bg-white border-gray-300 hover:border-gray-400"
selected === "FEMALE" ? "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!">
Female
</div>
<div className="tooltip bg-pink-400! border-pink-400! before:border-b-pink-400!">Female</div>
<Icon icon="foundation:female" className="text-pink-400" />
</button>
{platform !== "THREE_DS" && (
<button
onClick={() => handleClick("NONBINARY")}
aria-label="Filter for Nonbinary Miis"
data-tooltip-span
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
selected === "NONBINARY" ? "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!">Nonbinary</div>
<Icon icon="mdi:gender-non-binary" className="text-purple-400" />
</button>
)}
</div>
);
}

View file

@ -38,7 +38,7 @@ export default function PlatformSelect() {
selected === "THREE_DS" ? "bg-sky-100 border-sky-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
<div className="tooltip !bg-sky-400 !border-sky-400 before:!border-b-sky-400">3DS</div>
<div className="tooltip bg-sky-400! border-sky-400! before:border-b-sky-400!">3DS</div>
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
</button>
@ -50,7 +50,7 @@ export default function PlatformSelect() {
selected === "SWITCH" ? "bg-red-100 border-red-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
<div className="tooltip !bg-red-400 !border-red-400 before:!border-b-red-400">Switch</div>
<div className="tooltip bg-red-400! border-red-400! before:border-b-red-400!">Switch</div>
<Icon icon="cib:nintendo-switch" className="text-red-400" />
</button>
</div>

View file

@ -279,7 +279,8 @@ export default function SubmitForm() {
type="button"
onClick={() => setGender("MALE")}
aria-label="Filter for Male Miis"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
data-tooltip="Male"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-blue-400! after:border-blue-400! before:border-b-blue-400! ${
gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
@ -290,12 +291,25 @@ export default function SubmitForm() {
type="button"
onClick={() => setGender("FEMALE")}
aria-label="Filter for Female Miis"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
data-tooltip="Female"
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! ${
gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
<Icon icon="foundation:female" className="text-pink-400" />
</button>
<button
type="button"
onClick={() => setGender("NONBINARY")}
aria-label="Filter for Nonbinary Miis"
data-tooltip="Nonbinary"
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! ${
gender === "NONBINARY" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
}`}
>
<Icon icon="mdi:gender-non-binary" className="text-purple-400" />
</button>
</div>
</div>
)}

View file

@ -59,7 +59,7 @@ export const searchSchema = z.object({
.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(),
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', 'FEMALE', or 'NONBINARY' if on Switch platform" }).optional(),
allowCopying: z.coerce.boolean({ error: "Allow Copying must be either true or false" }).optional(),
// todo: incorporate tagsSchema
// Pages