Skip to content

Commit

Permalink
Add team context, update user/episode context (#679)
Browse files Browse the repository at this point in the history
  • Loading branch information
acrantel authored Sep 30, 2023
1 parent ca2d7b2 commit 6eec880
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 79 deletions.
19 changes: 13 additions & 6 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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 (
<CurrentUserProvider>
<EpisodeContext.Provider value={{ episodeId, setEpisodeId }}>
<RouterProvider router={router} />
</EpisodeContext.Provider>
<EpisodeProvider>
<CurrentTeamProvider>
<RouterProvider router={router} />
</CurrentTeamProvider>
</EpisodeProvider>
</CurrentUserProvider>
);
};
Expand All @@ -49,6 +52,10 @@ const router = createBrowserRouter([
{
element: <EpisodeLayout />,
children: [
{
path: "/:episodeId/team",
element: <MyTeam />,
},
// TODO: /:episodeId/team, /:episodeId/submissions, /:episodeId/scrimmaging
],
},
Expand Down
29 changes: 16 additions & 13 deletions frontend2/src/components/CurrentUserProvider.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<void> => {
Expand All @@ -51,15 +52,17 @@ export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({
void checkLoggedIn();
}, []);

const providedValue = useMemo(
() => ({
...userData,
login,
logout,
}),
[login, logout, userData],
);

return (
<CurrentUserContext.Provider
value={{
authState: userData.authState,
user: userData.user,
login,
logout,
}}
>
<CurrentUserContext.Provider value={providedValue}>
{children}
</CurrentUserContext.Provider>
);
Expand Down
10 changes: 5 additions & 5 deletions frontend2/src/components/EpisodeLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
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 <Outlet />
const EpisodeLayout: React.FC = () => {
const episodeContext = useContext(EpisodeContext);
const episodeContext = useEpisodeId();
const { episodeId } = useParams();
useEffect(() => {
if (episodeId !== undefined && episodeId !== episodeContext.episodeId) {
episodeContext.setEpisodeId(episodeId);
}
}, [episodeId]);
return (
<div className="h-screen overflow-auto">
<div className="h-screen">
<Header />
<Sidebar />
<div className="fixed right-0 h-full pt-16 sm:left-52">
<div className="h-full pt-16 sm:pl-52">
<Outlet />
</div>
</div>
Expand Down
7 changes: 3 additions & 4 deletions frontend2/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="fixed top-0 h-16 w-full bg-gray-700">
Expand Down Expand Up @@ -80,7 +80,6 @@ const Header: React.FC = () => {
alt="Battlecode Logo"
/>
</div>
<div className="hidden sm:ml-6 sm:block"></div>
</div>
{/* profile menu (if the user is logged in) */}
{authState === AuthStateEnum.AUTHENTICATED && (
Expand Down
6 changes: 3 additions & 3 deletions frontend2/src/components/compete/RatingDelta.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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;
ranked: boolean;
}

const RatingDelta: React.FC<RatingDeltaProps> = ({ participant, ranked }) => {
const episodeId = useContext(EpisodeContext).episodeId;
const { episodeId } = useEpisodeId();

const newRating = ranked
? Math.round(participant.rating)
Expand Down
28 changes: 8 additions & 20 deletions frontend2/src/components/sidebar/__test__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter>
<Sidebar />
</MemoryRouter>,
);
const linkElement = screen
.getByText("Resources")
.closest("a")
?.getAttribute("href");
expect(linkElement).toEqual(
expect.stringContaining(`/${DEFAULT_EPISODE}/resources`),
);
});

test("UI: should collapse sidebar", () => {
render(
<MemoryRouter>
<Sidebar collapsed={true} />
<EpisodeIdContext.Provider
value={{ episodeId: "something", setEpisodeId: (_) => undefined }}
>
<Sidebar collapsed={true} />
</EpisodeIdContext.Provider>
</MemoryRouter>,
);
expect(screen.queryByText("Home")).toBeNull();
Expand All @@ -32,11 +20,11 @@ test("UI: should collapse sidebar", () => {
test("UI: should link to episode in surrounding context", () => {
render(
<MemoryRouter>
<EpisodeContext.Provider
<EpisodeIdContext.Provider
value={{ episodeId: "something", setEpisodeId: (_) => undefined }}
>
<Sidebar />
</EpisodeContext.Provider>
</EpisodeIdContext.Provider>
</MemoryRouter>,
);
const linkElement = screen
Expand Down
6 changes: 3 additions & 3 deletions frontend2/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<SidebarProps> = ({ collapsed }) => {
collapsed = collapsed ?? false;
const { episodeId } = useContext(EpisodeContext);
const { episodeId } = useEpisodeId();

return collapsed ? null : (
<nav className="fixed top-16 z-10 hidden h-full w-52 flex-col gap-8 bg-gray-50 py-4 drop-shadow-[2px_0_2px_rgba(0,0,0,0.25)] sm:flex">
Expand Down
30 changes: 30 additions & 0 deletions frontend2/src/contexts/CurrentTeamContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createContext, useContext } from "react";
import { type TeamPrivate } from "../utils/types";

export enum TeamStateEnum {
// the current user is not part of a team (or is not logged in)
NO_TEAM = "no_team",
// the current user is part of a team
IN_TEAM = "has_team",
}

interface CurrentTeamContextType {
teamState: TeamStateEnum;
team?: TeamPrivate;
}

export const CurrentTeamContext = createContext<CurrentTeamContextType | null>(
null,
);

export const useCurrentTeam = (): CurrentTeamContextType => {
const currentTeamContext = useContext(CurrentTeamContext);

if (currentTeamContext === null) {
throw new Error(
"useCurrentTeam has to be used within <CurrentTeamProvider>",
);
}

return currentTeamContext;
};
44 changes: 44 additions & 0 deletions frontend2/src/contexts/CurrentTeamProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useState, useEffect } from "react";
import { type TeamPrivate } from "../utils/types";
import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext";
import { CurrentTeamContext, TeamStateEnum } from "./CurrentTeamContext";
import { useEpisodeId } from "./EpisodeContext";
import { retrieveTeam } from "../utils/api/team";

export const CurrentTeamProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [teamData, setTeamData] = useState<{
team?: TeamPrivate;
teamState: TeamStateEnum;
}>({
teamState: TeamStateEnum.NO_TEAM,
});
const { authState } = useCurrentUser();
const { episodeId } = useEpisodeId();

useEffect(() => {
const loadTeam = async (): Promise<void> => {
try {
const team = await retrieveTeam(episodeId);
setTeamData({ team, teamState: TeamStateEnum.IN_TEAM });
} catch {
setTeamData({ teamState: TeamStateEnum.NO_TEAM });
}
};

if (authState === AuthStateEnum.AUTHENTICATED) {
void loadTeam();
} else {
setTeamData({
teamState: TeamStateEnum.NO_TEAM,
});
}
}, [authState, episodeId]);

return (
<CurrentTeamContext.Provider value={teamData}>
{children}
</CurrentTeamContext.Provider>
);
};
39 changes: 29 additions & 10 deletions frontend2/src/contexts/EpisodeContext.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { createContext } from "react";
import { DEFAULT_EPISODE } from "../utils/constants";

export const EpisodeContext = createContext({
// the default episode.
episodeId: DEFAULT_EPISODE,
setEpisodeId: (episodeId: string) => {
console.log("default episode");
},
});
import { createContext, useContext } from "react";
import { type Episode } from "../utils/types";
import { type Maybe } from "../utils/utilTypes";

interface EpisodeIdContextType {
episodeId: string;
setEpisodeId: (episodeId: string) => void;
}

export const EpisodeContext = createContext<Maybe<Episode>>(undefined);
export const EpisodeIdContext = createContext<EpisodeIdContextType | null>(
null,
);

// Use this function to retrieve full episode information. If the api call to
// retrieve full episode information has not completed, then
// episodeContext.episode will be undefined.
export const useEpisode = (): Maybe<Episode> => {
return useContext(EpisodeContext);
};

// Use this function to retrieve and update the episodeId.
export const useEpisodeId = (): EpisodeIdContextType => {
const episodeIdContext = useContext(EpisodeIdContext);
if (episodeIdContext === null) {
throw new Error("useEpisodeId has to be used within <EpisodeProvider>");
}
return episodeIdContext;
};
Loading

0 comments on commit 6eec880

Please sign in to comment.