feat: view instructions on mii page

This commit is contained in:
trafficlunar 2026-02-28 16:57:43 +00:00
parent 5995afe3db
commit 13941e849c
23 changed files with 625 additions and 311 deletions

View file

@ -121,7 +121,7 @@ body {
/* Range input */ /* Range input */
input[type="range"] { input[type="range"] {
@apply appearance-none bg-transparent cursor-pointer; @apply appearance-none bg-transparent not-disabled:cursor-pointer;
} }
/* Track */ /* Track */
@ -135,8 +135,7 @@ input[type="range"]::-moz-range-track {
/* Thumb */ /* Thumb */
input[type="range"]::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
@apply appearance-none size-4 bg-orange-400 border-2 border-orange-500 rounded-full shadow-md transition; @apply appearance-none size-4 bg-orange-400 border-2 border-orange-500 rounded-full shadow-md transition -mt-1.5;
margin-top: -6px; /* center thumb vertically */
} }
input[type="range"]::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
@ -145,9 +144,9 @@ input[type="range"]::-moz-range-thumb {
/* Hover */ /* Hover */
input[type="range"]:hover::-webkit-slider-thumb { input[type="range"]:hover::-webkit-slider-thumb {
@apply bg-orange-500; @apply not-disabled:bg-orange-500;
} }
input[type="range"]:hover::-moz-range-thumb { input[type="range"]:hover::-moz-range-thumb {
@apply bg-orange-500; @apply not-disabled:bg-orange-500;
} }

View file

@ -7,15 +7,18 @@ import { Icon } from "@iconify/react";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { MiiPlatform } from "@prisma/client";
import LikeButton from "@/components/like-button"; import LikeButton from "@/components/like-button";
import ImageViewer from "@/components/image-viewer"; import ImageViewer from "@/components/image-viewer";
import DeleteMiiButton from "@/components/delete-mii"; import DeleteMiiButton from "@/components/mii/delete-mii-button";
import ShareMiiButton from "@/components/share-mii-button"; import ShareMiiButton from "@/components/mii/share-mii-button";
import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan"; import ThreeDsScanTutorialButton from "@/components/tutorial/3ds-scan";
import SwitchScanTutorialButton from "@/components/tutorial/switch-scan"; import SwitchScanTutorialButton from "@/components/tutorial/switch-scan";
import Description from "@/components/description"; import Description from "@/components/description";
import { MiiPlatform } from "@prisma/client"; import MiiInstructions from "@/components/mii/instructions";
import { SwitchMiiInstructions } from "@/types";
interface Props { interface Props {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@ -120,7 +123,7 @@ export default async function MiiPage({ params }: Props) {
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="max-w-5xl w-full flex flex-col gap-4"> <div className="max-w-5xl w-full flex flex-col gap-4">
<div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1"> <div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1">
<div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2"> <div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 h-min flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2">
{/* Mii Image */} {/* Mii Image */}
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center"> <div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center">
<ImageViewer <ImageViewer
@ -232,13 +235,15 @@ export default async function MiiPage({ params }: Props) {
<Icon icon="foundation:female" className="text-pink-400" /> <Icon icon="foundation:female" className="text-pink-400" />
</div> </div>
<div {mii.platform !== "THREE_DS" && (
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ <div
mii.gender === "NONBINARY" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300" className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
}`} mii.gender === "NONBINARY" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300"
> }`}
<Icon icon="mdi:gender-non-binary" className="text-purple-400" /> >
</div> <Icon icon="mdi:gender-non-binary" className="text-purple-400" />
</div>
)}
</div> </div>
</div> </div>
@ -305,7 +310,7 @@ export default async function MiiPage({ params }: Props) {
</div> </div>
{/* Instructions */} {/* Instructions */}
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4">{JSON.stringify(mii.instructions)}</div> {mii.platform === "SWITCH" && <MiiInstructions instructions={mii.instructions as Partial<SwitchMiiInstructions>} />}
</div> </div>
</div> </div>
@ -343,7 +348,7 @@ export default async function MiiPage({ params }: Props) {
))} ))}
</div> </div>
) : ( ) : (
<p className="indent-8 text-black/50">There is nothing here...</p> <p className="indent-7.5 text-black/50">There is nothing here...</p>
)} )}
</div> </div>
</div> </div>

