Compare commits

...

10 commits

22 changed files with 451 additions and 364 deletions

View file

@ -2,38 +2,11 @@
This is probably outdated. 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 front-end and back-end, [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, sometimes you might need to:
```bash
# Generate Prisma client types
$ pnpm prisma generate
# Or, if you've added new database properties
$ pnpm prisma migrate dev
$ pnpm prisma generate
```
I recommend opting out of Next.js' telemetry program but it is not a requirement.
```bash
$ pnpm 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:
@ -62,10 +35,11 @@ services:
After starting the docker applications, apply TomodachiShare's database schema migrations. After starting the docker applications, apply TomodachiShare's database schema migrations.
```bash ```bash
$ pnpm prisma migrate dev $ pnpm --filter backend prisma migrate dev
``` ```
After, make a copy of the `.env.example` file and rename it to `.env`. The database variables should be pre-configured, but you'll need to fill in the rest of the variables. After, in both the backend and frontend, make a copy of the `.env.example` file and rename it to `.env`.
For the backend, the database variables should be pre-configured, but you'll need to fill in the rest of the variables.
For the `AUTH_SECRET`, run the following in the command line: For the `AUTH_SECRET`, run the following in the command line:
@ -74,7 +48,7 @@ $ pnpx auth secret
``` ```
> [!NOTE] > [!NOTE]
> This command may put the secret in a file named `.env.local`, if that happens copy it and paste it into `.env` > This command may put the secret in a file named `.env.local`, if that happens copy it and paste it into `backend/.env`
Now, let's get the Discord and GitHub authentication set up. If you don't plan on editing any code associated with authentication, you likely only need to setup one of these services. Now, let's get the Discord and GitHub authentication set up. If you don't plan on editing any code associated with authentication, you likely only need to setup one of these services.
@ -84,10 +58,43 @@ 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.
After configuring the environment variables, you can run a development server. ## Getting started
To get the project up and running locally, follow these steps:
```bash ```bash
$ pnpm dev $ 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.
```bash
# Terminal 1
$ pnpm --filter backend dev
# Terminal 2
$ pnpm --filter frontend dev
``` ```
## Building ## Building
@ -96,8 +103,10 @@ It's a good idea to build the project locally before submitting a pull request.
```bash ```bash
# Build the project # Build the project
$ pnpm build $ pnpm --filter backend build
$ pnpm --filter frontend build
# Run the built version # Run the built version (Note: Vite likes to change the port when this happens, so you probably need to change both .env files)
$ pnpm start $ pnpm --filter backend start
$ pnpm --filter frontend build
``` ```

View file

@ -13,6 +13,14 @@ const nextConfig: NextConfig = {
{ key: "Access-Control-Allow-Headers", value: "Content-Type" }, { key: "Access-Control-Allow-Headers", value: "Content-Type" },
], ],
}, },
{
// for images
source: "/mii/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: process.env.NEXT_PUBLIC_FRONTEND_URL || "http://localhost:4321" },
{ key: "Access-Control-Allow-Credentials", value: "true" },
],
},
]; ];
}, },
}; };

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "miis" ADD COLUMN "needsFixing" TEXT;

View file

@ -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?

View file

@ -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;

View file

@ -43,13 +43,12 @@ export async function GET(request: NextRequest) {
? { in_queue: true } // Only show queued Miis ? { in_queue: true } // Only show queued Miis
: userId : userId
? { ? {
// Include queued Miis if user is on their profile
...(Number(session?.user?.id) === userId ? {} : { in_queue: false }),
userId, userId,
} }
: { : {
// Don't show queued Miis on main page // Don't show queued Miis on main page
in_queue: false, in_queue: false,
needsFixing: null,
}), }),
// Only show liked miis on likes page // Only show liked miis on likes page
...(parentPage === "likes" && miiIdsLiked && { id: { in: miiIdsLiked } }), ...(parentPage === "likes" && miiIdsLiked && { id: { in: miiIdsLiked } }),
@ -99,6 +98,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 && {

View file

@ -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 },

4
frontend/.env.example Normal file
View file

@ -0,0 +1,4 @@
VITE_BASE_URL="http://localhost:5173"
VITE_API_URL="http://localhost:3000"
VITE_ADMIN_USER_ID=1
VITE_CONTRIBUTORS_USER_IDS=1

View file

@ -1,4 +1,4 @@
import { useState, useTransition } from "react"; import { useTransition } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import type { MiiGender, MiiPlatform } from "@tomodachi-share/shared"; import type { MiiGender, MiiPlatform } from "@tomodachi-share/shared";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
@ -8,12 +8,11 @@ export default function GenderSelect() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const [selected, setSelected] = useState<MiiGender | null>((searchParams.get("gender") as MiiGender) ?? null); const selected = (searchParams.get("gender") as MiiGender) ?? null;
const platform = (searchParams.get("platform") as MiiPlatform) || undefined; const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
const handleClick = (gender: MiiGender) => { const handleClick = (gender: MiiGender) => {
const filter = selected === gender ? null : gender; const filter = selected === gender ? null : gender;
setSelected(filter);
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set("page", "1"); params.set("page", "1");

View file

@ -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

View file

@ -1,4 +1,4 @@
import { useState, useTransition } from "react"; import { useTransition } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import type { MiiMakeup } from "@tomodachi-share/shared"; import type { MiiMakeup } from "@tomodachi-share/shared";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
@ -8,11 +8,10 @@ export default function MakeupSelect() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const [selected, setSelected] = useState<MiiMakeup | null>((searchParams.get("makeup") as MiiMakeup) ?? null); const selected = (searchParams.get("makeup") as MiiMakeup) ?? null;
const handleClick = (makeup: MiiMakeup) => { const handleClick = (makeup: MiiMakeup) => {
const filter = selected === makeup ? null : makeup; const filter = selected === makeup ? null : makeup;
setSelected(filter);
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set("page", "1"); params.set("page", "1");

View file

@ -1,5 +1,5 @@
import type { MiiPlatform } from "@tomodachi-share/shared"; import type { MiiPlatform } from "@tomodachi-share/shared";
import { type ChangeEvent, useState, useTransition } from "react"; import { type ChangeEvent, useTransition } from "react";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
export default function OtherFilters() { export default function OtherFilters() {
@ -9,16 +9,14 @@ export default function OtherFilters() {
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const platform = (searchParams.get("platform") as MiiPlatform) || undefined; const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
const [allowCopying, setAllowCopying] = useState<boolean>((searchParams.get("allowCopying") as unknown as boolean) ?? false); const allowCopying = searchParams.get("allowCopying") === "true";
const [quarantined, setQuarantined] = useState<boolean>((searchParams.get("quarantined") as unknown as boolean) ?? false); const quarantined = searchParams.get("quarantined") === "true";
const handleChangeAllowCopying = (e: ChangeEvent<HTMLInputElement>) => { const handleChangeAllowCopying = (e: ChangeEvent<HTMLInputElement>) => {
setAllowCopying(e.target.checked);
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set("page", "1"); params.set("page", "1");
if (!allowCopying) { if (e.target.checked) {
params.set("allowCopying", "true"); params.set("allowCopying", "true");
} else { } else {
params.delete("allowCopying"); params.delete("allowCopying");
@ -30,12 +28,10 @@ export default function OtherFilters() {
}; };
const handleChangeQuarantined = (e: ChangeEvent<HTMLInputElement>) => { const handleChangeQuarantined = (e: ChangeEvent<HTMLInputElement>) => {
setQuarantined(e.target.checked);
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set("page", "1"); params.set("page", "1");
if (!quarantined) { if (e.target.checked) {
params.set("quarantined", "true"); params.set("quarantined", "true");
} else { } else {
params.delete("quarantined"); params.delete("quarantined");

View file

@ -1,4 +1,4 @@
import { useState, useTransition } from "react"; import { useTransition } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import type { MiiPlatform } from "@tomodachi-share/shared"; import type { MiiPlatform } from "@tomodachi-share/shared";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
@ -7,19 +7,22 @@ export default function PlatformSelect() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const selected = (searchParams.get("platform") as MiiPlatform) ?? null;
const [selected, setSelected] = useState<MiiPlatform | null>((searchParams.get("platform") as MiiPlatform) ?? null);
const handleClick = (platform: MiiPlatform) => { const handleClick = (platform: MiiPlatform) => {
const filter = selected === platform ? null : platform; const filter = selected === platform ? null : platform;
setSelected(filter);
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set("page", "1");
if (filter) { if (filter) {
params.set("platform", filter); params.set("platform", filter);
} else { } else {
params.delete("platform"); params.delete("platform");
} }
if (params.get("gender") === "NONBINARY") params.delete("gender");
params.delete("makeup");
params.delete("allowCopying");
startTransition(() => { startTransition(() => {
navigate(`?${params.toString()}`); navigate(`?${params.toString()}`);

View file

@ -92,7 +92,7 @@ export default function ImageEditorPortrait({ isOpen, setIsOpen, image, setImage
<div className="relative w-full flex justify-center"> <div className="relative w-full flex justify-center">
<ReactCrop crop={crop} onChange={(c) => setCrop(c)} className="rounded-2xl border-2 border-amber-500 overflow-hidden max-h-96"> <ReactCrop crop={crop} onChange={(c) => setCrop(c)} className="rounded-2xl border-2 border-amber-500 overflow-hidden max-h-96">
<img ref={imageRef} src={image} /> <img ref={imageRef} src={image} crossOrigin="anonymous" />
</ReactCrop> </ReactCrop>
<canvas ref={canvasRef} className="hidden" /> <canvas ref={canvasRef} className="hidden" />
</div> </div>

View file

@ -69,7 +69,7 @@ export default function MiiEditor({ instructions }: Props) {
{(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => { {(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => {
const TabComponent = TAB_COMPONENTS[t]; const TabComponent = TAB_COMPONENTS[t];
return ( return (
<div key={t} className={t === tab ? "grow relative p-3" : "hidden"}> <div key={t} className={t === tab ? "grow relative p-3 min-h-0" : "hidden"}>
<TabComponent instructions={instructions} /> <TabComponent instructions={instructions} />
</div> </div>
); );

View file

@ -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>
</> </>
)} )}

View file

@ -9,7 +9,6 @@ export default function IndexPage() {
<h1 className="sr-only"> <h1 className="sr-only">
{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"} {searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}
</h1> </h1>
<p className="text-center mb-4">We're currently going through some major code changes therefore some features won't work.</p>
<MiiList /> <MiiList />
</> </>
); );

View file

@ -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 */}

View file

@ -70,6 +70,11 @@ export default function ReportMiiPage() {
</div> </div>
</div> </div>
<p className="text-sm bg-orange-100 border border-orange-400 rounded-lg px-3 py-2">
<span className="font-semibold">REMINDER:</span> Miis without instructions are allowed, as mentioned in the submit form. Be sure to add notes so we
know what's wrong.
</p>
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="reason" className="font-semibold"> <label htmlFor="reason" className="font-semibold">
Reason Reason

View file

@ -78,6 +78,10 @@ export default function ReportUserPage() {
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p> <p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
</div> </div>
<p className="text-sm bg-orange-100 border border-orange-400 rounded-lg px-3 py-2">
<span className="font-semibold">REMINDER:</span> Be sure to add notes so we know what's wrong.
</p>
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="reason" className="font-semibold"> <label htmlFor="reason" className="font-semibold">
Reason Reason

View file

@ -413,7 +413,7 @@ export default function SubmitPage() {
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70" className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70"
/> />
</div> </div>
<SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} /> <SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} forceCrop />
</div> </div>
</div> </div>