mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 19:23:15 +00:00
Compare commits
No commits in common. "d0f32333e9c9d7cfe59787e6c85a5a4161dd5f73" and "a6b444fcea0ae9c378a229304930708f07f855ab" have entirely different histories.
d0f32333e9
...
a6b444fcea
10 changed files with 53 additions and 57 deletions
|
|
@ -118,9 +118,7 @@ export async function POST(request: NextRequest) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const firstIssue = parsed.error.issues[0];
|
const error = parsed.error.issues[0].message;
|
||||||
const path = firstIssue.path.length ? firstIssue.path.join(".") : "root";
|
|
||||||
const error = `${path}: ${firstIssue.message}`;
|
|
||||||
const issues = parsed.error.issues;
|
const issues = parsed.error.issues;
|
||||||
const hasInstructionsErrors = issues.some((issue) => issue.path[0] === "instructions");
|
const hasInstructionsErrors = issues.some((issue) => issue.path[0] === "instructions");
|
||||||
|
|
||||||
|
|
@ -161,7 +159,7 @@ export async function POST(request: NextRequest) {
|
||||||
if (imageValidation.valid) {
|
if (imageValidation.valid) {
|
||||||
customImages.push(img);
|
customImages.push(img);
|
||||||
} else {
|
} else {
|
||||||
return rateLimit.sendResponse({ error: `Failed to verify custom image: ${imageValidation.error}` }, imageValidation.status ?? 400);
|
return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,10 +167,8 @@ export async function POST(request: NextRequest) {
|
||||||
if (platform === "SWITCH") {
|
if (platform === "SWITCH") {
|
||||||
const portraitValidation = await validateImage(miiPortraitImage);
|
const portraitValidation = await validateImage(miiPortraitImage);
|
||||||
const featuresValidation = await validateImage(miiFeaturesImage);
|
const featuresValidation = await validateImage(miiFeaturesImage);
|
||||||
if (!portraitValidation.valid)
|
if (!portraitValidation.valid) return rateLimit.sendResponse({ error: portraitValidation.error }, portraitValidation.status ?? 400);
|
||||||
return rateLimit.sendResponse({ error: `Failed to verify portrait: ${portraitValidation.error}` }, portraitValidation.status ?? 400);
|
if (!featuresValidation.valid) return rateLimit.sendResponse({ error: featuresValidation.error }, featuresValidation.status ?? 400);
|
||||||
if (!featuresValidation.valid)
|
|
||||||
return rateLimit.sendResponse({ error: `Failed to verify features: ${featuresValidation.error}` }, featuresValidation.status ?? 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrBytes = new Uint8Array(qrBytesRaw ?? []);
|
const qrBytes = new Uint8Array(qrBytesRaw ?? []);
|
||||||
|
|
@ -246,6 +242,7 @@ export async function POST(request: NextRequest) {
|
||||||
const fileLocation = path.join(miiUploadsDirectory, "mii.png");
|
const fileLocation = path.join(miiUploadsDirectory, "mii.png");
|
||||||
|
|
||||||
await fs.writeFile(fileLocation, pngBuffer);
|
await fs.writeFile(fileLocation, pngBuffer);
|
||||||
|
await generateMetadataImage(miiRecord, session.user?.name!);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up if something went wrong
|
// Clean up if something went wrong
|
||||||
await prisma.mii.delete({ where: { id: miiRecord.id } });
|
await prisma.mii.delete({ where: { id: miiRecord.id } });
|
||||||
|
|
@ -255,13 +252,6 @@ export async function POST(request: NextRequest) {
|
||||||
return rateLimit.sendResponse({ error: "Failed to download/store Mii portrait/features" }, 500);
|
return rateLimit.sendResponse({ error: "Failed to download/store Mii portrait/features" }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await generateMetadataImage(miiRecord, session.user?.name!);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to generate metadata image:", error);
|
|
||||||
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "metadata-image-generation" } });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platform === "THREE_DS") {
|
if (platform === "THREE_DS") {
|
||||||
try {
|
try {
|
||||||
// Generate a new QR code for aesthetic reasons
|
// Generate a new QR code for aesthetic reasons
|
||||||
|
|
|
||||||
|
|
@ -150,13 +150,13 @@ export default function MiiInstructions({ instructions }: Props) {
|
||||||
{eyebrows && <Section name="Eyebrows" instructions={eyebrows}></Section>}
|
{eyebrows && <Section name="Eyebrows" instructions={eyebrows}></Section>}
|
||||||
{eyes && (
|
{eyes && (
|
||||||
<Section name="Eyes" instructions={eyes}>
|
<Section name="Eyes" instructions={eyes}>
|
||||||
<Section isSubSection name="Tab 1" instructions={eyes.main} />
|
<Section isSubSection name="Main" instructions={eyes.main} />
|
||||||
<Section isSubSection name="Tab 2" instructions={eyes.eyelashesTop} />
|
<Section isSubSection name="Eyelashes Top" instructions={eyes.eyelashesTop} />
|
||||||
<Section isSubSection name="Tab 3" instructions={eyes.eyelashesBottom} />
|
<Section isSubSection name="Eyelashes Bottom" instructions={eyes.eyelashesBottom} />
|
||||||
<Section isSubSection name="Tab 4" instructions={eyes.eyelidTop} />
|
<Section isSubSection name="Eyelid Top" instructions={eyes.eyelidTop} />
|
||||||
<Section isSubSection name="Tab 5" instructions={eyes.eyelidBottom} />
|
<Section isSubSection name="Eyelid Bottom" instructions={eyes.eyelidBottom} />
|
||||||
<Section isSubSection name="Tab 6" instructions={eyes.eyeliner} />
|
<Section isSubSection name="Eyeliner" instructions={eyes.eyeliner} />
|
||||||
<Section isSubSection name="Tab 7" instructions={eyes.pupil} />
|
<Section isSubSection name="Pupil" instructions={eyes.pupil} />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
{nose && <Section name="Nose" instructions={nose}></Section>}
|
{nose && <Section name="Nose" instructions={nose}></Section>}
|
||||||
|
|
@ -182,16 +182,16 @@ export default function MiiInstructions({ instructions }: Props) {
|
||||||
)}
|
)}
|
||||||
{other && (
|
{other && (
|
||||||
<Section name="Other" instructions={other}>
|
<Section name="Other" instructions={other}>
|
||||||
<Section isSubSection name="Tab 1" instructions={other.wrinkles1} />
|
<Section isSubSection name="Wrinkles 1" instructions={other.wrinkles1} />
|
||||||
<Section isSubSection name="Tab 2" instructions={other.wrinkles2} />
|
<Section isSubSection name="Wrinkles 2" instructions={other.wrinkles2} />
|
||||||
<Section isSubSection name="Tab 3" instructions={other.beard} />
|
<Section isSubSection name="Beard" instructions={other.beard} />
|
||||||
<Section isSubSection name="Tab 4" instructions={other.moustache}>
|
<Section isSubSection name="Moustache" instructions={other.moustache}>
|
||||||
{other.moustache && other.moustache.isFlipped && <TableCell label="Flipped">{other.moustache.isFlipped ? "Yes" : "No"}</TableCell>}
|
{other.moustache && other.moustache.isFlipped && <TableCell label="Flipped">{other.moustache.isFlipped ? "Yes" : "No"}</TableCell>}
|
||||||
</Section>
|
</Section>
|
||||||
<Section isSubSection name="Tab 5" instructions={other.goatee} />
|
<Section isSubSection name="Goatee" instructions={other.goatee} />
|
||||||
<Section isSubSection name="Tab 6" instructions={other.mole} />
|
<Section isSubSection name="Mole" instructions={other.mole} />
|
||||||
<Section isSubSection name="Tab 7" instructions={other.eyeShadow} />
|
<Section isSubSection name="Eye Shadow" instructions={other.eyeShadow} />
|
||||||
<Section isSubSection name="Tab 8" instructions={other.blush} />
|
<Section isSubSection name="Blush" instructions={other.blush} />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
if (name != mii.name) formData.append("name", name);
|
if (name != mii.name) formData.append("name", name);
|
||||||
if (tags != mii.tags) formData.append("tags", JSON.stringify(tags));
|
if (tags != mii.tags) formData.append("tags", JSON.stringify(tags));
|
||||||
if (description && description != mii.description) formData.append("description", description);
|
if (description && description != mii.description) formData.append("description", description);
|
||||||
|
console.log(minifyInstructions(structuredClone(instructions.current)));
|
||||||
|
console.log(mii.instructions);
|
||||||
if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object))
|
if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object))
|
||||||
formData.append("instructions", JSON.stringify(instructions.current));
|
formData.append("instructions", JSON.stringify(instructions.current));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,8 +336,8 @@ export default function SubmitForm() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={setMiiPortraitUri} forceCrop />
|
<SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={setMiiPortraitUri} hasCrop />
|
||||||
<SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} />
|
<SwitchFileUpload text="a screenshot of your Mii's features here" setImage={setMiiFeaturesUri} />
|
||||||
<SwitchSubmitTutorialButton />
|
<SwitchSubmitTutorialButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ export default function NumberInputs({ target }: Props) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="height"
|
id="height"
|
||||||
min={-15}
|
min={-5}
|
||||||
max={15}
|
max={5}
|
||||||
value={height}
|
value={height}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -44,8 +44,8 @@ export default function NumberInputs({ target }: Props) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="distance"
|
id="distance"
|
||||||
min={-15}
|
min={-5}
|
||||||
max={15}
|
max={5}
|
||||||
value={distance}
|
value={distance}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -65,8 +65,8 @@ export default function NumberInputs({ target }: Props) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="rotation"
|
id="rotation"
|
||||||
min={-15}
|
min={-5}
|
||||||
max={15}
|
max={5}
|
||||||
value={rotation}
|
value={rotation}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -86,8 +86,8 @@ export default function NumberInputs({ target }: Props) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="size"
|
id="size"
|
||||||
min={-15}
|
min={-5}
|
||||||
max={15}
|
max={5}
|
||||||
value={size}
|
value={size}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -107,8 +107,8 @@ export default function NumberInputs({ target }: Props) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="stretch"
|
id="stretch"
|
||||||
min={-15}
|
min={-5}
|
||||||
max={15}
|
max={5}
|
||||||
value={stretch}
|
value={stretch}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export default function EyesTab({ instructions }: Props) {
|
||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
const [colors, setColors] = useState<number[]>(() =>
|
const [colors, setColors] = useState<number[]>(() =>
|
||||||
TABS.map((t) => {
|
TABS.map((t) => {
|
||||||
const entry = instructions.current.eyes[t.name] ?? {};
|
const entry = instructions.current.eyes[t.name];
|
||||||
const color = entry && "color" in entry ? entry.color : null;
|
const color = "color" in entry ? entry.color : null;
|
||||||
return color ?? 122;
|
return color ?? 122;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ export default function OtherTab({ instructions }: Props) {
|
||||||
|
|
||||||
const [colors, setColors] = useState<number[]>(() =>
|
const [colors, setColors] = useState<number[]>(() =>
|
||||||
TABS.map((t) => {
|
TABS.map((t) => {
|
||||||
const entry = instructions.current.other[t.name] ?? {};
|
const entry = instructions.current.other[t.name];
|
||||||
const color = entry && "color" in entry ? entry.color : null;
|
const color = "color" in entry ? entry.color : null;
|
||||||
return color ?? t.defaultColor ?? 0;
|
return color ?? t.defaultColor ?? 0;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ import CropPortrait from "./crop-portrait";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
forceCrop?: boolean;
|
hasCrop?: boolean;
|
||||||
image?: string | undefined;
|
image?: string | undefined;
|
||||||
setImage: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setImage: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SwitchFileUpload({ text, forceCrop, image, setImage }: Props) {
|
export default function SwitchFileUpload({ text, hasCrop = false, image, setImage }: Props) {
|
||||||
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
||||||
const [isCropOpen, setIsCropOpen] = useState(false);
|
const [isCropOpen, setIsCropOpen] = useState(false);
|
||||||
const [hasImage, setHasImage] = useState(false);
|
const [hasImage, setHasImage] = useState(false);
|
||||||
|
|
@ -27,7 +27,7 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
setImage(event.target!.result as string);
|
setImage(event.target!.result as string);
|
||||||
setHasImage(true);
|
setHasImage(true);
|
||||||
if (forceCrop) setIsCropOpen(true);
|
if (hasCrop) setIsCropOpen(true);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
},
|
},
|
||||||
|
|
@ -36,7 +36,7 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCameraOpen) return;
|
if (!isCameraOpen) return;
|
||||||
if (forceCrop) setIsCropOpen(true);
|
if (hasCrop) setIsCropOpen(true);
|
||||||
}, [isCameraOpen]);
|
}, [isCameraOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -61,13 +61,17 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
|
||||||
<Icon icon="mdi:camera" fontSize={20} />
|
<Icon icon="mdi:camera" fontSize={20} />
|
||||||
Use your camera
|
Use your camera
|
||||||
</button>
|
</button>
|
||||||
<button type="button" aria-label="Crop image" onClick={() => setIsCropOpen(true)} className="pill button gap-2">
|
{hasCrop && (
|
||||||
<Icon icon="material-symbols:crop" fontSize={20} />
|
<>
|
||||||
Crop Image
|
<button type="button" aria-label="Crop image" onClick={() => setIsCropOpen(true)} className="pill button gap-2">
|
||||||
</button>
|
<Icon icon="material-symbols:crop" fontSize={20} />
|
||||||
|
Crop Image
|
||||||
|
</button>
|
||||||
|
<CropPortrait isOpen={isCropOpen} setIsOpen={setIsCropOpen} image={image} setImage={setImage} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Camera isOpen={isCameraOpen} setIsOpen={setIsCameraOpen} setImage={setImage} />
|
<Camera isOpen={isCameraOpen} setIsOpen={setIsCameraOpen} setImage={setImage} />
|
||||||
<CropPortrait isOpen={isCropOpen} setIsOpen={setIsCropOpen} image={image} setImage={setImage} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { Mii } from "@prisma/client";
|
||||||
|
|
||||||
const MIN_IMAGE_DIMENSIONS = [128, 128];
|
const MIN_IMAGE_DIMENSIONS = [128, 128];
|
||||||
const MAX_IMAGE_DIMENSIONS = [2000, 2000];
|
const MAX_IMAGE_DIMENSIONS = [2000, 2000];
|
||||||
const MAX_IMAGE_SIZE = 8 * 1024 * 1024; // 8 MB
|
const MAX_IMAGE_SIZE = 4 * 1024 * 1024; // 4 MB
|
||||||
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||||
|
|
||||||
//#region Image validation
|
//#region Image validation
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const userNameSchema = z
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorSchema = z.number().int().min(0).max(152).optional();
|
const colorSchema = z.number().int().min(0).max(152).optional();
|
||||||
const geometrySchema = z.number().int().min(-15).max(15).optional();
|
const geometrySchema = z.number().int().min(-10).max(10).optional();
|
||||||
|
|
||||||
export const switchMiiInstructionsSchema = z
|
export const switchMiiInstructionsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue