mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
feat: edit gender for switch miis
This commit is contained in:
parent
64efd8e7e6
commit
b8a4808595
3 changed files with 53 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Mii, MiiMakeup, Prisma } from "@prisma/client";
|
import { Mii, MiiGender, MiiMakeup, Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
@ -27,6 +27,7 @@ const editSchema = z.object({
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
.optional(),
|
.optional(),
|
||||||
|
gender: z.enum(MiiGender).optional(),
|
||||||
makeup: z.enum(MiiMakeup).optional(),
|
makeup: z.enum(MiiMakeup).optional(),
|
||||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||||
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||||
|
|
@ -41,7 +42,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
|
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
|
||||||
|
|
||||||
const rateLimit = new RateLimit(request, 3); // no grouped pathname; edit each mii 1 time a minute
|
const rateLimit = new RateLimit(request, 2); // no grouped pathname; edit each mii 2 times a minute
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
@ -82,6 +83,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
tags: rawTags,
|
tags: rawTags,
|
||||||
description: formData.get("description") ?? undefined,
|
description: formData.get("description") ?? undefined,
|
||||||
quarantined: formData.get("quarantined") ?? undefined,
|
quarantined: formData.get("quarantined") ?? undefined,
|
||||||
|
gender: formData.get("gender") ?? undefined,
|
||||||
makeup: formData.get("makeup") ?? undefined,
|
makeup: formData.get("makeup") ?? undefined,
|
||||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||||
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
||||||
|
|
@ -92,7 +94,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
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 { name, tags, description, quarantined, makeup, miiPortraitImage, miiFeaturesImage, instructions, image1, image2, image3 } = parsed.data;
|
const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, instructions, image1, image2, image3 } = parsed.data;
|
||||||
|
|
||||||
// Validate image files
|
// Validate image files
|
||||||
const images: File[] = [];
|
const images: File[] = [];
|
||||||
|
|
@ -129,6 +131,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t));
|
if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t));
|
||||||
if (description !== undefined) updateData.description = profanity.censor(description);
|
if (description !== undefined) updateData.description = profanity.censor(description);
|
||||||
if (quarantined !== undefined) updateData.quarantined = quarantined;
|
if (quarantined !== undefined) updateData.quarantined = quarantined;
|
||||||
|
if (mii.platform === "SWITCH" && gender !== undefined) updateData.gender = gender;
|
||||||
if (makeup !== undefined) updateData.makeup = makeup;
|
if (makeup !== undefined) updateData.makeup = makeup;
|
||||||
if (instructions !== undefined) updateData.instructions = instructions;
|
if (instructions !== undefined) updateData.instructions = instructions;
|
||||||
if (images.length > 0) updateData.imageCount = images.length;
|
if (images.length > 0) updateData.imageCount = images.length;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default function Countdown() {
|
||||||
const [minutes, setMinutes] = useState(59);
|
const [minutes, setMinutes] = useState(59);
|
||||||
const [seconds, setSeconds] = useState(59);
|
const [seconds, setSeconds] = useState(59);
|
||||||
|
|
||||||
const targetDate = new Date("2026-04-16T00:00:00Z").getTime();
|
const targetDate = new Date("2026-04-16T12:00:00Z").getTime();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FileWithPath } from "react-dropzone";
|
import { FileWithPath } from "react-dropzone";
|
||||||
import { Mii, MiiMakeup } from "@prisma/client";
|
import { Mii, MiiGender, MiiMakeup } from "@prisma/client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
import { nameSchema, tagsSchema } from "@/lib/schemas";
|
import { nameSchema, tagsSchema } from "@/lib/schemas";
|
||||||
|
|
@ -65,6 +65,7 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
const [name, setName] = useState(mii.name);
|
const [name, setName] = useState(mii.name);
|
||||||
const [tags, setTags] = useState(mii.tags);
|
const [tags, setTags] = useState(mii.tags);
|
||||||
const [description, setDescription] = useState(mii.description);
|
const [description, setDescription] = useState(mii.description);
|
||||||
|
const [gender, setGender] = useState<MiiGender>(mii.gender ?? "MALE");
|
||||||
const [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "PARTIAL");
|
const [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "PARTIAL");
|
||||||
const [miiPortraitUri, setMiiPortraitUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=mii`);
|
const [miiPortraitUri, setMiiPortraitUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=mii`);
|
||||||
const [miiFeaturesUri, setMiiFeaturesUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=features`);
|
const [miiFeaturesUri, setMiiFeaturesUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=features`);
|
||||||
|
|
@ -91,6 +92,7 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
if (name != mii.name) formData.append("name", name);
|
if (name != mii.name) formData.append("name", name);
|
||||||
if (tags != mii.tags) formData.append("tags", JSON.stringify(tags));
|
if (tags != mii.tags) formData.append("tags", JSON.stringify(tags));
|
||||||
if (description && description != mii.description) formData.append("description", description);
|
if (description && description != mii.description) formData.append("description", description);
|
||||||
|
if (gender != mii.gender) formData.append("gender", gender);
|
||||||
if (makeup != mii.makeup) formData.append("makeup", makeup);
|
if (makeup != mii.makeup) formData.append("makeup", makeup);
|
||||||
if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
||||||
if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined));
|
if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined));
|
||||||
|
|
@ -266,6 +268,49 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
{/* Makeup/Images/Instructions (Switch only) */}
|
{/* Makeup/Images/Instructions (Switch only) */}
|
||||||
{mii.platform === "SWITCH" && (
|
{mii.platform === "SWITCH" && (
|
||||||
<>
|
<>
|
||||||
|
<div className="w-full grid grid-cols-3 items-start z-20">
|
||||||
|
<label htmlFor="gender" className="font-semibold py-2">
|
||||||
|
Gender
|
||||||
|
</label>
|
||||||
|
<div className="col-span-2 flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGender("MALE")}
|
||||||
|
aria-label="Filter for Male Miis"
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="foundation:male" className="text-blue-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGender("FEMALE")}
|
||||||
|
aria-label="Filter for Female Miis"
|
||||||
|
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>
|
||||||
|
|
||||||
<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">
|
||||||
Face Paint
|
Face Paint
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue