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.
## 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
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
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.
## 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.

View file

@ -13,14 +13,6 @@ const nextConfig: NextConfig = {
{ 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)
quarantined Boolean @default(false)
in_queue Boolean @default(false)
needsFixing String?
instructions Json?
youtubeId String?

View file

@ -26,11 +26,6 @@ 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(),
@ -91,7 +86,6 @@ 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"),
@ -109,22 +103,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const error = `${path}: ${firstIssue.message}`;
return rateLimit.sendResponse({ error }, 400);
}
const {
name,
tags,
description,
quarantined,
needsFixingReason,
gender,
makeup,
miiPortraitImage,
miiFeaturesImage,
youtubeId,
instructions,
image1,
image2,
image3,
} = parsed.data;
const { name, tags, description, quarantined, gender, makeup, miiPortraitImage, miiFeaturesImage, youtubeId, instructions, image1, image2, image3 } =
parsed.data;
// Validate image files
const customImages: File[] = [];
@ -153,7 +133,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
// 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);
// 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 (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;

View file

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

View file

@ -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, needsFixing: { not: null } },
where: { in_queue: false, quarantined: false },
skip: randomIndex,
take: 1,
select: { id: true },

View file

@ -86,20 +86,12 @@ 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"}`}
>
<div className="absolute top-2 left-2 z-10 flex flex-col gap-1">
{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" />
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">
<img

View file

@ -92,7 +92,7 @@ export default function ImageEditorPortrait({ isOpen, setIsOpen, image, setImage
<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">
<img ref={imageRef} src={image} crossOrigin="anonymous" />
<img ref={imageRef} src={image} />
</ReactCrop>
<canvas ref={canvasRef} className="hidden" />
</div>

View file

@ -54,7 +54,6 @@ export default function EditMiiPage() {
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
const [quarantined, setQuarantined] = useState(false);
const [needsFixingReason, setNeedsFixingReason] = useState("");
const hasCustomImagesChanged = useRef(false);
const hasMiiPortraitChanged = useRef(false);
const hasMiiFeaturesChanged = useRef(false);
@ -81,7 +80,6 @@ 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));
@ -187,7 +185,6 @@ 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);
})
@ -300,22 +297,6 @@ export default function EditMiiPage() {
<input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={(e) => setQuarantined(e.target.checked)} />
</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">
{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}
</h1>
<p className="text-center mb-4">We're currently going through some major code changes therefore some features won't work.</p>
<MiiList />
</>
);

View file

@ -101,16 +101,6 @@ export default function MiiPage() {
</p>
</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="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 */}

View file

@ -70,11 +70,6 @@ export default function ReportMiiPage() {
</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">
<label htmlFor="reason" className="font-semibold">
Reason

View file

@ -78,10 +78,6 @@ export default function ReportUserPage() {
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
</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">
<label htmlFor="reason" className="font-semibold">
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"
/>
</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>