diff --git a/.env.example b/.env.example index 9018594..8911495 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,8 @@ AUTH_DISCORD_ID=XXXXXXXXXXXXXXXX AUTH_DISCORD_SECRET=XXXXXXXXXXXXXXXX AUTH_GITHUB_ID=XXXXXXXXXXXXXXXX AUTH_GITHUB_SECRET=XXXXXXXXXXXXXXXX +AUTH_GOOGLE_ID=XXXXXXXXXXXXXXXX +AUTH_GOOGLE_SECRET=XXXXXXXXXXXXXXXX # Currently only supports one admin NEXT_PUBLIC_ADMIN_USER_ID=1 @@ -25,4 +27,4 @@ NEXT_PUBLIC_ADMIN_USER_ID=1 NEXT_PUBLIC_CONTRIBUTORS_USER_IDS=176 # Sends notifications (such as admin reports) to ntfy -NTFY_URL="https://ntfy.yourdomain.com/tomodachi-share" \ No newline at end of file +NTFY_URL="https://ntfy.yourdomain.com/tomodachi-share" diff --git a/next.config.ts b/next.config.ts index 9b88254..25ed264 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,7 +4,34 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", images: { - unoptimized: true, + localPatterns: [ + { + pathname: "/mii/*/image", + }, + { + pathname: "/profile/*/picture", + }, + { + pathname: "/tutorial/**", + }, + { + pathname: "/guest.webp", + }, + ], + remotePatterns: [ + { + hostname: "avatars.githubusercontent.com", + }, + { + hostname: "cdn.discordapp.com", + }, + { + hostname: "studio.mii.nintendo.com", + }, + { + hostname: "*.googleusercontent.com", + }, + ], }, }; diff --git a/prisma/migrations/20260326221722_bad_quality_report/migration.sql b/prisma/migrations/20260326221722_bad_quality_report/migration.sql new file mode 100644 index 0000000..cbc949c --- /dev/null +++ b/prisma/migrations/20260326221722_bad_quality_report/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ReportReason" ADD VALUE 'BAD_QUALITY'; diff --git a/prisma/migrations/20260328112742_makeup_filter/migration.sql b/prisma/migrations/20260328112742_makeup_filter/migration.sql new file mode 100644 index 0000000..c331452 --- /dev/null +++ b/prisma/migrations/20260328112742_makeup_filter/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "MiiMakeup" AS ENUM ('FULL', 'PARTIAL', 'NONE'); + +-- AlterTable +ALTER TABLE "miis" ADD COLUMN "makeup" "MiiMakeup"; diff --git a/prisma/migrations/20260328144523_longer_descriptions/migration.sql b/prisma/migrations/20260328144523_longer_descriptions/migration.sql new file mode 100644 index 0000000..d40eb70 --- /dev/null +++ b/prisma/migrations/20260328144523_longer_descriptions/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "miis" ALTER COLUMN "description" SET DATA TYPE VARCHAR(512); + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "description" SET DATA TYPE VARCHAR(512); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a5aeeba..72d1d89 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,7 @@ model User { email String @unique emailVerified DateTime? image String? - description String? @db.VarChar(256) + description String? @db.VarChar(512) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -73,13 +73,15 @@ model Mii { name String @db.VarChar(64) imageCount Int @default(0) tags String[] - description String? @db.VarChar(256) + description String? @db.VarChar(512) platform MiiPlatform @default(THREE_DS) - instructions Json? + instructions Json? + gender MiiGender? + makeup MiiMakeup? + firstName String? lastName String? - gender MiiGender? islandName String? allowedCopying Boolean? @@ -166,6 +168,12 @@ enum MiiGender { NONBINARY } +enum MiiMakeup { + FULL + PARTIAL + NONE +} + enum ReportType { MII USER @@ -175,6 +183,7 @@ enum ReportReason { INAPPROPRIATE SPAM COPYRIGHT + BAD_QUALITY OTHER } diff --git a/src/app/api/mii/[id]/edit/route.ts b/src/app/api/mii/[id]/edit/route.ts index b2ed57b..e4987cf 100644 --- a/src/app/api/mii/[id]/edit/route.ts +++ b/src/app/api/mii/[id]/edit/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import * as Sentry from "@sentry/nextjs"; import { z } from "zod"; -import { Mii, Prisma } from "@prisma/client"; +import { Mii, MiiMakeup, Prisma } from "@prisma/client"; import fs from "fs/promises"; import path from "path"; @@ -22,7 +22,8 @@ const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); const editSchema = z.object({ name: nameSchema.optional(), tags: tagsSchema.optional(), - description: z.string().trim().max(256).optional(), + description: z.string().trim().max(512).optional(), + makeup: z.enum(MiiMakeup).optional(), instructions: switchMiiInstructionsSchema, image1: z.union([z.instanceof(File), z.any()]).optional(), image2: z.union([z.instanceof(File), z.any()]).optional(), @@ -74,6 +75,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< name: formData.get("name") ?? undefined, tags: rawTags, description: formData.get("description") ?? undefined, + makeup: formData.get("makeup") ?? undefined, instructions: minifiedInstructions, image1: formData.get("image1"), image2: formData.get("image2"), @@ -81,7 +83,7 @@ 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, instructions, image1, image2, image3 } = parsed.data; + const { name, tags, description, makeup, instructions, image1, image2, image3 } = parsed.data; // Validate image files const images: File[] = []; @@ -102,6 +104,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< if (name !== undefined) updateData.name = profanity.censor(name); // Censor potentially inappropriate words if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t)); if (description !== undefined) updateData.description = profanity.censor(description); + if (makeup !== undefined) updateData.makeup = makeup; if (instructions !== undefined) updateData.instructions = instructions; if (images.length > 0) updateData.imageCount = images.length; diff --git a/src/app/api/report/route.ts b/src/app/api/report/route.ts index 3897082..4973e12 100644 --- a/src/app/api/report/route.ts +++ b/src/app/api/report/route.ts @@ -10,8 +10,8 @@ import { RateLimit } from "@/lib/rate-limit"; const reportSchema = z.object({ 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'", + reason: z.enum(["inappropriate", "spam", "copyright", "bad_quality", "other"], { + message: "Reason must be either 'inappropriate', 'spam', 'copyright', 'bad_quality' or 'other'", }), notes: z.string().trim().max(256).optional(), }); diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index 3dadc43..136477f 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -8,7 +8,7 @@ import sharp from "sharp"; import qrcode from "qrcode-generator"; import { profanity } from "@2toad/profanity"; -import { MiiGender, MiiPlatform } from "@prisma/client"; +import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; @@ -29,10 +29,11 @@ const submitSchema = z platform: z.enum(MiiPlatform).default("THREE_DS"), name: nameSchema, tags: tagsSchema, - description: z.string().trim().max(256).optional(), + description: z.string().trim().max(512).optional(), // Switch gender: z.enum(MiiGender).default("MALE"), + makeup: z.enum(MiiMakeup).default("PARTIAL"), miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(), miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(), instructions: switchMiiInstructionsSchema, @@ -106,6 +107,7 @@ export async function POST(request: NextRequest) { description: formData.get("description"), gender: formData.get("gender") ?? undefined, // ZOD MOMENT + makeup: formData.get("makeup") ?? undefined, miiPortraitImage: formData.get("miiPortraitImage"), miiFeaturesImage: formData.get("miiFeaturesImage"), instructions: minifiedInstructions, @@ -139,6 +141,7 @@ export async function POST(request: NextRequest) { description: uncensoredDescription, qrBytesRaw, gender, + makeup, miiPortraitImage, miiFeaturesImage, image1, @@ -209,6 +212,7 @@ export async function POST(request: NextRequest) { } : { instructions: minifiedInstructions, + makeup: makeup ?? "PARTIAL", }), }, }); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1a83b13..09fe591 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,6 +11,8 @@ import Providers from "./provider"; import Header from "@/components/header"; import Footer from "@/components/footer"; import AdminBanner from "@/components/admin/banner"; +import { SessionProvider } from "next-auth/react"; +import { Suspense } from "react"; const lexend = Lexend({ subsets: ["latin"], @@ -91,7 +93,11 @@ export default function RootLayout({ )} -
+ Loading header...}> + +
+ +
{children}