chore: update packages

also migrate zod to v4
This commit is contained in:
trafficlunar 2025-07-14 13:03:19 +01:00
parent afb73ec3a6
commit 8b4842b584
20 changed files with 918 additions and 922 deletions

View file

@ -13,10 +13,10 @@
},
"dependencies": {
"@2toad/profanity": "^3.1.1",
"@auth/prisma-adapter": "2.9.1",
"@auth/prisma-adapter": "2.10.0",
"@bprogress/next": "^3.2.12",
"@hello-pangea/dnd": "^18.0.1",
"@prisma/client": "^6.9.0",
"@prisma/client": "^6.11.1",
"@types/sjcl": "^1.0.34",
"bit-buffer": "^0.2.5",
"canvas-confetti": "^1.9.3",
@ -26,32 +26,32 @@
"file-type": "^21.0.0",
"ioredis": "^5.6.1",
"jsqr": "^1.4.0",
"next": "15.3.3",
"next": "15.3.5",
"next-auth": "5.0.0-beta.25",
"qrcode-generator": "^1.5.0",
"qrcode-generator": "^2.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-webcam": "^7.2.0",
"satori": "^0.15.2",
"sharp": "^0.34.2",
"sharp": "^0.34.3",
"sjcl-with-all": "1.0.8",
"swr": "^2.3.3",
"zod": "^3.25.63"
"swr": "^2.3.4",
"zod": "^4.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@iconify/react": "^6.0.0",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/postcss": "^4.1.11",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^24.0.1",
"@types/node": "^24.0.13",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"eslint": "^9.28.0",
"eslint-config-next": "15.3.3",
"prisma": "^6.9.0",
"tailwindcss": "^4.1.10",
"eslint": "^9.31.0",
"eslint-config-next": "15.3.5",
"prisma": "^6.11.1",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"vitest": "^3.2.3"
"vitest": "^3.2.4"
}
}

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsed = idSchema.safeParse(searchParams.get("id"));
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
const userId = parsed.data;
const user = await prisma.user.findUnique({

View file

@ -11,18 +11,15 @@ import { PunishmentType } from "@prisma/client";
const punishSchema = z.object({
type: z.enum([PunishmentType.WARNING, PunishmentType.TEMP_EXILE, PunishmentType.PERM_EXILE]),
duration: z
.number({ message: "Duration (days) must be a number" })
.int({ message: "Duration (days) must be an integer" })
.positive({ message: "Duration (days) must be valid" }),
.number({ error: "Duration (days) must be a number" })
.int({ error: "Duration (days) must be an integer" })
.positive({ error: "Duration (days) must be valid" }),
notes: z.string(),
reasons: z.array(z.string()).optional(),
miiReasons: z
.array(
z.object({
id: z
.number({ message: "Mii ID must be a number" })
.int({ message: "Mii ID must be an integer" })
.positive({ message: "Mii ID must be valid" }),
id: z.number({ error: "Mii ID must be a number" }).int({ error: "Mii ID must be an integer" }).positive({ error: "Mii ID must be valid" }),
reason: z.string(),
})
)
@ -38,13 +35,13 @@ export async function POST(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsedUserId = idSchema.safeParse(searchParams.get("id"));
if (!parsedUserId.success) return NextResponse.json({ error: parsedUserId.error.errors[0].message }, { status: 400 });
if (!parsedUserId.success) return NextResponse.json({ error: parsedUserId.error.issues[0].message }, { status: 400 });
const userId = parsedUserId.data;
const body = await request.json();
const parsed = punishSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
const { type, duration, notes, reasons, miiReasons } = parsed.data;
const expiresAt = type === "TEMP_EXILE" ? dayjs().add(duration, "days").toDate() : null;
@ -77,7 +74,7 @@ export async function DELETE(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsedPunishmentId = idSchema.safeParse(searchParams.get("id"));
if (!parsedPunishmentId.success) return NextResponse.json({ error: parsedPunishmentId.error.errors[0].message }, { status: 400 });
if (!parsedPunishmentId.success) return NextResponse.json({ error: parsedPunishmentId.error.issues[0].message }, { status: 400 });
const punishmentId = parsedPunishmentId.data;
await prisma.punishment.delete({

View file

@ -18,7 +18,7 @@ export async function PATCH(request: NextRequest) {
if (!displayName) return rateLimit.sendResponse({ error: "New display name is required" }, 400);
const validation = displayNameSchema.safeParse(displayName);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.errors[0].message }, 400);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.issues[0].message }, 400);
// Check for inappropriate words
if (profanity.exists(displayName)) return rateLimit.sendResponse({ error: "Display name contains inappropriate words" }, 400);

View file

@ -40,7 +40,7 @@ export async function PATCH(request: NextRequest) {
image: formData.get("image"),
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { image } = parsed.data;
// If there is no image, set the profile picture to the guest image

View file

@ -29,7 +29,7 @@ export async function PATCH(request: NextRequest) {
}
const validation = usernameSchema.safeParse(username);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.errors[0].message }, 400);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.issues[0].message }, 400);
// Check for inappropriate words
if (profanity.exists(username)) return rateLimit.sendResponse({ error: "Username contains inappropriate words" }, 400);

View file

@ -20,7 +20,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data;
// Check ownership of Mii

View file

@ -37,7 +37,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
// Get Mii ID
const { id: slugId } = await params;
const parsedId = idSchema.safeParse(slugId);
if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.errors[0].message }, 400);
if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.issues[0].message }, 400);
const miiId = parsedId.data;
// Check ownership of Mii
@ -78,7 +78,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
image3: formData.get("image3"),
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { name, tags, description, image1, image2, image3 } = parsed.data;
// Validate image files

