From 4275f710b05af0ea0a717ff2ba450491baa10356 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 24 Apr 2026 21:32:53 +0100 Subject: [PATCH] feat: needs fixing miis --- DEVELOPMENT.md | 56 +++++++++---------- .../migration.sql | 2 + backend/prisma/schema.prisma | 1 + backend/src/app/api/mii/[id]/edit/route.ts | 27 ++++++++- backend/src/app/api/mii/list/route.ts | 1 + backend/src/app/random/page.tsx | 2 +- frontend/src/components/mii/list/index.tsx | 20 +++++-- frontend/src/pages/edit.tsx | 19 +++++++ frontend/src/pages/mii.tsx | 10 ++++ 9 files changed, 100 insertions(+), 38 deletions(-) create mode 100644 backend/prisma/migrations/20260424201540_needs_fixing_reason/migration.sql diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 149521f..30373c0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,36 +4,9 @@ This is probably outdated. Welcome to the TomodachiShare development guide! This project uses [pnpm](https://pnpm.io/) for package management, [Next.js](https://nextjs.org/) with the app router for the backend, [Vite with React](https://vite.dev/) for the frontend, [Prisma](https://prisma.io) for the database, [TailwindCSS](https://tailwindcss.com/) for styling, and [TypeScript](https://www.typescriptlang.org/) for type safety. -## Getting started - -To get the project up and running locally, follow these steps: - -```bash -$ git clone https://github.com/trafficlunar/tomodachi-share -$ cd tomodachi-share -$ pnpm install -``` - -Prisma types are generated automatically, however, if you changed the schema or need to trigger a manual refresh: - -```bash -# Generate Prisma client types -$ pnpm --filter backend prisma generate - -# Or, if you've added new database properties -$ pnpm --filter backend prisma migrate dev -$ pnpm --filter backend prisma generate -``` - -I recommend opting out of Next.js' telemetry program but it is not a requirement. - -```bash -$ pnpm --filter backend exec next telemetry disable -``` - ## Environment variables -You'll need a PostgreSQL database and Redis database. I would recommend using [Docker](https://www.docker.com/) to set these up quickly. Just create a `docker-compose.yaml` with the following content and run `docker compose up -d`: +This step needs to be done before installing packages. You'll need a PostgreSQL database and Redis database. I would recommend using [Docker](https://www.docker.com/) to set these up quickly. Just create a `docker-compose.yaml` with the following content and run `docker compose up -d`: ```yaml services: @@ -85,6 +58,33 @@ For GitHub, navigate to your profile settings, then 'Developer Settings', and cr Google is annoying so I'm not explaining it. +## Getting started + +To get the project up and running locally, follow these steps: + +```bash +$ git clone https://github.com/trafficlunar/tomodachi-share +$ cd tomodachi-share +$ pnpm install +``` + +Prisma types are generated automatically, however, if you changed the schema or need to trigger a manual refresh: + +```bash +# Generate Prisma client types +$ pnpm --filter backend prisma generate + +# Or, if you've added new database properties +$ pnpm --filter backend prisma migrate dev +$ pnpm --filter backend prisma generate +``` + +I recommend opting out of Next.js' telemetry program but it is not a requirement. + +```bash +$ pnpm --filter backend exec next telemetry disable +``` + ## Development Server The frontend and backend need to be ran simulatenously, therefore you need two separate terminals. diff --git a/backend/prisma/migrations/20260424201540_needs_fixing_reason/migration.sql b/backend/prisma/migrations/20260424201540_needs_fixing_reason/migration.sql new file mode 100644 index 0000000..59ee506 --- /dev/null +++ b/backend/prisma/migrations/20260424201540_needs_fixing_reason/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "miis" ADD COLUMN "needsFixing" TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index a5ccbae..2cda82c 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -77,6 +77,7 @@ model Mii { platform MiiPlatform @default(THREE_DS) quarantined Boolean @default(false) in_queue Boolean @default(false) + needsFixing String? instructions Json? youtubeId String? diff --git a/backend/src/app/api/mii/[id]/edit/route.ts b/backend/src/app/api/mii/[id]/edit/route.ts index d840838..7de330a 100644 --- a/backend/src/app/api/mii/[id]/edit/route.ts +++ b/backend/src/app/api/mii/[id]/edit/route.ts @@ -26,6 +26,11 @@ const editSchema = z.object({ .enum(["true", "false"]) .transform((v) => v === "true") .optional(), + needsFixingReason: z + .string() + .max(256) + .optional() + .transform((val) => (val === "" ? null : val)), gender: z.enum(MiiGender).optional(), makeup: z.enum(MiiMakeup).optional(), miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(), @@ -86,6 +91,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ tags: rawTags, description: formData.get("description") ?? undefined, quarantined: formData.get("quarantined") ?? undefined, + needsFixingReason: formData.get("needsFixingReason") ?? undefined, gender: formData.get("gender") ?? undefined, makeup: formData.get("makeup") ?? undefined, miiPortraitImage: formData.get("miiPortraitImage"), @@ -103,8 +109,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const error = `${path}: ${firstIssue.message}`; return rateLimit.sendResponse({ error }, 400); } - const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, youtubeId, instructions, image1, image2, image3 } = - parsed.data; + const { + name, + tags, + description, + quarantined, + needsFixingReason, + gender, + makeup, + miiPortraitImage, + miiFeaturesImage, + youtubeId, + instructions, + image1, + image2, + image3, + } = parsed.data; // Validate image files const customImages: File[] = []; @@ -133,7 +153,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } // Prevent non-admins from quarantining Miis - if (quarantined && session.user?.id?.toString() !== process.env.NEXT_PUBLIC_ADMIN_USER_ID) + if (quarantined && needsFixingReason && session.user?.id?.toString() !== process.env.NEXT_PUBLIC_ADMIN_USER_ID) return rateLimit.sendResponse({ error: `You're not an admin!` }, 401); // Edit Mii in database @@ -142,6 +162,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t)); if (description !== undefined) updateData.description = profanity.censor(description); if (quarantined !== undefined) updateData.quarantined = quarantined; + if (needsFixingReason !== undefined) updateData.needsFixing = needsFixingReason; if (mii.platform === "SWITCH" && gender !== undefined) updateData.gender = gender; if (makeup !== undefined) updateData.makeup = makeup; if (youtubeId !== undefined) updateData.youtubeId = youtubeId; diff --git a/backend/src/app/api/mii/list/route.ts b/backend/src/app/api/mii/list/route.ts index 9b3beda..46db2b0 100644 --- a/backend/src/app/api/mii/list/route.ts +++ b/backend/src/app/api/mii/list/route.ts @@ -99,6 +99,7 @@ export async function GET(request: NextRequest) { allowedCopying: true, quarantined: true, in_queue: true, + needsFixing: true, likeCount: true, // Mii liked check ...(session?.user?.id && { diff --git a/backend/src/app/random/page.tsx b/backend/src/app/random/page.tsx index 36bcee9..d5b9b93 100644 --- a/backend/src/app/random/page.tsx +++ b/backend/src/app/random/page.tsx @@ -9,7 +9,7 @@ export default async function RandomPage() { const randomIndex = Math.floor(Math.random() * count); const randomMii = await prisma.mii.findFirst({ - where: { in_queue: false, quarantined: false }, + where: { in_queue: false, quarantined: false, needsFixing: { not: null } }, skip: randomIndex, take: 1, select: { id: true }, diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx index 9f04154..2b146b2 100644 --- a/frontend/src/components/mii/list/index.tsx +++ b/frontend/src/components/mii/list/index.tsx @@ -86,12 +86,20 @@ export default function MiiList({ parentPage, userId, bypassCache }: Props) { key={mii.id} className={`flex flex-col relative bg-zinc-50 rounded-3xl border-2 shadow-lg p-[0.8rem] transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600 ${mii.quarantined ? "border-red-300 bg-red-50!" : mii.in_queue ? "border-zinc-400 opacity-70" : "border-zinc-300"}`} > - {mii.in_queue && ( -
- - In Queue -
- )} +
+ {mii.in_queue && ( +
+ + In Queue +
+ )} + {mii.needsFixing && ( +
+ + Needs Fixing +
+ )} +
(defaultInstructions); const [quarantined, setQuarantined] = useState(false); + const [needsFixingReason, setNeedsFixingReason] = useState(""); const hasCustomImagesChanged = useRef(false); const hasMiiPortraitChanged = useRef(false); const hasMiiFeaturesChanged = useRef(false); @@ -80,6 +81,7 @@ export default function EditMiiPage() { if (makeup != mii.makeup) formData.append("makeup", makeup); if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri); if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined)); + if (needsFixingReason !== mii.needsFixing) formData.append("needsFixingReason", needsFixingReason); if (youtubeId != mii.youtubeId) formData.append("youtubeId", youtubeId); if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object)) formData.append("instructions", JSON.stringify(instructions.current)); @@ -185,6 +187,7 @@ export default function EditMiiPage() { setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`); setYouTubeId(data.youtubeId ?? ""); setQuarantined(data.quarantined); + setNeedsFixingReason(data.needsFixing); instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {}); setLoading(false); }) @@ -297,6 +300,22 @@ export default function EditMiiPage() { setQuarantined(e.target.checked)} /> + +
+ + +
+ setNeedsFixingReason(e.target.value)} + /> +
+
)} diff --git a/frontend/src/pages/mii.tsx b/frontend/src/pages/mii.tsx index a7b5e5b..c3d21e3 100644 --- a/frontend/src/pages/mii.tsx +++ b/frontend/src/pages/mii.tsx @@ -101,6 +101,16 @@ export default function MiiPage() {

)} + {mii.needsFixing && ( +
+ +

+ This Mii won't show up on the main page until fixes are made. +
+ Reason: {mii.needsFixing} +

+
+ )}
{/* Mii Image */}