import { useStore } from "@nanostores/react"; import { Navigate, useNavigate } from "react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import type { FileWithPath } from "react-dropzone"; import { Icon } from "@iconify/react"; import { convertQrCode, defaultInstructions, ThreeDsTomodachiLifeMii, type MiiGender, type MiiMakeup, type MiiPlatform, type SwitchMiiInstructions, } from "@tomodachi-share/shared"; import { nameSchema, tagsSchema } from "@tomodachi-share/shared/schemas"; import type { Mii } from "@tomodachi-share/shared/miijs"; import Carousel from "../components/carousel"; import LikeButton from "../components/like-button"; import TagSelector from "../components/tag-selector"; import SwitchFileUpload from "../components/submit-form/switch-file-upload"; import SwitchSubmitTutorialButton from "../components/tutorial/switch-submit"; import QrUpload from "../components/submit-form/qr-upload"; import Camera from "../components/submit-form/camera"; import ThreeDsSubmitTutorialButton from "../components/tutorial/3ds-submit"; import Dropzone from "../components/dropzone"; import ImageList from "../components/submit-form/image-list"; import SubmitButton from "../components/submit-button"; import MiiEditor from "../components/submit-form/mii-editor"; import { session } from "../session"; import qrcode from "qrcode-generator"; export default function SubmitPage() { const navigate = useNavigate(); const $session = useStore(session); const [files, setFiles] = useState([]); const handleDrop = useCallback( (acceptedFiles: FileWithPath[]) => { if (files.length >= 3) return; setFiles((prev) => [...prev, ...acceptedFiles]); }, [files.length], ); const [isQrScannerOpen, setIsQrScannerOpen] = useState(false); const [miiPortraitUri, setMiiPortraitUri] = useState(); const [miiFeaturesUri, setMiiFeaturesUri] = useState(); const [generatedQrCodeUri, setGeneratedQrCodeUri] = useState(); const [name, setName] = useState(""); const [tags, setTags] = useState([]); const [description, setDescription] = useState(""); const [qrBytesRaw, setQrBytesRaw] = useState([]); const [platform, setPlatform] = useState("SWITCH"); const [gender, setGender] = useState("MALE"); const [makeup, setMakeup] = useState("PARTIAL"); const [way, setWay] = useState<"savedata" | "manual" | null>(null); const [miiDataFile, setMiiDataFile] = useState(); const [youtubeId, setYouTubeId] = useState(""); const instructions = useRef(defaultInstructions); const [error, setError] = useState(undefined); const handleSubmit = async () => { 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; } const formData = new FormData(); formData.append("platform", platform); formData.append("name", name); formData.append("tags", JSON.stringify(tags)); formData.append("description", description); files.forEach((file, index) => { formData.append(`image${index + 1}`, file); }); if (platform === "THREE_DS") { formData.append("qrBytesRaw", JSON.stringify(qrBytesRaw)); } else if (platform === "SWITCH" && way) { const portraitResponse = await fetch(miiPortraitUri!); if (!portraitResponse.ok) { setError("Failed to get Mii portrait screenshot. Did you upload one?"); return; } const portraitBlob = await portraitResponse.blob(); if (!portraitBlob.type.startsWith("image/")) { setError("Invalid image file found"); return; } formData.append("gender", gender); formData.append("makeup", makeup); formData.append("miiPortraitImage", portraitBlob); formData.append("way", way); const featuresResponse = await fetch(miiFeaturesUri!); if (!featuresResponse.ok) { setError("Failed to get Mii features screenshot. Did you upload one?"); return; } const featuresBlob = await featuresResponse.blob(); if (!featuresBlob.type.startsWith("image/")) { setError("Invalid image file found"); return; } if (way === "savedata") { if (!miiDataFile) { setError("Failed to find Mii data file, did you upload one?"); return; } formData.append("miiDataFile", miiDataFile); } formData.append("miiFeaturesImage", featuresBlob); formData.append("instructions", JSON.stringify(instructions.current)); formData.append("youtubeId", youtubeId); } const response = await fetch(`${import.meta.env.VITE_API_URL}/api/submit`, { method: "POST", body: formData, credentials: "include", }); const { id, error } = await response.json(); if (!response.ok) { setError(String(error)); return; } navigate(`/mii/${id}`); }; useEffect(() => { if (platform !== "THREE_DS" || qrBytesRaw.length === 0) return; const qrBytes = new Uint8Array(qrBytesRaw); const preview = async () => { setError(""); if (qrBytesRaw.length !== 372) { setError("QR code size is not a valid Tomodachi Life QR code"); return; } let conversion: { mii: Mii; tomodachiLifeMii: ThreeDsTomodachiLifeMii }; try { conversion = convertQrCode(qrBytes); setMiiPortraitUri(conversion.mii.studioUrl({ width: 512 })); } catch (error) { setError(error instanceof Error ? error.message : String(error)); return; } try { const byteString = String.fromCharCode(...qrBytes); const generatedCode = qrcode(0, "L"); generatedCode.addData(byteString, "Byte"); generatedCode.make(); setGeneratedQrCodeUri(generatedCode.createDataURL()); } catch { setError("Failed to regenerate QR code"); } }; preview(); }, [qrBytesRaw, platform]); if ($session === undefined) return
Loading...
; if ($session === null) return ; return (
URL.createObjectURL(file)), ]} />

{name || "Mii name"}

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

Submit your Mii

Share your creation for others to see.


Info
{/* Platform select */}
{/* Name */}
setName(e.target.value)} />
{/* Description */}