From d153addf5119116dfce025c0dc3cc8cdc8f719bd Mon Sep 17 00:00:00 2001 From: Lowell Torola Date: Fri, 8 Nov 2024 22:35:11 -0500 Subject: [PATCH] implemented user chart and team chart --- frontend2/src/App.tsx | 9 ++++- frontend2/src/api/compete/useCompete.ts | 2 +- frontend2/src/api/loaders/accountLoader.ts | 26 ++++++++++++++ frontend2/src/api/user/useUser.ts | 2 -- frontend2/src/components/Header.tsx | 2 +- .../components/compete/chart/ChartBase.tsx | 5 ++- .../components/compete/chart/TeamChart.tsx | 1 + .../components/compete/chart/UserChart.tsx | 33 +++++++++++++++-- frontend2/src/components/team/MemberList.tsx | 9 +++-- frontend2/src/contexts/CurrentUserContext.ts | 3 +- .../src/contexts/CurrentUserProvider.tsx | 2 +- frontend2/src/views/Account.tsx | 35 ++++++++++--------- frontend2/src/views/MyTeam.tsx | 23 +++++++++--- 13 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 frontend2/src/api/loaders/accountLoader.ts diff --git a/frontend2/src/App.tsx b/frontend2/src/App.tsx index 643ce8466..87cf253fd 100644 --- a/frontend2/src/App.tsx +++ b/frontend2/src/App.tsx @@ -51,6 +51,7 @@ import PageNotFound from "views/PageNotFound"; import TeamProfile from "views/TeamProfile"; import { homeIfLoggedIn } from "api/loaders/homeIfLoggedIn"; import { episodeLoader } from "api/loaders/episodeLoader"; +import { accountLoader } from "api/loaders/accountLoader"; const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -118,7 +119,13 @@ const router = createBrowserRouter([ children: [ { element: , - children: [{ path: "/account", element: }], + children: [ + { + path: "/account", + element: , + loader: accountLoader(queryClient), + }, + ], }, ], }, diff --git a/frontend2/src/api/compete/useCompete.ts b/frontend2/src/api/compete/useCompete.ts index f831a5454..7329f08a7 100644 --- a/frontend2/src/api/compete/useCompete.ts +++ b/frontend2/src/api/compete/useCompete.ts @@ -202,7 +202,7 @@ export const useTournamentMatchList = ( }); /** - * For retrieving the hisotrical rating for the given team's in the given episode. + * For retrieving the historical rating of the given teams in the given episode. */ export const useTeamsRatingHistoryList = ({ episodeId, diff --git a/frontend2/src/api/loaders/accountLoader.ts b/frontend2/src/api/loaders/accountLoader.ts new file mode 100644 index 000000000..41cfe6bd9 --- /dev/null +++ b/frontend2/src/api/loaders/accountLoader.ts @@ -0,0 +1,26 @@ +import type { QueryClient } from "@tanstack/react-query"; +import { CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum } from "api/_autogen"; +import { scrimmagingRecordFactory } from "api/compete/competeFactories"; +import { safeEnsureQueryData } from "api/helpers"; +import type { LoaderFunction } from "react-router-dom"; + +export const accountLoader = + (queryClient: QueryClient): LoaderFunction => + ({ params }) => { + const episodeId = params.episodeId; + + if (episodeId === undefined) return null; + + // User team scrimmage record + safeEnsureQueryData( + { + episodeId, + scrimmageType: + CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.All, + }, + scrimmagingRecordFactory, + queryClient, + ); + + return null; + }; diff --git a/frontend2/src/api/user/useUser.ts b/frontend2/src/api/user/useUser.ts index b03f9026e..3db0af820 100644 --- a/frontend2/src/api/user/useUser.ts +++ b/frontend2/src/api/user/useUser.ts @@ -101,8 +101,6 @@ export const useTeamsByUser = ({ Error > => useQuery({ - // queryKey: userQueryKeys.otherTeams({ id }), - // queryFn: async () => await getTeamsByUser({ id }), queryKey: buildKey(otherUserTeamsFactory.queryKey, { id }), queryFn: async () => await otherUserTeamsFactory.queryFn({ id }), }); diff --git a/frontend2/src/components/Header.tsx b/frontend2/src/components/Header.tsx index 9f4c84a6a..e6aee611d 100644 --- a/frontend2/src/components/Header.tsx +++ b/frontend2/src/components/Header.tsx @@ -93,7 +93,7 @@ const Header: React.FC = () => { Open user menu Profile Picture diff --git a/frontend2/src/components/compete/chart/ChartBase.tsx b/frontend2/src/components/compete/chart/ChartBase.tsx index f4db33adb..29fbc3e30 100644 --- a/frontend2/src/components/compete/chart/ChartBase.tsx +++ b/frontend2/src/components/compete/chart/ChartBase.tsx @@ -16,6 +16,7 @@ export interface ChartBaseProps { loading?: boolean; loadingMessage?: string; plotLines?: PlotLine[]; + crownTop?: boolean; } const ChartBase: React.FC = ({ @@ -24,6 +25,7 @@ const ChartBase: React.FC = ({ loading = false, loadingMessage, plotLines, + crownTop = false, }) => { // Translate values into Highcharts compatible options const [myChart, setChart] = useState(); @@ -115,7 +117,8 @@ const ChartBase: React.FC = ({ ); }); - if (index !== -1) names[index + 1] = "👑 " + names[index + 1]; + if (index !== -1 && crownTop) + names[index + 1] = "👑 " + names[index + 1]; return names; }, diff --git a/frontend2/src/components/compete/chart/TeamChart.tsx b/frontend2/src/components/compete/chart/TeamChart.tsx index 2efe6076c..7d537cabb 100644 --- a/frontend2/src/components/compete/chart/TeamChart.tsx +++ b/frontend2/src/components/compete/chart/TeamChart.tsx @@ -40,6 +40,7 @@ const TeamChart: React.FC = ({ teamIds, loading = false }) => { loading={loading || teamRatingHistories.isLoading} loadingMessage="Loading rating data..." plotLines={tournamentData} + crownTop={true} /> ); }; diff --git a/frontend2/src/components/compete/chart/UserChart.tsx b/frontend2/src/components/compete/chart/UserChart.tsx index ebaa17648..58c10aaea 100644 --- a/frontend2/src/components/compete/chart/UserChart.tsx +++ b/frontend2/src/components/compete/chart/UserChart.tsx @@ -11,8 +11,13 @@ import { type PlotLine, type ChartData, } from "./chartUtils"; +import { useTeamsByUser } from "api/user/useUser"; -const UserChart: React.FC = () => { +interface UserChartProps { + userId: number; +} + +const UserChart: React.FC = ({ userId }) => { const { episodeId } = useEpisodeId(); const queryClient = useQueryClient(); @@ -23,6 +28,7 @@ const UserChart: React.FC = () => { { episodeId: selectedEpisode }, queryClient, ); + const teamList = useTeamsByUser({ id: userId }); const ratingHistory = useUserRatingHistoryList({ episodeId: selectedEpisode, }); @@ -37,11 +43,24 @@ const UserChart: React.FC = () => { return formatTournamentList(tournamentList.data.results ?? []); }, [tournamentList]); + const teamListMap = useMemo( + () => new Map(Object.entries(teamList.data ?? {})), + [teamList], + ); + + const episodeListFiltered = useMemo( + () => + (episodeList.data?.results ?? []).filter((ep) => + teamListMap.has(ep.name_short), + ), + [episodeList], + ); + return (
({ + episodeListFiltered.map((ep) => ({ value: ep.name_short, label: ep.name_long, })) ?? [] @@ -52,12 +71,22 @@ const UserChart: React.FC = () => { loading={tournamentList.isLoading} /> + {(episodeList.isLoading || teamList.isLoading) && ( + Loading team... + )} + {episodeList.isSuccess && teamList.isSuccess && ( + + {teamListMap.get(selectedEpisode)?.name ?? "ERROR"} + + )} +
); diff --git a/frontend2/src/components/team/MemberList.tsx b/frontend2/src/components/team/MemberList.tsx index a2fd2c6d6..e17389ef7 100644 --- a/frontend2/src/components/team/MemberList.tsx +++ b/frontend2/src/components/team/MemberList.tsx @@ -41,13 +41,12 @@ const MemberList: React.FC = ({ members, className = "" }) => { return (
{/* display current user first */} - {currentUser !== undefined && - members.find((user) => user.id === currentUser.id) !== undefined && ( - - )} + {currentUser.isSuccess && + members.find((user) => user.id === currentUser.data.id) !== + undefined && } {members.map( (member) => - member.id !== currentUser?.id && ( + member.id !== currentUser.data?.id && ( ), )} diff --git a/frontend2/src/contexts/CurrentUserContext.ts b/frontend2/src/contexts/CurrentUserContext.ts index 39177e68b..cbb923382 100644 --- a/frontend2/src/contexts/CurrentUserContext.ts +++ b/frontend2/src/contexts/CurrentUserContext.ts @@ -1,5 +1,6 @@ import { createContext, useContext } from "react"; import { type UserPrivate } from "../api/_autogen"; +import { type UseQueryResult } from "@tanstack/react-query"; export enum AuthStateEnum { LOADING = "loading", @@ -11,7 +12,7 @@ export type AuthState = `${AuthStateEnum}`; interface CurrentUserContextType { authState: AuthState; - user?: UserPrivate; + user: UseQueryResult; } export const CurrentUserContext = createContext( diff --git a/frontend2/src/contexts/CurrentUserProvider.tsx b/frontend2/src/contexts/CurrentUserProvider.tsx index 1fea959de..4831fc13c 100644 --- a/frontend2/src/contexts/CurrentUserProvider.tsx +++ b/frontend2/src/contexts/CurrentUserProvider.tsx @@ -19,7 +19,7 @@ export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({ const userData = useCurrentUserInfo(); return ( - + {children} ); diff --git a/frontend2/src/views/Account.tsx b/frontend2/src/views/Account.tsx index 54bb9c7e4..6dad69801 100644 --- a/frontend2/src/views/Account.tsx +++ b/frontend2/src/views/Account.tsx @@ -31,14 +31,15 @@ interface FileInput { file: FileList; } -// TODO: create account loader! const Account: React.FC = () => { const { episodeId } = useEpisodeId(); const queryClient = useQueryClient(); + const uploadAvatar = useUpdateUserAvatar({ episodeId }, queryClient); const uploadResume = useResumeUpload({ episodeId }, queryClient); const downloadResume = useDownloadResume({ episodeId }); - const { user } = useCurrentUser(); + + const { user: userData } = useCurrentUser(); const { register: avatarRegister, handleSubmit: handleAvatarSubmit } = useForm(); @@ -106,14 +107,14 @@ const Account: React.FC = () => { loading={uploadResume.isPending} disabled={uploadResume.isPending} /> - {user?.profile?.has_resume ?? false ? ( + {userData.data?.profile?.has_resume ?? false ? (

Resume uploaded!{" "}

- + {userData.isSuccess && } @@ -139,7 +140,7 @@ const ProfileForm: React.FC<{ episodeId: string; queryClient: QueryClient; }> = ({ episodeId, queryClient }) => { - const { user } = useCurrentUser(); + const { user: userData } = useCurrentUser(); const updateCurrentUser = useUpdateCurrentUserInfo( { episodeId }, queryClient, @@ -153,13 +154,13 @@ const ProfileForm: React.FC<{ formState: { errors }, } = useForm({ defaultValues: { - email: user?.email, - first_name: user?.first_name, - last_name: user?.last_name, + email: userData.data?.email, + first_name: userData.data?.first_name, + last_name: userData.data?.last_name, profile: { - school: user?.profile?.school, - kerberos: user?.profile?.kerberos, - biography: user?.profile?.biography, + school: userData.data?.profile?.school, + kerberos: userData.data?.profile?.kerberos, + biography: userData.data?.profile?.biography, }, }, }); @@ -167,10 +168,10 @@ const ProfileForm: React.FC<{ const watchFirstName = watch("first_name"); const watchLastName = watch("last_name"); const [gender, setGender] = useState>( - user?.profile?.gender, + userData.data?.profile?.gender, ); const [country, setCountry] = useState>( - user?.profile?.country, + userData.data?.profile?.country, ); const onProfileSubmit: SubmitHandler = (data) => { @@ -183,7 +184,7 @@ const ProfileForm: React.FC<{
{`${watchFirstName ?? ""} ${watchLastName ?? ""}`} @@ -196,7 +197,7 @@ const ProfileForm: React.FC<{ className="flex flex-1 flex-col gap-4" >
- + { {membersList} + + - { hideAllScrimmages={true} /> - - + + +
{/* Display the members list, file upload, and win/loss to the right when on a big screen. */}
{membersList} + + + { hideAllScrimmages={true} /> - - + +