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}
+
+
+ )}