diff --git a/backend/src/app/api/mii/list/route.ts b/backend/src/app/api/mii/list/route.ts index d3b0465..ba90a2a 100644 --- a/backend/src/app/api/mii/list/route.ts +++ b/backend/src/app/api/mii/list/route.ts @@ -9,7 +9,7 @@ export async function GET(request: NextRequest) { const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 }); - const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, seed, parentPage, userId } = parsed.data; + const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, parentPage, userId } = parsed.data; // My Likes page let miiIdsLiked: number[] | undefined = undefined; @@ -107,7 +107,7 @@ export async function GET(request: NextRequest) { } [totalCount, miis] = await Promise.all([ - prisma.mii.count({ where: { ...where } }), // TODO: User id + prisma.mii.count({ where: { ...where, userId } }), prisma.mii.findMany({ where, orderBy, diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..58309aa 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg deleted file mode 100644 index 58309aa..0000000 --- a/frontend/public/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 96467b7..0c515bf 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -11,7 +11,7 @@ export default function Header() { aria-label="Go to Home Page" className="font-black text-3xl text-orange-400 flex items-center gap-2 max-md:justify-center max-md:col-span-2" > - logo + logo TomodachiShare diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx index ab399ea..6c3ddba 100644 --- a/frontend/src/components/mii/list/index.tsx +++ b/frontend/src/components/mii/list/index.tsx @@ -1,184 +1,164 @@ -// import crypto from "crypto"; -// import seedrandom from "seedrandom"; +import { useEffect, useState } from "react"; +import { Link, useSearchParams } from "react-router"; +import Skeleton from "./skeleton"; +import FilterMenu from "./filter-menu"; +import SortSelect from "./sort-select"; +import Pagination from "../../pagination"; +import DeleteMiiButton from "../delete-mii-button"; +import { Icon } from "@iconify/react"; +import LikeButton from "../../like-button"; +import { useStore } from "@nanostores/react"; +import { session } from "../../../session"; -// import { searchSchema } from "@tomodachi-share/shared/schemas"; +interface ApiResponse { + totalCount: number; + miis: any[]; + lastPage: number; +} -// import SortSelect from "./sort-select"; -// import Pagination from "./pagination"; -// import FilterMenu from "./filter-menu"; -// import MiiGrid from "./mii-grid"; +interface Props { + userId?: number; + parentPage?: "likes" | "admin"; +} -// interface Props { -// searchParams: URLSearchParams; -// userId?: number; // Profiles -// parentPage?: "likes" | "admin"; -// } +export default function MiiList({ parentPage, userId }: Props) { + const [searchParams] = useSearchParams(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); -// export default async function MiiList({ searchParams, userId, parentPage }: Props) { -// const session = await auth(); -// const parsed = searchSchema.safeParse(searchParams); -// if (!parsed.success) return

{parsed.error.issues[0].message}