View file

@ -15,7 +15,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data;
const result = await prisma.$transaction(async (tx) => {

View file

@ -8,8 +8,8 @@ import { RateLimit } from "@/lib/rate-limit";
import { MiiWithUsername } from "@/types";
const reportSchema = z.object({
id: z.coerce.number({ message: "ID must be a number" }).int({ message: "ID must be an integer" }).positive({ message: "ID must be valid" }),
type: z.enum(["mii", "user"], { message: "Type must be either 'mii' or 'user'" }),
id: z.coerce.number({ error: "ID must be a number" }).int({ error: "ID must be an integer" }).positive({ error: "ID must be valid" }),
type: z.enum(["mii", "user"], { error: "Type must be either 'mii' or 'user'" }),
reason: z.enum(["inappropriate", "spam", "copyright", "other"], {
message: "Reason must be either 'inappropriate', 'spam', 'copyright', or 'other'",
}),
@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = reportSchema.safeParse(body);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { id, type, reason, notes } = parsed.data;
let mii: MiiWithUsername | null = null;

View file

@ -25,9 +25,7 @@ const submitSchema = z.object({
name: nameSchema,
tags: tagsSchema,
description: z.string().trim().max(256).optional(),
qrBytesRaw: z
.array(z.number(), { required_error: "A QR code is required" })
.length(372, { message: "QR code size is not a valid Tomodachi Life QR code" }),
qrBytesRaw: z.array(z.number(), { error: "A QR code is required" }).length(372, { error: "QR code size is not a valid Tomodachi Life QR code" }),
image1: z.union([z.instanceof(File), z.any()]).optional(),
image2: z.union([z.instanceof(File), z.any()]).optional(),
image3: z.union([z.instanceof(File), z.any()]).optional(),
@ -67,7 +65,7 @@ export async function POST(request: NextRequest) {
image3: formData.get("image3"),
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data;
// Censor potential inappropriate words

View file

@ -25,11 +25,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data;
const searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.errors[0].message }, 400);
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
const { type: imageType } = searchParamsParsed.data;
const fileExtension = imageType === "metadata" ? ".png" : ".webp";

View file

@ -13,7 +13,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const userId = parsed.data;
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`);

View file

@ -36,15 +36,15 @@ const searchSchema = z.object({
// todo: incorporate tagsSchema
// Pages
limit: z.coerce
.number({ message: "Limit must be a number" })
.int({ message: "Limit must be an integer" })
.min(1, { message: "Limit must be at least 1" })
.max(100, { message: "Limit cannot be more than 100" })
.number({ error: "Limit must be a number" })
.int({ error: "Limit must be an integer" })
.min(1, { error: "Limit must be at least 1" })
.max(100, { error: "Limit cannot be more than 100" })
.optional(),
page: z.coerce
.number({ message: "Page must be a number" })
.int({ message: "Page must be an integer" })
.min(1, { message: "Page must be at least 1" })
.number({ error: "Page must be a number" })
.int({ error: "Page must be an integer" })
.min(1, { error: "Page must be at least 1" })
.optional(),
});
@ -52,7 +52,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
const session = await auth();
const parsed = searchSchema.safeParse(searchParams);
if (!parsed.success) return <h1>{parsed.error.errors[0].message}</h1>;
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
const { q: query, sort, tags, page = 1, limit = 24 } = parsed.data;

View file

@ -24,7 +24,7 @@ export default function ProfileSettings() {
const handleSubmitDisplayNameChange = async (close: () => void) => {
const parsed = displayNameSchema.safeParse(displayName);
if (!parsed.success) {
setDisplayNameChangeError(parsed.error.errors[0].message);
setDisplayNameChangeError(parsed.error.issues[0].message);
return;
}
@ -47,7 +47,7 @@ export default function ProfileSettings() {
const handleSubmitUsernameChange = async (close: () => void) => {
const parsed = usernameSchema.safeParse(username);
if (!parsed.success) {
setUsernameChangeError(parsed.error.errors[0].message);
setUsernameChangeError(parsed.error.issues[0].message);
return;
}

View file

@ -44,12 +44,12 @@ export default function EditForm({ mii, likes }: Props) {
// Validate before sending request
const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) {
setError(nameValidation.error.errors[0].message);
setError(nameValidation.error.issues[0].message);
return;
}
const tagsValidation = tagsSchema.safeParse(tags);
if (!tagsValidation.success) {
setError(tagsValidation.error.errors[0].message);
setError(tagsValidation.error.issues[0].message);
return;
}

View file

@ -49,12 +49,12 @@ export default function SubmitForm() {
// Validate before sending request
const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) {
setError(nameValidation.error.errors[0].message);
setError(nameValidation.error.issues[0].message);
return;
}
const tagsValidation = tagsSchema.safeParse(tags);
if (!tagsValidation.success) {
setError(tagsValidation.error.errors[0].message);
setError(tagsValidation.error.issues[0].message);
return;
}

View file

@ -11,7 +11,7 @@ export default function UsernameForm() {
const handleSubmit = async () => {
const parsed = usernameSchema.safeParse(username);
if (!parsed.success) setError(parsed.error.errors[0].message);
if (!parsed.success) setError(parsed.error.issues[0].message);
const response = await fetch("/api/auth/username", {
method: "PATCH",

View file

@ -5,39 +5,39 @@ import { z } from "zod";
export const querySchema = z
.string()
.trim()
.min(2, { message: "Search query must be at least 2 characters long" })
.max(64, { message: "Search query cannot be more than 64 characters long" })
.min(2, { error: "Search query must be at least 2 characters long" })
.max(64, { error: "Search query cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Search query can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
error: "Search query can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
});
// Miis
export const nameSchema = z
.string()
.trim()
.min(2, { message: "Name must be at least 2 characters long" })
.max(64, { message: "Name cannot be more than 64 characters long" })
.min(2, { error: "Name must be at least 2 characters long" })
.max(64, { error: "Name cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
error: "Name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
});
export const tagsSchema = z
.array(
z
.string()
.min(2, { message: "Tags must be at least 2 characters long" })
.max(64, { message: "Tags cannot be more than 20 characters long" })
.min(2, { error: "Tags must be at least 2 characters long" })
.max(64, { error: "Tags cannot be more than 20 characters long" })
.regex(/^[a-z0-9-_]+$/, {
message: "Tags can only contain lowercase letters, numbers, dashes, and underscores.",
error: "Tags can only contain lowercase letters, numbers, dashes, and underscores.",
})
)
.min(1, { message: "There must be at least 1 tag" })
.max(8, { message: "There cannot be more than 8 tags" });
.min(1, { error: "There must be at least 1 tag" })
.max(8, { error: "There cannot be more than 8 tags" });
export const idSchema = z.coerce
.number({ message: "ID must be a number" })
.int({ message: "ID must be an integer" })
.positive({ message: "ID must be valid" });
.number({ error: "ID must be a number" })
.int({ error: "ID must be an integer" })
.positive({ error: "ID must be valid" });
// Account Info
export const usernameSchema = z
@ -50,8 +50,8 @@ export const usernameSchema = z
export const displayNameSchema = z
.string()
.trim()
.min(2, { message: "Display name must be at least 2 characters long" })
.max(64, { message: "Display name cannot be more than 64 characters long" })
.min(2, { error: "Display name must be at least 2 characters long" })
.max(64, { error: "Display name cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Display name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
error: "Display name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
});