import { useStore } from "@nanostores/react"; import { Navigate, useNavigate, useParams } from "react-router"; import { session } from "../session"; import { useCallback, useEffect, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; import { nameSchema, tagsSchema } from "@tomodachi-share/shared/schemas"; import { type MiiGender, type MiiMakeup, type SwitchMiiInstructions, deepMerge, defaultInstructions, minifyInstructions } from "@tomodachi-share/shared"; import Carousel from "../components/carousel"; import LikeButton from "../components/like-button"; import TagSelector from "../components/tag-selector"; import { Icon } from "@iconify/react"; import SwitchFileUpload from "../components/submit-form/switch-file-upload"; import SwitchSubmitTutorialButton from "../components/tutorial/switch-submit"; import MiiEditor from "../components/submit-form/mii-editor"; import ImageList from "../components/submit-form/image-list"; import Dropzone from "../components/dropzone"; import SubmitButton from "../components/submit-button"; export default function EditMiiPage() { const { id } = useParams(); const navigate = useNavigate(); const $session = useStore(session); const [mii, setMii] = useState(null); const [loading, setLoading] = useState(true); 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(""); const [tags, setTags] = useState([]); const [description, setDescription] = useState(""); const [gender, setGender] = useState("MALE"); const [makeup, setMakeup] = useState("PARTIAL"); const [miiPortraitUri, setMiiPortraitUri] = useState(undefined); const [miiFeaturesUri, setMiiFeaturesUri] = useState(undefined); const [youtubeId, setYouTubeId] = useState(""); const instructions = useRef(defaultInstructions); const [quarantined, setQuarantined] = useState(false); const [needsFixingReason, setNeedsFixingReason] = useState(""); 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 (needsFixingReason !== mii.needsFixing) formData.append("needsFixingReason", needsFixingReason); 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) { // image1, image2, etc. files.forEach((file, index) => formData.append(`image${index + 1}`, file)); if (files.length === 0) formData.append("clearImages", "true"); } // 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(`${import.meta.env.VITE_API_URL}/api/mii/${mii.id}/edit`, { method: "POST", body: formData, credentials: "include", }); const { error } = await response.json(); if (!response.ok) { setError(error); return; } navigate(`/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(() => { if (!mii) return; const loadExistingImages = async () => { try { const existing = await Promise.all( Array.from({ length: mii.imageCount }, async (_, index) => { const path = `${API_URL}/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, mii?.id, mii?.imageCount]); const API_URL = import.meta.env.VITE_API_URL; useEffect(() => { fetch(`${API_URL}/api/mii/${id}/info`) .then((res) => { if (!res.ok) throw new Error("Failed to fetch Miis"); return res.json(); }) .then((data) => { setMii(data); setName(data.name); setTags(data.tags); setDescription(data.description); setGender(data.gender ?? "MALE"); setMakeup(data.makeup ?? "PARTIAL"); setMiiPortraitUri(`${API_URL}/mii/${data.id}/image?type=mii`); setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`); setYouTubeId(data.youtubeId ?? ""); setQuarantined(data.quarantined); setNeedsFixingReason(data.needsFixing); instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {}); setLoading(false); }) .catch((err) => { console.error(err); setLoading(false); navigate("/404"); }); }, [id]); if ($session === undefined) return
Loading...
; if ($session === null) return ; if (loading || !mii) return
Loading...
; if (Number($session?.user?.id) !== mii.userId && Number($session?.user?.id) !== Number(import.meta.env.VITE_ADMIN_USER_ID)) // Check ownership return ; return (
URL.createObjectURL(file)), ]} />

{name || "Mii name"}

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

Edit your Mii

Make changes to your existing Mii.

{/* Separator */}

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