; + const $session = useStore(session); -// const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, seed } = parsed.data; + useEffect(() => { + const params = new URLSearchParams(searchParams.toString()); + if (userId) params.append("userId", userId.toString()); + if (parentPage) params.append("parentPage", parentPage); -// // My Likes page -// let miiIdsLiked: number[] | undefined = undefined; + fetch(`${import.meta.env.VITE_API_URL}/api/mii/list?${params.toString()}`, { credentials: "include" }) + .then((res) => { + if (!res.ok) throw new Error("Failed to fetch Miis"); + return res.json(); + }) + .then((data) => { + setData(data); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setLoading(false); + }); + }, [searchParams, userId, parentPage]); -// if (parentPage === "likes" && session?.user?.id) { -// const likedMiis = await prisma.like.findMany({ -// where: { userId: Number(session.user.id) }, -// select: { miiId: true }, -// }); -// miiIdsLiked = likedMiis.map((like) => like.miiId); -// } + return ( + <> + {loading ? ( + + ) : data ? ( +
+
+
+ {data.totalCount} + {data.totalCount === 1 ? "Mii" : "Miis"} +
-// const where: Prisma.MiiWhereInput = { -// // In queue logic -// ...(parentPage === "admin" -// ? { 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, -// }), -// // Only show liked miis on likes page -// ...(parentPage === "likes" && miiIdsLiked && { id: { in: miiIdsLiked } }), -// // Searching -// ...(query && { -// OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }, { description: { contains: query, mode: "insensitive" } }], -// }), -// // Tag filtering -// ...(tags && tags.length > 0 && { tags: { hasEvery: tags } }), -// ...(exclude && exclude.length > 0 && { NOT: { tags: { hasSome: exclude } } }), -// // Platform -// ...(platform && { platform: { equals: platform } }), -// // Gender -// ...(gender && { gender: { equals: gender } }), -// // Allow Copying -// ...(allowCopying && { allowedCopying: true }), -// // Makeup -// ...(makeup && { makeup: { equals: makeup } }), -// // Quarantined -// ...(!quarantined && !userId && { quarantined: false }), -// }; +
+ + +
+
-// const select: Prisma.MiiSelect = { -// id: true, -// // Don't show when userId is specified -// ...(!userId && { -// user: { -// select: { -// id: true, -// name: true, -// }, -// }, -// }), -// platform: true, -// name: true, -// imageCount: true, -// tags: true, -// createdAt: true, -// gender: true, -// makeup: true, -// allowedCopying: true, -// quarantined: true, -// in_queue: true, -// // Mii liked check -// ...(session?.user?.id && { -// likedBy: { -// where: { userId: Number(session.user.id) }, -// select: { userId: true }, -// }, -// }), -// // Like count -// _count: { -// select: { likedBy: true }, -// }, -// }; +
+ {data.miis.map((mii) => ( +
+ {mii.in_queue && ( +
+ + In Queue +
+ )} -// const skip = (page - 1) * limit; + + mii image + -// let totalCount: number; -// let miis: Prisma.MiiGetPayload<{ select: typeof select }>[]; +
+
+ + {mii.name} + +
+ {mii.platform === "SWITCH" ? ( + + ) : ( + + )} +
+
+
+ {mii.tags.map((tag: string) => ( + + {tag} + + ))} +
-// if (sort === "random") { -// // Get all IDs that match the where conditions -// const matchingIds = await prisma.mii.findMany({ -// where, -// select: { id: true }, -// }); +
+ -// totalCount = matchingIds.length; + {!userId && ( + + @{mii.user?.name} + + )} -// if (matchingIds.length === 0) return; + {userId && Number($session?.user?.id) == userId && ( +
+ + + + +
+ )} -// // Use seed for consistent random results -// const randomSeed = seed || crypto.randomInt(0, 1_000_000_000); -// const rng = seedrandom(randomSeed.toString()); + {/* Admin Controls */} + {parentPage === "admin" && ( +
+
+ +
+ +
+
-// // Randomize all IDs using the Durstenfeld algorithm -// for (let i = matchingIds.length - 1; i > 0; i--) { -// const j = Math.floor(rng() * (i + 1)); -// [matchingIds[i], matchingIds[j]] = [matchingIds[j], matchingIds[i]]; -// } - -// // Convert to number[] array -// const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id); - -// miis = await prisma.mii.findMany({ -// where: { -// id: { in: selectedIds }, -// }, -// select, -// }); -// } else { -// // Sorting by likes, newest, or oldest -// let orderBy: Prisma.MiiOrderByWithRelationInput[]; - -// if (sort === "likes") { -// orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }]; -// } else if (sort === "oldest") { -// orderBy = [{ createdAt: "asc" }, { name: "asc" }]; -// } else { -// // default to newest -// orderBy = [{ createdAt: "desc" }, { name: "asc" }]; -// } - -// [totalCount, miis] = await Promise.all([ -// prisma.mii.count({ where: { ...where, userId } }), -// prisma.mii.findMany({ -// where, -// orderBy, -// select, -// skip, -// take: limit, -// }), -// ]); -// } - -// const lastPage = Math.ceil(totalCount / limit); - -// return ( -//
-//
-//
-// {totalCount} -// {totalCount === 1 ? "Mii" : "Miis"} -//
- -//
-// -// -//
-//
- -// -// -//
-// ); -// } + {new Date(mii.createdAt).toLocaleString("en-GB", { timeZone: "UTC" })} +
+ )} +
+
+
+ ))} +
+ +
+ ) : ( +

No Miis found, has the server died?

+ )} + + ); +} diff --git a/frontend/src/components/mii/list/mii-grid.tsx b/frontend/src/components/mii/list/mii-grid.tsx deleted file mode 100644 index f9a2e0a..0000000 --- a/frontend/src/components/mii/list/mii-grid.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Icon } from "@iconify/react"; - -import LikeButton from "../../like-button"; -import DeleteMiiButton from "../delete-mii-button"; -import { Link } from "react-router"; - -interface Props { - // miis: Prisma.MiiGetPayload<{ include: { user: { select: { id: true; name: true } }; _count: { select: { likedBy: true } } } }>[]; - miis: any[]; - userId?: number; - parentPage?: string; -} - -export default function MiiGrid({ miis, userId, parentPage }: Props) { - return ( -
- {miis.map((mii) => ( -
- {mii.in_queue && ( -
- - In Queue -
- )} - - - mii image - - -
-
- - {mii.name} - -
- {mii.platform === "SWITCH" ? ( - - ) : ( - - )} -
-
-
- {mii.tags.map((tag: string) => ( - - {tag} - - ))} -
- -
- - - {!userId && ( - - @{mii.user?.name} - - )} - - {/* {userId && Number(session.data?.user?.id) == userId && ( -
- - - - -
- )} */} - - {/* Admin Controls */} - {parentPage === "admin" && ( -
-
- -
- -
-
- - {new Date(mii.createdAt).toLocaleString("en-GB", { timeZone: "UTC" })} -
- )} -
-
-
- ))} -
- ); -} diff --git a/frontend/src/components/mii/list/skeleton.tsx b/frontend/src/components/mii/list/skeleton.tsx index 5a44dbe..9e067d4 100644 --- a/frontend/src/components/mii/list/skeleton.tsx +++ b/frontend/src/components/mii/list/skeleton.tsx @@ -1,53 +1,54 @@ -import FilterSelect from "./tag-filter"; -import SortSelect from "./sort-select"; -import Pagination from "../../pagination"; - -export default function Skeleton() { - return ( -
-
-

- ??? Miis -

- -
- - -
-
- -
- {[...Array(24)].map((_, index) => ( -
- {/* Carousel Skeleton */} -
-
-
- - {/* Content */} -
- {/* Name */} -
- - {/* Tags */} -
-
-
-
- - {/* Bottom row */} -
-
-
-
-
-
- ))} -
- -
- -
-
- ); -} +import SortSelect from "./sort-select"; +import Pagination from "../../pagination"; +import FilterMenu from "./filter-menu"; + +export default function Skeleton() { + return ( +
+
+
+ ??? + Miis +
+ +
+ + +
+
+ +
+ {[...Array(24)].map((_, index) => ( +
+ {/* Carousel Skeleton */} +
+
+
+ + {/* Content */} +
+ {/* Name */} +
+ + {/* Tags */} +
+
+
+
+ + {/* Bottom row */} +
+
+
+
+
+
+ ))} +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/components/profile-information.tsx b/frontend/src/components/profile-information.tsx index 209f9d0..67285ae 100644 --- a/frontend/src/components/profile-information.tsx +++ b/frontend/src/components/profile-information.tsx @@ -3,19 +3,20 @@ import { Icon } from "@iconify/react"; import Description from "./description"; import { useStore } from "@nanostores/react"; import { session } from "../session"; -import { Link } from "react-router"; +import { Link, useLocation } from "react-router"; interface Props { user?: any; - page?: "settings" | "likes"; } -export default function ProfileInformation({ user, page }: Props) { +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; @@ -24,8 +25,8 @@ export default function ProfileInformation({ user, page }: Props) {
{/* Profile picture */} - - + + {/* User information */}
@@ -72,19 +73,19 @@ export default function ProfileInformation({ user, page }: Props) { Admin )} - {/* {isOwnProfile && page !== "likes" && ( + {isOwnProfile && page !== "/profile/likes" && ( My Likes - )} */} - {isOwnProfile && page !== "settings" && ( + )} + {isOwnProfile && page !== "/profile/settings" && ( Settings )} - {page && ( + {(page === "/profile/likes" || page === "/profile/settings") && ( Back diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 101ab9a..80b72d0 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -8,14 +8,16 @@ import PrivacyPage from "./pages/privacy.tsx"; import TermsOfServicePage from "./pages/terms-of-service.tsx"; import NotFoundPage from "./pages/not-found.tsx"; import LoginPage from "./pages/login.tsx"; -import ProfilePage from "./pages/profile.tsx"; +import ProfilePage from "./pages/profile"; import MiiPage from "./pages/mii.tsx"; import SubmitPage from "./pages/submit.tsx"; import IndexPage from "./pages/index.tsx"; -import ProfileSettingsPage from "./pages/settings.tsx"; +import ProfileSettingsPage from "./pages/profile/settings.tsx"; import { ProgressProvider } from "@bprogress/react"; 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"; createRoot(document.getElementById("root")!).render( @@ -25,8 +27,9 @@ createRoot(document.getElementById("root")!).render( } /> } /> - + }> } /> + } /> } /> } /> diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 5d72df9..a3f6a22 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,68 +1,16 @@ -import { Suspense, useEffect, useState } from "react"; -import FilterMenu from "../components/mii/list/filter-menu"; -import SortSelect from "../components/mii/list/sort-select"; -import MiiGrid from "../components/mii/list/mii-grid"; -import Pagination from "../components/pagination"; -import Skeleton from "../components/mii/list/skeleton"; import { useSearchParams } from "react-router"; - -interface ApiResponse { - totalCount: number; - miis: any[]; - lastPage: number; -} +import MiiList from "../components/mii/list"; export default function IndexPage() { const [searchParams] = useSearchParams(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch(`${import.meta.env.VITE_API_URL}/api/mii/list?${searchParams.toString()}`) - .then((res) => { - if (!res.ok) throw new Error("Failed to fetch Miis"); - return res.json(); - }) - .then((data) => { - setData(data); - setLoading(false); - }) - .catch((err) => { - console.error(err); - setLoading(false); - }); - }, [searchParams]); return ( <>

{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}

-

We're currently going through some major code changes therefore some features won't work.

- - }> - {!loading && data ? ( -
-
-
- {data.totalCount} - {data.totalCount === 1 ? "Mii" : "Miis"} -
- -
- - -
-
- - - -
- ) : ( -

No Miis found :( Has the server died?

- )} -
+ ); } diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 4f2bd5b..864e649 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -5,6 +5,7 @@ import { session } from "../session"; export default function LoginPage() { const $session = useStore(session); + if ($session === undefined) return
Loading...
; if ($session) return ; const API_URL = import.meta.env.VITE_API_URL; diff --git a/frontend/src/pages/profile/index.tsx b/frontend/src/pages/profile/index.tsx new file mode 100644 index 0000000..5b8c99b --- /dev/null +++ b/frontend/src/pages/profile/index.tsx @@ -0,0 +1,8 @@ +import { useParams } from "react-router"; +import MiiList from "../../components/mii/list"; + +export default function ProfilePage() { + const { id } = useParams(); + + return ; +} diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile/layout.tsx similarity index 52% rename from frontend/src/pages/profile.tsx rename to frontend/src/pages/profile/layout.tsx index c184a4f..ca67835 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile/layout.tsx @@ -1,15 +1,27 @@ +import { Outlet, useNavigate, useParams } from "react-router"; +import ProfileInformation from "../../components/profile-information"; import { useEffect, useState } from "react"; -import ProfileInformation from "../components/profile-information"; -import { useNavigate, useParams } from "react-router"; +import { useStore } from "@nanostores/react"; +import { session } from "../../session"; -export default function ProfilePage() { +export default function ProfileLayout() { const { id } = useParams(); const navigate = useNavigate(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + const $session = useStore(session); useEffect(() => { - fetch(`${import.meta.env.VITE_API_URL}/api/profile/${id}/info`) + if ($session === undefined) return; // session still loading + if ($session === null) { + // not logged in + navigate("/404"); + return; + } + + const userId = id ? id : $session.user!.id; + + fetch(`${import.meta.env.VITE_API_URL}/api/profile/${userId}/info`) .then((res) => { if (!res.ok) throw new Error("Failed to fetch profile"); return res.json(); @@ -23,7 +35,7 @@ export default function ProfilePage() { setLoading(false); navigate("/404"); }); - }, [id]); + }, [id, $session]); if (loading || !user) { return
Loading...
; @@ -32,9 +44,7 @@ export default function ProfilePage() { return (
- {/* }> - - */} +
); } diff --git a/frontend/src/pages/profile/likes.tsx b/frontend/src/pages/profile/likes.tsx new file mode 100644 index 0000000..86be006 --- /dev/null +++ b/frontend/src/pages/profile/likes.tsx @@ -0,0 +1,15 @@ +import MiiList from "../../components/mii/list"; + +export default function ProfileLikesPage() { + return ( + <> +
+
+

My Likes

+

View every Mii you have liked on TomodachiShare.

+
+
+ + + ); +} diff --git a/frontend/src/pages/settings.tsx b/frontend/src/pages/profile/settings.tsx similarity index 61% rename from frontend/src/pages/settings.tsx rename to frontend/src/pages/profile/settings.tsx index 20b5268..1a78eb4 100644 --- a/frontend/src/pages/settings.tsx +++ b/frontend/src/pages/profile/settings.tsx @@ -1,4 +1,4 @@ -import ProfileSettings from "../components/profile-settings"; +import ProfileSettings from "../../components/profile-settings"; export default function ProfileSettingsPage() { return ; diff --git a/frontend/src/pages/submit.tsx b/frontend/src/pages/submit.tsx index 170e1b7..4b4c303 100644 --- a/frontend/src/pages/submit.tsx +++ b/frontend/src/pages/submit.tsx @@ -5,6 +5,7 @@ import { Navigate } from "react-router"; export default function SubmitPage() { const $session = useStore(session); - if (!$session) return ; + if ($session === undefined) return
Loading...
; + if ($session === null) return ; return ; } diff --git a/frontend/src/session.ts b/frontend/src/session.ts index 4dfe942..cd33d74 100644 --- a/frontend/src/session.ts +++ b/frontend/src/session.ts @@ -8,4 +8,5 @@ interface SessionData { }; } -export const session = atom(null); +// Undefined means still loading, null means no session +export const session = atom(undefined);