"use client"; import { redirect } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { FileWithPath } from "react-dropzone"; import { Icon } from "@iconify/react"; import qrcode from "qrcode-generator"; import { MiiGender, MiiPlatform } from "@prisma/client"; import { nameSchema, tagsSchema } from "@/lib/schemas"; import { convertQrCode } from "@/lib/qr-codes"; import Mii from "@/lib/mii.js/mii"; import { ThreeDsTomodachiLifeMii } from "@/lib/three-ds-tomodachi-life-mii"; import { SwitchMiiInstructions } from "@/types"; import TagSelector from "../tag-selector"; import ImageList from "./image-list"; import PortraitUpload from "./portrait-upload"; import QrUpload from "./qr-upload"; import QrScanner from "./qr-scanner"; import ThreeDsSubmitTutorialButton from "../tutorial/3ds-submit"; import MiiEditor from "./mii-editor"; import SwitchSubmitTutorialButton from "../tutorial/switch-submit"; import LikeButton from "../like-button"; import Carousel from "../carousel"; import SubmitButton from "../submit-button"; import Dropzone from "../dropzone"; export default function SubmitForm() { 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 [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 instructions = useRef({ head: { skinColor: null }, hair: { color: null, subColor: null, subColor2: null, style: null, isFlipped: false, }, eyebrows: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, eyes: { main: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, eyelashesTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, eyelashesBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, eyelidTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, eyelidBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, eyeliner: { color: null }, pupil: { height: null, distance: null, rotation: null, size: null, stretch: null }, }, nose: { height: null, size: null }, lips: { color: null, height: null, rotation: null, size: null, stretch: null, hasLipstick: false }, ears: { height: null, size: null }, glasses: { ringColor: null, shadesColor: null, height: null, size: null, stretch: null }, other: { wrinkles1: { height: null, distance: null, size: null, stretch: null }, wrinkles2: { height: null, distance: null, size: null, stretch: null }, beard: { color: null }, moustache: { color: null, height: null, isFlipped: false, size: null, stretch: null }, goatee: { color: null }, mole: { color: null, height: null, distance: null, size: null }, eyeShadow: { color: null, height: null, distance: null, size: null, stretch: null }, blush: { color: null, height: null, distance: null, size: null, stretch: null }, }, height: null, weight: null, datingPreferences: [], voice: { speed: null, pitch: null, depth: null, delivery: null, tone: null }, personality: { movement: null, speech: null, energy: null, thinking: null, overall: null }, }); const [error, setError] = useState(undefined); 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(); formData.append("platform", platform); formData.append("name", name); formData.append("tags", JSON.stringify(tags)); formData.append("description", description); files.forEach((file, index) => { // image1, image2, etc. formData.append(`image${index + 1}`, file); }); if (platform === "THREE_DS") { formData.append("qrBytesRaw", JSON.stringify(qrBytesRaw)); } else if (platform === "SWITCH") { const response = await fetch(miiPortraitUri!); if (!response.ok) { setError("Failed to check Mii portrait. Did you upload one?"); return; } const blob = await response.blob(); if (!blob.type.startsWith("image/")) { setError("Invalid image file returned"); return; } formData.append("gender", gender); formData.append("miiPortraitImage", blob); formData.append("instructions", JSON.stringify(instructions.current)); } const response = await fetch("/api/submit", { method: "POST", body: formData, }); const { id, error } = await response.json(); if (!response.ok) { setError(String(error)); // app can crash if error message is not a string return; } redirect(`/mii/${id}`); }; useEffect(() => { if (platform === "SWITCH" || qrBytesRaw.length == 0) return; const qrBytes = new Uint8Array(qrBytesRaw); const preview = async () => { setError(""); // Validate QR code size if (qrBytesRaw.length !== 372) { setError("QR code size is not a valid Tomodachi Life QR code"); return; } // Convert QR code to JS (3DS) 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; } // Generate a new QR code for aesthetic reasons 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]); 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.

{/* Separator */}

Info
{/* Platform select */}
{/* Animated indicator */} {/* TODO: maybe change width as part of animation? */}
{/* Switch button */} {/* 3DS button */}
{/* Name */}
setName(e.target.value)} />
{/* Description */}