Compare commits

..

No commits in common. "e8249154d98a268709152a9ba6b4e38620c13099" and "af7f1380bc8e9ca4032e1f7f5d19f8b2391d1380" have entirely different histories.

15 changed files with 43 additions and 120 deletions

View file

@ -4,9 +4,36 @@ 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
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`: 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:
@ -58,33 +85,6 @@ 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.

View file

@ -13,14 +13,6 @@ 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

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

View file

@ -77,7 +77,6 @@ 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,11 +26,6 @@ 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(),
@ -91,7 +86,6 @@ 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"),
@ -109,22 +103,8 @@ 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 { const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, youtubeId, instructions, image1, image2, image3 } =
name, parsed.data;
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[] = [];
@ -153,7 +133,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
} }
// Prevent non-admins from quarantining Miis // Prevent non-admins from quarantining Miis
if (quarantined && needsFixingReason && session.user?.id?.toString() !== process.env.NEXT_PUBLIC_ADMIN_USER_ID) if (quarantined && 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
@ -162,7 +142,6 @@ 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,12 +43,13 @@ 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 } }),
@ -98,7 +99,6 @@ 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, needsFixing: { not: null } }, where: { in_queue: false, quarantined: false },
skip: randomIndex, skip: randomIndex,
take: 1, take: 1,
select: { id: true }, select: { id: true },

View file

@ -86,20 +86,12 @@ 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"}`}
> >
<div className="absolute top-2 left-2 z-10 flex flex-col gap-1">
{mii.in_queue && ( {mii.in_queue && (
<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"> <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">
<Icon icon="mdi:clock-outline" className="text-base" /> <Icon icon="mdi:clock-outline" className="text-base" />
In Queue In Queue
</div> </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

@ -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} crossOrigin="anonymous" /> <img ref={imageRef} src={image} />
</ReactCrop> </ReactCrop>
<canvas ref={canvasRef} className="hidden" /> <canvas ref={canvasRef} className="hidden" />
</div> </div>

View file

@ -54,7 +54,6 @@ 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);
@ -81,7 +80,6 @@ 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));
@ -187,7 +185,6 @@ 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);
}) })
@ -300,22 +297,6 @@ 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,6 +9,7 @@ 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,16 +101,6 @@ 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,11 +70,6 @@ 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,10 +78,6 @@ 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} forceCrop /> <SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} />
</div> </div>
</div> </div>