mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 11:13:16 +00:00
feat: view instructions on mii page
This commit is contained in:
parent
5995afe3db
commit
13941e849c
23 changed files with 625 additions and 311 deletions
|
|
@ -121,7 +121,7 @@ body {
|
|||
|
||||
/* Range input */
|
||||
input[type="range"] {
|
||||
@apply appearance-none bg-transparent cursor-pointer;
|
||||
@apply appearance-none bg-transparent not-disabled:cursor-pointer;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
|
|
@ -135,8 +135,7 @@ input[type="range"]::-moz-range-track {
|
|||
|
||||
/* 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;
|
||||
margin-top: -6px; /* center thumb vertically */
|
||||
@apply appearance-none size-4 bg-orange-400 border-2 border-orange-500 rounded-full shadow-md transition -mt-1.5;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
|
|
@ -145,9 +144,9 @@ input[type="range"]::-moz-range-thumb {
|
|||
|
||||
/* Hover */
|
||||
input[type="range"]:hover::-webkit-slider-thumb {
|
||||
@apply bg-orange-500;
|
||||
@apply not-disabled:bg-orange-500;
|
||||
}
|
||||
|
||||
input[type="range"]:hover::-moz-range-thumb {
|
||||
@apply bg-orange-500;
|
||||
@apply not-disabled:bg-orange-500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ import { Icon } from "@iconify/react";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { MiiPlatform } from "@prisma/client";
|
||||
|
||||
import LikeButton from "@/components/like-button";
|
||||
import ImageViewer from "@/components/image-viewer";
|
||||
import DeleteMiiButton from "@/components/delete-mii";
|
||||
import ShareMiiButton from "@/components/share-mii-button";
|
||||
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 Description from "@/components/description";
|
||||
import { MiiPlatform } from "@prisma/client";
|
||||
import MiiInstructions from "@/components/mii/instructions";
|
||||
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>;
|
||||
|
|
@ -120,7 +123,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
<div className="flex flex-col items-center">
|
||||
<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="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 */}
|
||||
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center">
|
||||
<ImageViewer
|
||||
|
|
@ -232,6 +235,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
<Icon icon="foundation:female" className="text-pink-400" />
|
||||
</div>
|
||||
|
||||
{mii.platform !== "THREE_DS" && (
|
||||
<div
|
||||
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"
|
||||
|
|
@ -239,6 +243,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
>
|
||||
<Icon icon="mdi:gender-non-binary" className="text-purple-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -305,7 +310,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
|
|
@ -343,7 +348,7 @@ export default async function MiiPage({ params }: Props) {
|
|||
))}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { auth } from "@/lib/auth";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import Countdown from "@/components/countdown";
|
||||
import MiiList from "@/components/mii-list";
|
||||
import Skeleton from "@/components/mii-list/skeleton";
|
||||
import MiiList from "@/components/mii/list";
|
||||
import Skeleton from "@/components/mii/list/skeleton";
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Suspense } from "react";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import ProfileInformation from "@/components/profile-information";
|
||||
import MiiList from "@/components/mii-list";
|
||||
import Skeleton from "@/components/mii-list/skeleton";
|
||||
import MiiList from "@/components/mii/list";
|
||||
import Skeleton from "@/components/mii/list/skeleton";
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Suspense } from "react";
|
|||
import { auth } from "@/lib/auth";
|
||||
|
||||
import ProfileInformation from "@/components/profile-information";
|
||||
import Skeleton from "@/components/mii-list/skeleton";
|
||||
import MiiList from "@/components/mii-list";
|
||||
import Skeleton from "@/components/mii/list/skeleton";
|
||||
import MiiList from "@/components/mii/list";
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
|
|
|||
38
src/components/mii/dating-preferences.tsx
Normal file
38
src/components/mii/dating-preferences.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import { useEffect, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import LikeButton from "./like-button";
|
||||
import SubmitButton from "./submit-button";
|
||||
import LikeButton from "../like-button";
|
||||
import SubmitButton from "../submit-button";
|
||||
|
||||
interface Props {
|
||||
miiId: number;
|
||||
272
src/components/mii/instructions.tsx
Normal file
272
src/components/mii/instructions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ import { auth } from "@/lib/auth";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import SortSelect from "./sort-select";
|
||||
import Carousel from "../carousel";
|
||||
import LikeButton from "../like-button";
|
||||
import DeleteMiiButton from "../delete-mii";
|
||||
import Carousel from "../../carousel";
|
||||
import LikeButton from "../../like-button";
|
||||
import DeleteMiiButton from "../delete-mii-button";
|
||||
import Pagination from "./pagination";
|
||||
import FilterMenu from "./filter-menu";
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||
import TagSelector from "../tag-selector";
|
||||
import TagSelector from "../../tag-selector";
|
||||
|
||||
interface Props {
|
||||
isExclude?: boolean;
|
||||
49
src/components/mii/personality-viewer.tsx
Normal file
49
src/components/mii/personality-viewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/mii/voice-viewer.tsx
Normal file
59
src/components/mii/voice-viewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { COLORS } from "@/lib/switch";
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
|
|
@ -58,10 +59,7 @@ export default function ColorPicker({ disabled, color, setColor }: Props) {
|
|||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setColor(i);
|
||||
close();
|
||||
}}
|
||||
onClick={() => setColor(i)}
|
||||
className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i ? "ring-2 z-10" : ""}`}
|
||||
style={{
|
||||
backgroundColor: `#${c}`,
|
||||
|
|
@ -80,10 +78,7 @@ export default function ColorPicker({ disabled, color, setColor }: Props) {
|
|||
<button
|
||||
type="button"
|
||||
key={i + 8}
|
||||
onClick={() => {
|
||||
setColor(i + 8);
|
||||
close();
|
||||
}}
|
||||
onClick={() => setColor(i + 8)}
|
||||
className={`size-7.5 cursor-pointer rounded-md ring-orange-500 ring-offset-2 ${color === i + 8 ? "ring-2 z-10" : ""}`}
|
||||
style={{
|
||||
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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import { useState } from "react";
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
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 {
|
||||
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) {
|
||||
const [height, setHeight] = useState(50);
|
||||
const [weight, setWeight] = useState(50);
|
||||
|
|
@ -94,59 +90,15 @@ export default function HeadTab({ instructions }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="male"
|
||||
className="checkbox"
|
||||
checked={datingPreferences.includes("MALE")}
|
||||
onChange={(e) => {
|
||||
<DatingPreferencesViewer
|
||||
data={datingPreferences}
|
||||
onChecked={(e, gender) => {
|
||||
setDatingPreferences((prev) =>
|
||||
e.target.checked ? (prev.includes("MALE") ? prev : [...prev, "MALE"]) : prev.filter((p) => p !== "MALE"),
|
||||
e.target.checked ? (prev.includes(gender) ? prev : [...prev, gender]) : prev.filter((p) => p !== gender),
|
||||
);
|
||||
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>
|
||||
|
||||
|
|
@ -157,49 +109,17 @@ export default function HeadTab({ instructions }: Props) {
|
|||
<hr className="grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<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={voice[label as keyof typeof voice]}
|
||||
onChange={(e) => {
|
||||
<VoiceViewer
|
||||
data={voice}
|
||||
onClick={(e, label) => {
|
||||
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={() => {
|
||||
onClickTone={(i) => {
|
||||
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>
|
||||
|
||||
|
|
@ -209,35 +129,13 @@ export default function HeadTab({ instructions }: Props) {
|
|||
<hr className="grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 mb-3">
|
||||
{PERSONALITY_SETTINGS.map(({ label, left, right }) => {
|
||||
const key = label.toLowerCase() as keyof typeof personality;
|
||||
return (
|
||||
<div key={label} className="flex justify-center items-center gap-2">
|
||||
<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={() => {
|
||||
<PersonalityViewer
|
||||
data={personality}
|
||||
onClick={(key, i) => {
|
||||
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>
|
||||
|
|
|
|||
136
src/lib/switch.ts
Normal file
136
src/lib/switch.ts
Normal 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",
|
||||
];
|
||||
Loading…
Reference in a new issue