mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 05:07:46 +00:00
feat: needs fixing miis
This commit is contained in:
parent
781682e24e
commit
4275f710b0
9 changed files with 100 additions and 38 deletions
|
|
@ -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.
|
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
|
## 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
|
```yaml
|
||||||
services:
|
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.
|
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
|
## Development Server
|
||||||
|
|
||||||
The frontend and backend need to be ran simulatenously, therefore you need two separate terminals.
|
The frontend and backend need to be ran simulatenously, therefore you need two separate terminals.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "miis" ADD COLUMN "needsFixing" TEXT;
|
||||||
|
|
@ -77,6 +77,7 @@ model Mii {
|
||||||
platform MiiPlatform @default(THREE_DS)
|
platform MiiPlatform @default(THREE_DS)
|
||||||
quarantined Boolean @default(false)
|
quarantined Boolean @default(false)
|
||||||
in_queue Boolean @default(false)
|
in_queue Boolean @default(false)
|
||||||
|
needsFixing String?
|
||||||
|
|
||||||
instructions Json?
|
instructions Json?
|
||||||
youtubeId String?
|
youtubeId String?
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ const editSchema = z.object({
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
.optional(),
|
.optional(),
|
||||||
|
needsFixingReason: z
|
||||||
|
.string()
|
||||||
|
.max(256)
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val === "" ? null : val)),
|
||||||
gender: z.enum(MiiGender).optional(),
|
gender: z.enum(MiiGender).optional(),
|
||||||
makeup: z.enum(MiiMakeup).optional(),
|
makeup: z.enum(MiiMakeup).optional(),
|
||||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).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,
|
tags: rawTags,
|
||||||
description: formData.get("description") ?? undefined,
|
description: formData.get("description") ?? undefined,
|
||||||
quarantined: formData.get("quarantined") ?? undefined,
|
quarantined: formData.get("quarantined") ?? undefined,
|
||||||
|
needsFixingReason: formData.get("needsFixingReason") ?? undefined,
|
||||||
gender: formData.get("gender") ?? undefined,
|
gender: formData.get("gender") ?? undefined,
|
||||||
makeup: formData.get("makeup") ?? undefined,
|
makeup: formData.get("makeup") ?? undefined,
|
||||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||||
|
|
@ -103,8 +109,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||||
const error = `${path}: ${firstIssue.message}`;
|
const error = `${path}: ${firstIssue.message}`;
|
||||||
return rateLimit.sendResponse({ error }, 400);
|
return rateLimit.sendResponse({ error }, 400);
|
||||||
}
|
}
|
||||||
const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, youtubeId, instructions, image1, image2, image3 } =
|
const {
|
||||||
parsed.data;
|
name,
|
||||||
|
tags,
|
||||||
|
description,
|
||||||
|
quarantined,
|
||||||
|
needsFixingReason,
|
||||||
|
gender,
|
||||||
|
makeup,
|
||||||
|
miiPortraitImage,
|
||||||
|
miiFeaturesImage,
|
||||||
|
youtubeId,
|
||||||
|
instructions,
|
||||||
|
image1,
|
||||||
|
image2,
|
||||||
|
image3,
|
||||||
|
} = parsed.data;
|
||||||
|
|
||||||
// Validate image files
|
// Validate image files
|
||||||
const customImages: File[] = [];
|
const customImages: File[] = [];
|
||||||
|
|
@ -133,7 +153,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent non-admins from quarantining Miis
|
// 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);
|
return rateLimit.sendResponse({ error: `You're not an admin!` }, 401);
|
||||||
|
|
||||||
// Edit Mii in database
|
// 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 (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t));
|
||||||
if (description !== undefined) updateData.description = profanity.censor(description);
|
if (description !== undefined) updateData.description = profanity.censor(description);
|
||||||
if (quarantined !== undefined) updateData.quarantined = quarantined;
|
if (quarantined !== undefined) updateData.quarantined = quarantined;
|
||||||
|
if (needsFixingReason !== undefined) updateData.needsFixing = needsFixingReason;
|
||||||
if (mii.platform === "SWITCH" && gender !== undefined) updateData.gender = gender;
|
if (mii.platform === "SWITCH" && gender !== undefined) updateData.gender = gender;
|
||||||
if (makeup !== undefined) updateData.makeup = makeup;
|
if (makeup !== undefined) updateData.makeup = makeup;
|
||||||
if (youtubeId !== undefined) updateData.youtubeId = youtubeId;
|
if (youtubeId !== undefined) updateData.youtubeId = youtubeId;
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ export async function GET(request: NextRequest) {
|
||||||
allowedCopying: true,
|
allowedCopying: true,
|
||||||
quarantined: true,
|
quarantined: true,
|
||||||
in_queue: true,
|
in_queue: true,
|
||||||
|
needsFixing: true,
|
||||||
likeCount: true,
|
likeCount: true,
|
||||||
// Mii liked check
|
// Mii liked check
|
||||||
...(session?.user?.id && {
|
...(session?.user?.id && {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default async function RandomPage() {
|
||||||
|
|
||||||
const randomIndex = Math.floor(Math.random() * count);
|
const randomIndex = Math.floor(Math.random() * count);
|
||||||
const randomMii = await prisma.mii.findFirst({
|
const randomMii = await prisma.mii.findFirst({
|
||||||
where: { in_queue: false, quarantined: false },
|
where: { in_queue: false, quarantined: false, needsFixing: { not: null } },
|
||||||
skip: randomIndex,
|
skip: randomIndex,
|
||||||
take: 1,
|
take: 1,
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,20 @@ export default function MiiList({ parentPage, userId, bypassCache }: Props) {
|
||||||
key={mii.id}
|
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"}`}
|
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 && (
|
<div className="absolute top-2 left-2 z-10 flex flex-col gap-1">
|
||||||
<div className="absolute top-2 left-2 z-10 bg-zinc-500 text-white text-xs font-semibold px-2 py-1 rounded-full shadow-sm flex items-center gap-1">
|
{mii.in_queue && (
|
||||||
<Icon icon="mdi:clock-outline" className="text-base" />
|
<div className="bg-zinc-500 text-white text-xs font-semibold px-2 py-1 rounded-full shadow-sm flex items-center gap-1 w-fit">
|
||||||
In Queue
|
<Icon icon="mdi:clock-outline" className="text-base" />
|
||||||
</div>
|
In Queue
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{mii.needsFixing && (
|
||||||
|
<div className="bg-orange-500 text-white text-xs font-semibold px-2 py-1 rounded-full shadow-sm flex items-center gap-1 w-fit">
|
||||||
|
<Icon icon="mdi:alert-outline" className="text-base" />
|
||||||
|
Needs Fixing
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export default function EditMiiPage() {
|
||||||
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
||||||
|
|
||||||
const [quarantined, setQuarantined] = useState(false);
|
const [quarantined, setQuarantined] = useState(false);
|
||||||
|
const [needsFixingReason, setNeedsFixingReason] = useState("");
|
||||||
const hasCustomImagesChanged = useRef(false);
|
const hasCustomImagesChanged = useRef(false);
|
||||||
const hasMiiPortraitChanged = useRef(false);
|
const hasMiiPortraitChanged = useRef(false);
|
||||||
const hasMiiFeaturesChanged = useRef(false);
|
const hasMiiFeaturesChanged = useRef(false);
|
||||||
|
|
@ -80,6 +81,7 @@ export default function EditMiiPage() {
|
||||||
if (makeup != mii.makeup) formData.append("makeup", makeup);
|
if (makeup != mii.makeup) formData.append("makeup", makeup);
|
||||||
if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
||||||
if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined));
|
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 (youtubeId != mii.youtubeId) formData.append("youtubeId", youtubeId);
|
||||||
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));
|
||||||
|
|
@ -185,6 +187,7 @@ export default function EditMiiPage() {
|
||||||
setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`);
|
setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`);
|
||||||
setYouTubeId(data.youtubeId ?? "");
|
setYouTubeId(data.youtubeId ?? "");
|
||||||
setQuarantined(data.quarantined);
|
setQuarantined(data.quarantined);
|
||||||
|
setNeedsFixingReason(data.needsFixing);
|
||||||
instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {});
|
instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
|
|
@ -297,6 +300,22 @@ export default function EditMiiPage() {
|
||||||
<input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={(e) => setQuarantined(e.target.checked)} />
|
<input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={(e) => setQuarantined(e.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full grid grid-cols-3 items-center">
|
||||||
|
<label htmlFor="needsFixing" className="font-semibold py-2">
|
||||||
|
Needs Fixing
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="col-span-2 flex gap-1">
|
||||||
|
<input
|
||||||
|
id="needsFixing"
|
||||||
|
placeholder="Put the reason here..."
|
||||||
|
className="pill input w-full col-span-2"
|
||||||
|
value={needsFixingReason ?? ""}
|
||||||
|
onChange={(e) => setNeedsFixingReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,16 @@ export default function MiiPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{mii.needsFixing && (
|
||||||
|
<div className="bg-orange-50 border-2 border-orange-400 rounded-2xl shadow-lg p-4 flex items-start gap-3 text-orange-700">
|
||||||
|
<Icon icon="material-symbols:warning-rounded" className="text-2xl shrink-0" />
|
||||||
|
<p className="font-medium">
|
||||||
|
This Mii won't show up on the main page until fixes are made.
|
||||||
|
<br />
|
||||||
|
Reason: {mii.needsFixing}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1">
|
<div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1">
|
||||||
<div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 h-min flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2">
|
<div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 h-min flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2">
|
||||||
{/* Mii Image */}
|
{/* Mii Image */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue