feat: metadata images for switch platform

also some other changes
This commit is contained in:
trafficlunar 2026-02-28 12:33:36 +00:00
parent e31141ea39
commit 5995afe3db
5 changed files with 71 additions and 50 deletions

View file

@ -91,6 +91,34 @@ export async function POST(request: NextRequest) {
return rateLimit.sendResponse({ error: "Invalid JSON in tags or QR code data" }, 400); return rateLimit.sendResponse({ error: "Invalid JSON in tags or QR code data" }, 400);
} }
// Minify instructions to save space and improve user experience
let minifiedInstructions: Partial<SwitchMiiInstructions> | undefined;
if (formData.get("platform") === "SWITCH") {
function minify(object: Partial<SwitchMiiInstructions>): Partial<SwitchMiiInstructions> {
for (const key in object) {
const value = object[key as keyof SwitchMiiInstructions];
if (!value) {
delete object[key as keyof SwitchMiiInstructions];
continue;
}
// Recurse into nested objects
if (typeof value === "object") {
minify(value as Partial<SwitchMiiInstructions>);
if (Object.keys(value).length === 0) {
delete object[key as keyof SwitchMiiInstructions];
}
}
}
return object;
}
minifiedInstructions = minify(JSON.parse((formData.get("instructions") as string) ?? "{}") as SwitchMiiInstructions);
}
// Parse and check all submission info // Parse and check all submission info
const parsed = submitSchema.safeParse({ const parsed = submitSchema.safeParse({
platform: formData.get("platform"), platform: formData.get("platform"),
@ -100,7 +128,7 @@ export async function POST(request: NextRequest) {
gender: formData.get("gender") ?? undefined, // ZOD MOMENT gender: formData.get("gender") ?? undefined, // ZOD MOMENT
miiPortraitImage: formData.get("miiPortraitImage"), miiPortraitImage: formData.get("miiPortraitImage"),
instructions: JSON.parse((formData.get("instructions") as string) ?? {}), instructions: minifiedInstructions,
qrBytesRaw: rawQrBytesRaw, qrBytesRaw: rawQrBytesRaw,
@ -156,35 +184,9 @@ export async function POST(request: NextRequest) {
} }
// Check Mii portrait image as well (Switch) // Check Mii portrait image as well (Switch)
let minifiedInstructions: Partial<SwitchMiiInstructions>;
if (platform === "SWITCH") { if (platform === "SWITCH") {
const imageValidation = await validateImage(miiPortraitImage); const imageValidation = await validateImage(miiPortraitImage);
if (!imageValidation.valid) return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400); if (!imageValidation.valid) return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400);
// Minimize instructions to save space and improve user experience
function minimize(object: Partial<SwitchMiiInstructions>): Partial<SwitchMiiInstructions> {
for (const key in object) {
const value = object[key as keyof SwitchMiiInstructions];
if (!value) {
delete object[key as keyof SwitchMiiInstructions];
continue;
}
// Recurse into nested objects
if (typeof value === "object") {
minimize(value as Partial<SwitchMiiInstructions>);
if (Object.keys(value).length === 0) {
delete object[key as keyof SwitchMiiInstructions];
}
}
}
return object;
}
minifiedInstructions = minimize(instructions as SwitchMiiInstructions);
} }
const qrBytes = new Uint8Array(qrBytesRaw ?? []); const qrBytes = new Uint8Array(qrBytesRaw ?? []);
@ -220,7 +222,7 @@ export async function POST(request: NextRequest) {
allowedCopying: conversion.mii.allowCopying, allowedCopying: conversion.mii.allowCopying,
} }
: { : {
instructions, instructions: minifiedInstructions,
}), }),
}, },
}); });

View file

@ -128,7 +128,7 @@ export default async function MiiPage({ params }: Props) {
alt="mii headshot" alt="mii headshot"
width={250} width={250}
height={250} height={250}
className="drop-shadow-lg hover:scale-105 transition-transform" className="drop-shadow-lg hover:scale-105 transition-transform w-full max-h-96 object-contain"
/> />
</div> </div>
{/* QR Code */} {/* QR Code */}
@ -303,6 +303,9 @@ export default async function MiiPage({ params }: Props) {
</Link> </Link>
{mii.platform === "THREE_DS" ? <ThreeDsScanTutorialButton /> : <SwitchScanTutorialButton />} {mii.platform === "THREE_DS" ? <ThreeDsScanTutorialButton /> : <SwitchScanTutorialButton />}
</div> </div>
{/* Instructions */}
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4">{JSON.stringify(mii.instructions)}</div>
</div> </div>
</div> </div>

View file

@ -173,15 +173,13 @@ export default function SubmitForm() {
} }
// Convert QR code to JS (3DS) // Convert QR code to JS (3DS)
if (platform === "THREE_DS") { let conversion: { mii: Mii; tomodachiLifeMii: ThreeDsTomodachiLifeMii };
let conversion: { mii: Mii; tomodachiLifeMii: ThreeDsTomodachiLifeMii }; try {
try { conversion = convertQrCode(qrBytes);
conversion = convertQrCode(qrBytes); setMiiPortraitUri(conversion.mii.studioUrl({ width: 512 }));
setMiiPortraitUri(conversion.mii.studioUrl({ width: 512 })); } catch (error) {
} catch (error) { setError(error instanceof Error ? error.message : String(error));
setError(error instanceof Error ? error.message : String(error)); return;
return;
}
} }
// Generate a new QR code for aesthetic reasons // Generate a new QR code for aesthetic reasons

View file

@ -23,6 +23,7 @@ export default function GlassesTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto"> <div className="flex justify-center h-74 mt-auto">
<TypeSelector <TypeSelector
hasNoneOption
length={50} length={50}
type={type} type={type}
setType={(i) => { setType={(i) => {

View file

@ -134,31 +134,38 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
fs.readFile(path.join(miiUploadsDirectory, "mii.webp")).then((buffer) => fs.readFile(path.join(miiUploadsDirectory, "mii.webp")).then((buffer) =>
sharp(buffer) sharp(buffer)
.png() .png()
// extend to fix shadow bug on landscape pictures
.extend({
left: 16,
right: 16,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer() .toBuffer()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`), .then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
), ),
fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) => mii.platform === "THREE_DS"
sharp(buffer) ? fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) =>
.png() sharp(buffer)
.toBuffer() .png()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`), .toBuffer()
), .then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
)
: Promise.resolve(null),
loadFonts(), loadFonts(),
]); ]);
const jsx: ReactNode = ( const jsx: ReactNode = (
<div tw="w-full h-full bg-amber-50 border-2 border-amber-500 rounded-2xl p-4 flex flex-col"> <div tw="w-full h-full bg-amber-50 border-2 border-amber-500 rounded-2xl p-4 flex flex-col">
<div tw="flex w-full"> <div tw="flex w-full">
{/* Mii image */} {/* Mii portrait */}
<div <div
tw="w-80 h-62 rounded-xl flex justify-center mr-2 px-2" tw={`h-62 rounded-xl flex justify-center items-center mr-2 ${mii.platform === "THREE_DS" ? "w-80" : "w-100"}`}
style={{ style={{
backgroundImage: "linear-gradient(to bottom, #fef3c7, #fde68a);", backgroundImage: "linear-gradient(to bottom, #fef3c7, #fde68a);",
}} }}
> >
<img <img
src={miiImage} src={miiImage}
width={248}
height={248} height={248}
tw="w-full h-full" tw="w-full h-full"
style={{ style={{
@ -169,9 +176,19 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
</div> </div>
{/* QR code */} {/* QR code */}
<div tw="w-60 bg-amber-200 rounded-xl flex justify-center items-center"> {mii.platform === "THREE_DS" ? (
<img src={qrCodeImage} width={190} height={190} tw="border-2 border-amber-300 rounded-lg" /> <div tw="w-60 bg-amber-200 rounded-xl flex justify-center items-center">
</div> <img src={qrCodeImage!} width={190} height={190} tw="border-2 border-amber-300 rounded-lg" />
</div>
) : (
<div tw="w-40 bg-amber-200 rounded-xl flex flex-col justify-center items-center p-6">
<span tw="text-amber-900 font-extrabold text-xl text-center leading-tight">Switch Guide</span>
<p tw="text-amber-800 text-sm text-center mt-1.5">You need to manually create the Mii, visit site for instructions.</p>
<div tw="mt-auto bg-amber-600 rounded-lg w-full py-2 flex justify-center">
<span tw="text-white font-semibold">View Steps</span>
</div>
</div>
)}
</div> </div>
<div tw="flex flex-col w-full h-30 relative"> <div tw="flex flex-col w-full h-30 relative">