summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/pages
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-05-12 22:42:12 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-05-17 17:06:47 +0200
commit4397a959e806bf476be4c81bc804616adf58b969 (patch)
treea2bfdcb314594433413a235b516d18fa5416bf6d /opendc-web/opendc-web-ui/src/pages
parentd21606bd238702645690586df5ad5b5075ca09c9 (diff)
ui: Migrate from CRA to Next.js
This change updates the web frontend to use Next.js instead of Create React App (CRA). Next.js enables the possibility of rendering pages on the server side (which reduces the time to first frame) and overall provides a better development experience. Future commits will try to futher optimize the implementation for Next.js.
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.scss (renamed from opendc-web/opendc-web-ui/src/pages/NotFound.module.scss)0
-rw-r--r--opendc-web/opendc-web-ui/src/pages/App.js103
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.js15
-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.js42
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js96
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.js (renamed from opendc-web/opendc-web-ui/src/pages/Home.js)12
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.module.scss (renamed from opendc-web/opendc-web-ui/src/pages/Home.module.scss)0
-rw-r--r--opendc-web/opendc-web-ui/src/pages/profile.js54
-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.js36
14 files changed, 328 insertions, 184 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/NotFound.module.scss b/opendc-web/opendc-web-ui/src/pages/404.module.scss
index e91c2780..e91c2780 100644
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.module.scss
+++ b/opendc-web/opendc-web-ui/src/pages/404.module.scss
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/NotFound.js b/opendc-web/opendc-web-ui/src/pages/NotFound.js
deleted file mode 100644
index 409ffa0e..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 style from './NotFound.module.scss'
-import { useDocumentTitle } from '../util/hooks'
-
-const NotFound = () => {
- useDocumentTitle('Page Not Found - OpenDC')
- return (
- <div className={style['not-found-backdrop']}>
- <TerminalWindow />
- </div>
- )
-}
-
-export default NotFound
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..77ffa698
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -0,0 +1,42 @@
+/*
+ * 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 '../store/configure-store'
+import '../index.scss'
+
+export default function App({ Component, pageProps }) {
+ const store = useStore(pageProps.initialReduxState)
+
+ return (
+ <>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta name="theme-color" content="#00A6D6" />
+ </Head>
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ </>
+ )
+}
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..943ae395
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -0,0 +1,96 @@
+/*
+ * 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"
+ />
+ <script async src="https://use.fontawesome.com/ece66a2e7c.js" />
+
+ {/* 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 ee930fbe..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'
@@ -15,13 +16,14 @@ import {
simulationSection,
technologiesSection,
teamSection,
-} from './Home.module.scss'
-import { useDocumentTitle } from '../util/hooks'
+} from './index.module.scss'
function Home() {
- useDocumentTitle('OpenDC')
return (
- <div>
+ <>
+ <Head>
+ <title>OpenDC</title>
+ </Head>
<HomeNavbar />
<div className="body-wrapper page-container">
<JumbotronHeader />
@@ -33,7 +35,7 @@ function Home() {
<TeamSection className={teamSection} />
<ContactSection />
</div>
- </div>
+ </>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.module.scss b/opendc-web/opendc-web-ui/src/pages/index.module.scss
index aed1d88f..aed1d88f 100644
--- a/opendc-web/opendc-web-ui/src/pages/Home.module.scss
+++ b/opendc-web/opendc-web-ui/src/pages/index.module.scss
diff --git a/opendc-web/opendc-web-ui/src/pages/profile.js b/opendc-web/opendc-web-ui/src/pages/profile.js
new file mode 100644
index 00000000..de60037b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/profile.js
@@ -0,0 +1,54 @@
+import React, { useState } from 'react'
+import Head from 'next/head'
+import { useDispatch } from 'react-redux'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+import ConfirmationModal from '../components/modals/ConfirmationModal'
+import { deleteCurrentUser } from '../actions/users'
+import { useRequireAuth } from '../auth/hook'
+
+const Profile = () => {
+ useRequireAuth()
+
+ const dispatch = useDispatch()
+
+ const [isDeleteModalOpen, setDeleteModalOpen] = useState(false)
+ const onDelete = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteCurrentUser())
+ }
+ setDeleteModalOpen(false)
+ }
+
+ return (
+ <>
+ <Head>
+ <title>My Profile - OpenDC</title>
+ </Head>
+ <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={() => setDeleteModalOpen(true)}
+ >
+ 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>
+ <ConfirmationModal
+ title="Delete my account"
+ message="Are you sure you want to delete your OpenDC account?"
+ show={isDeleteModalOpen}
+ callback={onDelete}
+ />
+ </div>
+ </>
+ )
+}
+
+export default Profile
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..bea9ad93
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react'
+import Head from 'next/head'
+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 { useRequireAuth } from '../../auth/hook'
+
+function Projects() {
+ const dispatch = useDispatch()
+
+ useRequireAuth()
+ useEffect(() => dispatch(fetchAuthorizationsOfCurrentUser()))
+
+ return (
+ <>
+ <Head>
+ <title>My Projects - OpenDC</title>
+ </Head>
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <div className="container text-page-container full-height">
+ <ProjectFilterPanel />
+ <VisibleProjectList />
+ <NewProjectButtonContainer />
+ </div>
+ <NewProjectModal />
+ </div>
+ </>
+ )
+}
+
+export default Projects