summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-ui/src/pages')
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.js19
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.module.scss8
-rw-r--r--opendc-web/opendc-web-ui/src/pages/App.js103
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Home.sass9
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.js15
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.sass11
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Profile.js31
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Projects.js30
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js69
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js95
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.js (renamed from opendc-web/opendc-web-ui/src/pages/Home.js)31
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.module.scss16
-rw-r--r--opendc-web/opendc-web-ui/src/pages/logout.js39
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js37
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js37
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js37
16 files changed, 377 insertions, 210 deletions
diff --git a/opendc-web/opendc-web-ui/src/pages/404.js b/opendc-web/opendc-web-ui/src/pages/404.js
new file mode 100644
index 00000000..cc9281fc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/404.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import Head from 'next/head'
+import TerminalWindow from '../components/not-found/TerminalWindow'
+import style from './404.module.scss'
+
+const NotFound = () => {
+ return (
+ <>
+ <Head>
+ <title>Page Not Found - OpenDC</title>
+ </Head>
+ <div className={style['not-found-backdrop']}>
+ <TerminalWindow />
+ </div>
+ </>
+ )
+}
+
+export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/404.module.scss b/opendc-web/opendc-web-ui/src/pages/404.module.scss
new file mode 100644
index 00000000..e91c2780
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/404.module.scss
@@ -0,0 +1,8 @@
+.not-found-backdrop {
+ display: flex;
+
+ width: 100%;
+ height: 100%;
+
+ background-image: linear-gradient(135deg, #00678a, #008fbf, #00a6d6);
+}
diff --git a/opendc-web/opendc-web-ui/src/pages/App.js b/opendc-web/opendc-web-ui/src/pages/App.js
deleted file mode 100644
index ea62e8dc..00000000
--- a/opendc-web/opendc-web-ui/src/pages/App.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useEffect } from 'react'
-import { HotKeys } from 'react-hotkeys'
-import { useDispatch, useSelector } from 'react-redux'
-import { openPortfolioSucceeded } from '../actions/portfolios'
-import { openProjectSucceeded } from '../actions/projects'
-import ToolPanelComponent from '../components/app/map/controls/ToolPanelComponent'
-import LoadingScreen from '../components/app/map/LoadingScreen'
-import ScaleIndicatorContainer from '../containers/app/map/controls/ScaleIndicatorContainer'
-import MapStage from '../containers/app/map/MapStage'
-import TopologySidebarContainer from '../containers/app/sidebars/topology/TopologySidebarContainer'
-import DeleteMachineModal from '../containers/modals/DeleteMachineModal'
-import DeleteRackModal from '../containers/modals/DeleteRackModal'
-import DeleteRoomModal from '../containers/modals/DeleteRoomModal'
-import EditRackNameModal from '../containers/modals/EditRackNameModal'
-import EditRoomNameModal from '../containers/modals/EditRoomNameModal'
-import NewTopologyModal from '../containers/modals/NewTopologyModal'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import ProjectSidebarContainer from '../containers/app/sidebars/project/ProjectSidebarContainer'
-import { openScenarioSucceeded } from '../actions/scenarios'
-import NewPortfolioModal from '../containers/modals/NewPortfolioModal'
-import NewScenarioModal from '../containers/modals/NewScenarioModal'
-import PortfolioResultsContainer from '../containers/app/results/PortfolioResultsContainer'
-import KeymapConfiguration from '../shortcuts/keymap'
-import { useDocumentTitle } from '../util/hooks'
-
-const App = ({ projectId, portfolioId, scenarioId }) => {
- const projectName = useSelector(
- (state) =>
- state.currentProjectId !== '-1' &&
- state.objects.project[state.currentProjectId] &&
- state.objects.project[state.currentProjectId].name
- )
- const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
-
- const dispatch = useDispatch()
- useEffect(() => {
- if (scenarioId) {
- dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId))
- } else if (portfolioId) {
- dispatch(openPortfolioSucceeded(projectId, portfolioId))
- } else {
- dispatch(openProjectSucceeded(projectId))
- }
- }, [projectId, portfolioId, scenarioId, dispatch])
-
- const constructionElements = topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen />
- </div>
- ) : (
- <div className="full-height">
- <MapStage />
- <ScaleIndicatorContainer />
- <ToolPanelComponent />
- <ProjectSidebarContainer />
- <TopologySidebarContainer />
- </div>
- )
-
- const portfolioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <PortfolioResultsContainer />
- </div>
- </div>
- )
-
- const scenarioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <h2>Scenario loading</h2>
- </div>
- </div>
- )
-
- useDocumentTitle(projectName ? projectName + ' - OpenDC' : 'Simulation - OpenDC')
-
- return (
- <HotKeys keyMap={KeymapConfiguration} className="page-container full-height">
- <AppNavbarContainer fullWidth={true} />
- {scenarioId ? scenarioElements : portfolioId ? portfolioElements : constructionElements}
- <NewTopologyModal />
- <NewPortfolioModal />
- <NewScenarioModal />
- <EditRoomNameModal />
- <DeleteRoomModal />
- <EditRackNameModal />
- <DeleteRackModal />
- <DeleteMachineModal />
- </HotKeys>
- )
-}
-
-App.propTypes = {
- projectId: PropTypes.string.isRequired,
- portfolioId: PropTypes.string,
- scenarioId: PropTypes.string,
-}
-
-export default App
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.sass b/opendc-web/opendc-web-ui/src/pages/Home.sass
deleted file mode 100644
index 79cb9698..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Home.sass
+++ /dev/null
@@ -1,9 +0,0 @@
-.body-wrapper
- position: relative
- overflow-y: hidden
-
-.intro-section, .modeling-section, .technologies-section
- background-color: #fff
-
-.stakeholder-section, .simulation-section, .team-section
- background-color: #f2f2f2
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.js b/opendc-web/opendc-web-ui/src/pages/NotFound.js
deleted file mode 100644
index b933ffa5..00000000
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import TerminalWindow from '../components/not-found/TerminalWindow'
-import './NotFound.sass'
-import { useDocumentTitle } from '../util/hooks'
-
-const NotFound = () => {
- useDocumentTitle('Page Not Found - OpenDC')
- return (
- <div className="not-found-backdrop">
- <TerminalWindow />
- </div>
- )
-}
-
-export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.sass b/opendc-web/opendc-web-ui/src/pages/NotFound.sass
deleted file mode 100644
index 59231f7a..00000000
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.sass
+++ /dev/null
@@ -1,11 +0,0 @@
-.not-found-backdrop
- position: absolute
- left: 0
- top: 0
-
- margin: 0
- padding: 0
- width: 100%
- height: 100%
-
- background-image: linear-gradient(135deg, #00678a, #008fbf, #00A6D6)
diff --git a/opendc-web/opendc-web-ui/src/pages/Profile.js b/opendc-web/opendc-web-ui/src/pages/Profile.js
deleted file mode 100644
index ea781686..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Profile.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { openDeleteProfileModal } from '../actions/modals/profile'
-import DeleteProfileModal from '../containers/modals/DeleteProfileModal'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import { useDocumentTitle } from '../util/hooks'
-
-const Profile = () => {
- const dispatch = useDispatch()
- const onDelete = () => dispatch(openDeleteProfileModal())
-
- useDocumentTitle('My Profile - OpenDC')
- return (
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <div className="container text-page-container full-height">
- <button className="btn btn-danger mb-2 ml-auto mr-auto" style={{ maxWidth: 300 }} onClick={onDelete}>
- Delete my account on OpenDC
- </button>
- <p className="text-muted text-center">
- This does not delete your Google account, but simply disconnects it from the OpenDC platform and
- deletes any project info that is associated with you (projects you own and any authorizations you
- may have on other projects).
- </p>
- </div>
- <DeleteProfileModal />
- </div>
- )
-}
-
-export default Profile
diff --git a/opendc-web/opendc-web-ui/src/pages/Projects.js b/opendc-web/opendc-web-ui/src/pages/Projects.js
deleted file mode 100644
index 5e642a03..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Projects.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { useEffect } from 'react'
-import { useDispatch } from 'react-redux'
-import { fetchAuthorizationsOfCurrentUser } from '../actions/users'
-import ProjectFilterPanel from '../components/projects/FilterPanel'
-import NewProjectModal from '../containers/modals/NewProjectModal'
-import NewProjectButtonContainer from '../containers/projects/NewProjectButtonContainer'
-import VisibleProjectList from '../containers/projects/VisibleProjectAuthList'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import { useDocumentTitle } from '../util/hooks'
-
-function Projects() {
- const dispatch = useDispatch()
-
- useEffect(() => dispatch(fetchAuthorizationsOfCurrentUser()))
- useDocumentTitle('My Projects - OpenDC')
-
- return (
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <div className="container text-page-container full-height">
- <ProjectFilterPanel />
- <VisibleProjectList />
- <NewProjectButtonContainer />
- </div>
- <NewProjectModal />
- </div>
- )
-}
-
-export default Projects
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
new file mode 100644
index 00000000..4ef4445c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Head from 'next/head'
+import { Provider } from 'react-redux'
+import { useStore } from '../redux'
+import '../index.scss'
+import { AuthProvider, useAuth } from '../auth'
+import * as Sentry from '@sentry/react'
+import { Integrations } from '@sentry/tracing'
+
+// This setup is necessary to forward the Auth0 context to the Redux context
+const Inner = ({ Component, pageProps }) => {
+ const auth = useAuth()
+ const store = useStore(pageProps.initialReduxState, { auth })
+ return (
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ )
+}
+
+const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN
+// Initialize Sentry if the user has configured a DSN
+if (process.browser && dsn) {
+ if (dsn) {
+ Sentry.init({
+ environment: process.env.NODE_ENV,
+ dsn: dsn,
+ integrations: [new Integrations.BrowserTracing()],
+ tracesSampleRate: 0.1,
+ })
+ }
+}
+
+export default function App(props) {
+ return (
+ <>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta name="theme-color" content="#00A6D6" />
+ </Head>
+ <Sentry.ErrorBoundary fallback={"An error has occurred"}>
+ <AuthProvider>
+ <Inner {...props} />
+ </AuthProvider>
+ </Sentry.ErrorBoundary>
+ </>
+ )
+}
diff --git a/opendc-web/opendc-web-ui/src/pages/_document.js b/opendc-web/opendc-web-ui/src/pages/_document.js
new file mode 100644
index 00000000..8e4680c0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+class OpenDCDocument extends Document {
+ render() {
+ return (
+ <Html>
+ <Head>
+ <meta charSet="utf-8" />
+ <meta name="theme-color" content="#00A6D6" />
+ <meta
+ name="description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="author" content="@Large Research" />
+ <meta
+ name="keywords"
+ content="OpenDC, Datacenter, Simulation, Simulator, Collaborative, Distributed, Cluster"
+ />
+ <link rel="manifest" href="/manifest.json" />
+ <link rel="shortcut icon" href="/favicon.ico" />
+
+ {/* Twitter Card data */}
+ <meta name="twitter:card" content="summary" />
+ <meta name="twitter:site" content="@LargeResearch" />
+ <meta name="twitter:title" content="OpenDC" />
+ <meta
+ name="twitter:description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="twitter:creator" content="@LargeResearch" />
+ <meta name="twitter:image" content="http://opendc.org/img/logo.png" />
+
+ {/* OpenGraph meta tags */}
+ <meta property="og:title" content="OpenDC" />
+ <meta property="og:site_name" content="OpenDC" />
+ <meta property="og:type" content="website" />
+ <meta property="og:image" content="http://opendc.org/img/logo.png" />
+ <meta property="og:url" content="http://opendc.org/" />
+ <meta
+ property="og:description"
+ content="OpenDC provides collaborative online datacenter modeling, diverse and effective datacenter simulation, and exploratory datacenter performance feedback."
+ />
+ <meta property="og:locale" content="en_US" />
+
+ {/* CDN Dependencies */}
+ <link
+ href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
+ rel="stylesheet"
+ />
+
+ {/* Google Analytics */}
+ <script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" />
+ <script
+ dangerouslySetInnerHTML={{
+ __html: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'UA-84285092-3');
+ `,
+ }}
+ />
+ </Head>
+ <body>
+ <Main />
+ <NextScript />
+ </body>
+ </Html>
+ )
+ }
+}
+
+export default OpenDCDocument
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.js b/opendc-web/opendc-web-ui/src/pages/index.js
index fb383426..bb904eb6 100644
--- a/opendc-web/opendc-web-ui/src/pages/Home.js
+++ b/opendc-web/opendc-web-ui/src/pages/index.js
@@ -1,4 +1,5 @@
import React from 'react'
+import Head from 'next/head'
import ContactSection from '../components/home/ContactSection'
import IntroSection from '../components/home/IntroSection'
import JumbotronHeader from '../components/home/JumbotronHeader'
@@ -8,25 +9,33 @@ import StakeholderSection from '../components/home/StakeholderSection'
import TeamSection from '../components/home/TeamSection'
import TechnologiesSection from '../components/home/TechnologiesSection'
import HomeNavbar from '../components/navigation/HomeNavbar'
-import './Home.sass'
-import { useDocumentTitle } from '../util/hooks'
+import {
+ introSection,
+ stakeholderSection,
+ modelingSection,
+ simulationSection,
+ technologiesSection,
+ teamSection,
+} from './index.module.scss'
function Home() {
- useDocumentTitle('OpenDC')
return (
- <div>
+ <>
+ <Head>
+ <title>OpenDC</title>
+ </Head>
<HomeNavbar />
<div className="body-wrapper page-container">
<JumbotronHeader />
- <IntroSection />
- <StakeholderSection />
- <ModelingSection />
- <SimulationSection />
- <TechnologiesSection />
- <TeamSection />
+ <IntroSection className={introSection} />
+ <StakeholderSection className={stakeholderSection} />
+ <ModelingSection className={modelingSection} />
+ <SimulationSection className={simulationSection} />
+ <TechnologiesSection className={technologiesSection} />
+ <TeamSection className={teamSection} />
<ContactSection />
</div>
- </div>
+ </>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/index.module.scss b/opendc-web/opendc-web-ui/src/pages/index.module.scss
new file mode 100644
index 00000000..aed1d88f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/index.module.scss
@@ -0,0 +1,16 @@
+.bodyWrapper {
+ position: relative;
+ overflow-y: hidden;
+}
+
+.introSection,
+.modelingSection,
+.technologiesSection {
+ background-color: #fff;
+}
+
+.stakeholderSection,
+.simulationSection,
+.teamSection {
+ background-color: #f2f2f2;
+}
diff --git a/opendc-web/opendc-web-ui/src/pages/logout.js b/opendc-web/opendc-web-ui/src/pages/logout.js
new file mode 100644
index 00000000..e96e0605
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/logout.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import Head from 'next/head'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+
+function Logout() {
+ return (
+ <>
+ <Head>
+ <title>Logged Out - OpenDC</title>
+ </Head>
+ <AppNavbarContainer fullWidth={false} />
+ <span>Logged out successfully</span>
+ </>
+ )
+}
+
+export default Logout
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
new file mode 100644
index 00000000..72316bc9
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import App from '../../../containers/app/App'
+
+function Project() {
+ const router = useRouter()
+ const { project } = router.query
+
+ if (project) {
+ return <App projectId={project} />
+ }
+
+ return <div />
+}
+
+export default Project
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
new file mode 100644
index 00000000..76a8d23b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import App from '../../../../containers/app/App'
+
+function Project() {
+ const router = useRouter()
+ const { project, portfolio } = router.query
+
+ if (project && portfolio) {
+ return <App projectId={project} portfolioId={portfolio} />
+ }
+
+ return <div />
+}
+
+export default Project
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js
new file mode 100644
index 00000000..8603c228
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react'
+import Head from 'next/head'
+import { useDispatch } from 'react-redux'
+import ProjectFilterPanel from '../../components/projects/FilterPanel'
+import NewProjectContainer from '../../containers/projects/NewProjectContainer'
+import ProjectListContainer from '../../containers/projects/ProjectListContainer'
+import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
+import { useRequireAuth } from '../../auth'
+import { Container } from 'reactstrap'
+import { fetchProjects } from '../../redux/actions/projects'
+
+function Projects() {
+ useRequireAuth()
+
+ const dispatch = useDispatch()
+ const [filter, setFilter] = useState('SHOW_ALL')
+
+ useEffect(() => dispatch(fetchProjects()), [])
+
+ return (
+ <>
+ <Head>
+ <title>My Projects - OpenDC</title>
+ </Head>
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <Container className="text-page-container">
+ <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
+ <ProjectListContainer filter={filter} />
+ <NewProjectContainer />
+ </Container>
+ </div>
+ </>
+ )
+}
+
+export default Projects