View file

@ -7,8 +7,8 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import Countdown from "@/components/countdown"; import Countdown from "@/components/countdown";
import MiiList from "@/components/mii-list"; import MiiList from "@/components/mii/list";
import Skeleton from "@/components/mii-list/skeleton"; import Skeleton from "@/components/mii/list/skeleton";
interface Props { interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;

View file

@ -5,8 +5,8 @@ import { Suspense } from "react";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import ProfileInformation from "@/components/profile-information"; import ProfileInformation from "@/components/profile-information";
import MiiList from "@/components/mii-list"; import MiiList from "@/components/mii/list";
import Skeleton from "@/components/mii-list/skeleton"; import Skeleton from "@/components/mii/list/skeleton";
interface Props { interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;

View file

@ -5,8 +5,8 @@ import { Suspense } from "react";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import ProfileInformation from "@/components/profile-information"; import ProfileInformation from "@/components/profile-information";
import Skeleton from "@/components/mii-list/skeleton"; import Skeleton from "@/components/mii/list/skeleton";
import MiiList from "@/components/mii-list"; import MiiList from "@/components/mii/list";
interface Props { interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;

View file

@ -0,0 +1,38 @@
import { ChangeEvent } from "react";
import { MiiGender } from "@prisma/client";
import { SwitchMiiInstructions } from "@/types";
interface Props {
data: SwitchMiiInstructions["datingPreferences"];
onChecked?: (e: ChangeEvent<HTMLInputElement, HTMLInputElement>, gender: MiiGender) => void;
}
const DATING_PREFERENCES = ["Male", "Female", "Nonbinary"];
export default function DatingPreferencesViewer({ data, onChecked }: Props) {
return (
<div className="flex flex-col gap-1.5">
{DATING_PREFERENCES.map((gender) => {
const genderEnum = gender.toUpperCase() as MiiGender;
return (
<div className="flex gap-1.5">
<input
key={gender}
type="checkbox"
id={gender}
className="checkbox"
checked={data.includes(genderEnum)}
onChange={(e) => {
if (onChecked) onChecked(e, genderEnum);
}}
/>
<label htmlFor={gender} className="text-sm select-none">
{gender}
</label>
</div>
);
})}
</div>
);
}

View file

@ -6,8 +6,8 @@ import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import LikeButton from "./like-button"; import LikeButton from "../like-button";
import SubmitButton from "./submit-button"; import SubmitButton from "../submit-button";
interface Props { interface Props {
miiId: number; miiId: number;

View file

@ -0,0 +1,272 @@
import React from "react";
import DatingPreferencesViewer from "./dating-preferences";
import VoiceViewer from "./voice-viewer";
import PersonalityViewer from "./personality-viewer";
import { SwitchMiiInstructions } from "@/types";
import { Icon } from "@iconify/react";
import { COLORS } from "@/lib/switch";
interface Props {
instructions: Partial<SwitchMiiInstructions>;
}
interface SectionProps {
name: string;
instructions: Partial<SwitchMiiInstructions[keyof SwitchMiiInstructions]>;
children?: React.ReactNode;
isSubSection?: boolean;
}
const ORDINAL_SUFFIXES: Record<string, string> = {
one: "st",
two: "nd",
few: "rd",
other: "th",
};
const ordinalRules = new Intl.PluralRules("en-US", { type: "ordinal" });
function GridPosition({ index, cols = 5 }: { index: number; cols?: number }) {
const row = Math.floor(index / cols) + 1;
const col = (index % cols) + 1;
const rowSuffix = ORDINAL_SUFFIXES[ordinalRules.select(row)];
const colSuffix = ORDINAL_SUFFIXES[ordinalRules.select(col)];
return `${row}${rowSuffix} row, ${col}${colSuffix} column`;
}
function ColorPosition({ color }: { color: number }) {
if (!color) return null;
if (color <= 7) {
return (
<>
Color menu on left, <GridPosition index={color} cols={1} />
</>
);
}
if (color >= 108) {
return (
<>
Outside color menu, <GridPosition index={color - 108} cols={2} />
</>
);
}
return (
<span className="flex items-center">
<div className="size-5 rounded mr-1.5" style={{ backgroundColor: `#${COLORS[color]}` }}></div>
Color menu on right, <GridPosition index={color - 8} cols={10} />
</span>
);
}
interface TableCellProps {
label: string;
children: React.ReactNode;
}
function TableCell({ label, children }: TableCellProps) {
return (
<tr className={"border-b border-orange-300/50 last:border-0"}>
<td className={"py-0.5 pr-6 text-amber-700 font-semibold w-30 text-sm"}>{label}</td>
<td className={"py-0.5 text-amber-950"}>{children}</td>
</tr>
);
}
function Section({ name, instructions, children, isSubSection }: SectionProps) {
if (typeof instructions !== "object") return null;
const type = "type" in instructions ? instructions.type : undefined;
const color = "color" in instructions ? instructions.color : undefined;
const height = "height" in instructions ? instructions.height : undefined;
const distance = "distance" in instructions ? instructions.distance : undefined;
const rotation = "rotation" in instructions ? instructions.rotation : undefined;
const size = "size" in instructions ? instructions.size : undefined;
const stretch = "stretch" in instructions ? instructions.stretch : undefined;
return (
<div className={`p-3 ${isSubSection ? "mt-2" : "border-l-4 border-amber-400 bg-amber-100/50 rounded-r-lg py-2.5"}`}>
<h3 className="font-semibold text-xl text-amber-800 mb-1">{name}</h3>
<table className="w-full">
<tbody>
{type && (
<TableCell label="Type">
<GridPosition index={type} />
</TableCell>
)}
{color && (
<TableCell label="Color">
<ColorPosition color={color} />
</TableCell>
)}
{height && <TableCell label="Height">{height}</TableCell>}
{distance && <TableCell label="Distance">{distance}</TableCell>}
{rotation && <TableCell label="Rotation">{rotation}</TableCell>}
{size && <TableCell label="Size">{size}</TableCell>}
{stretch && <TableCell label="Stretch">{stretch}</TableCell>}
{children}
</tbody>
</table>
</div>
);
}
export default function MiiInstructions({ instructions }: Props) {
if (Object.keys(instructions).length === 0) return null;
const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, datingPreferences, voice, personality } = instructions;
return (
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-3 max-h-96 overflow-y-auto">
<h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2">
<Icon icon="fa7-solid:list" />
Instructions
</h2>
{head && <Section name="Head" instructions={head}></Section>}
{hair && (
<Section name="Hair" instructions={hair}>
{hair.setType && (
<TableCell label="Set Type">
<GridPosition index={hair.setType} />
</TableCell>
)}
{hair.bangsType && (
<TableCell label="Bangs Type">
<GridPosition index={hair.bangsType} />
</TableCell>
)}
{hair.backType && (
<TableCell label="Back Type">
<GridPosition index={hair.backType} />
</TableCell>
)}
{hair.subColor && (
<TableCell label="Sub Color">
<ColorPosition color={hair.subColor} />
</TableCell>
)}
</Section>
)}
{eyebrows && <Section name="Eyebrows" instructions={eyebrows}></Section>}
{eyes && (
<Section name="Eyes" instructions={eyes}>
{eyes.eyesType && (
<TableCell label="Eyes Type">
<GridPosition index={eyes.eyesType} />
</TableCell>
)}
{eyes.eyelashesTop && (
<TableCell label="Eyelashes Top Type">
<GridPosition index={eyes.eyelashesTop} />
</TableCell>
)}
{eyes.eyelashesBottom && (
<TableCell label="Eyelashes Bottom Type">
<GridPosition index={eyes.eyelashesBottom} />
</TableCell>
)}
{eyes.eyelidTop && (
<TableCell label="Eyelid Top Type">
<GridPosition index={eyes.eyelidTop} />
</TableCell>
)}
{eyes.eyelidBottom && (
<TableCell label="Eyelid Bottom Type">
<GridPosition index={eyes.eyelidBottom} />
</TableCell>
)}
{eyes.eyeliner && (
<TableCell label="Eyeliner Type">
<GridPosition index={eyes.eyeliner} />
</TableCell>
)}
{eyes.pupil && (
<TableCell label="Pupil Type">
<GridPosition index={eyes.pupil} />
</TableCell>
)}
</Section>
)}
{nose && <Section name="Nose" instructions={nose}></Section>}
{lips && <Section name="Lips" instructions={lips}></Section>}
{ears && <Section name="Ears" instructions={ears}></Section>}
{glasses && (
<Section name="Glasses" instructions={glasses}>
{glasses.ringColor && (
<TableCell label="Ring Color">
<ColorPosition color={glasses.ringColor} />
</TableCell>
)}
{glasses.shadesColor && (
<TableCell label="Shades Color">
<ColorPosition color={glasses.shadesColor} />
</TableCell>
)}
</Section>
)}
{other && (
<Section name="Other" instructions={other}>
<Section isSubSection name="Wrinkles 1" instructions={other.wrinkles1} />
<Section isSubSection name="Wrinkles 2" instructions={other.wrinkles2} />
<Section isSubSection name="Beard" instructions={other.beard} />
<Section isSubSection name="Moustache" instructions={other.moustache} />
<Section isSubSection name="Goatee" instructions={other.goatee} />
<Section isSubSection name="Mole" instructions={other.mole} />
<Section isSubSection name="Eye Shadow" instructions={other.eyeShadow} />
<Section isSubSection name="Blush" instructions={other.blush} />
</Section>
)}
{(height || weight || datingPreferences || voice || personality) && (
<div className="pl-3 text-sm border-l-4 border-amber-400 bg-amber-100/50 rounded-r-lg py-2.5 text-amber-950">
<h3 className="font-semibold text-xl text-amber-800 mb-1">Misc</h3>
{height && (
<div className="flex mb-1">
<label htmlFor="height" className="w-16">
Height
</label>
<input id="height" type="range" min={0} max={100} step={1} disabled value={height} />
</div>
)}
{weight && (
<div className="flex">
<label htmlFor="weight" className="w-16">
Weight
</label>
<input id="weight" type="range" min={0} max={100} step={1} disabled value={weight} />
</div>
)}
{datingPreferences && (
<div className="pl-2">
<h4 className="text-lg font-semibold mt-4">Dating Preferences</h4>
<div className="w-min">
<DatingPreferencesViewer data={datingPreferences} />
</div>
</div>
)}
{voice && (
<div className="pl-2">
<h4 className="font-semibold text-xl text-amber-800 mb-1 mt-4">Voice</h4>
<div className="w-min">
<VoiceViewer data={voice} />
</div>
</div>
)}
{personality && (
<div className="pl-2">
<h4 className="font-semibold text-xl text-amber-800 mb-1 mt-4">Personality</h4>
<div className="w-min">
<PersonalityViewer data={personality} />
</div>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -11,9 +11,9 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import SortSelect from "./sort-select"; import SortSelect from "./sort-select";
import Carousel from "../carousel"; import Carousel from "../../carousel";
import LikeButton from "../like-button"; import LikeButton from "../../like-button";
import DeleteMiiButton from "../delete-mii"; import DeleteMiiButton from "../delete-mii-button";
import Pagination from "./pagination"; import Pagination from "./pagination";
import FilterMenu from "./filter-menu"; import FilterMenu from "./filter-menu";

View file

@ -2,7 +2,7 @@
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react"; import { useEffect, useMemo, useState, useTransition } from "react";
import TagSelector from "../tag-selector"; import TagSelector from "../../tag-selector";
interface Props { interface Props {
isExclude?: boolean; isExclude?: boolean;

View file

@ -0,0 +1,49 @@
"use client";
import { SwitchMiiInstructions } from "@/types";
interface Props {
data: SwitchMiiInstructions["personality"];
onClick?: (key: string, i: number) => void;
}
const PERSONALITY_SETTINGS: { label: string; left: string; right: string }[] = [
{ label: "Movement", left: "Slow", right: "Quick" },
{ label: "Speech", left: "Polite", right: "Honest" },
{ label: "Energy", left: "Flat", right: "Varied" },
{ label: "Thinking", left: "Serious", right: "Chill" },
{ label: "Overall", left: "Normal", right: "Quirky" },
];
export default function PersonalityViewer({ data, onClick }: Props) {
return (
<div className="flex flex-col gap-1.5 mb-3">
{PERSONALITY_SETTINGS.map(({ label, left, right }) => {
const key = label.toLowerCase() as keyof typeof data;
return (
<div key={label} className="flex justify-center items-center gap-2">
<span className="text-sm font-semibold w-24 shrink-0">{label}</span>
<span className="text-sm text-zinc-500 w-14 text-right">{left}</span>
<div className="flex gap-0.5">
{Array.from({ length: 6 }).map((_, i) => {
const colors = ["bg-green-400", "bg-green-300", "bg-teal-200", "bg-orange-200", "bg-orange-300", "bg-orange-400"];
return (
<button
key={i}
type="button"
onClick={() => {
if (onClick) onClick(key, i);
}}
className={`size-7 rounded-lg transition-opacity duration-100 border-orange-500
${colors[i]} ${data[key] === i ? "border-2 opacity-100" : "opacity-70"} ${onClick ? "cursor-pointer" : ""}`}
></button>
);
})}
</div>
<span className="text-sm text-zinc-500 w-12 shrink-0">{right}</span>
</div>
);
})}
</div>
);
}

View file

@ -0,0 +1,59 @@
"use client";
import { SwitchMiiInstructions } from "@/types";
import { ChangeEvent } from "react";
interface Props {
data: SwitchMiiInstructions["voice"];
onClick?: (e: ChangeEvent<HTMLInputElement, HTMLInputElement>, label: string) => void;
onClickTone?: (i: number) => void;
}
const VOICE_SETTINGS: string[] = ["Speed", "Pitch", "Depth", "Delivery"];
export default function VoiceViewer({ data, onClick, onClickTone }: Props) {
return (
<div className="flex flex-col gap-1">
{VOICE_SETTINGS.map((label) => (
<div key={label} className="flex gap-3">
<label htmlFor={label} className="text-sm w-14">
{label}
</label>
<input
type="range"
name={label}
className="grow"
min={0}
max={100}
step={1}
value={data[label as keyof typeof data]}
disabled={!onClick}
onChange={(e) => {
if (onClick) onClick(e, label);
}}
/>
</div>
))}
<div className="flex gap-3">
<label htmlFor="delivery" className="text-sm w-14">
Tone
</label>
<div className="grid grid-cols-6 gap-1 grow">
{Array.from({ length: 6 }).map((_, i) => (
<button
type="button"
key={i}
onClick={() => {
if (onClickTone) onClickTone(i);
}}
className={`transition-colors duration-100 rounded-xl ${data.tone === i ? "bg-orange-400!" : ""} ${onClick ? "hover:bg-orange-300 cursor-pointer" : ""}`}
>
{i + 1}
</button>
))}
</div>
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
import { Icon } from "@iconify/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Icon } from "@iconify/react";
import { COLORS } from "@/lib/switch";
interface Props { interface Props {
disabled?: boolean; disabled?: boolean;
@ -58,10 +59,7 @@ export default function ColorPicker({ disabled, color, setColor }: Props) {
<button <button
type="button" type="button"
key={i} key={i}
onClick={() => { onClick={() => setColor(i)}
setColor(i);
close();
}}
className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i ? "ring-2 z-10" : ""}`} className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i ? "ring-2 z-10" : ""}`}
style={{ style={{
backgroundColor: `#${c}`, backgroundColor: `#${c}`,
@ -80,10 +78,7 @@ export default function ColorPicker({ disabled, color, setColor }: Props) {
<button <button
type="button" type="button"
key={i + 8} key={i + 8}
onClick={() => { onClick={() => setColor(i + 8)}
setColor(i + 8);
close();
}}
className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i + 8 ? "ring-2 z-10" : ""}`} className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i + 8 ? "ring-2 z-10" : ""}`}
style={{ style={{
backgroundColor: `#${c}`, backgroundColor: `#${c}`,
@ -108,140 +103,3 @@ export default function ColorPicker({ disabled, color, setColor }: Props) {
</> </>
); );
} }
const COLORS: string[] = [
// Outside
"000000",
"8E8E93",
"6B4F0F",
"5A2A0A",
"7A1E0E",
"A0522D",
"A56B2A",
"D4A15A",
// Row 1
"F2F2F2",
"E6D5C3",
"F3E6A2",
"CDE6A1",
"A9DFA3",
"8ED8B0",
"8FD3E8",
"C9C2E6",
"F3C1CF",
"F0A8A8",
// Row 2
"D8D8D8",
"E8C07D",
"F0D97A",
"CDE07A",
"7BC96F",
"6BC4B2",
"5BBAD6",
"D9A7E0",
"F7B6C2",
"F47C6C",
// Row 3
"C0C0C0",
"D9A441",
"F4C542",
"D4C86A",
"8FD14F",
"58B88A",
"6FA8DC",
"B4A7D6",
"F06277",
"FF6F61",
// Row 4
"A8A8A8",
"D29B62",
"F2CF75",
"D8C47A",
"8DB600",
"66C2A5",
"4DA3D9",
"C27BA0",
"D35D6E",
"FF4C3B",
// Row 5
"9A9A9A",
"C77800",
"F4B183",
"D6BF3A",
"3FA34D",
"4CA3A3",
"7EA6E0",
"B56576",
"FF1744",
"FF2A00",
// Row 6
"8A817C",
"B85C1E",
"FF8C00",
"D2B48C",
"2E8B57",
"2F7E8C",
"2E86C1",
"7D5BA6",
"C2185B",
"E0193A",
// Row 7
"6E6E6E",
"95543A",
"F4A460",
"B7A369",
"3B7A0A",
"1F6F78",
"3F51B5",
"673AB7",
"B71C1C",
"C91F3A",
// Row 8
"3E3E3E",
"8B5A2B",
"F0986C",
"9E8F2A",
"0B5D3B",
"0E3A44",
"1F2A44",
"4B2E2E",
"9C1B1B",
"7A3B2E",
// Row 9
"2E2E2E",
"7A4A2A",
"A86A1D",
"6E6B2A",
"2F6F55",
"004E52",
"1C2F6E",
"3A1F4D",
"A52A2A",
"8B4513",
// Row 10
"000000",
"5A2E0C",
"7B3F00",
"5C4A00",
"004225",
"003B44",
"0A1F44",
"2B1B3F",
"7B2D2D",
"8B3A0E",
// Head tab extra colors
"FFD8BA",
"FFD5AC",
"FEC1A4",
"FEC68F",
"FEB089",
"FEBA6B",
"F39866",
"E89854",
"E37E3F",
"B45627",
"914220",
"59371F",
"662D16",
"392D1E",
];

View file

@ -1,20 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { SwitchMiiInstructions } from "@/types";
import { MiiGender } from "@prisma/client"; import { MiiGender } from "@prisma/client";
import DatingPreferencesViewer from "@/components/mii/dating-preferences";
import VoiceViewer from "@/components/mii/voice-viewer";
import PersonalityViewer from "@/components/mii/personality-viewer";
import { SwitchMiiInstructions } from "@/types";
interface Props { interface Props {
instructions: React.RefObject<SwitchMiiInstructions>; instructions: React.RefObject<SwitchMiiInstructions>;
} }
const VOICE_SETTINGS: string[] = ["Speed", "Pitch", "Depth", "Delivery"];
const PERSONALITY_SETTINGS: { label: string; left: string; right: string }[] = [
{ label: "Movement", left: "Slow", right: "Quick" },
{ label: "Speech", left: "Polite", right: "Honest" },
{ label: "Energy", left: "Flat", right: "Varied" },
{ label: "Thinking", left: "Serious", right: "Chill" },
{ label: "Overall", left: "Normal", right: "Quirky" },
];
export default function HeadTab({ instructions }: Props) { export default function HeadTab({ instructions }: Props) {
const [height, setHeight] = useState(50); const [height, setHeight] = useState(50);
const [weight, setWeight] = useState(50); const [weight, setWeight] = useState(50);
@ -94,59 +90,15 @@ export default function HeadTab({ instructions }: Props) {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="flex gap-1.5"> <DatingPreferencesViewer
<input data={datingPreferences}
type="checkbox" onChecked={(e, gender) => {
id="male" setDatingPreferences((prev) =>
className="checkbox" e.target.checked ? (prev.includes(gender) ? prev : [...prev, gender]) : prev.filter((p) => p !== gender),
checked={datingPreferences.includes("MALE")} );
onChange={(e) => { instructions.current.datingPreferences = datingPreferences;
setDatingPreferences((prev) => }}
e.target.checked ? (prev.includes("MALE") ? prev : [...prev, "MALE"]) : prev.filter((p) => p !== "MALE"), />
);
instructions.current.datingPreferences = datingPreferences;
}}
/>
<label htmlFor="male" className="text-sm">
Male
</label>
</div>
<div className="flex gap-1.5">
<input
type="checkbox"
id="female"
className="checkbox"
checked={datingPreferences.includes("FEMALE")}
onChange={(e) => {
setDatingPreferences((prev) =>
e.target.checked ? (prev.includes("FEMALE") ? prev : [...prev, "FEMALE"]) : prev.filter((p) => p !== "FEMALE"),
);
instructions.current.datingPreferences = datingPreferences;
}}
/>
<label htmlFor="female" className="text-sm">
Female
</label>
</div>
<div className="flex gap-1.5">
<input
type="checkbox"
id="nonbinary"
className="checkbox"
checked={datingPreferences.includes("NONBINARY")}
onChange={(e) => {
setDatingPreferences((prev) =>
e.target.checked ? (prev.includes("NONBINARY") ? prev : [...prev, "NONBINARY"]) : prev.filter((p) => p !== "NONBINARY"),
);
instructions.current.datingPreferences = datingPreferences;
}}
/>
<label htmlFor="nonbinary" className="text-sm">
Nonbinary
</label>
</div>
</div> </div>
</div> </div>
@ -157,49 +109,17 @@ export default function HeadTab({ instructions }: Props) {
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<div className="flex flex-col gap-1"> <VoiceViewer
{VOICE_SETTINGS.map((label) => ( data={voice}
<div key={label} className="flex gap-3"> onClick={(e, label) => {
<label htmlFor={label} className="text-sm w-14"> setVoice((p) => ({ ...p, [label]: e.target.valueAsNumber }));
{label} instructions.current.voice[label as keyof typeof voice] = e.target.valueAsNumber;
</label> }}
<input onClickTone={(i) => {
type="range" setVoice((p) => ({ ...p, tone: i }));
name={label} instructions.current.voice.tone = i;
className="grow" }}
min={0} />
max={100}
step={1}
value={voice[label as keyof typeof voice]}
onChange={(e) => {
setVoice((p) => ({ ...p, [label]: e.target.valueAsNumber }));
instructions.current.voice[label as keyof typeof voice] = e.target.valueAsNumber;
}}
/>
</div>
))}
<div className="flex gap-3">
<label htmlFor="delivery" className="text-sm w-14">
Tone
</label>
<div className="grid grid-cols-6 gap-1 grow">
{Array.from({ length: 6 }).map((_, i) => (
<button
type="button"
key={i}
onClick={() => {
setVoice((p) => ({ ...p, tone: i }));
instructions.current.voice.tone = i;
}}
className={`cursor-pointer hover:bg-orange-300 transition-colors duration-100 rounded-xl ${voice.tone === i ? "bg-orange-400!" : ""}`}
>
{i + 1}
</button>
))}
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -209,35 +129,13 @@ export default function HeadTab({ instructions }: Props) {
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<div className="flex flex-col gap-1.5 mb-3"> <PersonalityViewer
{PERSONALITY_SETTINGS.map(({ label, left, right }) => { data={personality}
const key = label.toLowerCase() as keyof typeof personality; onClick={(key, i) => {
return ( setPersonality((p) => ({ ...p, [key]: i }));
<div key={label} className="flex justify-center items-center gap-2"> instructions.current.personality = personality;
<span className="text-sm font-bold w-24 shrink-0">{label}</span> }}
<span className="text-sm text-zinc-500 w-14 text-right">{left}</span> />
<div className="flex gap-0.5">
{Array.from({ length: 6 }).map((_, i) => {
const colors = ["bg-green-400", "bg-green-300", "bg-teal-200", "bg-orange-200", "bg-orange-300", "bg-orange-400"];
return (
<button
key={i}
type="button"
onClick={() => {
setPersonality((p) => ({ ...p, [key]: i }));
instructions.current.personality = personality;
}}
className={`size-7 cursor-pointer rounded-lg transition-opacity duration-100 border-orange-500
${colors[i]} ${personality[key] === i ? "border-2 opacity-100" : "opacity-70"}`}
></button>
);
})}
</div>
<span className="text-sm text-zinc-500 w-12 shrink-0">{right}</span>
</div>
);
})}
</div>
</div> </div>
</div> </div>
</div> </div>

136
src/lib/switch.ts Normal file
View file

@ -0,0 +1,136 @@
export const COLORS: string[] = [
// Outside
"000000",
"8E8E93",
"6B4F0F",
"5A2A0A",
"7A1E0E",
"A0522D",
"A56B2A",
"D4A15A",
// Row 1
"F2F2F2",
"E6D5C3",
"F3E6A2",
"CDE6A1",
"A9DFA3",
"8ED8B0",
"8FD3E8",
"C9C2E6",
"F3C1CF",
"F0A8A8",
// Row 2
"D8D8D8",
"E8C07D",
"F0D97A",
"CDE07A",
"7BC96F",
"6BC4B2",
"5BBAD6",
"D9A7E0",
"F7B6C2",
"F47C6C",
// Row 3
"C0C0C0",
"D9A441",
"F4C542",
"D4C86A",
"8FD14F",
"58B88A",
"6FA8DC",
"B4A7D6",
"F06277",
"FF6F61",
// Row 4
"A8A8A8",
"D29B62",
"F2CF75",
"D8C47A",
"8DB600",
"66C2A5",
"4DA3D9",
"C27BA0",
"D35D6E",
"FF4C3B",
// Row 5
"9A9A9A",
"C77800",
"F4B183",
"D6BF3A",
"3FA34D",
"4CA3A3",
"7EA6E0",
"B56576",
"FF1744",
"FF2A00",
// Row 6
"8A817C",
"B85C1E",
"FF8C00",
"D2B48C",
"2E8B57",
"2F7E8C",
"2E86C1",
"7D5BA6",
"C2185B",
"E0193A",
// Row 7
"6E6E6E",
"95543A",
"F4A460",
"B7A369",
"3B7A0A",
"1F6F78",
"3F51B5",
"673AB7",
"B71C1C",
"C91F3A",
// Row 8
"3E3E3E",
"8B5A2B",
"F0986C",
"9E8F2A",
"0B5D3B",
"0E3A44",
"1F2A44",
"4B2E2E",
"9C1B1B",
"7A3B2E",
// Row 9
"2E2E2E",
"7A4A2A",
"A86A1D",
"6E6B2A",
"2F6F55",
"004E52",
"1C2F6E",
"3A1F4D",
"A52A2A",
"8B4513",
// Row 10
"000000",
"5A2E0C",
"7B3F00",
"5C4A00",
"004225",
"003B44",
"0A1F44",
"2B1B3F",
"7B2D2D",
"8B3A0E",
// Head tab extra colors
"FFD8BA",
"FFD5AC",
"FEC1A4",
"FEC68F",
"FEB089",
"FEBA6B",
"F39866",
"E89854",
"E37E3F",
"B45627",
"914220",
"59371F",
"662D16",
"392D1E",
];