diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index d92ce0c..88db816 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -1,5 +1,7 @@
# TomodachiShare Development Instructions
+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.
## Getting started
diff --git a/README.md b/README.md
index 37072a8..9282cb9 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
diff --git a/backend/src/app/report/mii/[id]/page.tsx b/backend/src/app/report/mii/[id]/page.tsx
deleted file mode 100644
index da40d1a..0000000
--- a/backend/src/app/report/mii/[id]/page.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { auth } from "@/lib/auth";
-import { prisma } from "@/lib/prisma";
-
-import ReportMiiForm from "@/components/report/mii-form";
-
-interface Props {
- params: Promise<{ id: string }>;
-}
-
-export const metadata: Metadata = {
- title: "Report Mii - TomodachiShare",
- description: "Report a Mii on TomodachiShare",
- robots: {
- index: false,
- follow: false,
- },
-};
-
-export default async function ReportMiiPage({ params }: Props) {
- const session = await auth();
- const { id } = await params;
-
- const mii = await prisma.mii.findUnique({
- where: {
- id: Number(id),
- },
- include: {
- _count: {
- select: {
- likedBy: true,
- },
- },
- },
- });
-
- if (!session) redirect("/login");
- if (!mii) redirect("/404");
-
- return (
-
-
-
- );
-}
diff --git a/backend/src/app/report/user/[id]/page.tsx b/backend/src/app/report/user/[id]/page.tsx
deleted file mode 100644
index fae91bc..0000000
--- a/backend/src/app/report/user/[id]/page.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { auth } from "@/lib/auth";
-import { prisma } from "@/lib/prisma";
-
-import ReportUserForm from "@/components/report/user-form";
-
-interface Props {
- params: Promise<{ id: string }>;
-}
-
-export const metadata: Metadata = {
- title: "Report User - TomodachiShare",
- description: "Report a user on TomodachiShare",
- robots: {
- index: false,
- follow: false,
- },
-};
-
-export default async function ReportUserPage({ params }: Props) {
- const session = await auth();
- const { id } = await params;
-
- const user = await prisma.user.findUnique({
- where: {
- id: Number(id),
- },
- });
-
- if (!session) redirect("/login");
- if (!user) redirect("/404");
-
- return (
-
-
-
- );
-}
diff --git a/backend/src/components/report/mii-form.tsx b/backend/src/components/report/mii-form.tsx
deleted file mode 100644
index fb65fdf..0000000
--- a/backend/src/components/report/mii-form.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-"use client";
-
-import { useState } from "react";
-
-import ReasonSelector from "./reason-selector";
-import SubmitButton from "../submit-button";
-import { Mii, ReportReason } from "@prisma/client";
-
-interface Props {
- mii: Mii;
- likes: number;
-}
-
-export default function ReportMiiForm({ mii, likes }: Props) {
- const [reason, setReason] = useState();
- const [notes, setNotes] = useState();
- const [error, setError] = useState(undefined);
-
- const handleSubmit = async () => {
- const response = await fetch(`/api/report`, {
- method: "POST",
- body: JSON.stringify({ id: mii.id, type: "mii", reason: reason?.toLowerCase(), notes }),
- });
- const { error } = await response.json();
-
- if (!response.ok) {
- setError(error);
- return;
- }
-
- // redirect(`/`);
- window.location.href = "https://tomodachishare.com";
- };
-
- return (
-
-
-
Report a Mii
-
If you encounter a rule-breaking Mii, please report it here
-
-
-
-
-
-
-
-
-
-
-
- Reason
-
-
-
-
-
-
- Reason notes
-
-
-
-
-
- {error && Error: {error} }
-
-
-
-
- );
-}
diff --git a/backend/src/components/report/user-form.tsx b/backend/src/components/report/user-form.tsx
deleted file mode 100644
index b7851ce..0000000
--- a/backend/src/components/report/user-form.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-"use client";
-
-import { useState } from "react";
-
-import ReasonSelector from "./reason-selector";
-import SubmitButton from "../submit-button";
-import { ReportReason, User } from "@prisma/client";
-import Image from "next/image";
-
-interface Props {
- user: User;
-}
-
-export default function ReportUserForm({ user }: Props) {
- const [reason, setReason] = useState();
- const [notes, setNotes] = useState();
- const [error, setError] = useState(undefined);
-
- const handleSubmit = async () => {
- const response = await fetch(`/api/report`, {
- method: "POST",
- body: JSON.stringify({ id: user.id, type: "user", reason: reason?.toLowerCase(), notes }),
- });
- const { error } = await response.json();
-
- if (!response.ok) {
- setError(error);
- return;
- }
-
- window.location.href = "https://tomodachishare.com";
- };
-
- return (
-
-
-
Report a User
-
If you encounter a user causing issues, please report them here
-
-
-
-
-
-
-
-
- Reason
-
-
-
-
-
-
- Reason notes
-
-
-
-
-
- {error && Error: {error} }
-
-
-
-
- );
-}
diff --git a/backend/src/components/submit-button.tsx b/backend/src/components/submit-button.tsx
deleted file mode 100644
index c5067a7..0000000
--- a/backend/src/components/submit-button.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Icon } from "@iconify/react";
-
-interface Props {
- onClick: () => void | Promise;
- disabled?: boolean;
- text?: string;
- className?: string;
-}
-
-export default function SubmitButton({ onClick, disabled = false, text = "Submit", className }: Props) {
- const [isLoading, setIsLoading] = useState(false);
-
- const handleClick = async (event: React.FormEvent) => {
- event.preventDefault();
-
- setIsLoading(true);
- try {
- await onClick();
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
- {text}
- {isLoading && }
-
- );
-}
diff --git a/frontend/src/components/profile-information.tsx b/frontend/src/components/profile-information.tsx
deleted file mode 100644
index 67285ae..0000000
--- a/frontend/src/components/profile-information.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Icon } from "@iconify/react";
-
-import Description from "./description";
-import { useStore } from "@nanostores/react";
-import { session } from "../session";
-import { Link, useLocation } from "react-router";
-
-interface Props {
- user?: any;
-}
-
-export default function ProfileInformation({ user }: Props) {
- const location = useLocation();
- const $session = useStore(session);
-
- if (!user) return null;
-
- const currentUser = user ?? $session?.user;
- const page = location.pathname;
- const isAdmin = currentUser?.id === Number(import.meta.env.VITE_ADMIN_USER_ID);
- const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(user?.id);
- const isOwnProfile = currentUser?.id === user?.id;
-
- return (
-
-
- {/* Profile picture */}
-
-
-
- {/* User information */}
-
-
-
{user.name}
- {isAdmin && (
-
-
-
- )}
- {isContributor && (
-
-
-
- )}
-
-
ID: {user?.id}
-
-
-
- Created: {" "}
- {new Date(user.createdAt).toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
-
-
- Liked {user._count.likes} Miis
-
-
-
- {user.description &&
}
-
-
-
- {/* Buttons */}
-
- {!isOwnProfile && (
-
-
- Report
-
- )}
- {isOwnProfile && isAdmin && (
-
-
- Admin
-
- )}
- {isOwnProfile && page !== "/profile/likes" && (
-
-
- My Likes
-
- )}
- {isOwnProfile && page !== "/profile/settings" && (
-
-
- Settings
-
- )}
- {(page === "/profile/likes" || page === "/profile/settings") && (
-
-
- Back
-
- )}
-
-
- );
-}
diff --git a/backend/src/components/report/reason-selector.tsx b/frontend/src/components/reason-selector.tsx
similarity index 96%
rename from backend/src/components/report/reason-selector.tsx
rename to frontend/src/components/reason-selector.tsx
index c3513d1..2f60226 100644
--- a/backend/src/components/report/reason-selector.tsx
+++ b/frontend/src/components/reason-selector.tsx
@@ -1,7 +1,5 @@
-"use client";
-
import { Icon } from "@iconify/react";
-import { ReportReason } from "@prisma/client";
+import type { ReportReason } from "@tomodachi-share/shared";
import { useSelect } from "downshift";
interface Props {
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 80b72d0..d863c9d 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -18,6 +18,8 @@ import LinkOutPage from "./pages/out.tsx";
import Layout from "./layout.tsx";
import ProfileLayout from "./pages/profile/layout.tsx";
import ProfileLikesPage from "./pages/profile/likes.tsx";
+import ReportMiiPage from "./pages/report/mii.tsx";
+import ReportUserPage from "./pages/report/user.tsx";
createRoot(document.getElementById("root")!).render(
@@ -34,6 +36,10 @@ createRoot(document.getElementById("root")!).render(
} />
} />
+
+ } />
+ } />
+
} />
} />
} />
diff --git a/frontend/src/pages/mii.tsx b/frontend/src/pages/mii.tsx
index 96de775..66e6163 100644
--- a/frontend/src/pages/mii.tsx
+++ b/frontend/src/pages/mii.tsx
@@ -294,7 +294,7 @@ export default function MiiPage() {
{/* */}
-
+
Report
diff --git a/frontend/src/pages/profile/layout.tsx b/frontend/src/pages/profile/layout.tsx
index ca67835..7b762b2 100644
--- a/frontend/src/pages/profile/layout.tsx
+++ b/frontend/src/pages/profile/layout.tsx
@@ -1,8 +1,10 @@
import { Outlet, useNavigate, useParams } from "react-router";
-import ProfileInformation from "../../components/profile-information";
import { useEffect, useState } from "react";
import { useStore } from "@nanostores/react";
import { session } from "../../session";
+import { Icon } from "@iconify/react";
+import { Link } from "react-router";
+import Description from "../../components/description";
export default function ProfileLayout() {
const { id } = useParams();
@@ -41,9 +43,93 @@ export default function ProfileLayout() {
return Loading...
;
}
+ const currentUser = user ?? $session?.user;
+ const page = location.pathname;
+ const isAdmin = currentUser?.id === Number(import.meta.env.VITE_ADMIN_USER_ID);
+ const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(user?.id);
+ const isOwnProfile = currentUser?.id === user?.id;
+
return (
-
+
+
+ {/* Profile picture */}
+
+
{
+ e.currentTarget.onerror = null; // Prevent infinite loops
+ e.currentTarget.src = "/guest.png";
+ }}
+ className="rounded-full bg-white border-2 border-orange-400 shadow w-full max-md:self-center"
+ />
+
+ {/* User information */}
+
+
+
{user.name}
+ {isAdmin && (
+
+
+
+ )}
+ {isContributor && (
+
+
+
+ )}
+
+
ID: {user?.id}
+
+
+
+ Created: {" "}
+ {new Date(user.createdAt).toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
+
+
+ Liked {user._count.likes} Miis
+
+
+
+ {user.description &&
}
+
+
+
+ {/* Buttons */}
+
+ {!isOwnProfile && (
+
+
+ Report
+
+ )}
+ {isOwnProfile && isAdmin && (
+
+
+ Admin
+
+ )}
+ {isOwnProfile && page !== "/profile/likes" && (
+
+
+ My Likes
+
+ )}
+ {isOwnProfile && page !== "/profile/settings" && (
+
+
+ Settings
+
+ )}
+ {(page === "/profile/likes" || page === "/profile/settings") && (
+
+
+ Back
+
+ )}
+
+
+
);
diff --git a/frontend/src/pages/report/mii.tsx b/frontend/src/pages/report/mii.tsx
new file mode 100644
index 0000000..66a7180
--- /dev/null
+++ b/frontend/src/pages/report/mii.tsx
@@ -0,0 +1,103 @@
+import { useNavigate, useParams } from "react-router";
+import ReasonSelector from "../../components/reason-selector";
+import { useEffect, useState } from "react";
+import { type ReportReason } from "@tomodachi-share/shared";
+import SubmitButton from "../../components/submit-button";
+
+export default function ReportMiiPage() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [reason, setReason] = useState();
+ const [notes, setNotes] = useState();
+ const [error, setError] = useState(undefined);
+
+ const [mii, setMii] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const API_URL = import.meta.env.VITE_API_URL;
+
+ useEffect(() => {
+ fetch(`${API_URL}/api/mii/${id}/info`)
+ .then((res) => {
+ if (!res.ok) throw new Error("Failed to fetch Mii");
+ return res.json();
+ })
+ .then((data) => {
+ setMii(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ setLoading(false);
+ navigate("/404");
+ });
+ }, [id]);
+
+ if (loading || !mii) {
+ return Loading...
;
+ }
+
+ const handleSubmit = async () => {
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/api/report`, {
+ method: "POST",
+ body: JSON.stringify({ id: mii.id, type: "mii", reason: reason?.toLowerCase(), notes }),
+ credentials: "include",
+ });
+ const { error } = await response.json();
+
+ if (!response.ok) {
+ setError(error);
+ return;
+ }
+
+ navigate("/");
+ };
+
+ return (
+
+
+
+
Report a Mii
+
If you encounter a rule-breaking Mii, please report it here
+
+
+
+
+
+
+
+
+
+
+
+ Reason
+
+
+
+
+
+
+ Reason notes
+
+
+
+
+
+ {error && Error: {error} }
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/report/user.tsx b/frontend/src/pages/report/user.tsx
new file mode 100644
index 0000000..378e1b6
--- /dev/null
+++ b/frontend/src/pages/report/user.tsx
@@ -0,0 +1,111 @@
+import { useNavigate, useParams } from "react-router";
+import ReasonSelector from "../../components/reason-selector";
+import { useEffect, useState } from "react";
+import { type ReportReason } from "@tomodachi-share/shared";
+import SubmitButton from "../../components/submit-button";
+
+export default function ReportUserPage() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [reason, setReason] = useState();
+ const [notes, setNotes] = useState();
+ const [error, setError] = useState(undefined);
+
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const API_URL = import.meta.env.VITE_API_URL;
+
+ useEffect(() => {
+ fetch(`${API_URL}/api/profile/${id}/info`)
+ .then((res) => {
+ if (!res.ok) throw new Error("Failed to fetch profile");
+ return res.json();
+ })
+ .then((data) => {
+ setUser(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ setLoading(false);
+ navigate("/404");
+ });
+ }, [id]);
+
+ if (loading || !user) {
+ return Loading...
;
+ }
+
+ const handleSubmit = async () => {
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/api/report`, {
+ method: "POST",
+ body: JSON.stringify({ id: user.id, type: "user", reason: reason?.toLowerCase(), notes }),
+ credentials: "include",
+ });
+ const { error } = await response.json();
+
+ if (!response.ok) {
+ setError(error);
+ return;
+ }
+
+ navigate("/");
+ };
+
+ return (
+
+
+
+
Report a User
+
If you encounter a user causing issues, please report them here
+
+
+
+
+
+
{
+ e.currentTarget.onerror = null; // Prevent infinite loops
+ e.currentTarget.src = "/guest.png";
+ }}
+ alt="profile picture"
+ width={96}
+ height={96}
+ className="aspect-square rounded-full border-2 border-orange-400"
+ />
+
{user.name}
+
+
+
+
+ Reason
+
+
+
+
+
+
+ Reason notes
+
+
+
+
+
+ {error && Error: {error} }
+
+
+
+
+
+ );
+}
diff --git a/shared/src/index.ts b/shared/src/index.ts
index 87d2715..9154268 100644
--- a/shared/src/index.ts
+++ b/shared/src/index.ts
@@ -2,4 +2,4 @@ export * from "./constants";
export * from "./qr-codes";
export * from "./switch";
export * from "./three-ds-tomodachi-life-mii";
-export type { SwitchMiiInstructions, MiiGender, MiiMakeup, MiiPlatform } from "./types";
+export type { SwitchMiiInstructions, MiiGender, MiiMakeup, MiiPlatform, ReportReason } from "./types";
diff --git a/shared/src/types.d.ts b/shared/src/types.d.ts
index 1b513b0..d3a3e72 100644
--- a/shared/src/types.d.ts
+++ b/shared/src/types.d.ts
@@ -1,6 +1,7 @@
type MiiGender = "MALE" | "FEMALE" | "NONBINARY";
type MiiPlatform = "THREE_DS" | "SWITCH";
type MiiMakeup = "FULL" | "PARTIAL" | "NONE";
+type ReportReason = "INAPPROPRIATE" | "SPAM" | "BAD_QUALITY" | "OTHER";
export interface SwitchMiiInstructions {
head: {