mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
feat: show miis on profiles
and other changes
This commit is contained in:
parent
9795849830
commit
896dc40553
17 changed files with 271 additions and 408 deletions
|
|
@ -9,7 +9,7 @@ export async function GET(request: NextRequest) {
|
||||||
const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
||||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
|
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
|
// My Likes page
|
||||||
let miiIdsLiked: number[] | undefined = undefined;
|
let miiIdsLiked: number[] | undefined = undefined;
|
||||||
|
|
@ -107,7 +107,7 @@ export async function GET(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
[totalCount, miis] = await Promise.all([
|
[totalCount, miis] = await Promise.all([
|
||||||
prisma.mii.count({ where: { ...where } }), // TODO: User id
|
prisma.mii.count({ where: { ...where, userId } }),
|
||||||
prisma.mii.findMany({
|
prisma.mii.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="130.734" height="105.615" viewBox="0 0 34.59 27.944"><rect width="32.208" height="25.562" x="1.191" y="1.191" rx="1.874" fill="#f8f8f8" stroke="#ff8904" stroke-width="2.381" paint-order="stroke fill markers"/><rect width="29.369" height="22.49" x="2.611" y="2.727" rx=".966" fill="#c8c8c8" paint-order="stroke fill markers"/><g fill="#fef3c6"><rect width="13.371" height="20.989" x="17.918" y="3.478" rx=".423" paint-order="stroke fill markers"/><rect width="13.371" height="20.989" x="3.301" y="3.478" rx=".423" paint-order="stroke fill markers"/></g><g fill="#ff8904"><use href="#B" paint-order="stroke fill markers"/><circle cx="9.986" cy="13.076" r="5.512" paint-order="stroke fill markers"/><use href="#B" x="14.204" y="-0.093" paint-order="stroke fill markers"/><circle cx="24.191" cy="12.983" r="5.512" paint-order="stroke fill markers"/></g><g fill="none" stroke="#c8c8c8" stroke-linejoin="round"><rect width="13.791" height="20.704" x="17.295" y="3.62" ry="1.146" rx="1.095" stroke-width="1.786" paint-order="stroke fill markers"/><rect width="13.366" height="21.167" x="3.301" y="3.389" ry="1.146" rx="1.095" stroke-width="1.323" paint-order="stroke fill markers"/></g><defs ><path id="B" d="M15.03 24.516c0-2.307-.961-4.439-2.522-5.592s-3.483-1.153-5.044 0-2.522 3.285-2.522 5.592h5.044z"/></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -11,7 +11,7 @@ export default function Header() {
|
||||||
aria-label="Go to Home Page"
|
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"
|
className="font-black text-3xl text-orange-400 flex items-center gap-2 max-md:justify-center max-md:col-span-2"
|
||||||
>
|
>
|
||||||
<img src="/logo.svg" width={56} height={45} alt="logo" />
|
<img src="/favicon.svg" width={56} height={45} alt="logo" />
|
||||||
TomodachiShare
|
TomodachiShare
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,184 +1,164 @@
|
||||||
// import crypto from "crypto";
|
import { useEffect, useState } from "react";
|
||||||
// import seedrandom from "seedrandom";
|
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";
|
interface Props {
|
||||||
// import Pagination from "./pagination";
|
userId?: number;
|
||||||
// import FilterMenu from "./filter-menu";
|
parentPage?: "likes" | "admin";
|
||||||
// import MiiGrid from "./mii-grid";
|
}
|
||||||
|
|
||||||
// interface Props {
|
export default function MiiList({ parentPage, userId }: Props) {
|
||||||
// searchParams: URLSearchParams;
|
const [searchParams] = useSearchParams();
|
||||||
// userId?: number; // Profiles
|
const [data, setData] = useState<ApiResponse | null>(null);
|
||||||
// parentPage?: "likes" | "admin";
|
const [loading, setLoading] = useState(true);
|
||||||
// }
|
|
||||||
|
|
||||||
// export default async function MiiList({ searchParams, userId, parentPage }: Props) {
|
const $session = useStore(session);
|
||||||
// const session = await auth();
|
|
||||||
// const parsed = searchSchema.safeParse(searchParams);
|
|
||||||
// if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
|
||||||
|
|
||||||
// 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
|
fetch(`${import.meta.env.VITE_API_URL}/api/mii/list?${params.toString()}`, { credentials: "include" })
|
||||||
// let miiIdsLiked: number[] | undefined = undefined;
|
.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) {
|
return (
|
||||||
// const likedMiis = await prisma.like.findMany({
|
<>
|
||||||
// where: { userId: Number(session.user.id) },
|
{loading ? (
|
||||||
// select: { miiId: true },
|
<Skeleton />
|
||||||
// });
|
) : data ? (
|
||||||
// miiIdsLiked = likedMiis.map((like) => like.miiId);
|
<div className="w-full">
|
||||||
// }
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold text-amber-900">{data.totalCount}</span>
|
||||||
|
<span className="text-lg text-amber-700">{data.totalCount === 1 ? "Mii" : "Miis"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
// const where: Prisma.MiiWhereInput = {
|
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||||
// // In queue logic
|
<FilterMenu />
|
||||||
// ...(parentPage === "admin"
|
<SortSelect />
|
||||||
// ? { in_queue: true } // Only show queued Miis
|
</div>
|
||||||
// : userId
|
</div>
|
||||||
// ? {
|
|
||||||
// // 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 = {
|
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
||||||
// id: true,
|
{data.miis.map((mii) => (
|
||||||
// // Don't show when userId is specified
|
<div
|
||||||
// ...(!userId && {
|
key={mii.id}
|
||||||
// user: {
|
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 && parentPage !== "admin" ? "border-zinc-400 opacity-70" : "border-zinc-300"}`}
|
||||||
// select: {
|
>
|
||||||
// id: true,
|
{mii.in_queue && (
|
||||||
// name: true,
|
<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>
|
||||||
// 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 },
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const skip = (page - 1) * limit;
|
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`}
|
||||||
|
width={240}
|
||||||
|
height={160}
|
||||||
|
alt="mii image"
|
||||||
|
className="w-full h-auto aspect-3/2 object-contain"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
// let totalCount: number;
|
<div className="p-4 flex flex-col gap-1 h-full">
|
||||||
// let miis: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
<div className="flex justify-between">
|
||||||
|
<Link to={`/mii/${mii.id}`} className="relative font-bold text-2xl line-clamp-1 w-full text-ellipsis wrap-break-word" title={mii.name}>
|
||||||
|
{mii.name}
|
||||||
|
</Link>
|
||||||
|
<div title={mii.platform === "SWITCH" ? "Switch" : "3DS"} className="text-[1.25rem] opacity-25">
|
||||||
|
{mii.platform === "SWITCH" ? (
|
||||||
|
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||||
|
) : (
|
||||||
|
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tags" className="flex flex-wrap gap-1">
|
||||||
|
{mii.tags.map((tag: string) => (
|
||||||
|
<Link to={`?tags=${tag}`} key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
||||||
|
{tag}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
// if (sort === "random") {
|
<div className="mt-auto grid grid-cols-2 items-center">
|
||||||
// // Get all IDs that match the where conditions
|
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={false} abbreviate />
|
||||||
// const matchingIds = await prisma.mii.findMany({
|
|
||||||
// where,
|
|
||||||
// select: { id: true },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// totalCount = matchingIds.length;
|
{!userId && (
|
||||||
|
<Link to={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
@{mii.user?.name}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
// if (matchingIds.length === 0) return;
|
{userId && Number($session?.user?.id) == userId && (
|
||||||
|
<div className="flex gap-1 text-2xl justify-end text-zinc-400">
|
||||||
|
<Link to={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
||||||
|
<Icon icon="mdi:pencil" />
|
||||||
|
</Link>
|
||||||
|
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
// // Use seed for consistent random results
|
{/* Admin Controls */}
|
||||||
// const randomSeed = seed || crypto.randomInt(0, 1_000_000_000);
|
{parentPage === "admin" && (
|
||||||
// const rng = seedrandom(randomSeed.toString());
|
<div className="flex justify-between w-full col-span-2 mt-2">
|
||||||
|
<div className="flex gap-1 text-3xl justify-center">
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch(`/api/admin/accept-mii?id=${mii.id}`, { method: "PATCH" });
|
||||||
|
}}
|
||||||
|
className="cursor-pointer text-zinc-400 hover:text-green-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-green-500"
|
||||||
|
title="Accept Mii"
|
||||||
|
>
|
||||||
|
<Icon icon="material-symbols:check-rounded" />
|
||||||
|
</button>
|
||||||
|
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
|
||||||
|
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// // Randomize all IDs using the Durstenfeld algorithm
|
<span className="text-sm w-1/2 text-right">{new Date(mii.createdAt).toLocaleString("en-GB", { timeZone: "UTC" })}</span>
|
||||||
// for (let i = matchingIds.length - 1; i > 0; i--) {
|
</div>
|
||||||
// const j = Math.floor(rng() * (i + 1));
|
)}
|
||||||
// [matchingIds[i], matchingIds[j]] = [matchingIds[j], matchingIds[i]];
|
</div>
|
||||||
// }
|
</div>
|
||||||
|
</div>
|
||||||
// // Convert to number[] array
|
))}
|
||||||
// const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id);
|
</div>
|
||||||
|
<Pagination lastPage={data.lastPage} />
|
||||||
// miis = await prisma.mii.findMany({
|
</div>
|
||||||
// where: {
|
) : (
|
||||||
// id: { in: selectedIds },
|
<p>No Miis found, has the server died?</p>
|
||||||
// },
|
)}
|
||||||
// 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 (
|
|
||||||
// <div className="w-full">
|
|
||||||
// <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
|
||||||
// <div className="flex items-center gap-2">
|
|
||||||
// <span className="text-2xl font-bold text-amber-900">{totalCount}</span>
|
|
||||||
// <span className="text-lg text-amber-700">{totalCount === 1 ? "Mii" : "Miis"}</span>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// <div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
|
||||||
// <FilterMenu />
|
|
||||||
// <SortSelect />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// <MiiGrid miis={miis} userId={userId} parentPage={parentPage} />
|
|
||||||
// <Pagination lastPage={lastPage} />
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
|
||||||
{miis.map((mii) => (
|
|
||||||
<div
|
|
||||||
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 && parentPage !== "admin" ? "border-zinc-400 opacity-70" : "border-zinc-300"}`}
|
|
||||||
>
|
|
||||||
{mii.in_queue && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
|
||||||
<img
|
|
||||||
src={`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`}
|
|
||||||
width={240}
|
|
||||||
height={160}
|
|
||||||
alt="mii image"
|
|
||||||
className="w-full h-auto aspect-3/2 object-contain"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="p-4 flex flex-col gap-1 h-full">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Link to={`/mii/${mii.id}`} className="relative font-bold text-2xl line-clamp-1 w-full text-ellipsis wrap-break-word" title={mii.name}>
|
|
||||||
{mii.name}
|
|
||||||
</Link>
|
|
||||||
<div title={mii.platform === "SWITCH" ? "Switch" : "3DS"} className="text-[1.25rem] opacity-25">
|
|
||||||
{mii.platform === "SWITCH" ? (
|
|
||||||
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
|
||||||
) : (
|
|
||||||
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="tags" className="flex flex-wrap gap-1">
|
|
||||||
{mii.tags.map((tag: string) => (
|
|
||||||
<Link to={`?tags=${tag}`} key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
|
||||||
{tag}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-auto grid grid-cols-2 items-center">
|
|
||||||
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={false} abbreviate />
|
|
||||||
|
|
||||||
{!userId && (
|
|
||||||
<Link to={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
@{mii.user?.name}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* {userId && Number(session.data?.user?.id) == userId && (
|
|
||||||
<div className="flex gap-1 text-2xl justify-end text-zinc-400">
|
|
||||||
<Link to={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
|
||||||
<Icon icon="mdi:pencil" />
|
|
||||||
</Link>
|
|
||||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* Admin Controls */}
|
|
||||||
{parentPage === "admin" && (
|
|
||||||
<div className="flex justify-between w-full col-span-2 mt-2">
|
|
||||||
<div className="flex gap-1 text-3xl justify-center">
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
await fetch(`/api/admin/accept-mii?id=${mii.id}`, { method: "PATCH" });
|
|
||||||
}}
|
|
||||||
className="cursor-pointer text-zinc-400 hover:text-green-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-green-500"
|
|
||||||
title="Accept Mii"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:check-rounded" />
|
|
||||||
</button>
|
|
||||||
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
|
|
||||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-sm w-1/2 text-right">{new Date(mii.createdAt).toLocaleString("en-GB", { timeZone: "UTC" })}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
import FilterSelect from "./tag-filter";
|
import SortSelect from "./sort-select";
|
||||||
import SortSelect from "./sort-select";
|
import Pagination from "../../pagination";
|
||||||
import Pagination from "../../pagination";
|
import FilterMenu from "./filter-menu";
|
||||||
|
|
||||||
export default function Skeleton() {
|
export default function Skeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full animate-pulse">
|
<div className="w-full animate-pulse">
|
||||||
<div className="flex justify-between items-end mb-2 max-[32rem]:flex-col max-[32rem]:items-center">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
||||||
<p className="text-lg">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-extrabold">???</span> Miis
|
<span className="text-2xl font-bold text-amber-900">???</span>
|
||||||
</p>
|
<span className="text-lg text-amber-700">Miis</span>
|
||||||
|
</div>
|
||||||
<div className="flex gap-2 pointer-events-none">
|
|
||||||
<FilterSelect />
|
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||||
<SortSelect />
|
<FilterMenu />
|
||||||
</div>
|
<SortSelect />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
|
||||||
{[...Array(24)].map((_, index) => (
|
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-[30rem]:grid-cols-1">
|
||||||
<div key={index} className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
{[...Array(24)].map((_, index) => (
|
||||||
{/* Carousel Skeleton */}
|
<div key={index} className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
||||||
<div className="relative rounded-xl bg-zinc-300 border-2 border-zinc-300 mb-1">
|
{/* Carousel Skeleton */}
|
||||||
<div className="aspect-3/2"></div>
|
<div className="relative rounded-xl bg-zinc-300 border-2 border-zinc-300 mb-1">
|
||||||
</div>
|
<div className="aspect-3/2"></div>
|
||||||
|
</div>
|
||||||
{/* Content */}
|
|
||||||
<div className="p-4 flex flex-col gap-1 h-full">
|
{/* Content */}
|
||||||
{/* Name */}
|
<div className="p-4 flex flex-col gap-1 h-full">
|
||||||
<div className="h-7 bg-zinc-300 rounded w-2/3 mb-0.5" />
|
{/* Name */}
|
||||||
|
<div className="h-7 bg-zinc-300 rounded w-2/3 mb-0.5" />
|
||||||
{/* Tags */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
{/* Tags */}
|
||||||
<div className="px-4 py-2 bg-orange-200 rounded-full w-14 h-6" />
|
<div className="flex flex-wrap gap-1">
|
||||||
<div className="px-4 py-2 bg-orange-200 rounded-full w-10 h-6" />
|
<div className="px-4 py-2 bg-orange-200 rounded-full w-14 h-6" />
|
||||||
</div>
|
<div className="px-4 py-2 bg-orange-200 rounded-full w-10 h-6" />
|
||||||
|
</div>
|
||||||
{/* Bottom row */}
|
|
||||||
<div className="mt-0.5 grid grid-cols-2 items-center">
|
{/* Bottom row */}
|
||||||
<div className="h-6 w-12 bg-red-200 rounded" />
|
<div className="mt-0.5 grid grid-cols-2 items-center">
|
||||||
<div className="h-4 w-24 bg-zinc-200 rounded justify-self-end" />
|
<div className="h-6 w-12 bg-red-200 rounded" />
|
||||||
</div>
|
<div className="h-4 w-24 bg-zinc-200 rounded justify-self-end" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
<div className="pointer-events-none">
|
|
||||||
<Pagination lastPage={10} />
|
<div className="pointer-events-none">
|
||||||
</div>
|
<Pagination lastPage={10} />
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,20 @@ import { Icon } from "@iconify/react";
|
||||||
import Description from "./description";
|
import Description from "./description";
|
||||||
import { useStore } from "@nanostores/react";
|
import { useStore } from "@nanostores/react";
|
||||||
import { session } from "../session";
|
import { session } from "../session";
|
||||||
import { Link } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user?: any;
|
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);
|
const $session = useStore(session);
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const currentUser = user ?? $session?.user;
|
const currentUser = user ?? $session?.user;
|
||||||
|
const page = location.pathname;
|
||||||
const isAdmin = currentUser?.id === Number(import.meta.env.VITE_ADMIN_USER_ID);
|
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 isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(user?.id);
|
||||||
const isOwnProfile = currentUser?.id === user?.id;
|
const isOwnProfile = currentUser?.id === user?.id;
|
||||||
|
|
@ -24,8 +25,8 @@ export default function ProfileInformation({ user, page }: Props) {
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex gap-4 mb-2 max-md:flex-col">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex gap-4 mb-2 max-md:flex-col">
|
||||||
<div className="flex w-full gap-4 overflow-x-scroll">
|
<div className="flex w-full gap-4 overflow-x-scroll">
|
||||||
{/* Profile picture */}
|
{/* Profile picture */}
|
||||||
<Link to={`/profile/${user.id}`} className="size-28 aspect-square">
|
<Link to={`${import.meta.env.VITE_API_URL}/profile/${user.id}`} className="size-28 aspect-square">
|
||||||
<img src={user.image ?? "/guest.png"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
|
<img src={user.image ?? "/guest.png"} className="rounded-full bg-white border-2 border-orange-400 shadow w-full max-md:self-center" />
|
||||||
</Link>
|
</Link>
|
||||||
{/* User information */}
|
{/* User information */}
|
||||||
<div className="flex flex-col w-full relative py-3">
|
<div className="flex flex-col w-full relative py-3">
|
||||||
|
|
@ -72,19 +73,19 @@ export default function ProfileInformation({ user, page }: Props) {
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{/* {isOwnProfile && page !== "likes" && (
|
{isOwnProfile && page !== "/profile/likes" && (
|
||||||
<Link aria-label="Go to My Likes" to="/profile/likes">
|
<Link aria-label="Go to My Likes" to="/profile/likes">
|
||||||
<Icon icon="icon-park-solid:like" />
|
<Icon icon="icon-park-solid:like" />
|
||||||
<span>My Likes</span>
|
<span>My Likes</span>
|
||||||
</Link>
|
</Link>
|
||||||
)} */}
|
)}
|
||||||
{isOwnProfile && page !== "settings" && (
|
{isOwnProfile && page !== "/profile/settings" && (
|
||||||
<Link aria-label="Go to Settings" to="/profile/settings">
|
<Link aria-label="Go to Settings" to="/profile/settings">
|
||||||
<Icon icon="material-symbols:settings-rounded" />
|
<Icon icon="material-symbols:settings-rounded" />
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{page && (
|
{(page === "/profile/likes" || page === "/profile/settings") && (
|
||||||
<Link aria-label="Go Back to Profile" to={`/profile/${user.id}`}>
|
<Link aria-label="Go Back to Profile" to={`/profile/${user.id}`}>
|
||||||
<Icon icon="tabler:chevron-left" />
|
<Icon icon="tabler:chevron-left" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@ import PrivacyPage from "./pages/privacy.tsx";
|
||||||
import TermsOfServicePage from "./pages/terms-of-service.tsx";
|
import TermsOfServicePage from "./pages/terms-of-service.tsx";
|
||||||
import NotFoundPage from "./pages/not-found.tsx";
|
import NotFoundPage from "./pages/not-found.tsx";
|
||||||
import LoginPage from "./pages/login.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 MiiPage from "./pages/mii.tsx";
|
||||||
import SubmitPage from "./pages/submit.tsx";
|
import SubmitPage from "./pages/submit.tsx";
|
||||||
import IndexPage from "./pages/index.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 { ProgressProvider } from "@bprogress/react";
|
||||||
import LinkOutPage from "./pages/out.tsx";
|
import LinkOutPage from "./pages/out.tsx";
|
||||||
import Layout from "./layout.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(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
@ -25,8 +27,9 @@ createRoot(document.getElementById("root")!).render(
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<IndexPage />} />
|
<Route path="/" element={<IndexPage />} />
|
||||||
<Route path="/mii/:id" element={<MiiPage />} />
|
<Route path="/mii/:id" element={<MiiPage />} />
|
||||||
<Route path="/profile">
|
<Route path="/profile" element={<ProfileLayout />}>
|
||||||
<Route path=":id" element={<ProfilePage />} />
|
<Route path=":id" element={<ProfilePage />} />
|
||||||
|
<Route path="likes" element={<ProfileLikesPage />} />
|
||||||
<Route path="settings" element={<ProfileSettingsPage />} />
|
<Route path="settings" element={<ProfileSettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/submit" element={<SubmitPage />} />
|
<Route path="/submit" element={<SubmitPage />} />
|
||||||
|
|
|
||||||
|
|
@ -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";
|
import { useSearchParams } from "react-router";
|
||||||
|
import MiiList from "../components/mii/list";
|
||||||
interface ApiResponse {
|
|
||||||
totalCount: number;
|
|
||||||
miis: any[];
|
|
||||||
lastPage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [data, setData] = useState<ApiResponse | null>(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<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>
|
<p className="text-center mb-4">We're currently going through some major code changes therefore some features won't work.</p>
|
||||||
|
<MiiList />
|
||||||
<Suspense fallback={<Skeleton />}>
|
|
||||||
{!loading && data ? (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-2xl font-bold text-amber-900">{data.totalCount}</span>
|
|
||||||
<span className="text-lg text-amber-700">{data.totalCount === 1 ? "Mii" : "Miis"}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
|
||||||
<FilterMenu />
|
|
||||||
<SortSelect />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MiiGrid miis={data.miis} />
|
|
||||||
<Pagination lastPage={data.lastPage} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>No Miis found :( Has the server died?</p>
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { session } from "../session";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const $session = useStore(session);
|
const $session = useStore(session);
|
||||||
|
if ($session === undefined) return <div className="p-6 text-center">Loading...</div>;
|
||||||
if ($session) return <Navigate to="/" replace />;
|
if ($session) return <Navigate to="/" replace />;
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
|
||||||
8
frontend/src/pages/profile/index.tsx
Normal file
8
frontend/src/pages/profile/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import MiiList from "../../components/mii/list";
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
return <MiiList userId={Number(id)} />;
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
|
import { Outlet, useNavigate, useParams } from "react-router";
|
||||||
|
import ProfileInformation from "../../components/profile-information";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ProfileInformation from "../components/profile-information";
|
import { useStore } from "@nanostores/react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { session } from "../../session";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfileLayout() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const $session = useStore(session);
|
||||||
|
|
||||||
useEffect(() => {
|
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) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error("Failed to fetch profile");
|
if (!res.ok) throw new Error("Failed to fetch profile");
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|
@ -23,7 +35,7 @@ export default function ProfilePage() {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
navigate("/404");
|
navigate("/404");
|
||||||
});
|
});
|
||||||
}, [id]);
|
}, [id, $session]);
|
||||||
|
|
||||||
if (loading || !user) {
|
if (loading || !user) {
|
||||||
return <div className="p-6 text-center">Loading...</div>;
|
return <div className="p-6 text-center">Loading...</div>;
|
||||||
|
|
@ -32,9 +44,7 @@ export default function ProfilePage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProfileInformation user={user} />
|
<ProfileInformation user={user} />
|
||||||
{/* <Suspense fallback={<Skeleton />}>
|
<Outlet />
|
||||||
<MiiList searchParams={await searchParams} userId={user.id} />
|
|
||||||
</Suspense> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
15
frontend/src/pages/profile/likes.tsx
Normal file
15
frontend/src/pages/profile/likes.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import MiiList from "../../components/mii/list";
|
||||||
|
|
||||||
|
export default function ProfileLikesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-4 mb-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold">My Likes</h2>
|
||||||
|
<p className="text-sm text-zinc-500">View every Mii you have liked on TomodachiShare.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MiiList parentPage="likes" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ProfileSettings from "../components/profile-settings";
|
import ProfileSettings from "../../components/profile-settings";
|
||||||
|
|
||||||
export default function ProfileSettingsPage() {
|
export default function ProfileSettingsPage() {
|
||||||
return <ProfileSettings currentDescription={null} />;
|
return <ProfileSettings currentDescription={null} />;
|
||||||
|
|
@ -5,6 +5,7 @@ import { Navigate } from "react-router";
|
||||||
|
|
||||||
export default function SubmitPage() {
|
export default function SubmitPage() {
|
||||||
const $session = useStore(session);
|
const $session = useStore(session);
|
||||||
if (!$session) return <Navigate to="/login" replace />;
|
if ($session === undefined) return <div className="p-6 text-center">Loading...</div>;
|
||||||
|
if ($session === null) return <Navigate to="/login" replace />;
|
||||||
return <SubmitForm />;
|
return <SubmitForm />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ interface SessionData {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const session = atom<SessionData | null>(null);
|
// Undefined means still loading, null means no session
|
||||||
|
export const session = atom<SessionData | null | undefined>(undefined);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue