Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add routing #653

Merged
merged 5 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend2/public/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="h-full bg-gray-100">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
Expand All @@ -26,7 +26,7 @@
-->
<title>React App</title>
</head>
<body>
<body class="h-full">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
Expand Down
64 changes: 48 additions & 16 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
import React from "react";
import React, { useState } from "react";
import EpisodeLayout from "./components/EpisodeLayout";
import Home from "./views/Home";
import Logout from "./views/Logout";
import Register from "./views/Register";
import PasswordForgot from "./views/PasswordForgot";
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,
Navigate,
} from "react-router-dom";
import { DEFAULT_EPISODE } from "./utils/constants";
import NotFound from "./views/NotFound";

const App: React.FC = () => {
const [episode, setEpisode] = useState(DEFAULT_EPISODE);
return (
<div className="App">
<header className="App-header text-3xl font-bold underline">
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<EpisodeContext.Provider value={{ episode, setEpisode }}>
<RouterProvider router={router} />
</EpisodeContext.Provider>
);
};

const router = createBrowserRouter([
// Pages that should render without a sidebar/navbar
{ path: "/login", element: <Login /> },
{ path: "/logout", element: <Logout /> },
{ path: "/register", element: <Register /> },
{ path: "/password_forgot", element: <PasswordForgot /> },
{ path: "/password_change", element: <PasswordChange /> },
// Pages that will contain the episode specific sidebar and navbar
{
element: <EpisodeLayout />,
children: [
// Pages that should always be visible
// TODO: /:episode/resources, /:episode/tournaments, /:episode/rankings, /:episode/queue
{ path: "/:episode/home", element: <Home /> },
{ path: "/:episode/quickstart", element: <QuickStart /> },
{ path: "/:episode/*", element: <NotFound /> },
// Pages that should only be visible when logged in
// TODO: /:episode/team, /:episode/submissions, /:episode/scrimmaging
{ path: "/account", element: <Account /> },
// etc
],
},
// Pages that should redirect
{ path: "/*", element: <Navigate to={`/${DEFAULT_EPISODE}/home`} /> },
]);

export default App;
9 changes: 0 additions & 9 deletions frontend2/src/__test__/App.test.tsx

This file was deleted.

26 changes: 26 additions & 0 deletions frontend2/src/components/EpisodeLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useContext } from "react";
import Navbar from "./Navbar";
import Sidebar from "./sidebar";
import { Outlet, useParams } from "react-router-dom";
import { EpisodeContext } from "../contexts/EpisodeContext";

// This component contains the NavBar and SideBar.
// Child route components are rendered with <Outlet />
const EpisodeLayout: React.FC = () => {
const episodeContext = useContext(EpisodeContext);
const { episode } = useParams();
if (episode !== undefined && episode !== episodeContext.episode) {
episodeContext.setEpisode(episode);
}
return (
<div className="h-screen">
<Navbar/>
<div className="flex flex-row h-full">
<Sidebar />
<Outlet />
</div>
</div>
);
};

export default EpisodeLayout;
7 changes: 7 additions & 0 deletions frontend2/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const Navbar: React.FC = () => {
return <p>navbar here</p>;
};

export default Navbar;
34 changes: 34 additions & 0 deletions frontend2/src/components/sidebar/SidebarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { NavLink } from "react-router-dom";

interface SidebarItemProps {
icon: React.ReactNode;
text: string;
linkTo: string;
}

const SidebarItem: React.FC<SidebarItemProps> = ({
icon,
text,
linkTo,
}) => {
const baseStyle =
"text-base flex items-center gap-3 ";
const colorVariants = {
gray: "text-gray-800 hover:text-gray-400",
color: "text-teal",
};
return (
<NavLink
className={({ isActive }) =>
baseStyle + (isActive ? colorVariants.color : colorVariants.gray)
}
to={linkTo}
>
{icon}
{text}
</NavLink>
);
};

export default SidebarItem;
19 changes: 19 additions & 0 deletions frontend2/src/components/sidebar/SidebarSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

interface SidebarSectionProps {
children?: React.ReactNode,
title?: string;
}

const SidebarSection: React.FC<SidebarSectionProps> = ({ children, title }) => {
return (
<div className="pl-5 pr-8">
{title !== undefined && <h2 className="uppercase text-gray-500 tracking-wider text-sm mb-3">{title}</h2>}
<div className="flex flex-col gap-5">
{children}
</div>
</div>
);
};

export default SidebarSection;
27 changes: 27 additions & 0 deletions frontend2/src/components/sidebar/__test__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 { MemoryRouter } from "react-router-dom";

