From 6eec880c9a3317830613c3328c2c3df9d0fd7cd5 Mon Sep 17 00:00:00 2001 From: Serena Li <40174697+acrantel@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:28:11 -0400 Subject: [PATCH] Add team context, update user/episode context (#679) --- frontend2/src/App.tsx | 19 +++++-- .../src/components/CurrentUserProvider.tsx | 29 +++++----- frontend2/src/components/EpisodeLayout.tsx | 10 ++-- frontend2/src/components/Header.tsx | 7 +-- .../src/components/compete/RatingDelta.tsx | 6 +- .../sidebar/__test__/Sidebar.test.tsx | 28 +++------ frontend2/src/components/sidebar/index.tsx | 6 +- frontend2/src/contexts/CurrentTeamContext.ts | 30 ++++++++++ .../src/contexts/CurrentTeamProvider.tsx | 44 ++++++++++++++ frontend2/src/contexts/EpisodeContext.ts | 39 +++++++++---- frontend2/src/contexts/EpisodeProvider.tsx | 57 +++++++++++++++++++ frontend2/src/utils/api/team.ts | 9 +++ frontend2/src/views/Account.tsx | 6 +- frontend2/src/views/Login.tsx | 6 +- frontend2/src/views/Queue.tsx | 6 +- frontend2/src/views/Rankings.tsx | 6 +- frontend2/src/views/Register.tsx | 6 +- 17 files changed, 235 insertions(+), 79 deletions(-) create mode 100644 frontend2/src/contexts/CurrentTeamContext.ts create mode 100644 frontend2/src/contexts/CurrentTeamProvider.tsx create mode 100644 frontend2/src/contexts/EpisodeProvider.tsx diff --git a/frontend2/src/App.tsx b/frontend2/src/App.tsx index f50897731..ad3479985 100644 --- a/frontend2/src/App.tsx +++ b/frontend2/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import EpisodeLayout from "./components/EpisodeLayout"; import Home from "./views/Home"; import Logout from "./views/Logout"; @@ -8,7 +8,6 @@ import PasswordChange from "./views/PasswordChange"; import Account from "./views/Account"; import Login from "./views/Login"; import QuickStart from "./views/QuickStart"; -import { EpisodeContext } from "./contexts/EpisodeContext"; import { RouterProvider, createBrowserRouter, @@ -22,14 +21,18 @@ import { CurrentUserProvider } from "./components/CurrentUserProvider"; import PrivateRoute from "./components/PrivateRoute"; import Queue from "./views/Queue"; import Resources from "./views/Resources"; +import MyTeam from "./views/MyTeam"; +import { CurrentTeamProvider } from "./contexts/CurrentTeamProvider"; +import { EpisodeProvider } from "./contexts/EpisodeProvider"; const App: React.FC = () => { - const [episodeId, setEpisodeId] = useState(DEFAULT_EPISODE); return ( - - - + + + + + ); }; @@ -49,6 +52,10 @@ const router = createBrowserRouter([ { element: , children: [ + { + path: "/:episodeId/team", + element: , + }, // TODO: /:episodeId/team, /:episodeId/submissions, /:episodeId/scrimmaging ], }, diff --git a/frontend2/src/components/CurrentUserProvider.tsx b/frontend2/src/components/CurrentUserProvider.tsx index 297cc4f66..84b1d6cef 100644 --- a/frontend2/src/components/CurrentUserProvider.tsx +++ b/frontend2/src/components/CurrentUserProvider.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useCallback, useMemo, useEffect } from "react"; import { type UserPrivate } from "../utils/types"; import { AuthStateEnum, @@ -20,19 +20,20 @@ export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({ authState: AuthStateEnum.LOADING, }); - const login = (user: UserPrivate): void => { + // useCallback to avoid redefining login/logout each rerender + const login = useCallback((user: UserPrivate): void => { setUserData({ user, authState: AuthStateEnum.AUTHENTICATED, }); - }; - const logout = (): void => { + }, []); + const logout = useCallback((): void => { Cookies.remove("access"); Cookies.remove("refresh"); setUserData({ authState: AuthStateEnum.NOT_AUTHENTICATED, }); - }; + }, []); useEffect(() => { const checkLoggedIn = async (): Promise => { @@ -51,15 +52,17 @@ export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({ void checkLoggedIn(); }, []); + const providedValue = useMemo( + () => ({ + ...userData, + login, + logout, + }), + [login, logout, userData], + ); + return ( - + {children} ); diff --git a/frontend2/src/components/EpisodeLayout.tsx b/frontend2/src/components/EpisodeLayout.tsx index b51fea020..0d88dd1cc 100644 --- a/frontend2/src/components/EpisodeLayout.tsx +++ b/frontend2/src/components/EpisodeLayout.tsx @@ -1,13 +1,13 @@ -import React, { useContext, useEffect } from "react"; +import React, { useEffect } from "react"; import Header from "./Header"; import Sidebar from "./sidebar"; import { Outlet, useParams } from "react-router-dom"; -import { EpisodeContext } from "../contexts/EpisodeContext"; +import { useEpisodeId } from "../contexts/EpisodeContext"; // This component contains the Header and SideBar. // Child route components are rendered with const EpisodeLayout: React.FC = () => { - const episodeContext = useContext(EpisodeContext); + const episodeContext = useEpisodeId(); const { episodeId } = useParams(); useEffect(() => { if (episodeId !== undefined && episodeId !== episodeContext.episodeId) { @@ -15,10 +15,10 @@ const EpisodeLayout: React.FC = () => { } }, [episodeId]); return ( -
+
-
+
diff --git a/frontend2/src/components/Header.tsx b/frontend2/src/components/Header.tsx index 7f9e4c1af..356d04dff 100644 --- a/frontend2/src/components/Header.tsx +++ b/frontend2/src/components/Header.tsx @@ -1,14 +1,14 @@ -import React, { Fragment, useContext } from "react"; +import React, { Fragment } from "react"; import { Menu, Transition } from "@headlessui/react"; import { Link, NavLink } from "react-router-dom"; import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext"; import Icon from "./elements/Icon"; -import { EpisodeContext } from "../contexts/EpisodeContext"; +import { useEpisodeId } from "../contexts/EpisodeContext"; import { SIDEBAR_ITEM_DATA } from "./sidebar"; const Header: React.FC = () => { const { authState, logout, user } = useCurrentUser(); - const { episodeId } = useContext(EpisodeContext); + const { episodeId } = useEpisodeId(); return (
-
{/* profile menu (if the user is logged in) */} {authState === AuthStateEnum.AUTHENTICATED && ( diff --git a/frontend2/src/components/compete/RatingDelta.tsx b/frontend2/src/components/compete/RatingDelta.tsx index 84152a005..372989a09 100644 --- a/frontend2/src/components/compete/RatingDelta.tsx +++ b/frontend2/src/components/compete/RatingDelta.tsx @@ -1,7 +1,7 @@ import { NavLink } from "react-router-dom"; import { type MatchParticipant } from "../../utils/types"; -import React, { useContext } from "react"; -import { EpisodeContext } from "../../contexts/EpisodeContext"; +import React from "react"; +import { useEpisodeId } from "../../contexts/EpisodeContext"; interface RatingDeltaProps { participant: MatchParticipant; @@ -9,7 +9,7 @@ interface RatingDeltaProps { } const RatingDelta: React.FC = ({ participant, ranked }) => { - const episodeId = useContext(EpisodeContext).episodeId; + const { episodeId } = useEpisodeId(); const newRating = ranked ? Math.round(participant.rating) diff --git a/frontend2/src/components/sidebar/__test__/Sidebar.test.tsx b/frontend2/src/components/sidebar/__test__/Sidebar.test.tsx index c4630b6d8..1925573b8 100644 --- a/frontend2/src/components/sidebar/__test__/Sidebar.test.tsx +++ b/frontend2/src/components/sidebar/__test__/Sidebar.test.tsx @@ -1,29 +1,17 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import Sidebar from "../"; -import { DEFAULT_EPISODE } from "../../../utils/constants"; -import { EpisodeContext } from "../../../contexts/EpisodeContext"; +import { EpisodeIdContext } from "../../../contexts/EpisodeContext"; import { MemoryRouter } from "react-router-dom"; -test("UI: should link to default episode", () => { - render( - - - , - ); - const linkElement = screen - .getByText("Resources") - .closest("a") - ?.getAttribute("href"); - expect(linkElement).toEqual( - expect.stringContaining(`/${DEFAULT_EPISODE}/resources`), - ); -}); - test("UI: should collapse sidebar", () => { render( - + undefined }} + > + + , ); expect(screen.queryByText("Home")).toBeNull(); @@ -32,11 +20,11 @@ test("UI: should collapse sidebar", () => { test("UI: should link to episode in surrounding context", () => { render( - undefined }} > - + , ); const linkElement = screen diff --git a/frontend2/src/components/sidebar/index.tsx b/frontend2/src/components/sidebar/index.tsx index 1740e697b..d2d14c6ca 100644 --- a/frontend2/src/components/sidebar/index.tsx +++ b/frontend2/src/components/sidebar/index.tsx @@ -1,7 +1,7 @@ -import React, { useContext } from "react"; +import React from "react"; import SidebarSection from "./SidebarSection"; import SidebarItem from "./SidebarItem"; -import { EpisodeContext } from "../../contexts/EpisodeContext"; +import { useEpisodeId } from "../../contexts/EpisodeContext"; import { type IconName } from "../elements/Icon"; interface SidebarProps { @@ -89,7 +89,7 @@ export const generateSidebarItems = ( // IMPORTANT: When changing this file, also remember to change the mobile menu that appears on small screens. const Sidebar: React.FC = ({ collapsed }) => { collapsed = collapsed ?? false; - const { episodeId } = useContext(EpisodeContext); + const { episodeId } = useEpisodeId(); return collapsed ? null : (