summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-08-09 14:29:14 +0300
committerGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-09-23 10:05:18 +0200
commit67a771cbb02ec9da3c60704901f3150b46a7262b (patch)
treef5e8e28cc0b7539196e7cdc2f4f4e7cc2c165fbd
parentd1194f0706789287b98996b629451042f62bf6eb (diff)
Create basic projects page with add-button
-rw-r--r--package.json10
-rw-r--r--public/index.html12
-rw-r--r--src/actions/projects.js38
-rw-r--r--src/components/modals/Modal.js82
-rw-r--r--src/components/modals/TextInputModal.js41
-rw-r--r--src/components/navigation/Navbar.js6
-rw-r--r--src/components/navigation/Navbar.sass4
-rw-r--r--src/components/not-found/BlinkingCursor.js8
-rw-r--r--src/components/not-found/BlinkingCursor.sass35
-rw-r--r--src/components/not-found/CodeBlock.js30
-rw-r--r--src/components/not-found/CodeBlock.sass3
-rw-r--r--src/components/not-found/TerminalWindow.js26
-rw-r--r--src/components/not-found/TerminalWindow.sass70
-rw-r--r--src/components/projects/FilterButton.js23
-rw-r--r--src/components/projects/FilterButton.sass23
-rw-r--r--src/components/projects/FilterPanel.js15
-rw-r--r--src/components/projects/FilterPanel.sass21
-rw-r--r--src/components/projects/NewProjectButton.js16
-rw-r--r--src/components/projects/NewProjectButton.sass31
-rw-r--r--src/components/projects/NoProjectsAlert.js11
-rw-r--r--src/components/projects/NoProjectsAlert.sass10
-rw-r--r--src/components/projects/ProjectAuth.js22
-rw-r--r--src/components/projects/ProjectAuthList.js31
-rw-r--r--src/components/projects/ProjectAuthList.sass98
-rw-r--r--src/containers/projects/FilterLink.js22
-rw-r--r--src/containers/projects/NewProjectModal.js34
-rw-r--r--src/containers/projects/VisibleProjectAuthList.js25
-rw-r--r--src/index.js19
-rw-r--r--src/index.sass7
-rw-r--r--src/pages/NotFound.js11
-rw-r--r--src/pages/Projects.js35
-rw-r--r--src/pages/Projects.sass2
-rw-r--r--src/reducers/index.js10
-rw-r--r--src/reducers/projects.js42
-rw-r--r--src/routes/index.js17
-rw-r--r--src/shapes/index.js28
-rw-r--r--src/store/configureStore.js16
-rw-r--r--src/style-globals/_mixins.sass (renamed from src/style-globals/mixins.sass)6
-rw-r--r--src/style-globals/_variables.sass (renamed from src/style-globals/variables.sass)0
-rw-r--r--src/util/authorizations.js11
-rw-r--r--src/util/date-time.js32
41 files changed, 942 insertions, 41 deletions
diff --git a/package.json b/package.json
index 7178e213..03fbeb84 100644
--- a/package.json
+++ b/package.json
@@ -4,16 +4,24 @@
"private": true,
"dependencies": {
"bootstrap": "4.0.0-alpha.6",
+ "classnames": "^2.2.5",
"history": "^4.6.3",
+ "isomorphic-fetch": "^2.2.1",
"node-sass-chokidar": "^0.0.3",
+ "normalizr": "^3.2.3",
"npm-run-all": "^4.0.2",
+ "prop-types": "^15.5.10",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-fontawesome": "^1.6.1",
"react-google-login": "^2.9.2",
"react-mailto": "^0.4.0",
+ "react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
- "react-scripts": "1.0.10"
+ "react-scripts": "1.0.10",
+ "redux": "^3.7.2",
+ "redux-logger": "^3.0.6",
+ "redux-thunk": "^2.2.0"
},
"scripts": {
"build-css": "node-sass-chokidar src/ -o src/",
diff --git a/public/index.html b/public/index.html
index 8429885a..54689af2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -18,7 +18,8 @@
content="311799954046-jv2inpg9nu7m0avcg6gulvkuvfgbtgb4.apps.googleusercontent.com">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
- integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ">
+ integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
+ <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<script src="https://use.fontawesome.com/ece66a2e7c.js"></script>
</head>
<body>
@@ -27,10 +28,13 @@
</noscript>
<div id="root"></div>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"
- integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n"></script>
+ integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n"
+ crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"
- integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb"></script>
+ integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb"
+ crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"
- integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn"></script>
+ integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn"
+ crossorigin="anonymous"></script>
</body>
</html>
diff --git a/src/actions/projects.js b/src/actions/projects.js
new file mode 100644
index 00000000..57304326
--- /dev/null
+++ b/src/actions/projects.js
@@ -0,0 +1,38 @@
+export const SET_AUTH_VISIBILITY_FILTER = "SET_AUTH_VISIBILITY_FILTER";
+export const OPEN_NEW_PROJECT_MODAL = "OPEN_NEW_PROJECT_MODAL";
+export const CLOSE_NEW_PROJECT_MODAL = "CLOSE_PROJECT_POPUP";
+export const ADD_PROJECT = "ADD_PROJECT";
+export const DELETE_PROJECT = "DELETE_PROJECT";
+
+export const setAuthVisibilityFilter = (filter) => {
+ return {
+ type: SET_AUTH_VISIBILITY_FILTER,
+ filter: filter
+ };
+};
+
+export const openNewProjectModal = () => {
+ return {
+ type: OPEN_NEW_PROJECT_MODAL
+ };
+};
+
+export const closeNewProjectModal = () => {
+ return {
+ type: CLOSE_NEW_PROJECT_MODAL
+ };
+};
+
+export const addProject = (name) => {
+ return {
+ type: ADD_PROJECT,
+ name
+ };
+};
+
+export const deleteProject = (id) => {
+ return {
+ type: DELETE_PROJECT,
+ id
+ };
+};
diff --git a/src/components/modals/Modal.js b/src/components/modals/Modal.js
new file mode 100644
index 00000000..e2d19fcb
--- /dev/null
+++ b/src/components/modals/Modal.js
@@ -0,0 +1,82 @@
+import PropTypes from "prop-types";
+import React from "react";
+
+class Modal extends React.Component {
+ static propTypes = {
+ title: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ };
+ static idCounter = 0;
+
+ constructor() {
+ super();
+ this.id = "modal-" + Modal.idCounter;
+ }
+
+ componentDidMount() {
+ this.openOrCloseModal();
+ }
+
+ componentDidUpdate() {
+ this.openOrCloseModal();
+ }
+
+ onSubmit() {
+ this.props.onSubmit();
+ this.closeModal();
+ }
+
+ onCancel() {
+ this.props.onCancel();
+ this.closeModal();
+ }
+
+ openModal() {
+ window["$"]("#" + this.id).modal("show");
+ }
+
+ closeModal() {
+ window["$"]("#" + this.id).modal("hide");
+ }
+
+ openOrCloseModal() {
+ if (this.props.show) {
+ this.openModal();
+ } else {
+ this.closeModal();
+ }
+ }
+
+ render() {
+ return (
+ <div className="modal" id={this.id} role="dialog">
+ <div className="modal-dialog" role="document">
+ <div className="modal-content">
+ <div className="modal-header">
+ <h5 className="modal-title">{this.props.title}</h5>
+ <button type="button" className="close" onClick={this.onCancel.bind(this)}
+ aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div className="modal-body">
+ {this.props.children}
+ </div>
+ <div className="modal-footer">
+ <button type="button" className="btn btn-secondary" onClick={this.onCancel.bind(this)}>
+ Close
+ </button>
+ <button type="button" className="btn btn-primary" onClick={this.onSubmit.bind(this)}>
+ Save
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default Modal;
diff --git a/src/components/modals/TextInputModal.js b/src/components/modals/TextInputModal.js
new file mode 100644
index 00000000..4acf25b3
--- /dev/null
+++ b/src/components/modals/TextInputModal.js
@@ -0,0 +1,41 @@
+import PropTypes from "prop-types";
+import React from "react";
+import Modal from "./Modal";
+
+class TextInputModal extends React.Component {
+ static propTypes = {
+ title: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
+ initialValue: PropTypes.string,
+ };
+
+ onSubmit() {
+ this.props.callback(this.refs.textInput.value);
+ this.refs.textInput.value = "";
+ }
+
+ onCancel() {
+ this.props.callback(undefined);
+ this.refs.textInput.value = "";
+ }
+
+ render() {
+ return (
+ <Modal title={this.props.title}
+ show={this.props.show}
+ onSubmit={this.onSubmit.bind(this)}
+ onCancel={this.onCancel.bind(this)}>
+ <form>
+ <div className="form-group">
+ <label className="form-control-label">{this.props.label}:</label>
+ <input type="text" className="form-control" ref="textInput" value={this.props.initialValue}/>
+ </div>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+export default TextInputModal;
diff --git a/src/components/navigation/Navbar.js b/src/components/navigation/Navbar.js
index a5f0510f..bd6fd750 100644
--- a/src/components/navigation/Navbar.js
+++ b/src/components/navigation/Navbar.js
@@ -1,8 +1,8 @@
import React, {Component} from 'react';
+import FontAwesome from "react-fontawesome";
+import Mailto from "react-mailto";
import {Link} from "react-router-dom";
import "./Navbar.css";
-import Mailto from "react-mailto";
-import FontAwesome from "react-fontawesome";
class Navbar extends Component {
render() {
@@ -19,7 +19,7 @@ class Navbar extends Component {
</div>
<div className="user-controls navbar-button-group">
<Mailto className="support" title="Support" email="opendc.tudelft@gmail.com"
- headers={{subject: "OpenDC%20Support"}}>
+ headers={{subject: "OpenDC Support"}}>
<FontAwesome name="question-circle" size="lg"/>
</Mailto>
<Link className="username" title="My Profile" to="/profile">Profile</Link>
diff --git a/src/components/navigation/Navbar.sass b/src/components/navigation/Navbar.sass
index d40eecfb..a592eab0 100644
--- a/src/components/navigation/Navbar.sass
+++ b/src/components/navigation/Navbar.sass
@@ -1,5 +1,5 @@
-@import ../../style-globals/mixins.sass
-@import ../../style-globals/variables.sass
+@import ../../style-globals/_mixins.sass
+@import ../../style-globals/_variables.sass
.opendc-navbar
position: relative
diff --git a/src/components/not-found/BlinkingCursor.js b/src/components/not-found/BlinkingCursor.js
new file mode 100644
index 00000000..f6c9768c
--- /dev/null
+++ b/src/components/not-found/BlinkingCursor.js
@@ -0,0 +1,8 @@
+import React from "react";
+import "./BlinkingCursor.css";
+
+const BlinkingCursor = () => (
+ <span className="blinking-cursor">_</span>
+);
+
+export default BlinkingCursor;
diff --git a/src/components/not-found/BlinkingCursor.sass b/src/components/not-found/BlinkingCursor.sass
new file mode 100644
index 00000000..6be1476d
--- /dev/null
+++ b/src/components/not-found/BlinkingCursor.sass
@@ -0,0 +1,35 @@
+.blinking-cursor
+ -webkit-animation: 1s blink step-end infinite
+ -moz-animation: 1s blink step-end infinite
+ -o-animation: 1s blink step-end infinite
+ animation: 1s blink step-end infinite
+
+@keyframes blink
+ from, to
+ color: #eeeeee
+ 50%
+ color: #333333
+
+@-moz-keyframes blink
+ from, to
+ color: #eeeeee
+ 50%
+ color: #333333
+
+@-webkit-keyframes blink
+ from, to
+ color: #eeeeee
+ 50%
+ color: #333333
+
+@-ms-keyframes blink
+ from, to
+ color: #eeeeee
+ 50%
+ color: #333333
+
+@-o-keyframes blink
+ from, to
+ color: #eeeeee
+ 50%
+ color: #333333
diff --git a/src/components/not-found/CodeBlock.js b/src/components/not-found/CodeBlock.js
new file mode 100644
index 00000000..24d100cc
--- /dev/null
+++ b/src/components/not-found/CodeBlock.js
@@ -0,0 +1,30 @@
+import React from "react";
+import "./CodeBlock.css";
+
+const CodeBlock = () => {
+ const textBlock =
+ " oo oooo oo <br/>" +
+ " oo oo oo oo <br/>" +
+ " oo oo oo oo <br/>" +
+ " oooooo oo oo oooooo <br/>" +
+ " oo oo oo oo <br/>" +
+ " oo oooo oo <br/>";
+ const charList = textBlock.split('');
+
+ // Binary representation of the string "OpenDC!" ;)
+ const binaryString = "01001111011100000110010101101110010001000100001100100001";
+
+ let binaryIndex = 0;
+ for (let i = 0; i < charList.length; i++) {
+ if (charList[i] === "o") {
+ charList[i] = binaryString[binaryIndex];
+ binaryIndex++;
+ }
+ }
+
+ return (
+ <div className="code-block" dangerouslySetInnerHTML={{__html: textBlock}}/>
+ );
+};
+
+export default CodeBlock;
diff --git a/src/components/not-found/CodeBlock.sass b/src/components/not-found/CodeBlock.sass
new file mode 100644
index 00000000..51a3d3d0
--- /dev/null
+++ b/src/components/not-found/CodeBlock.sass
@@ -0,0 +1,3 @@
+.code-block
+ white-space: pre-wrap
+ margin-top: 60px
diff --git a/src/components/not-found/TerminalWindow.js b/src/components/not-found/TerminalWindow.js
new file mode 100644
index 00000000..52d3c062
--- /dev/null
+++ b/src/components/not-found/TerminalWindow.js
@@ -0,0 +1,26 @@
+import React from "react";
+import {Link} from "react-router-dom";
+import BlinkingCursor from "./BlinkingCursor";
+import CodeBlock from "./CodeBlock";
+import "./TerminalWindow.css";
+
+const TerminalWindow = () => (
+ <div className="terminal-window">
+ <div className="terminal-header">Terminal -- bash</div>
+ <div className="terminal-body">
+ <div className="segfault">$ status<br/>
+ opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4<br/>
+ opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4<br/>
+ opendc[4270]: STDERR Page does not exist<br/>
+ </div>
+ <CodeBlock/>
+ <div className="sub-title">Got lost?<BlinkingCursor/></div>
+ <Link to="/" className="home-btn">
+ <span className="fa fa-home"/> GET ME BACK TO OPENDC
+ </Link>
+ </div>
+ </div>
+);
+
+
+export default TerminalWindow;
diff --git a/src/components/not-found/TerminalWindow.sass b/src/components/not-found/TerminalWindow.sass
new file mode 100644
index 00000000..4f51a77f
--- /dev/null
+++ b/src/components/not-found/TerminalWindow.sass
@@ -0,0 +1,70 @@
+.terminal-window
+ width: 600px
+ height: 400px
+ display: block
+
+ position: absolute
+ top: 0
+ bottom: 0
+ left: 0
+ right: 0
+
+ margin: auto
+
+ -webkit-user-select: none
+ -moz-user-select: none
+ -ms-user-select: none
+ user-select: none
+ cursor: default
+
+ overflow: hidden
+
+ box-shadow: 5px 5px 20px #444444
+
+.terminal-header
+ font-family: monospace
+ background: #cccccc
+ color: #444444
+ height: 30px
+ line-height: 30px
+ padding-left: 10px
+
+ border-top-left-radius: 7px
+ border-top-right-radius: 7px
+
+.terminal-body
+ font-family: monospace
+ text-align: center
+ background-color: #333333
+ color: #eeeeee
+ padding: 10px
+
+ height: 100%
+
+.segfault
+ text-align: left
+
+.sub-title
+ margin-top: 20px
+
+.home-btn
+ margin-top: 10px
+ padding: 5px
+ display: inline-block
+ border: 1px solid #eeeeee
+ color: #eeeeee
+ text-decoration: none
+ cursor: pointer
+
+ -webkit-transition: all 200ms
+ -moz-transition: all 200ms
+ -o-transition: all 200ms
+ transition: all 200ms
+
+.home-btn:hover
+ background: #eeeeee
+ color: #333333
+
+.home-btn:active
+ background: #333333
+ color: #eeeeee
diff --git a/src/components/projects/FilterButton.js b/src/components/projects/FilterButton.js
new file mode 100644
index 00000000..8d6b7146
--- /dev/null
+++ b/src/components/projects/FilterButton.js
@@ -0,0 +1,23 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import "./FilterButton.css";
+
+const FilterButton = ({active, children, onClick}) => (
+ <div className={classNames("project-filter-button", {"active": active})}
+ onClick={() => {
+ if (!active) {
+ onClick();
+ }
+ }}>
+ {children}
+ </div>
+);
+
+FilterButton.propTypes = {
+ active: PropTypes.bool.isRequired,
+ children: PropTypes.node.isRequired,
+ onClick: PropTypes.func.isRequired
+};
+
+export default FilterButton;
diff --git a/src/components/projects/FilterButton.sass b/src/components/projects/FilterButton.sass
new file mode 100644
index 00000000..0cad68e3
--- /dev/null
+++ b/src/components/projects/FilterButton.sass
@@ -0,0 +1,23 @@
+@import ../../style-globals/_mixins.sass
+@import ../../style-globals/_variables.sass
+
+.project-filter-button
+ display: inline-block
+ width: 33.3%
+ //margin-right: -4px
+ padding: 10px $global-padding
+
+ font-size: 12pt
+ border-right: 1px solid #06326b
+
+ +clickable
+ +transition(background, $transition-length)
+
+.project-filter-button:last-of-type
+ border: 0
+
+.project-filter-button:hover
+ background: #0c60bf
+
+.project-filter-button:active, .project-filter-button.active
+ background: #073d7d
diff --git a/src/components/projects/FilterPanel.js b/src/components/projects/FilterPanel.js
new file mode 100644
index 00000000..050bf0aa
--- /dev/null
+++ b/src/components/projects/FilterPanel.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import FilterLink from "../../containers/projects/FilterLink";
+import "./FilterPanel.css";
+
+const ProjectFilterPanel = () => (
+ <div className="filter-menu">
+ <div className="project-filters">
+ <FilterLink filter="SHOW_ALL">All Projects</FilterLink>
+ <FilterLink filter="SHOW_OWN">My Projects</FilterLink>
+ <FilterLink filter="SHOW_SHARED">Projects shared with me</FilterLink>
+ </div>
+ </div>
+);
+
+export default ProjectFilterPanel;
diff --git a/src/components/projects/FilterPanel.sass b/src/components/projects/FilterPanel.sass
new file mode 100644
index 00000000..a70c7a90
--- /dev/null
+++ b/src/components/projects/FilterPanel.sass
@@ -0,0 +1,21 @@
+@import ../../style-globals/_mixins.sass
+@import ../../style-globals/_variables.sass
+
+.filter-menu
+ display: block
+
+ background: #0761b1
+ border: 1px solid #06326b
+ color: #eee
+
+ text-align: center
+
+ +border-radius($standard-border-radius)
+ overflow: hidden
+
+ margin-bottom: 20px
+
+ .project-filters
+ display: block
+ overflow: hidden
+ margin: 0 -1px
diff --git a/src/components/projects/NewProjectButton.js b/src/components/projects/NewProjectButton.js
new file mode 100644
index 00000000..9eaf6df4
--- /dev/null
+++ b/src/components/projects/NewProjectButton.js
@@ -0,0 +1,16 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import './NewProjectButton.css';
+
+const NewProjectButton = ({onClick}) => (
+ <div className="new-project-btn" onClick={onClick}>
+ <span className="fa fa-plus"/>
+ New Project
+ </div>
+);
+
+NewProjectButton.propTypes = {
+ onClick: PropTypes.func.isRequired,
+};
+
+export default NewProjectButton;
diff --git a/src/components/projects/NewProjectButton.sass b/src/components/projects/NewProjectButton.sass
new file mode 100644
index 00000000..89435902
--- /dev/null
+++ b/src/components/projects/NewProjectButton.sass
@@ -0,0 +1,31 @@
+@import ../../style-globals/_mixins.sass
+@import ../../style-globals/_variables.sass
+
+.new-project-btn
+ $button-height: 35px
+
+ display: inline-block
+ position: absolute
+ bottom: $navbar-height + 40px
+ right: 40px
+ padding: 0 10px
+ height: $button-height
+ line-height: $button-height
+ font-size: 14pt
+
+ background: #679436
+ color: #eee
+ border: 1px solid #507830
+
+ +border-radius($standard-border-radius)
+ +clickable
+ +transition(all, $transition-length)
+
+ span
+ margin-right: 10px
+
+.new-project-btn:hover
+ background: #73ac45
+
+.new-project-btn:active
+ background: #5c8835
diff --git a/src/components/projects/NoProjectsAlert.js b/src/components/projects/NoProjectsAlert.js
new file mode 100644
index 00000000..957435c7
--- /dev/null
+++ b/src/components/projects/NoProjectsAlert.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import "./NoProjectsAlert.css";
+
+const NoProjectsAlert = () => (
+ <div className="no-projects-alert alert alert-info">
+ <span className="info-icon fa fa-2x fa-question-circle"/>
+ <strong>No projects here yet...</strong> Add some with the 'New Project' button!
+ </div>
+);
+
+export default NoProjectsAlert;
diff --git a/src/components/projects/NoProjectsAlert.sass b/src/components/projects/NoProjectsAlert.sass
new file mode 100644
index 00000000..a526f9ad
--- /dev/null
+++ b/src/components/projects/NoProjectsAlert.sass
@@ -0,0 +1,10 @@
+.no-projects-alert
+ position: relative
+ padding-left: 50px
+
+ .info-icon
+ position: absolute
+ top: 11px
+ left: 15px
+ bottom: 10px
+ font-size: 20pt
diff --git a/src/components/projects/ProjectAuth.js b/src/components/projects/ProjectAuth.js
new file mode 100644
index 00000000..7e3abae1
--- /dev/null
+++ b/src/components/projects/ProjectAuth.js
@@ -0,0 +1,22 @@
+import classNames from 'classnames';
+import React from 'react';
+import Shapes from "../../shapes/index";
+import {AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP} from "../../util/authorizations";
+import {parseAndFormatDateTime} from "../../util/date-time";
+
+const ProjectAuth = ({projectAuth}) => (
+ <div className="project-row">
+ <div>{projectAuth.simulation.name}</div>
+ <div>{parseAndFormatDateTime(projectAuth.simulation.datetimeLastEdited)}</div>
+ <div>
+ <span className={classNames("fa", "fa-" + AUTH_ICON_MAP[projectAuth.authorizationLevel])}/>
+ {AUTH_DESCRIPTION_MAP[projectAuth.authorizationLevel]}
+ </div>
+ </div>
+);
+
+ProjectAuth.propTypes = {
+ projectAuth: Shapes.Authorization.isRequired,
+};
+
+export default ProjectAuth;
diff --git a/src/components/projects/ProjectAuthList.js b/src/components/projects/ProjectAuthList.js
new file mode 100644
index 00000000..093b3279
--- /dev/null
+++ b/src/components/projects/ProjectAuthList.js
@@ -0,0 +1,31 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Shapes from "../../shapes/index";
+import NoProjectsAlert from "./NoProjectsAlert";
+import ProjectAuth from "./ProjectAuth";
+import "./ProjectAuthList.css";
+
+const ProjectAuthList = ({authorizations, onProjectClick}) => (
+ <div className="project-list">
+ <div className="list-head">
+ <div>Project name</div>
+ <div>Last edited</div>
+ <div>Access rights</div>
+ </div>
+ <div className="list-body">
+ {authorizations.length === 0 ?
+ <NoProjectsAlert/> :
+ authorizations.map(authorization => (
+ <ProjectAuth projectAuth={authorization} key={authorization.simulation.id}/>
+ ))
+ }
+ </div>
+ </div>
+);
+
+ProjectAuthList.propTypes = {
+ authorizations: PropTypes.arrayOf(Shapes.Authorization).isRequired,
+ onProjectClick: PropTypes.func.isRequired,
+};
+
+export default ProjectAuthList;
diff --git a/src/components/projects/ProjectAuthList.sass b/src/components/projects/ProjectAuthList.sass
new file mode 100644
index 00000000..86e1123c
--- /dev/null
+++ b/src/components/projects/ProjectAuthList.sass
@@ -0,0 +1,98 @@
+@import ../../style-globals/_mixins.sass
+@import ../../style-globals/_variables.sass
+
+.project-list
+ display: block
+ font-size: 12pt
+ border: 0
+
+ .list-head, .list-body .project-row
+ display: block
+ position: relative
+
+ .list-head div, .list-body .project-row div
+ padding: 0 10px
+ display: inline-block
+
+ .list-head
+ font-weight: bold
+
+ // Address default margin between inline-blocks
+ div
+ margin-right: -4px
+
+.project-row
+ background: #f8f8f8
+ border: 1px solid #b6b6b6
+ height: 40px
+ line-height: 40px
+ clear: both
+
+ +transition(background, $transition-length)
+ +clickable
+
+.project-row:hover
+ background: #fff
+
+.project-row:active
+ background: #cccccc
+
+.project-row:not(:first-of-type)
+ margin-top: -1px
+
+// Sizing of table columns
+.project-row, .project-list .list-head
+ div:first-of-type
+ width: 50%
+
+ div:nth-of-type(2)
+ width: 30%
+
+ div:last-of-type
+ width: 20%
+
+ span
+ margin-right: 10px
+
+.project-row.active
+ border-bottom: 0
+ background: #3442b1
+ color: #eee
+
+.project-view
+ padding: 10px
+ overflow: hidden
+ border: 1px solid #b6b6b6
+ border-top: 0
+
+ background: #3442b1
+ color: #eee
+
+ .participants
+ display: inline-block
+ float: left
+
+ .access-buttons
+ display: inline-block
+ float: right
+
+ .inline-btn
+ margin-left: 10px
+
+ .open
+ background: #e38829
+
+ .open:hover
+ background: #ff992e
+
+ .open:active
+ background: #ba6f21
+
+ .edit
+ background: #2c3897
+
+ .edit:hover
+ background: #3a4ac8
+
+ .edit:active
+ background: #242d7a
diff --git a/src/containers/projects/FilterLink.js b/src/containers/projects/FilterLink.js
new file mode 100644
index 00000000..e9a13436
--- /dev/null
+++ b/src/containers/projects/FilterLink.js
@@ -0,0 +1,22 @@
+import {connect} from "react-redux";
+import {setAuthVisibilityFilter} from "../../actions/projects";
+import FilterButton from "../../components/projects/FilterButton";
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ active: state.authVisibilityFilter === ownProps.filter
+ };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onClick: () => dispatch(setAuthVisibilityFilter(ownProps.filter))
+ };
+};
+
+const FilterLink = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(FilterButton);
+
+export default FilterLink;
diff --git a/src/containers/projects/NewProjectModal.js b/src/containers/projects/NewProjectModal.js
new file mode 100644
index 00000000..7321cb4d
--- /dev/null
+++ b/src/containers/projects/NewProjectModal.js
@@ -0,0 +1,34 @@
+import React from "react";
+import {connect} from "react-redux";
+import {addProject, closeNewProjectModal} from "../../actions/projects";
+import TextInputModal from "../../components/modals/TextInputModal";
+
+const NewProjectModalComponent = ({visible, callback}) => (
+ <TextInputModal title="New Project" label="Project title"
+ show={visible}
+ callback={callback}/>
+);
+
+const mapStateToProps = state => {
+ return {
+ visible: state.newProjectModalVisible
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ callback: (text) => {
+ if (text) {
+ dispatch(addProject(text));
+ dispatch(closeNewProjectModal());
+ }
+ }
+ };
+};
+
+const NewProjectModal = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(NewProjectModalComponent);
+
+export default NewProjectModal;
diff --git a/src/containers/projects/VisibleProjectAuthList.js b/src/containers/projects/VisibleProjectAuthList.js
new file mode 100644
index 00000000..746380f6
--- /dev/null
+++ b/src/containers/projects/VisibleProjectAuthList.js
@@ -0,0 +1,25 @@
+import {connect} from "react-redux";
+import ProjectList from "../../components/projects/ProjectAuthList";
+
+const getVisibleProjectAuths = (projectAuths, filter) => {
+ switch (filter) {
+ case 'SHOW_ALL':
+ return projectAuths;
+ case 'SHOW_OWN':
+ return projectAuths.filter(projectAuth => projectAuth.authorizationLevel === "OWN");
+ case 'SHOW_SHARED':
+ return projectAuths.filter(projectAuth => projectAuth.authorizationLevel !== "OWN");
+ default:
+ return projectAuths;
+ }
+};
+
+const mapStateToProps = state => {
+ return {
+ authorizations: getVisibleProjectAuths(state.authorizations, state.authVisibilityFilter)
+ };
+};
+
+const VisibleProjectAuthList = connect(mapStateToProps)(ProjectList);
+
+export default VisibleProjectAuthList;
diff --git a/src/index.js b/src/index.js
index 25c33e82..1176224e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,20 +1,17 @@
import React from "react";
import ReactDOM from "react-dom";
-import {BrowserRouter, Route, Switch} from "react-router-dom";
+import {Provider} from "react-redux";
import "./index.css";
import registerServiceWorker from "./registerServiceWorker";
-import Home from "./pages/Home";
-import Projects from "./pages/Projects";
-import NotFound from "./pages/NotFound";
+import Routes from "./routes";
+import configureStore from "./store/configureStore";
+
+const store = configureStore();
ReactDOM.render(
- <BrowserRouter>
- <Switch>
- <Route exact path="/" component={Home}/>
- <Route exact path="/projects" component={Projects}/>
- <Route path="/*" component={NotFound}/>
- </Switch>
- </BrowserRouter>,
+ <Provider store={store}>
+ <Routes/>
+ </Provider>,
document.getElementById('root')
);
diff --git a/src/index.sass b/src/index.sass
index 1ea3b0c5..80c72a77 100644
--- a/src/index.sass
+++ b/src/index.sass
@@ -1,12 +1,15 @@
-body
+html, body, #root
margin: 0
padding: 0
width: 100%
height: 100%
- font-family: Helvetica, Verdana, sans-serif
+ font-family: Roboto, Helvetica, Verdana, sans-serif
overflow: hidden
background: #eee
+.full-height
+ height: 100%
+
a, a:hover
text-decoration: none
diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js
new file mode 100644
index 00000000..51141c3e
--- /dev/null
+++ b/src/pages/NotFound.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import TerminalWindow from "../components/not-found/TerminalWindow";
+import './NotFound.css';
+
+const NotFound = () => (
+ <div className="not-found-backdrop">
+ <TerminalWindow/>
+ </div>
+);
+
+export default NotFound;
diff --git a/src/pages/Projects.js b/src/pages/Projects.js
index 6d377e92..40902d97 100644
--- a/src/pages/Projects.js
+++ b/src/pages/Projects.js
@@ -1,8 +1,35 @@
import React from 'react';
+import {connect} from "react-redux";
+import {addProject, openNewProjectModal} from "../actions/projects";
import Navbar from "../components/navigation/Navbar";
+import ProjectFilterPanel from "../components/projects/FilterPanel";
+import NewProjectButton from "../components/projects/NewProjectButton";
+import NewProjectModal from "../containers/projects/NewProjectModal";
+import VisibleProjectList from "../containers/projects/VisibleProjectAuthList";
+import "./Projects.css";
-const Projects = () => (
- <Navbar/>
-);
+class Projects extends React.Component {
+ componentDidMount() {
+ // TODO perform initial fetch
+ }
-export default Projects;
+ onInputSubmission(text) {
+ this.props.dispatch(addProject(text));
+ }
+
+ render() {
+ return (
+ <div className="full-height">
+ <Navbar/>
+ <div className="container project-page-container full-height">
+ <ProjectFilterPanel/>
+ <VisibleProjectList/>
+ <NewProjectButton onClick={() => {this.props.dispatch(openNewProjectModal())}}/>
+ </div>
+ <NewProjectModal/>
+ </div>
+ );
+ }
+}
+
+export default connect()(Projects);
diff --git a/src/pages/Projects.sass b/src/pages/Projects.sass
new file mode 100644
index 00000000..11a52e1a
--- /dev/null
+++ b/src/pages/Projects.sass
@@ -0,0 +1,2 @@
+.project-page-container
+ padding-top: 2rem
diff --git a/src/reducers/index.js b/src/reducers/index.js
new file mode 100644
index 00000000..3974ce4a
--- /dev/null
+++ b/src/reducers/index.js
@@ -0,0 +1,10 @@
+import {combineReducers} from "redux";
+import {authorizations, authVisibilityFilter, newProjectModalVisible} from "./projects";
+
+const rootReducer = combineReducers({
+ authorizations,
+ newProjectModalVisible,
+ authVisibilityFilter,
+});
+
+export default rootReducer;
diff --git a/src/reducers/projects.js b/src/reducers/projects.js
new file mode 100644
index 00000000..20f17a3c
--- /dev/null
+++ b/src/reducers/projects.js
@@ -0,0 +1,42 @@
+import {
+ ADD_PROJECT,
+ CLOSE_NEW_PROJECT_MODAL,
+ OPEN_NEW_PROJECT_MODAL,
+ SET_AUTH_VISIBILITY_FILTER
+} from "../actions/projects";
+
+export const authorizations = (state = [], action) => {
+ switch (action.type) {
+ case ADD_PROJECT:
+ return [
+ ...state,
+ {
+ userId: -1,
+ simulation: {name: action.name, datetimeLastEdited: "2017-08-06T12:43:00"},
+ authorizationLevel: "OWN"
+ }
+ ];
+ default:
+ return state;
+ }
+};
+
+export const newProjectModalVisible = (state = false, action) => {
+ switch (action.type) {
+ case OPEN_NEW_PROJECT_MODAL:
+ return true;
+ case CLOSE_NEW_PROJECT_MODAL:
+ return false;
+ default:
+ return state;
+ }
+};
+
+export const authVisibilityFilter = (state = "SHOW_ALL", action) => {
+ switch (action.type) {
+ case SET_AUTH_VISIBILITY_FILTER:
+ return action.filter;
+ default:
+ return state;
+ }
+};
diff --git a/src/routes/index.js b/src/routes/index.js
new file mode 100644
index 00000000..54dc0703
--- /dev/null
+++ b/src/routes/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import {BrowserRouter, Route, Switch} from "react-router-dom";
+import Home from "../pages/Home";
+import NotFound from "../pages/NotFound";
+import Projects from "../pages/Projects";
+
+const Routes = () => (
+ <BrowserRouter>
+ <Switch>
+ <Route exact path="/" component={Home}/>
+ <Route exact path="/projects" component={Projects}/>
+ <Route path="/*" component={NotFound}/>
+ </Switch>
+ </BrowserRouter>
+);
+
+export default Routes;
diff --git a/src/shapes/index.js b/src/shapes/index.js
new file mode 100644
index 00000000..72153f54
--- /dev/null
+++ b/src/shapes/index.js
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+
+const Shapes = {};
+
+Shapes.User = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ googleId: PropTypes.number.isRequired,
+ email: PropTypes.string.isRequired,
+ givenName: PropTypes.string.isRequired,
+ familyName: PropTypes.string.isRequired,
+});
+
+Shapes.Simulation = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ datetimeCreated: PropTypes.string.isRequired,
+ datetimeLastEdited: PropTypes.string.isRequired,
+});
+
+Shapes.Authorization = PropTypes.shape({
+ userId: PropTypes.number.isRequired,
+ user: Shapes.User,
+ simulationId: PropTypes.number.isRequired,
+ simulation: Shapes.Simulation,
+ authorizationLevel: PropTypes.string.isRequired,
+});
+
+export default Shapes; \ No newline at end of file
diff --git a/src/store/configureStore.js b/src/store/configureStore.js
new file mode 100644
index 00000000..9584f591
--- /dev/null
+++ b/src/store/configureStore.js
@@ -0,0 +1,16 @@
+import {applyMiddleware, createStore} from "redux";
+import {createLogger} from "redux-logger";
+import thunkMiddleware from "redux-thunk";
+import rootReducer from "../reducers/index";
+
+const logger = createLogger();
+
+const configureStore = () => createStore(
+ rootReducer,
+ applyMiddleware(
+ thunkMiddleware,
+ logger,
+ )
+);
+
+export default configureStore;
diff --git a/src/style-globals/mixins.sass b/src/style-globals/_mixins.sass
index 40c07a6d..4ac5a9bc 100644
--- a/src/style-globals/mixins.sass
+++ b/src/style-globals/_mixins.sass
@@ -4,13 +4,13 @@
-o-transition: $property $time
transition: $property $time
-=user-select-def
+=user-select
-webkit-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
-=border-radius-def($length)
+=border-radius($length)
-webkit-border-radius: $length
-moz-border-radius: $length
border-radius: $length
@@ -18,4 +18,4 @@
/* General Button Abstractions */
=clickable
cursor: pointer
- +user-select-def
+ +user-select
diff --git a/src/style-globals/variables.sass b/src/style-globals/_variables.sass
index 4386059d..4386059d 100644
--- a/src/style-globals/variables.sass
+++ b/src/style-globals/_variables.sass
diff --git a/src/util/authorizations.js b/src/util/authorizations.js
new file mode 100644
index 00000000..9a7d4e36
--- /dev/null
+++ b/src/util/authorizations.js
@@ -0,0 +1,11 @@
+export const AUTH_ICON_MAP = {
+ "OWN": "home",
+ "EDIT": "pencil",
+ "VIEW": "eye",
+};
+
+export const AUTH_DESCRIPTION_MAP = {
+ "OWN": "Own",
+ "EDIT": "Can Edit",
+ "VIEW": "Can View",
+};
diff --git a/src/util/date-time.js b/src/util/date-time.js
index f8a2ac45..0093e846 100644
--- a/src/util/date-time.js
+++ b/src/util/date-time.js
@@ -1,4 +1,16 @@
/**
+ * Parses and formats the given date-time string representation.
+ *
+ * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ *
+ * @param dateTimeString A string expressing a date and a time, in the above mentioned format.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function parseAndFormatDateTime(dateTimeString) {
+ return formatDateTime(parseDateTime(dateTimeString));
+}
+
+/**
* Parses date-time string representations and returns a parsed object.
*
* The format assumed is "YYYY-MM-DDTHH:MM:SS".
@@ -13,30 +25,28 @@ export function parseDateTime(dateTimeString) {
day: 0,
hour: 0,
minute: 0,
- second: 0
+ second: 0,
};
const dateAndTime = dateTimeString.split("T");
const dateComponents = dateAndTime[0].split("-");
- output.year = parseInt(dateComponents[0]);
- output.month = parseInt(dateComponents[1]);
- output.day = parseInt(dateComponents[2]);
+ output.year = parseInt(dateComponents[0], 10);
+ output.month = parseInt(dateComponents[1], 10);
+ output.day = parseInt(dateComponents[2], 10);
const timeComponents = dateAndTime[1].split(":");
- output.hour = parseInt(timeComponents[0]);
- output.minute = parseInt(timeComponents[1]);
- output.second = parseInt(timeComponents[2]);
+ output.hour = parseInt(timeComponents[0], 10);
+ output.minute = parseInt(timeComponents[1], 10);
+ output.second = parseInt(timeComponents[2], 10);
return output;
}
/**
- * Serializes the given date and time value to a string.
- *
- * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ * Serializes the given date and time value to a human-friendly string.
*
* @param dateTime An object representation of a date and time.
- * @returns {string} A string representation of that date and time.
+ * @returns {string} A human-friendly string version of that date and time.
*/
export function formatDateTime(dateTime) {
let date;