test('should link to default episode', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My tests are currently not working but when they are we will have dozens... thinking this may be a good opportunity to use the API/UI: <test message> (STABLE/UNSTABLE) format for test messages?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test('should link to default episode', () => {
test('UI: should link to default episode', () => {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update after discussion: we won't include (stable/unstable), but we will include api/ui

render(<MemoryRouter><Sidebar /></MemoryRouter>);
const linkElement = screen.getByText('Resources').closest('a')?.getAttribute('href');
expect(linkElement).toEqual(expect.stringContaining(`/${DEFAULT_EPISODE}/resources`));
});

test('should collapse sidebar', () => {
render(<MemoryRouter><Sidebar collapsed={true} /></MemoryRouter>);
expect(screen.queryByText('Home')).toBeNull();
});

test('should link to episode in surrounding context', () => {
render(<MemoryRouter>
<EpisodeContext.Provider value={{ episode: "something", setEpisode: (_) => undefined }}>
<Sidebar />
</EpisodeContext.Provider></MemoryRouter>
);
const linkElement = screen.getByText('Resources').closest('a')?.getAttribute('href');
expect(linkElement).toEqual(expect.stringContaining(`/something/resources`));
});
83 changes: 83 additions & 0 deletions frontend2/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useContext } from "react";
import SidebarSection from "./SidebarSection";
import SidebarItem from "./SidebarItem";
import {
ClipboardDocumentIcon, HomeIcon, MapIcon,
TrophyIcon, ChartBarIcon, ClockIcon,
UserGroupIcon, ArrowUpTrayIcon, PlayCircleIcon,
} from "@heroicons/react/24/outline";
import { EpisodeContext } from "../../contexts/EpisodeContext";

interface SidebarProps {
collapsed?: boolean;
}

const Sidebar: React.FC<SidebarProps> = ({
collapsed
}) => {
collapsed = collapsed ?? false;
const { episode } = useContext(EpisodeContext);
const linkBase = `/${episode}/`;

return (
collapsed ? null : (
<div className="flex flex-col gap-8 py-4 h-full bg-gray-50 shadow-gray-200 shadow-sm">
<SidebarSection title="" >
<SidebarItem
icon={<HomeIcon className="h-6 w-6" />}
text="Home"
linkTo={`${linkBase}home`}
/>
<SidebarItem
icon={<MapIcon className="h-6 w-6" />}
text="Quick Start"
linkTo={`${linkBase}quickstart`}
/>
<SidebarItem
icon={<ClipboardDocumentIcon className="h-6 w-6" />}
text="Resources"
linkTo={`${linkBase}resources`}
/>
</SidebarSection>
<SidebarSection title="compete" >
<SidebarItem
icon={<TrophyIcon className="h-6 w-6" />}
text="Tournaments"
linkTo={`${linkBase}tournaments`}
/>
<SidebarItem
icon={<ChartBarIcon className="h-6 w-6" />}
text="Rankings"
linkTo={`${linkBase}rankings`}
/>
<SidebarItem
icon={<ClockIcon className="h-6 w-6" />}
text="Queue"
linkTo={`${linkBase}queue`}
/>
</SidebarSection>
<SidebarSection title="team management" >
<SidebarItem
icon={<UserGroupIcon className="h-6 w-6" />}
text="My Team"
linkTo={`${linkBase}team`}
/>
<SidebarItem
icon={<ArrowUpTrayIcon className="h-6 w-6" />}
text="Submissions"
linkTo={`${linkBase}submission`}
/>
<SidebarItem
icon={<PlayCircleIcon className="h-6 w-6" />}
text="Scrimmaging"
linkTo={`${linkBase}scrimmaging`}
/>
</SidebarSection>
</div>
)
);


};

export default Sidebar;
10 changes: 10 additions & 0 deletions frontend2/src/contexts/EpisodeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from "react";
import { DEFAULT_EPISODE } from "../utils/constants";

export const EpisodeContext = createContext({
// the default episode.
episode: DEFAULT_EPISODE,
setEpisode: (episode: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we standardize how we refer to the current episode? I have been calling it episodeId (referring to name_short like bc23) but I would be OK with using episode instead.

Copy link
Member Author

@acrantel acrantel Jun 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should def standardize this

What if we just call name_short episodeId and call name_long episodeName?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this ^^

console.log("default episode");
},
});
7 changes: 7 additions & 0 deletions frontend2/src/episodeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Home from "./views/Home";

export const AppModules = {
bc23: {
home: Home,
},
};
1 change: 1 addition & 0 deletions frontend2/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";

Expand Down
1 change: 1 addition & 0 deletions frontend2/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_EPISODE = "bc23";
9 changes: 9 additions & 0 deletions frontend2/src/views/Account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { useContext } from "react";
import { EpisodeContext } from "../contexts/EpisodeContext";

const Account: React.FC = () => {
const { episode } = useContext(EpisodeContext);
return <p>the episode is {episode} and this is the account page</p>;
};

export default Account;
7 changes: 7 additions & 0 deletions frontend2/src/views/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const Home: React.FC = () => {
return <p>Homepage</p>;
};

export default Home;
7 changes: 7 additions & 0 deletions frontend2/src/views/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const Login: React.FC = () => {
return <p>login page</p>;
};

export default Login;
7 changes: 7 additions & 0 deletions frontend2/src/views/Logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const Logout: React.FC = () => {
return <p>logout page</p>;
};

export default Logout;
7 changes: 7 additions & 0 deletions frontend2/src/views/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const NotFound: React.FC = () => {
return <p>That page was not found.</p>;
};

export default NotFound;
7 changes: 7 additions & 0 deletions frontend2/src/views/PasswordChange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const PasswordChange: React.FC = () => {
return <p>passwordchange page</p>;
};

export default PasswordChange;
7 changes: 7 additions & 0 deletions frontend2/src/views/PasswordForgot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

const PasswordForgot: React.FC = () => {
return <p>passwordforgot page</p>;
};

export default PasswordForgot;
9 changes: 9 additions & 0 deletions frontend2/src/views/QuickStart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { useContext } from "react";
import { EpisodeContext } from "../contexts/EpisodeContext";

const QuickStart: React.FC = () => {
const { episode, setEpisode } = useContext(EpisodeContext);
return <p>quickstart page</p>;
};

export default QuickStart;
Loading