mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-28 06:34:15 +00:00
feat: youtube video instructions
also the repo is public again
This commit is contained in:
parent
94389ddc8f
commit
d947f09112
11 changed files with 893 additions and 15 deletions
|
|
@ -32,6 +32,10 @@ const editSchema = z.object({
|
|||
makeup: z.enum(MiiMakeup).optional(),
|
||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
youtubeId: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9_-]{11}$/, "Invalid YouTube video ID")
|
||||
.optional(),
|
||||
instructions: switchMiiInstructionsSchema,
|
||||
image1: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
image2: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
|
|
@ -88,6 +92,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||
makeup: formData.get("makeup") ?? undefined,
|
||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
||||
youtubeId: formData.get("youtubeId"),
|
||||
instructions: minifiedInstructions,
|
||||
image1: formData.get("image1"),
|
||||
image2: formData.get("image2"),
|
||||
|
|
@ -95,7 +100,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||
});
|
||||
|
||||
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||
const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, instructions, image1, image2, image3 } = parsed.data;
|
||||
const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, youtubeId, instructions, image1, image2, image3 } =
|
||||
parsed.data;
|
||||
|
||||
// Validate image files
|
||||
let wasImagesModerated = false;
|
||||
|
|
@ -133,11 +139,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||
if (quarantined !== undefined) updateData.quarantined = quarantined;
|
||||
if (mii.platform === "SWITCH" && gender !== undefined) updateData.gender = gender;
|
||||
if (makeup !== undefined) updateData.makeup = makeup;
|
||||
if (youtubeId !== undefined) updateData.youtubeId = youtubeId;
|
||||
if (instructions !== undefined) updateData.instructions = instructions;
|
||||
if (images.length > 0) updateData.imageCount = images.length;
|
||||
|
||||
const imagesChanged = images.length > 0 || miiPortraitImage || miiFeaturesImage;
|
||||
if ((settings.queueEnabled && imagesChanged) || wasImagesModerated) updateData.in_queue = true;
|
||||
|
||||
const imagesChanged = images.length > 0 || miiPortraitImage || miiFeaturesImage;
|
||||
if ((settings.queueEnabled && imagesChanged) || wasImagesModerated) updateData.in_queue = true;
|
||||
|
||||
if (Object.keys(updateData).length === 0) return rateLimit.sendResponse({ error: "Nothing was changed" }, 400);
|
||||
const updatedMii = await prisma.mii.update({
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ const submitSchema = z
|
|||
makeup: z.enum(MiiMakeup).default("PARTIAL"),
|
||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
youtubeId: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9_-]{11}$/, "Invalid YouTube video ID")
|
||||
.optional(),
|
||||
instructions: switchMiiInstructionsSchema,
|
||||
|
||||
// QR code
|
||||
|
|
@ -108,6 +112,7 @@ export async function POST(request: NextRequest) {
|
|||
makeup: formData.get("makeup") ?? undefined,
|
||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
||||
youtubeId: formData.get("youtubeId"),
|
||||
instructions: minifiedInstructions,
|
||||
|
||||
qrBytesRaw: rawQrBytesRaw,
|
||||
|
|
@ -142,6 +147,7 @@ export async function POST(request: NextRequest) {
|
|||
makeup,
|
||||
miiPortraitImage,
|
||||
miiFeaturesImage,
|
||||
youtubeId,
|
||||
image1,
|
||||
image2,
|
||||
image3,
|
||||
|
|
@ -206,6 +212,7 @@ export async function POST(request: NextRequest) {
|
|||
allowedCopying: conversion.mii.allowCopying,
|
||||
}
|
||||
: {
|
||||
youtubeId,
|
||||
instructions: minifiedInstructions,
|
||||
makeup: makeup ?? "PARTIAL",
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -381,7 +381,28 @@ export default async function MiiPage({ params }: Props) {
|
|||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
{mii.platform === "SWITCH" && <MiiInstructions instructions={mii.instructions as Partial<SwitchMiiInstructions>} />}
|
||||
{mii.platform === "SWITCH" && (
|
||||
<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>
|
||||
|
||||
{mii.youtubeId && (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${mii.youtubeId}`}
|
||||
title="YouTube video player"
|
||||
allow="clipboard-write; encrypted-media;"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
className="aspect-video rounded-2xl w-full max-w-135"
|
||||
></iframe>
|
||||
)}
|
||||
|
||||
<MiiInstructions instructions={mii.instructions as Partial<SwitchMiiInstructions>} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -120,12 +120,7 @@ export default function MiiInstructions({ instructions }: Props) {
|
|||
const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, birthday, 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}>
|
||||
{not(head.skinColor) && (
|
||||
|
|
@ -264,6 +259,6 @@ export default function MiiInstructions({ instructions }: Props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export default function EditForm({ mii, likes }: Props) {
|
|||
const [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "PARTIAL");
|
||||
const [miiPortraitUri, setMiiPortraitUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=mii`);
|
||||
const [miiFeaturesUri, setMiiFeaturesUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=features`);
|
||||
const [youtubeId, setYouTubeId] = useState(mii.youtubeId ?? "");
|
||||
const instructions = useRef<SwitchMiiInstructions>(deepMerge(defaultInstructions, (mii.instructions as object) ?? {}));
|
||||
|
||||
const [quarantined, setQuarantined] = useState(mii.quarantined);
|
||||
|
|
@ -98,6 +99,7 @@ export default function EditForm({ mii, likes }: Props) {
|
|||
if (makeup != mii.makeup) formData.append("makeup", makeup);
|
||||
if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
||||
if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined));
|
||||
if (youtubeId != mii.youtubeId) formData.append("youtubeId", youtubeId);
|
||||
if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object))
|
||||
formData.append("instructions", JSON.stringify(instructions.current));
|
||||
|
||||
|
|
@ -394,6 +396,27 @@ export default function EditForm({ mii, likes }: Props) {
|
|||
<hr className="grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
{/* YouTube */}
|
||||
<div className="w-full grid grid-cols-3 items-center">
|
||||
<label htmlFor="youtube" className="font-semibold">
|
||||
YouTube Video
|
||||
</label>
|
||||
<input
|
||||
id="youtube"
|
||||
type="text"
|
||||
className="pill input w-full col-span-2"
|
||||
minLength={2}
|
||||
maxLength={64}
|
||||
placeholder="Paste a URL or video ID..."
|
||||
value={youtubeId}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const match = val.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
|
||||
setYouTubeId(match ? match[1] : val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MiiEditor instructions={instructions} />
|
||||
<SwitchSubmitTutorialButton />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
const [platform, setPlatform] = useState<MiiPlatform>("SWITCH");
|
||||
const [gender, setGender] = useState<MiiGender>("MALE");
|
||||
const [makeup, setMakeup] = useState<MiiMakeup>("PARTIAL");
|
||||
const [youtubeId, setYouTubeId] = useState("");
|
||||
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
||||
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
|
@ -108,6 +109,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
formData.append("makeup", makeup);
|
||||
formData.append("miiPortraitImage", portraitBlob);
|
||||
formData.append("miiFeaturesImage", featuresBlob);
|
||||
formData.append("youtubeId", youtubeId);
|
||||
formData.append("instructions", JSON.stringify(instructions.current));
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +301,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
</div>
|
||||
|
||||
{/* Gender (switch only) */}
|
||||
<div className={`w-full grid grid-cols-3 items-start z-10 ${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||
<div className={`w-full grid grid-cols-3 items-start z-20 ${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||
<label htmlFor="gender" className="font-semibold py-2">
|
||||
Gender
|
||||
</label>
|
||||
|
|
@ -354,7 +356,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
type="button"
|
||||
onClick={() => setMakeup("FULL")}
|
||||
aria-label="Full Face Paint"
|
||||
data-tooltip="Full Face Paint"
|
||||
data-tooltip="Face covered more than 80%"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
|
||||
makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
|
|
@ -367,7 +369,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
type="button"
|
||||
onClick={() => setMakeup("PARTIAL")}
|
||||
aria-label="Partial Face Paint"
|
||||
data-tooltip="Partial Face Paint"
|
||||
data-tooltip="For at least any face paint"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
|
||||
makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
|
|
@ -481,6 +483,27 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{/* YouTube */}
|
||||
<div className="w-full grid grid-cols-3 items-center">
|
||||
<label htmlFor="youtube" className="font-semibold">
|
||||
YouTube Video
|
||||
</label>
|
||||
<input
|
||||
id="youtube"
|
||||
type="text"
|
||||
className="pill input w-full col-span-2"
|
||||
minLength={2}
|
||||
maxLength={64}
|
||||
placeholder="Paste a URL or video ID..."
|
||||
value={youtubeId}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const match = val.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
|
||||
setYouTubeId(match ? match[1] : val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MiiEditor instructions={instructions} />
|
||||
<SwitchSubmitTutorialButton />
|
||||
<span className="text-xs text-zinc-400 text-center px-32 max-sm:px-8">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue