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);
}
// 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
const parsed = submitSchema.safeParse({
platform: formData.get("platform"),
@ -100,7 +128,7 @@ export async function POST(request: NextRequest) {
gender: formData.get("gender") ?? undefined, // ZOD MOMENT
miiPortraitImage: formData.get("miiPortraitImage"),
instructions: JSON.parse((formData.get("instructions") as string) ?? {}),
instructions: minifiedInstructions,
qrBytesRaw: rawQrBytesRaw,
@ -156,35 +184,9 @@ export async function POST(request: NextRequest) {
}
// Check Mii portrait image as well (Switch)
let minifiedInstructions: Partial<SwitchMiiInstructions>;
if (platform === "SWITCH") {
const imageValidation = await validateImage(miiPortraitImage);
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 ?? []);
@ -220,7 +222,7 @@ export async function POST(request: NextRequest) {
allowedCopying: conversion.mii.allowCopying,
}
: {
instructions,
instructions: minifiedInstructions,
}),
},
});

View file

@ -128,7 +128,7 @@ export default async function MiiPage({ params }: Props) {
alt="mii headshot"
width={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>
{/* QR Code */}
@ -303,6 +303,9 @@ export default async function MiiPage({ params }: Props) {
</Link>
{mii.platform === "THREE_DS" ? <ThreeDsScanTutorialButton /> : <SwitchScanTutorialButton />}
</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>

View file

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

View file

@ -23,6 +23,7 @@ export default function GlassesTab({ instructions }: Props) {
<div className="flex justify-center h-74 mt-auto">
<TypeSelector
hasNoneOption
length={50}
type={type}
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) =>
sharp(buffer)
.png()
// extend to fix shadow bug on landscape pictures
.extend({
left: 16,
right: 16,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
),
fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) =>
mii.platform === "THREE_DS"
? fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) =>
sharp(buffer)
.png()
.toBuffer()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
),
)
: Promise.resolve(null),
loadFonts(),
]);
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="flex w-full">
{/* Mii image */}
{/* Mii portrait */}
<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={{
backgroundImage: "linear-gradient(to bottom, #fef3c7, #fde68a);",
}}
>
<img
src={miiImage}
width={248}
height={248}
tw="w-full h-full"
style={{
@ -169,9 +176,19 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
</div>
{/* QR code */}
{mii.platform === "THREE_DS" ? (
<div tw="w-60 bg-amber-200 rounded-xl flex justify-center items-center">
<img src={qrCodeImage} width={190} height={190} tw="border-2 border-amber-300 rounded-lg" />
<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 tw="flex flex-col w-full h-30 relative">