feat: use data from demo

This commit is contained in:
trafficlunar 2026-03-25 19:57:58 +00:00
parent 22911804c0
commit 74139dd54e
28 changed files with 364 additions and 377 deletions

BIN
public/tutorial/switch/step1.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
public/tutorial/switch/step2.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
public/tutorial/switch/step3.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
public/tutorial/switch/step4.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
public/tutorial/switch/step5.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

View file

@ -94,17 +94,23 @@ export async function POST(request: NextRequest) {
// Minify instructions to save space and improve user experience
let minifiedInstructions: Partial<SwitchMiiInstructions> | undefined;
if (formData.get("platform") === "SWITCH") {
const DEFAULT_ZERO_FIELDS = new Set(["height", "distance", "rotation", "size", "stretch"]);
function minify(object: Partial<SwitchMiiInstructions>): Partial<SwitchMiiInstructions> {
for (const key in object) {
const value = object[key as keyof SwitchMiiInstructions];
if (!value) {
if (value === null || value === undefined) {
delete object[key as keyof SwitchMiiInstructions];
continue;
}
// Recurse into nested objects
if (typeof value === "object") {
if (DEFAULT_ZERO_FIELDS.has(key) && value === 0) {
delete object[key as keyof SwitchMiiInstructions];
continue;
}
if (typeof value === "object" && !Array.isArray(value)) {
minify(value as Partial<SwitchMiiInstructions>);
if (Object.keys(value).length === 0) {

View file

@ -14,7 +14,7 @@ import ImageViewer from "@/components/image-viewer";
import DeleteMiiButton from "@/components/mii/delete-mii-button";
import ShareMiiButton from "@/components/mii/share-mii-button";
import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan";
import SwitchScanTutorialButton from "@/components/tutorial/switch-scan";
import SwitchScanTutorialButton from "@/components/tutorial/switch-add-mii";
import Description from "@/components/description";
import MiiInstructions from "@/components/mii/instructions";

View file

@ -192,7 +192,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
{miis.map((mii) => (
<div
key={mii.id}
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-[0.8rem] transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
className={`flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-[0.8rem] transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600 ${mii.platform === "SWITCH" ? "border-red-300" : "border-blue-200"}`}
>
<Carousel
images={[

View file

@ -50,46 +50,41 @@ export default function SubmitForm() {
const [platform, setPlatform] = useState<MiiPlatform>("SWITCH");
const [gender, setGender] = useState<MiiGender>("MALE");
const instructions = useRef<SwitchMiiInstructions>({
head: { type: 0, skinColor: 0 },
const instructions = useRef<Partial<SwitchMiiInstructions>>({
head: { type: 1, skinColor: 1 },
hair: {
setType: 0,
bangsType: 0,
backType: 0,
setType: 43,
bangsType: null,
backType: null,
color: 0,
subColor: 0,
style: 0,
subColor: null,
subColor2: null,
style: 1,
isFlipped: false,
},
eyebrows: { type: 0, color: 0, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyebrows: { type: 28, color: 0, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyes: {
eyesType: 0,
eyelashesTop: 0,
eyelashesBottom: 0,
eyelidTop: 0,
eyelidBottom: 0,
eyeliner: 0,
pupil: 0,
color: 0,
height: 0,
distance: 0,
rotation: 0,
size: 0,
stretch: 0,
main: { type: 6, color: 0, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyelashesTop: { type: 1, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyelashesBottom: { type: 1, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyelidTop: { type: 1, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyelidBottom: { type: 1, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
eyeliner: { type: 1, color: 0 },
pupil: { type: 1, height: 0, distance: 0, rotation: 0, size: 0, stretch: 0 },
},
nose: { type: 0, height: 0, size: 0 },
lips: { type: 0, color: 0, height: 0, rotation: 0, size: 0, stretch: 0, hasLipstick: false },
ears: { type: 0, height: 0, size: 0 },
glasses: { type: 0, ringColor: 0, shadesColor: 0, height: 0, size: 0, stretch: 0 },
nose: { type: 6, height: 0, size: 0 },
lips: { type: 2, color: 0, height: 0, rotation: 0, size: 0, stretch: 0, hasLipstick: false },
ears: { type: 1, height: 0, size: 0 },
glasses: { type: 1, ringColor: 0, shadesColor: 0, height: 0, size: 0, stretch: 0 },
other: {
wrinkles1: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
wrinkles2: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
beard: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
moustache: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
goatee: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
mole: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
eyeShadow: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
blush: { type: 0, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
wrinkles1: { type: 1, height: 0, distance: 0, size: 0, stretch: 0 },
wrinkles2: { type: 1, height: 0, distance: 0, size: 0, stretch: 0 },
beard: { type: 1, color: 0 },
moustache: { type: 1, color: 0, height: 0, isFlipped: false, size: 0, stretch: 0 },
goatee: { type: 1, color: 0 },
mole: { type: 1, color: 0, height: 0, distance: 0, size: 0 },
eyeShadow: { type: 1, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
blush: { type: 1, color: 0, height: 0, distance: 0, size: 0, stretch: 0 },
},
height: 0,
weight: 0,
@ -378,6 +373,7 @@ export default function SubmitForm() {
<div className="flex flex-col items-center gap-2">
<PortraitUpload setImage={setMiiPortraitUri} />
<SwitchSubmitTutorialButton />
</div>
</div>
@ -416,7 +412,9 @@ export default function SubmitForm() {
<div className="flex flex-col items-center gap-2">
<MiiEditor instructions={instructions} />
<SwitchSubmitTutorialButton />
<span className="text-xs text-zinc-400">Instructions are recommended, but not required.</span>
<span className="text-xs text-zinc-400 text-center px-32">
Mii editor may be inaccurate. Instructions are recommended, but not required - you do not have to add every instruction.
</span>
</div>
</div>

View file

@ -14,7 +14,7 @@ import MiscTab from "./tabs/misc";
import { Icon } from "@iconify/react";
interface Props {
instructions: React.RefObject<SwitchMiiInstructions>;
instructions: React.RefObject<Partial<SwitchMiiInstructions>>;
}
type Tab = "head" | "hair" | "eyebrows" | "eyes" | "nose" | "lips" | "ears" | "glasses" | "other" | "misc";
@ -28,7 +28,7 @@ export const TAB_ICONS: Record<Tab, string> = {
lips: "material-symbols-light:lips",
ears: "ion:ear",
glasses: "solar:glasses-bold",
other: "mingcute:head-ai-fill",
other: "mdi:sparkles",
misc: "material-symbols:settings",
};

View file

@ -1,7 +1,7 @@
import { useState } from "react";
interface Props {
target: { height?: number; distance?: number; rotation?: number; size?: number; stretch?: number };
target: { height?: number; distance?: number; rotation?: number; size?: number; stretch?: number } | any;
}
export default function NumberInputs({ target }: Props) {
@ -11,8 +11,10 @@ export default function NumberInputs({ target }: Props) {
const [size, setSize] = useState(0);
const [stretch, setStretch] = useState(0);
if (!target) return null;
return (
<div className="grid grid-rows-5 h-full">
<div className="grid grid-rows-5 min-h-0">
{target.height != undefined && (
<div className="w-full">
<label htmlFor="height" className="text-xs">

View file

@ -9,7 +9,7 @@ interface Props {
}
export default function EyebrowsTab({ instructions }: Props) {
const [type, setType] = useState(0);
const [type, setType] = useState(27);
const [color, setColor] = useState(0);
return (
@ -23,7 +23,7 @@ export default function EyebrowsTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
hasNoneOption
length={35}
length={43}
type={type}
setType={(i) => {
setType(i);

View file

@ -8,21 +8,21 @@ interface Props {
instructions: React.RefObject<SwitchMiiInstructions>;
}
const TABS: { name: keyof SwitchMiiInstructions["eyes"]; length: number; colorsDisabled?: number[] }[] = [
{ name: "eyesType", length: 50 },
{ name: "eyelashesTop", length: 40 },
{ name: "eyelashesBottom", length: 20 },
{ name: "eyelidTop", length: 10 },
{ name: "eyelidBottom", length: 5 },
{ name: "eyeliner", length: 15 },
{ name: "pupil", length: 3 },
const TABS: { name: keyof SwitchMiiInstructions["eyes"]; length: number; colorsDisabled?: boolean }[] = [
{ name: "main", length: 121 },
{ name: "eyelashesTop", length: 6, colorsDisabled: true },
{ name: "eyelashesBottom", length: 2, colorsDisabled: true },
{ name: "eyelidTop", length: 3, colorsDisabled: true },
{ name: "eyelidBottom", length: 3, colorsDisabled: true },
{ name: "eyeliner", length: 2 },
{ name: "pupil", length: 10, colorsDisabled: true },
];
export default function OtherTab({ instructions }: Props) {
const [tab, setTab] = useState(0);
// One type/color state per tab
const [types, setTypes] = useState<number[]>(Array(TABS.length).fill(0));
const [types, setTypes] = useState<number[]>([5, 0, 0, 0, 0, 0, 0]);
const [colors, setColors] = useState<number[]>(Array(TABS.length).fill(0));
const currentTab = TABS[tab];
@ -34,7 +34,7 @@ export default function OtherTab({ instructions }: Props) {
return copy;
});
instructions.current.eyes[currentTab.name] = value;
instructions.current.eyes[currentTab.name].type = value;
};
const setColor = (value: number) => {
@ -44,8 +44,7 @@ export default function OtherTab({ instructions }: Props) {
return copy;
});
// TODO: check in actual game, temp
instructions.current.eyes.color = value;
if (!currentTab.colorsDisabled) (instructions.current.eyes[currentTab.name] as { color: number }).color = value;
};
return (
@ -53,7 +52,7 @@ export default function OtherTab({ instructions }: Props) {
<div className="flex h-full">
<div className="grow flex flex-col">
<div className="flex items-center h-8">
<h1 className="absolute font-bold text-xl">Other</h1>
<h1 className="absolute font-bold text-xl">Eyes</h1>
<div className="flex justify-center grow">
<div className="rounded-2xl bg-orange-200">
@ -72,15 +71,15 @@ export default function OtherTab({ instructions }: Props) {
</div>
<div className="flex justify-center h-74 mt-auto">
<TypeSelector hasNoneOption length={currentTab.length} type={types[tab]} setType={setType} />
<TypeSelector hasNoneOption={tab === 0} length={currentTab.length} type={types[tab]} setType={setType} />
</div>
</div>
<div className="shrink-0 w-21 pb-3 flex flex-col items-center">
<div className={`${tab !== 0 ? "hidden" : "w-full"}`}>
<div className={`${currentTab.colorsDisabled ? "hidden" : "w-full"}`}>
<ColorPicker color={colors[tab]} setColor={setColor} />
</div>
<NumberInputs target={instructions.current.eyes} />
<NumberInputs target={instructions.current.eyes[currentTab.name]} />
</div>
</div>
</div>

View file

@ -24,7 +24,8 @@ export default function GlassesTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
hasNoneOption
length={50}
isGlassesTab
length={58}
type={type}
setType={(i) => {
setType(i);
@ -44,6 +45,7 @@ export default function GlassesTab({ instructions }: Props) {
/>
<ColorPicker
color={shadesColor}
disabled={type < 44}
setColor={(i) => {
setShadesColor(i);
instructions.current.glasses.shadesColor = i;

View file

@ -11,24 +11,41 @@ type Tab = "sets" | "bangs" | "back";
export default function HairTab({ instructions }: Props) {
const [tab, setTab] = useState<Tab>("sets");
const [setsType, setSetsType] = useState(0);
const [bangsType, setBangsType] = useState(0);
const [backType, setBackType] = useState(0);
const [setsType, setSetsType] = useState<number | null>(43);
const [bangsType, setBangsType] = useState<number | null>(null);
const [backType, setBackType] = useState<number | null>(null);
const [color, setColor] = useState(0);
const [subColor, setSubColor] = useState<number | null>(null);
const [subColor2, setSubColor2] = useState<number | null>(null);
const [style, setStyle] = useState<number | null>(null);
const [isFlipped, setIsFlipped] = useState(false);
const type = tab === "sets" ? setsType : tab === "bangs" ? bangsType : backType;
const length = tab === "sets" ? 245 : tab === "bangs" ? 83 : 111;
const setType = (value: number) => {
if (tab === "sets") {
setSetsType(value);
instructions.current.hair.setType = value;
// Clear bangs and back
setBangsType(null);
setBackType(null);
setSubColor2(null);
instructions.current.hair.bangsType = null;
instructions.current.hair.backType = null;
instructions.current.hair.subColor2 = null;
} else if (tab === "bangs") {
setBangsType(value);
instructions.current.hair.bangsType = value;
// Clear set
setSetsType(null);
instructions.current.hair.setType = null;
} else {
setBackType(value);
instructions.current.hair.backType = value;
// Clear set
setSetsType(null);
instructions.current.hair.setType = null;
}
};
@ -68,22 +85,7 @@ export default function HairTab({ instructions }: Props) {
</div>
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
length={50}
type={type}
setType={(i) => {
setType(i);
// Update ref
if (tab === "sets") {
instructions.current.hair.setType = i;
} else if (tab === "bangs") {
instructions.current.hair.bangsType = i;
} else if (tab === "back") {
instructions.current.hair.backType = i;
}
}}
/>
<TypeSelector length={length} type={type} setType={setType} />
</div>
</div>
@ -97,23 +99,68 @@ export default function HairTab({ instructions }: Props) {
/>
<div className="flex gap-1.5 items-center mb-2 w-full">
<input type="checkbox" id="subcolor" className="checkbox" checked={subColor !== null} onChange={(e) => setSubColor(e.target.checked ? 0 : null)} />
<input
type="checkbox"
id="subcolor"
className="checkbox"
checked={tab === "back" ? subColor2 !== null : subColor !== null}
onChange={(e) => {
if (tab === "back") {
setSubColor2(e.target.checked ? 0 : null);
instructions.current.hair.subColor2 = e.target.checked ? 0 : null;
} else {
setSubColor(e.target.checked ? 0 : null);
instructions.current.hair.subColor = e.target.checked ? 0 : null;
}
}}
/>
<label htmlFor="subcolor" className="text-xs">
Sub color
</label>
</div>
<ColorPicker
disabled={subColor === null}
color={subColor ? subColor : 0}
disabled={tab === "back" ? subColor2 === null : subColor === null}
color={tab === "back" ? (subColor2 ?? 0) : (subColor ?? 0)}
setColor={(i) => {
setSubColor(i);
instructions.current.hair.subColor = i;
if (tab === "back") {
setSubColor2(i);
instructions.current.hair.subColor2 = i;
} else {
setSubColor(i);
instructions.current.hair.subColor = i;
}
}}
/>
<p className="text-sm mb-1">Tying style</p>
<div className="grid grid-cols-3 w-full gap-0.5">
{Array.from({ length: 3 }).map((_, i) => (
<button
type="button"
key={i}
onClick={() => {
setStyle(i);
instructions.current.hair.style = i;
}}
className={`size-full aspect-square cursor-pointer hover:bg-orange-300 transition-colors duration-100 rounded-lg ${style === i ? "bg-orange-400!" : ""}`}
>
{i + 1}
</button>
))}
</div>
<div className="flex gap-1.5 items-center w-full mt-auto">
<input type="checkbox" id="subcolor" className="checkbox" checked={isFlipped} onChange={(e) => setIsFlipped(e.target.checked)} />
<input
type="checkbox"
id="subcolor"
className="checkbox"
checked={isFlipped}
onChange={(e) => {
setIsFlipped(e.target.checked);
instructions.current.hair.isFlipped = e.target.checked;
}}
/>
<label htmlFor="subcolor" className="text-xs">
Flip
</label>

View file

@ -10,8 +10,8 @@ interface Props {
const COLORS = ["FFD8BA", "FFD5AC", "FEC1A4", "FEC68F", "FEB089", "FEBA6B", "F39866", "E89854", "E37E3F", "B45627", "914220", "59371F", "662D16", "392D1E"];
export default function HeadTab({ instructions }: Props) {
const [color, setColor] = useState(108);
const [type, setType] = useState(0);
const [color, setColor] = useState(109);
const [type, setType] = useState(1);
return (
<div className="relative grow p-3 pb-0!">

View file

@ -9,8 +9,9 @@ interface Props {
}
export default function LipsTab({ instructions }: Props) {
const [type, setType] = useState(0);
const [type, setType] = useState(1);
const [color, setColor] = useState(0);
const [hasLipstick, setHasLipstick] = useState(false);
return (
<div className="relative grow p-3 pb-0!">
@ -22,7 +23,7 @@ export default function LipsTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
length={35}
length={53}
type={type}
setType={(i) => {
setType(i);
@ -41,6 +42,22 @@ export default function LipsTab({ instructions }: Props) {
}}
/>
<NumberInputs target={instructions.current.lips} />
<div className="flex gap-1.5 items-center w-full mt-auto">
<input
type="checkbox"
id="subcolor"
className="checkbox"
checked={hasLipstick}
onChange={(e) => {
setHasLipstick(e.target.checked);
instructions.current.lips.hasLipstick = e.target.checked;
}}
/>
<label htmlFor="subcolor" className="text-xs">
Lipstick
</label>
</div>
</div>
</div>
</div>

View file

@ -8,7 +8,7 @@ interface Props {
}
export default function NoseTab({ instructions }: Props) {
const [type, setType] = useState(0);
const [type, setType] = useState(5);
return (
<div className="relative grow p-3 pb-0!">
@ -20,7 +20,7 @@ export default function NoseTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
length={35}
length={32}
type={type}
setType={(i) => {
setType(i);

View file

@ -8,15 +8,15 @@ interface Props {
instructions: React.RefObject<SwitchMiiInstructions>;
}
const TABS: { name: keyof SwitchMiiInstructions["other"]; length: number; colorsDisabled?: number[] }[] = [
{ name: "wrinkles1", length: 50 },
{ name: "wrinkles2", length: 40 },
{ name: "beard", length: 20 },
{ name: "moustache", length: 10 },
{ name: "goatee", length: 5 },
{ name: "mole", length: 15 },
{ name: "eyeShadow", length: 3 },
{ name: "blush", length: 8, colorsDisabled: [6] },
const TABS: { name: keyof SwitchMiiInstructions["other"]; length: number }[] = [
{ name: "wrinkles1", length: 9 },
{ name: "wrinkles2", length: 15 },
{ name: "beard", length: 15 },
{ name: "moustache", length: 16 },
{ name: "goatee", length: 14 },
{ name: "mole", length: 2 },
{ name: "eyeShadow", length: 4 },
{ name: "blush", length: 8 },
];
export default function OtherTab({ instructions }: Props) {
@ -73,7 +73,7 @@ export default function OtherTab({ instructions }: Props) {
</div>
<div className="flex justify-center h-74 mt-auto">
<TypeSelector hasNoneOption length={currentTab.length} type={types[tab]} setType={setType} />
<TypeSelector length={currentTab.length} type={types[tab]} setType={setType} />
</div>
</div>

View file

@ -1,22 +1,28 @@
import { Fragment } from "react/jsx-runtime";
interface Props {
hasNoneOption?: boolean;
isGlassesTab?: boolean;
length: number;
type: number;
type: number | null;
setType: (type: number) => void;
}
export default function TypeSelector({ hasNoneOption, length, type, setType }: Props) {
export default function TypeSelector({ hasNoneOption, isGlassesTab, length, type, setType }: Props) {
return (
<div className="grid grid-cols-5 gap-1 w-fit overflow-y-auto h-fit max-h-full">
{Array.from({ length }).map((_, i) => (
<button
type="button"
key={i}
onClick={() => setType(i)}
className={`size-12 cursor-pointer hover:bg-orange-300 transition-colors duration-100 rounded-xl ${type === i ? "bg-orange-400!" : ""} ${hasNoneOption && i === 0 ? "text-md" : "text-2xl"}`}
>
{hasNoneOption ? (i === 0 ? "None" : i) : i + 1}
</button>
<Fragment key={i}>
<button
type="button"
onClick={() => setType(i)}
className={`size-12 cursor-pointer hover:bg-orange-300 transition-colors duration-100 rounded-xl ${type === i ? "bg-orange-400!" : ""} ${hasNoneOption && i === 0 ? "text-md" : "text-2xl"}`}
>
{hasNoneOption ? (i === 0 ? "None" : i + 1) : i + 1}
</button>
{isGlassesTab && i === 43 && <div />}
</Fragment>
))}
</div>
);

View file

@ -143,7 +143,7 @@ export default function Tutorial({ tutorials, isOpen, setIsOpen }: Props) {
alt="tutorial thumbnail"
width={128}
height={128}
className="rounded-lg border-2 border-zinc-300"
className="rounded-lg border-2 border-zinc-300 object-cover"
/>
<p className="mt-2">{tutorial.title}</p>
{/* Set opacity to 0 to keep height the same with other tutorials */}

View file

@ -1,42 +0,0 @@
"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "@iconify/react";
import Tutorial from ".";
export default function ScanTutorialButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button aria-label="Tutorial" type="button" onClick={() => setIsOpen(true)} className="text-3xl cursor-pointer">
<Icon icon="fa:question-circle" />
<span>Tutorial</span>
</button>
{isOpen &&
createPortal(
<Tutorial
tutorials={[
{
title: "Adding Mii",
steps: [
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
{ text: "2. Go into 'QR Code'", imageSrc: "/tutorial/adding-mii/step2.png" },
{ text: "3. Press 'Scan QR Code'", imageSrc: "/tutorial/adding-mii/step3.png" },
{ text: "4. Click on the QR code below the Mii's image", imageSrc: "/tutorial/adding-mii/step4.png" },
{ text: "5. Scan with your 3DS", imageSrc: "/tutorial/adding-mii/step5.png" },
{ type: "finish" },
],
},
]}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>,
document.body,
)}
</>
);
}

View file

@ -1,64 +0,0 @@
"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import Tutorial from ".";
export default function SubmitTutorialButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setIsOpen(true)} className="text-sm text-orange-400 cursor-pointer underline-offset-2 hover:underline">
How to?
</button>
{isOpen &&
createPortal(
<Tutorial
tutorials={[
{
title: "Allow Copying",
thumbnail: "/tutorial/allow-copying/thumbnail.png",
hint: "Suggested!",
steps: [
{ type: "start" },
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
{ text: "2. Go into 'Mii List'", imageSrc: "/tutorial/allow-copying/step2.png" },
{ text: "3. Select and edit the Mii you wish to submit", imageSrc: "/tutorial/allow-copying/step3.png" },
{ text: "4. Click 'Other Settings' in the information screen", imageSrc: "/tutorial/allow-copying/step4.png" },
{ text: "5. Click on 'Don't Allow' under the 'Copying' text", imageSrc: "/tutorial/allow-copying/step5.png" },
{ text: "6. Press 'Allow'", imageSrc: "/tutorial/allow-copying/step6.png" },
{ text: "7. Confirm the edits to the Mii", imageSrc: "/tutorial/allow-copying/step7.png" },
{ type: "finish" },
],
},
{
title: "Create QR Code",
thumbnail: "/tutorial/create-qr-code/thumbnail.png",
steps: [
{ type: "start" },
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
{ text: "2. Go into 'QR Code'", imageSrc: "/tutorial/create-qr-code/step2.png" },
{ text: "3. Press 'Create QR Code'", imageSrc: "/tutorial/create-qr-code/step3.png" },
{ text: "4. Select and press 'OK' on the Mii you wish to submit", imageSrc: "/tutorial/create-qr-code/step4.png" },
{
text: "5. Pick any option; it doesn't matter since the QR code regenerates upon submission.",
imageSrc: "/tutorial/create-qr-code/step5.png",
},
{
text: "6. Exit the tutorial; Upload the QR code (scan with camera or upload file through SD card).",
imageSrc: "/tutorial/create-qr-code/step6.png",
},
{ type: "finish" },
],
},
]}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>,
document.body,
)}
</>
);
}

View file

@ -0,0 +1,52 @@
"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "@iconify/react";
import Tutorial from ".";
export default function SwitchAddMiiTutorialButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button aria-label="Tutorial" type="button" onClick={() => setIsOpen(true)} className="text-3xl cursor-pointer">
<Icon icon="fa:question-circle" />
<span>Tutorial</span>
</button>
{isOpen &&
createPortal(
<Tutorial
tutorials={[
{
title: "Adding Mii",
steps: [
{
text: "1. Press X to open the menu, then select 'Add a Mii'",
imageSrc: "/tutorial/switch/step1.jpg",
},
{
text: "2. Press 'From scratch' and choose the Male template (instructions may be slightly inaccurate if you select Female)",
imageSrc: "/tutorial/switch/step2.jpg",
},
{
text: "3. Follow all instructions (not all instructions will be there, check next slide for more)",
imageSrc: "/tutorial/switch/step3.jpg",
},
{
text: "4. If the instructions have height, distance, etc. the value will be relative to how many times to click the button - positive for up/left, negative for down/right",
imageSrc: "/tutorial/switch/step4.jpg",
},
{ type: "finish" },
],
},
]}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>,
document.body,
)}
</>
);
}

View file

@ -1,61 +0,0 @@
"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "@iconify/react";
import Tutorial from ".";
export default function ThreeDsScanTutorialButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
aria-label="Tutorial"
type="button"
onClick={() => setIsOpen(true)}
className="text-3xl cursor-pointer"
>
<Icon icon="fa:question-circle" />
<span>Tutorial</span>
</button>
{isOpen &&
createPortal(
<Tutorial
tutorials={[
{
title: "Adding Mii",
steps: [
{
text: "1. Enter the town hall",
imageSrc: "/tutorial/switch/step1.png",
},
{
text: "2. Go into 'QR Code'",
imageSrc: "/tutorial/switch/adding-mii/step2.png",
},
{
text: "3. Press 'Scan QR Code'",
imageSrc: "/tutorial/switch/adding-mii/step3.png",
},
{
text: "4. Click on the QR code below the Mii's image",
imageSrc: "/tutorial/switch/adding-mii/step4.png",
},
{
text: "5. Scan with your 3DS",
imageSrc: "/tutorial/switch/adding-mii/step5.png",
},
{ type: "finish" },
],
},
]}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>,
document.body
)}
</>
);
}

View file

@ -18,32 +18,27 @@ export default function SubmitTutorialButton() {
<Tutorial
tutorials={[
{
title: "Create QR Code",
thumbnail: "/tutorial/switch/create-qr-code/thumbnail.png",
title: "Mii Instructions",
steps: [
{
text: "1. Enter the town hall",
imageSrc: "/tutorial/switch/step1.png",
text: "1. Press X to open the menu, then select 'Add a Mii' (or 'Residents' if you're submitting an existing Mii)",
imageSrc: "/tutorial/switch/step1.jpg",
},
{
text: "2. Go into 'QR Code'",
imageSrc: "/tutorial/switch/create-qr-code/step2.png",
text: "2. Press 'From scratch' and choose the Male template (instructions may be slightly inaccurate if you select Female, it's fine if you change all defaults)",
imageSrc: "/tutorial/switch/step2.jpg",
},
{
text: "3. Press 'Create QR Code'",
imageSrc: "/tutorial/switch/create-qr-code/step3.png",
text: "3. Customize your Mii to your liking",
imageSrc: "/tutorial/switch/step3.jpg",
},
{
text: "4. Select and press 'OK' on the Mii you wish to submit",
imageSrc: "/tutorial/switch/create-qr-code/step4.png",
text: "4. All instructions are optional but if you want to add height, distance, etc. the value will be relative to how many times you clicked the button - positive for up/left, negative for down/right",
imageSrc: "/tutorial/switch/step4.jpg",
},
{
text: "5. Pick any option; it doesn't matter since the QR code regenerates upon submission.",
imageSrc: "/tutorial/switch/create-qr-code/step5.png",
},
{
text: "6. Exit the tutorial; Upload the QR code (scan with camera or upload file through SD card).",
imageSrc: "/tutorial/switch/create-qr-code/step6.png",
text: "5. Upload instructions, then screenshot the Mii for the portrait (feel free to crop it)",
imageSrc: "/tutorial/switch/step5.jpg",
},
{ type: "finish" },
],

View file

@ -84,25 +84,25 @@ export const userNameSchema = z
});
const colorSchema = z.number().int().min(0).max(107).optional();
const geometrySchema = z.number().int().min(-5).max(5).optional();
const geometrySchema = z.number().int().min(-10).max(10).optional();
export const switchMiiInstructionsSchema = z
.object({
head: z.object({ type: z.number().int().min(1).max(16).optional(), skinColor: z.number().int().min(0).max(121).optional() }).optional(),
head: z.object({ type: z.number().int().min(0).max(15).optional(), skinColor: z.number().int().min(0).max(121).optional() }).optional(),
hair: z
.object({
setType: z.number().int().min(0).max(25).optional(),
bangsType: z.number().int().min(0).max(25).optional(),
backType: z.number().int().min(0).max(25).optional(),
setType: z.number().int().min(0).max(244).optional(),
bangsType: z.number().int().min(0).max(82).optional(),
backType: z.number().int().min(0).max(110).optional(),
color: colorSchema,
subColor: colorSchema,
style: z.number().int().min(0).max(3).optional(),
style: z.number().int().min(1).max(3).optional(),
isFlipped: z.boolean().optional(),
})
.optional(),
eyebrows: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(42).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -113,12 +113,12 @@ export const switchMiiInstructionsSchema = z
.optional(),
eyes: z
.object({
eyesType: z.number().int().min(0).max(25).optional(),
eyelashesTop: z.number().int().min(0).max(6).optional(),
eyelashesBottom: z.number().int().min(0).max(25).optional(),
eyesType: z.number().int().min(0).max(120).optional(),
eyelashesTop: z.number().int().min(0).max(5).optional(),
eyelashesBottom: z.number().int().min(0).max(1).optional(),
eyelidTop: z.number().int().min(0).max(2).optional(),
eyelidBottom: z.number().int().min(0).max(25).optional(),
eyeliner: z.number().int().min(0).max(25).optional(),
eyelidBottom: z.number().int().min(0).max(2).optional(),
eyeliner: z.number().int().min(0).max(1).optional(),
pupil: z.number().int().min(0).max(9).optional(),
color: colorSchema,
height: geometrySchema,
@ -130,14 +130,14 @@ export const switchMiiInstructionsSchema = z
.optional(),
nose: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(31).optional(),
height: geometrySchema,
size: geometrySchema,
})
.optional(),
lips: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(52).optional(),
color: colorSchema,
height: geometrySchema,
rotation: geometrySchema,
@ -155,7 +155,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
glasses: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(57).optional(),
ringColor: colorSchema,
shadesColor: colorSchema,
height: geometrySchema,
@ -167,7 +167,7 @@ export const switchMiiInstructionsSchema = z
.object({
wrinkles1: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(8).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -177,7 +177,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
wrinkles2: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(14).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -187,7 +187,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
beard: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(14).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -197,7 +197,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
moustache: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(15).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -207,7 +207,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
goatee: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(13).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -217,7 +217,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
mole: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(1).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -227,7 +227,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
eyeShadow: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(3).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,
@ -237,7 +237,7 @@ export const switchMiiInstructionsSchema = z
.optional(),
blush: z
.object({
type: z.number().int().min(0).max(25).optional(),
type: z.number().int().min(0).max(7).optional(),
color: colorSchema,
height: geometrySchema,
distance: geometrySchema,

144
src/types.d.ts vendored
View file

@ -1,23 +1,24 @@
import { MiiGender, Prisma } from "@prisma/client";
import { DefaultSession } from "next-auth";
// All color properties are assumed to be the same 108 colors
// Some types have different options disabled, we're ignoring them for now
interface SwitchMiiInstructions {
head: {
type: number; // 16 types
skinColor: number; // additional 14 are not in color menu
type: number; // 16 types, default is 2
skinColor: number; // Additional 14 are not in color menu, default is 2
};
hair: {
setType: number; // at least 25
bangsType: number; // at least 25
backType: number; // at least 25
setType: number | null; // 245 types, default is 43
bangsType: number | null; // 83 types, default is none, if a set is selected, set bangs and back to none and vice-versa
backType: number | null; // 111 types, default is none, same here (set related)
color: number;
subColor: number;
style: number; // is this different for each hair?
isFlipped: boolean; // is this different for bangs/back?
subColor: number | null; // Default is none
subColor2: number | null; // Only used when bangs/back is selected
style: number | null; // is this different for each hair?
isFlipped: boolean; // Only for sets and fringe
};
eyebrows: {
type: number; // 0 is None, at least 25 (including None)
type: number; // 1 is None, 43 types, default is 28
color: number;
height: number;
distance: number;
@ -26,43 +27,83 @@ interface SwitchMiiInstructions {
stretch: number;
};
eyes: {
eyesType: number; // At least 25
eyelashesTop: number; // 6 types
eyelashesBottom: number; // unknown
eyelidTop: number; // 0 is None, 2 additional types
eyelidBottom: number; // unknown
eyeliner: number; // unknown
pupil: number; // 0 is default, 9 additional types
color: number; // is this same as hair?
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
main: {
type: number; // 1 is None, 121 types default is 6
color: number;
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
eyelashesTop: {
type: number; // 6 types, default is 1
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
eyelashesBottom: {
type: number; // 2 types, default is 1
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
eyelidTop: {
type: number; // 3 types, default is 1
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
eyelidBottom: {
type: number; // 3 types, default is 1
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
eyeliner: {
type: number; // 2 types, default is 1
color: number;
};
pupil: {
type: number; // 10 types, default is 1
height: number;
distance: number;
rotation: number;
size: number;
stretch: number;
};
};
nose: {
type: number; // 0 is None, at least 24 additional
type: number; // 1 is None, 32 types, default is 6
height: number;
size: number;
};
lips: {
type: number; // 0 is None, at least 24 additional
color: number; // is this same as hair?
type: number; // 1 is None, 53 types, default is 2
color: number;
height: number;
rotation: number;
size: number;
stretch: number;
hasLipstick: boolean; // is this what it's called?
hasLipstick: boolean;
};
ears: {
type: number; // 0 is Default, 4 additional
height: number;
size: number;
type: number; // 5 types, default is 1
height: number; // Does not work for default
size: number; // Does not work for default
};
glasses: {
type: number; // NOTE: THERE IS A GAP!!! 0 is None, at least 29 additional
ringColor: number; // i'm assuming based off icon
shadesColor: number; // i'm assuming based off icon
type: number; // NOTE: THERE IS A GAP AT 40!!! 1 is None, 58 types, default is 1
ringColor: number;
shadesColor: number; // Only works after gap
height: number;
size: number;
stretch: number;
@ -70,64 +111,53 @@ interface SwitchMiiInstructions {
other: {
// names were assumed
wrinkles1: {
type: number; // 0 is None, at least BLANK additional
color: number; // is this same as hair?
type: number; // 9 types, default is 1
height: number;
distance: number;
size: number;
stretch: number;
};
wrinkles2: {
type: number; // 0 is None, at least BLANK additional
color: number; // is this same as hair?
type: number; // 15 types, default is 1
height: number;
distance: number;
size: number;
stretch: number;
};
beard: {
type: number; // 0 is None, at least BLANK additional
color: number; // is this same as hair?
height: number;
distance: number;
size: number;
stretch: number;
type: number; // 15 types, default is 1
color: number;
};
moustache: {
type: number; // 0 is None, at least BLANK additional
type: number; // 16 types, default is 1
color: number; // is this same as hair?
height: number;
distance: number;
isFlipped: boolean;
size: number;
stretch: number;
};
goatee: {
type: number; // 0 is None, at least BLANK additional
color: number; // is this same as hair?
height: number;
distance: number;
size: number;
stretch: number;
type: number; // 14 types, default is 1
color: number;
};
mole: {
type: number; // 0 is None, at least BLANK additional
type: number; // 2 types, default is 1
color: number; // is this same as hair?
height: number;
distance: number;
size: number;
stretch: number;
};
eyeShadow: {
type: number; // 0 is None, at least 3 additional
color: number; // is this same as hair?
type: number; // 4 types, default is 1
color: number;
height: number;
distance: number;
size: number;
stretch: number;
};
blush: {
type: number; // 0 is None, at least 7 additional
color: number; // is this same as hair?
type: number; // 8 types, default is 1
color: number;
height: number;
distance: number;
size: number;