From 2a072440c0d3216f5f0fdcdd66cf91a93edd0ad8 Mon Sep 17 00:00:00 2001 From: saintnoodle <14948290+saintnoodle@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:43:32 +0100 Subject: [PATCH] refactor(client): unify user profile components (#1156) * disable query instead of throwing an error * stop header from fetching user stats when no user * unify user profiles components for logged in users viewing their own profiles, it now simply reads the UGS in context for viewing other users' games, it now uses react query instead of making a fetch every time the component is created * fix rivals button being shown to logged out users --- .../src/app/pages/dashboard/DashboardPage.tsx | 51 +------ .../pages/dashboard/users/UserGamesPage.tsx | 101 +------------- .../src/components/layout/header/Header.tsx | 6 +- .../components/layout/header/HeaderMenu.tsx | 18 ++- .../tables/dropdowns/PBDropdown.tsx | 8 +- client/src/components/user/UGPTProfiles.tsx | 130 ++++++++++++++++++ .../src/components/util/query/useApiQuery.tsx | 5 +- 7 files changed, 156 insertions(+), 163 deletions(-) create mode 100644 client/src/components/user/UGPTProfiles.tsx diff --git a/client/src/app/pages/dashboard/DashboardPage.tsx b/client/src/app/pages/dashboard/DashboardPage.tsx index 797ed5b05..c91b944a3 100644 --- a/client/src/app/pages/dashboard/DashboardPage.tsx +++ b/client/src/app/pages/dashboard/DashboardPage.tsx @@ -1,4 +1,3 @@ -import { APIFetchV1 } from "util/api"; import { CreateGoalMap } from "util/data"; import { RFA } from "util/misc"; import { NumericSOV } from "util/sorts"; @@ -9,7 +8,6 @@ import { DashboardHeader } from "components/dashboard/DashboardHeader"; import useSetSubheader from "components/layout/header/useSetSubheader"; import SessionCard from "components/sessions/SessionCard"; import ApiError from "components/util/ApiError"; -import AsyncLoader from "components/util/AsyncLoader"; import Divider from "components/util/Divider"; import GoalLink from "components/util/GoalLink"; import LinkButton from "components/util/LinkButton"; @@ -21,13 +19,12 @@ import { TachiConfig } from "lib/config"; import React, { useContext, useMemo } from "react"; import Alert from "react-bootstrap/Alert"; import Stack from "react-bootstrap/Stack"; -import Row from "react-bootstrap/Row"; import { Link, Route, Switch } from "react-router-dom"; -import { GetGameConfig, UserDocument } from "tachi-common"; -import { UGSWithRankingData, UserRecentSummary } from "types/api-returns"; +import { UserDocument } from "tachi-common"; +import { UserRecentSummary } from "types/api-returns"; import SessionCalendar from "components/sessions/SessionCalendar"; import { WindowContext } from "context/WindowContext"; -import { GameStatContainer } from "./users/UserGamesPage"; +import UGPTProfiles from "components/user/UGPTProfiles"; import SupportBanner from "./misc/SupportBanner"; export function DashboardPage() { @@ -70,7 +67,7 @@ function DashboardLoggedIn({ user }: { user: UserDocument }) { /> - + @@ -188,46 +185,6 @@ function RecentInfo({ user }: { user: UserDocument }) { ); } -function UserGameStatsInfo({ user }: { user: UserDocument }) { - return ( - - { - const res = await APIFetchV1( - `/users/${user.id}/game-stats` - ); - - if (!res.success) { - throw new Error(res.description); - } - - return res.body.sort((a, b) => { - if (a.game === b.game) { - const gameConfig = GetGameConfig(a.game); - - return ( - gameConfig.playtypes.indexOf(a.playtype) - - gameConfig.playtypes.indexOf(b.playtype) - ); - } - - const i1 = TachiConfig.GAMES.indexOf(a.game); - const i2 = TachiConfig.GAMES.indexOf(b.game); - - return i1 - i2; - }); - }} - > - {(ugs) => - ugs.map((e) => ( - - )) - } - - - ); -} - function DashboardNotLoggedIn() { const { breakpoint: { isMd }, diff --git a/client/src/app/pages/dashboard/users/UserGamesPage.tsx b/client/src/app/pages/dashboard/users/UserGamesPage.tsx index 2c9815c5a..59a8c1bb6 100644 --- a/client/src/app/pages/dashboard/users/UserGamesPage.tsx +++ b/client/src/app/pages/dashboard/users/UserGamesPage.tsx @@ -1,107 +1,14 @@ -import { APIFetchV1 } from "util/api"; -import { GetSortedGPTs } from "util/site"; import useSetSubheader from "components/layout/header/useSetSubheader"; -import Card from "components/layout/page/Card"; -import RankingData from "components/user/UGPTRankingData"; -import UGPTRatingsTable from "components/user/UGPTStatsOverview"; -import AsyncLoader from "components/util/AsyncLoader"; -import LinkButton from "components/util/LinkButton"; -import Muted from "components/util/Muted"; -import ReferToUser from "components/util/ReferToUser"; import React from "react"; -import { FormatGame, UserDocument, UserGameStats } from "tachi-common"; -import { UGSWithRankingData } from "types/api-returns"; -import Col from "react-bootstrap/Col"; -import Row from "react-bootstrap/Row"; +import UGPTProfiles from "components/user/UGPTProfiles"; +import { UserDocument } from "tachi-common"; -interface Props { - reqUser: UserDocument; -} - -export default function UserGamesPage({ reqUser }: Props) { +export default function UserGamesPage({ reqUser }: { reqUser: UserDocument }) { useSetSubheader( ["Users", reqUser.username, "Games"], [reqUser], `${reqUser.username}'s Game Profiles` ); - return ( - - { - const res = await APIFetchV1( - `/users/${reqUser.id}/game-stats` - ); - - if (!res.success) { - throw new Error(res.description); - } - - return res.body; - }} - > - {(ugs) => - ugs.length ? ( - - ) : ( -
- - not played anything. - -
- ) - } -
-
- ); -} - -function GamesInfo({ ugs, reqUser }: { ugs: UserGameStats[]; reqUser: UserDocument }) { - const gpts = GetSortedGPTs(); - - const ugsMap = new Map(); - - for (const u of ugs) { - ugsMap.set(`${u.game}:${u.playtype}`, u); - } - - return ( - <> - {gpts.map(({ game, playtype }, i) => { - const e = ugsMap.get(`${game}:${playtype}`); - - if (!e) { - return ; - } - - return ; - })} - - ); -} - -export function GameStatContainer({ ugs, reqUser }: { ugs: UGSWithRankingData } & Props) { - return ( - - - - View Game Profile - - - } - header={FormatGame(ugs.game, ugs.playtype)} - > - - - - - ); + return ; } diff --git a/client/src/components/layout/header/Header.tsx b/client/src/components/layout/header/Header.tsx index 972659802..03f46003a 100644 --- a/client/src/components/layout/header/Header.tsx +++ b/client/src/components/layout/header/Header.tsx @@ -54,11 +54,7 @@ export default function Header({ styles }: { styles: LayoutStyles }) { - + {user && (
diff --git a/client/src/components/layout/header/HeaderMenu.tsx b/client/src/components/layout/header/HeaderMenu.tsx index 99a3eb891..db2903aa1 100644 --- a/client/src/components/layout/header/HeaderMenu.tsx +++ b/client/src/components/layout/header/HeaderMenu.tsx @@ -1,10 +1,11 @@ import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext"; import { UserSettingsContext } from "context/UserSettingsContext"; import React, { useContext, useEffect } from "react"; -import { UserDocument, UserGameStats } from "tachi-common"; +import { UserGameStats } from "tachi-common"; import useApiQuery from "components/util/query/useApiQuery"; import Nav from "react-bootstrap/Nav"; import { SetState } from "types/react"; +import { UserContext } from "context/UserContext"; import GlobalInfoDropdown from "./GlobalInfoDropdown"; import ImportScoresDropdown from "./ImportScoresDropdown"; import UtilsDropdown from "./UtilsDropdown"; @@ -14,21 +15,24 @@ const toggleClassNames = "w-100 justify-content-between"; const menuClassNames = "shadow-none shadow-lg-lg"; export function HeaderMenu({ - user, dropdownMenuStyle, setState, }: { - user: UserDocument | null; dropdownMenuStyle?: React.CSSProperties; setState?: SetState; }) { + const { user } = useContext(UserContext); const { ugs, setUGS } = useContext(AllLUGPTStatsContext); const { settings } = useContext(UserSettingsContext); - const { data, error } = useApiQuery("/users/me/game-stats", undefined, [ - user?.id, - "game_stats", - ]); + const { data, error } = useApiQuery( + // We should generate a valid url just in case the skip somehow fails + `/users/${user?.id ?? "me"}/game-stats`, + undefined, + undefined, + // We should skip if a user isn't logged in. + !user + ); useEffect(() => { if (error) { diff --git a/client/src/components/tables/dropdowns/PBDropdown.tsx b/client/src/components/tables/dropdowns/PBDropdown.tsx index 819524bf9..360d01a50 100644 --- a/client/src/components/tables/dropdowns/PBDropdown.tsx +++ b/client/src/components/tables/dropdowns/PBDropdown.tsx @@ -181,9 +181,11 @@ export default function PBDropdown({ {targetData && ` (${targetData.goals.length})`} )} - - Rivals - + {currentUser && ( + + Rivals + + )} Debug Info diff --git a/client/src/components/user/UGPTProfiles.tsx b/client/src/components/user/UGPTProfiles.tsx new file mode 100644 index 000000000..c3d8b11dd --- /dev/null +++ b/client/src/components/user/UGPTProfiles.tsx @@ -0,0 +1,130 @@ +import { GetSortedGPTs } from "util/site"; +import Muted from "components/util/Muted"; +import ReferToUser from "components/util/ReferToUser"; +import React, { memo, useContext } from "react"; +import { Col, Row } from "react-bootstrap"; +import { FormatGame, UserDocument, UserGameStats } from "tachi-common"; +import { UGSWithRankingData } from "types/api-returns"; +import LinkButton from "components/util/LinkButton"; +import Card from "components/layout/page/Card"; +import { UserContext } from "context/UserContext"; +import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext"; +import useApiQuery from "components/util/query/useApiQuery"; +import LoadingWrapper from "components/util/LoadingWrapper"; +import UGPTRatingsTable from "./UGPTStatsOverview"; +import RankingData from "./UGPTRankingData"; + +interface GamesInfoProps { + ugsList: UserGameStats[]; + reqUser: UserDocument; +} + +interface GamesInfoUnitProps { + ugs: UGSWithRankingData; + reqUser: UserDocument; +} + +export default function UGPTProfiles({ reqUser }: { reqUser?: UserDocument }) { + const { user } = useContext(UserContext); + + return ( + + {/* + If a user is logged in and the component hasn't been provided reqUser or this is the logged in user's stats, + we can just grab the user's stats that have already been loaded into context on load. + */} + {user && (!reqUser || reqUser.id === user.id) ? ( + + ) : reqUser ? ( + + ) : ( + <>User not provided; can't show games for nobody! + )} + + ); +} + +const ContextualGamesInfo = memo(({ user }: { user: UserDocument }) => { + const { ugs } = useContext(AllLUGPTStatsContext); + + return ; +}); + +function QueryGamesInfo({ reqUser }: { reqUser: UserDocument }) { + const { data, error } = useApiQuery( + `/users/${reqUser.id}/game-stats`, + undefined, + undefined, + !reqUser + ); + + if (error) { + throw new Error("An error occurred fetching User Game Stats.", { cause: error }); + } + + return ( + + + + ); +} + +function GamesInfo({ ugsList, reqUser }: GamesInfoProps) { + if (ugsList.length === 0) { + return ( +
+ + not played anything. + +
+ ); + } + + const gpts = GetSortedGPTs(); + + const ugsMap = new Map(); + + for (const ugs of ugsList) { + ugsMap.set(`${ugs.game}:${ugs.playtype}`, ugs); + } + + return ( + <> + {gpts.map(({ game, playtype }) => { + const e = ugsMap.get(`${game}:${playtype}`); + + if (!e) { + return null; + } + + return ; + })} + + ); +} + +function GamesInfoUnit({ ugs, reqUser }: GamesInfoUnitProps) { + return ( + + + + View Game Profile + +
+ } + header={FormatGame(ugs.game, ugs.playtype)} + > + + + + + ); +} diff --git a/client/src/components/util/query/useApiQuery.tsx b/client/src/components/util/query/useApiQuery.tsx index 69ae058b7..70ed7b4d3 100644 --- a/client/src/components/util/query/useApiQuery.tsx +++ b/client/src/components/util/query/useApiQuery.tsx @@ -23,10 +23,6 @@ export default function useApiQuery( return useQuery( deps, async () => { - if (skip) { - throw new Error("Skipped"); - } - if (Array.isArray(url)) { const results = await Promise.all(url.map((u) => APIFetchV1(u, options))); @@ -48,6 +44,7 @@ export default function useApiQuery( }, { retry: false, + enabled: !skip, } ); }