feat: edit makeup filter

This commit is contained in:
trafficlunar 2026-03-28 12:01:51 +00:00
parent fd11f996df
commit 0396ad5b0d
2 changed files with 56 additions and 3 deletions

View file

@ -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, Prisma } from "@prisma/client"; import { Mii, MiiMakeup, Prisma } from "@prisma/client";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
@ -23,6 +23,7 @@ const editSchema = z.object({
name: nameSchema.optional(), name: nameSchema.optional(),
tags: tagsSchema.optional(), tags: tagsSchema.optional(),
description: z.string().trim().max(256).optional(), description: z.string().trim().max(256).optional(),
makeup: z.enum(MiiMakeup).optional(),
instructions: switchMiiInstructionsSchema, instructions: switchMiiInstructionsSchema,
image1: z.union([z.instanceof(File), z.any()]).optional(), image1: z.union([z.instanceof(File), z.any()]).optional(),
image2: z.union([z.instanceof(File), z.any()]).optional(), image2: z.union([z.instanceof(File), z.any()]).optional(),
@ -74,6 +75,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
name: formData.get("name") ?? undefined, name: formData.get("name") ?? undefined,
tags: rawTags, tags: rawTags,
description: formData.get("description") ?? undefined, description: formData.get("description") ?? undefined,
makeup: formData.get("makeup") ?? undefined,
instructions: minifiedInstructions, instructions: minifiedInstructions,
image1: formData.get("image1"), image1: formData.get("image1"),
image2: formData.get("image2"), image2: formData.get("image2"),
@ -81,7 +83,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, instructions, image1, image2, image3 } = parsed.data; const { name, tags, description, makeup, instructions, image1, image2, image3 } = parsed.data;
// Validate image files // Validate image files
const images: File[] = []; const images: File[] = [];
@ -102,6 +104,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
if (name !== undefined) updateData.name = profanity.censor(name); // Censor potentially inappropriate words if (name !== undefined) updateData.name = profanity.censor(name); // Censor potentially inappropriate words
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 (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;

View file

@ -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 } from "@prisma/client"; import { Mii, MiiMakeup } from "@prisma/client";
import { nameSchema, tagsSchema } from "@/lib/schemas"; import { nameSchema, tagsSchema } from "@/lib/schemas";
import { defaultInstructions, minifyInstructions } from "@/lib/switch"; import { defaultInstructions, minifyInstructions } from "@/lib/switch";
@ -18,6 +18,7 @@ import SubmitButton from "../submit-button";
import Dropzone from "../dropzone"; import Dropzone from "../dropzone";
import MiiEditor from "./mii-editor"; import MiiEditor from "./mii-editor";
import SwitchSubmitTutorialButton from "../tutorial/switch-submit"; import SwitchSubmitTutorialButton from "../tutorial/switch-submit";
import { Icon } from "@iconify/react";
interface Props { interface Props {
mii: Mii; mii: Mii;
@ -42,6 +43,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 [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "NONE");
const hasFilesChanged = useRef(false); const hasFilesChanged = useRef(false);
const instructions = useRef<SwitchMiiInstructions>({ ...defaultInstructions, ...(mii.instructions as object as Partial<SwitchMiiInstructions>) }); const instructions = useRef<SwitchMiiInstructions>({ ...defaultInstructions, ...(mii.instructions as object as Partial<SwitchMiiInstructions>) });
@ -64,6 +66,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 (makeup != mii.makeup) formData.append("makeup", makeup);
if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object)) if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object))
formData.append("instructions", JSON.stringify(instructions.current)); formData.append("instructions", JSON.stringify(instructions.current));
@ -190,6 +193,53 @@ export default function EditForm({ mii, likes }: Props) {
{/* Instructions (Switch only) */} {/* Instructions (Switch only) */}
{mii.platform === "SWITCH" && ( {mii.platform === "SWITCH" && (
<> <>
<div className="w-full grid grid-cols-3 items-start">
<label htmlFor="makeup" className="font-semibold py-2">
Makeup
</label>
<div className="col-span-2 flex gap-1">
{/* Full Makeup */}
<button
type="button"
onClick={() => setMakeup("FULL")}
aria-label="Full makeup"
data-tooltip="Full Makeup"
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"
}`}
>
<Icon icon="mdi:palette" className="text-pink-400" />
</button>
{/* Partial Makeup */}
<button
type="button"
onClick={() => setMakeup("PARTIAL")}
aria-label="Partial makeup"
data-tooltip="Partial Makeup"
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"
}`}
>
<Icon icon="mdi:lipstick" className="text-purple-400" />
</button>
{/* No Makeup */}
<button
type="button"
onClick={() => setMakeup("NONE")}
aria-label="No makeup"
data-tooltip="No Makeup"
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"
}`}
>
<Icon icon="codex:cross" className="text-gray-400" />
</button>
</div>
</div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Instructions</span> <span>Instructions</span>