diff options
Diffstat (limited to 'src')
39 files changed, 925 insertions, 36 deletions
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">×</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; |
