import type { LinksFunction, LoaderArgs, V2_MetaFunction, SerializeFrom, } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Outlet, useLoaderData, useLocation } from "@remix-run/react"; import * as React from "react"; import invariant from "tiny-invariant"; import { z } from "zod"; import { Main } from "~/components/Main"; import { SubNav, SubNavLink } from "~/components/SubNav"; import { db } from "~/db"; import { userTopPlacements } from "~/features/top-search"; import { findVods } from "~/features/vods"; import { useTranslation } from "~/hooks/useTranslation"; import { useUser } from "~/modules/auth"; import { getUserId } from "~/modules/auth/user.server"; import { canAddCustomizedColorsToUserProfile } from "~/permissions"; import styles from "~/styles/u.css"; import { notFoundIfFalsy, type SendouRouteHandle } from "~/utils/remix"; import { discordFullName, makeTitle } from "~/utils/strings"; import { isCustomUrl, navIconUrl, userBuildsPage, userEditProfilePage, userPage, userResultsPage, userVodsPage, USER_SEARCH_PAGE, } from "~/utils/urls"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; export const meta: V2_MetaFunction = ({ data, }: { data: UserPageLoaderData; }) => { if (!data) return []; return [{ title: makeTitle(discordFullName(data)) }]; }; export const handle: SendouRouteHandle = { i18n: "user", breadcrumb: ({ match }) => { const data = match.data as UserPageLoaderData | undefined; if (!data) return []; return [ { imgPath: navIconUrl("u"), href: USER_SEARCH_PAGE, type: "IMAGE", }, { text: data.discordName, href: userPage(data), type: "TEXT", }, ]; }, }; export const userParamsSchema = z.object({ identifier: z.string() }); export type UserPageLoaderData = SerializeFrom; export const loader = async ({ params, request }: LoaderArgs) => { const loggedInUser = await getUserId(request); const { identifier } = userParamsSchema.parse(params); const user = notFoundIfFalsy(db.users.findByIdentifier(identifier)); const { playerId, topPlacements } = userTopPlacements(user.id); return json({ id: user.id, discordAvatar: user.discordAvatar, discordDiscriminator: user.discordDiscriminator, discordId: user.discordId, discordName: user.discordName, twitch: user.twitch, twitter: user.twitter, youtubeId: user.youtubeId, bio: user.bio, customUrl: user.customUrl, motionSens: user.motionSens, stickSens: user.stickSens, inGameName: user.inGameName, weapons: user.weapons, team: user.team, country: user.country, css: canAddCustomizedColorsToUserProfile(user) ? user.css : undefined, badges: db.badges.findByOwnerId(user.id), results: db.calendarEvents.findResultsByUserId(user.id), buildsCount: db.builds.countByUserId({ userId: user.id, loggedInUserId: loggedInUser?.id, }), vods: findVods({ userId: user.id }), playerId, topPlacements, }); }; export default function UserPageLayout() { const data = useLoaderData(); const user = useUser(); const { t } = useTranslation(); const isOwnPage = data.id === user?.id; useReplaceWithCustomUrl(); return (
{t("header.profile")} {isOwnPage && ( {t("actions.edit")} )} {data.results.length > 0 && ( {t("results")} ({data.results.length}) )} {(isOwnPage || data.buildsCount > 0) && ( {t("pages.builds")} ({data.buildsCount}) )} {data.vods.length > 0 && ( {t("pages.vods")} ({data.vods.length}) )}
); } function useReplaceWithCustomUrl() { const data = useLoaderData(); const location = useLocation(); React.useEffect(() => { if (!data.customUrl) { return; } const identifier = location.pathname.replace("/u/", "").split("/")[0]; invariant(identifier); if (isCustomUrl(identifier)) { return; } window.history.replaceState( null, "", location.pathname .split("/") .map((part) => (part === identifier ? data.customUrl : part)) .join("/") ); }, [location, data.customUrl]); }