"use client"; import { redirect } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { FileWithPath } from "react-dropzone"; import { Mii, MiiGender, MiiMakeup } from "@prisma/client"; import { useSession } from "next-auth/react"; import { nameSchema, tagsSchema } from "@/lib/schemas"; import { defaultInstructions, minifyInstructions } from "@/lib/switch"; import { SwitchMiiInstructions } from "@/types"; import TagSelector from "../tag-selector"; import ImageList from "./image-list"; import LikeButton from "../like-button"; import Carousel from "../carousel"; import SubmitButton from "../submit-button"; import Dropzone from "../dropzone"; import MiiEditor from "./mii-editor"; import SwitchSubmitTutorialButton from "../tutorial/switch-submit"; import { Icon } from "@iconify/react"; import SwitchFileUpload from "./switch-file-upload"; interface Props { mii: Mii; likes: number; } function deepMerge(target: T, source: Partial): T { const output = structuredClone(target); if (typeof source !== "object" || source === null) return output; for (const key in source) { const sourceValue = source[key]; const targetValue = (output as any)[key]; if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) { (output as any)[key] = deepMerge(targetValue, sourceValue); } else { (output as any)[key] = sourceValue; } } return output; } export default function EditForm({ mii, likes }: Props) { const session = useSession(); const [files, setFiles] = useState([]); const handleFilesChange: React.Dispatch> = (updater) => { hasCustomImagesChanged.current = true; setFiles(updater); }; const handleDrop = useCallback( (acceptedFiles: FileWithPath[]) => { if (files.length >= 3) return; hasCustomImagesChanged.current = true; setFiles((prev) => [...prev, ...acceptedFiles]); }, [files.length], ); const [error, setError] = useState(undefined); const [name, setName] = useState(mii.name); const [tags, setTags] = useState(mii.tags); const [description, setDescription] = useState(mii.description); const [gender, setGender] = useState(mii.gender ?? "MALE"); const [makeup, setMakeup] = useState(mii.makeup ?? "PARTIAL"); const [miiPortraitUri, setMiiPortraitUri] = useState(`/mii/${mii.id}/image?type=mii`); const [miiFeaturesUri, setMiiFeaturesUri] = useState(`/mii/${mii.id}/image?type=features`); const [youtubeId, setYouTubeId] = useState(mii.youtubeId ?? ""); const instructions = useRef(deepMerge(defaultInstructions, (mii.instructions as object) ?? {})); const [quarantined, setQuarantined] = useState(mii.quarantined); const hasCustomImagesChanged = useRef(false); const hasMiiPortraitChanged = useRef(false); const hasMiiFeaturesChanged = useRef(false); const handleSubmit = async () => { // Validate before sending request const nameValidation = nameSchema.safeParse(name); if (!nameValidation.success) { setError(nameValidation.error.issues[0].message); return; } const tagsValidation = tagsSchema.safeParse(tags); if (!tagsValidation.success) { setError(tagsValidation.error.issues[0].message); return; } // Send request to server const formData = new FormData(); if (name != mii.name) formData.append("name", name); if (tags != mii.tags) formData.append("tags", JSON.stringify(tags)); 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 (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri); if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined)); if (youtubeId != mii.youtubeId) formData.append("youtubeId", youtubeId); if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object)) formData.append("instructions", JSON.stringify(instructions.current)); if (hasCustomImagesChanged.current) { files.forEach((file, index) => { // image1, image2, etc. formData.append(`image${index + 1}`, file); }); } // Switch pictures async function getBlob(uri: string): Promise { const response = await fetch(uri); if (!response.ok) { setError("Failed to get Mii portrait/features screenshot. Did you upload one?"); return null; } const blob = await response.blob(); if (!blob.type.startsWith("image/")) { setError("Invalid image file found"); return null; } return blob; } if (miiPortraitUri && hasMiiPortraitChanged.current) { const blob = await getBlob(miiPortraitUri); if (blob) formData.append("miiPortraitImage", blob); } if (miiFeaturesUri && hasMiiFeaturesChanged.current) { const blob = await getBlob(miiFeaturesUri); if (blob) formData.append("miiFeaturesImage", blob); } const response = await fetch(`/api/mii/${mii.id}/edit`, { method: "PATCH", body: formData, }); const { error } = await response.json(); if (!response.ok) { setError(error); return; } redirect(`/mii/${mii.id}`); }; const handleMiiPortraitChange = (uri: string | undefined) => { hasMiiPortraitChanged.current = true; setMiiPortraitUri(uri); }; const handleMiiFeaturesChange = (uri: string | undefined) => { hasMiiFeaturesChanged.current = true; setMiiFeaturesUri(uri); }; // Load existing images - converts image URLs to File objects useEffect(() => { const loadExistingImages = async () => { try { const existing = await Promise.all( Array.from({ length: mii.imageCount }, async (_, index) => { const path = `/mii/${mii.id}/image?type=image${index}`; const response = await fetch(path); const blob = await response.blob(); return Object.assign(new File([blob], `image${index}.png`, { type: "image/png" }), { path }); }), ); setFiles(existing); } catch (error) { console.error("Error loading existing images:", error); } }; loadExistingImages(); }, [mii.id, mii.imageCount]); return (
URL.createObjectURL(file)), ]} />

{name || "Mii name"}

{tags.length == 0 && tag} {tags.map((tag) => ( {tag} ))}

Edit your Mii

Make changes to your existing Mii.

{/* Separator */}

Info
setName(e.target.value)} />