diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b5217a1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.env.example b/.env.example index 56b7539..059f4ef 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,4 @@ CLIENT_SECRET=... NEO4J_USERNAME=... NEO4J_PASSWORD=... // OAuth callback URL -CALLBACK_URL=... \ No newline at end of file +CALLBACK_URL=... diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..18df5a1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +#folders# +public/ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json index cee3996..30cc9c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,24 @@ { - "extends": "airbnb", - "env": { - "browser": true, - "node": true - } + "extends": "airbnb", + "plugins": ["prettier"], + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "arrow-parens": ["error", "as-needed"], + "no-console": 0, + "react/prop-types": 0, + "comma-dangle": [ + "error", + { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "ignore" + } + ] + } } diff --git a/.gitignore b/.gitignore index d54406e..19acf05 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/bundle.js npm-debug.log .env *hot-update* +c855bcf167bb6f333cfe6dfb9f3771ba.bmp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3eac9f..0832d55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ Use github’s interface to make a fork of the repo, then add that repo as an upstream remote: ``` -git remote add upstream https://github.com/cranebaes/gitbud.git +git remote add upstream https://github.com/Toucans456/gitpal.git ``` ### Cut a namespaced feature branch from master diff --git a/PRESS_RELEASE.md b/PRESS_RELEASE.md index f5cbf36..12f98d0 100644 --- a/PRESS_RELEASE.md +++ b/PRESS_RELEASE.md @@ -1,8 +1,8 @@ -## Gitbud +## GitPal ### Sub-Heading -> Gitbud is an application that allows users to connect with others who are either at the same level or higher to work on open source projects. Users can view current projects, interested users, and pair up to work on a project together. +> GitPal is an application that allows users to connect with others who are either at the same level or higher to work on open source projects. Users can view current projects, interested users, and pair up to work on a project together. ### Summary @@ -18,16 +18,12 @@ ### Quote from You -> "GitBud provides new coders with their first real challenge; pushing them beyond tutorials on syntax and online games like CodeWars. It gets developers' hands dirty with their first real project or experience with with real-world code". +> "GitPal provides new coders with their first real challenge; pushing them beyond tutorials on syntax and online games like CodeWars. It gets developers' hands dirty with their first real project or experience with with real-world code". ### How to Get Started -> Login to Gitbud with your Github account. From there, navigate through the list of projects and select a project that you want to get started on. Inside the project's detail, you can find a list of users who are also interested. Feel free to message them and let them know you want to pair up with them. Once the two of you are paired up, you can begin on the project. +> Login to GitPal with your Github account. From there, navigate through the list of projects and select a project that you want to get started on. Inside the project's detail, you can find a list of users who are also interested. Feel free to message them and let them know you want to pair up with them. Once the two of you are paired up, you can begin on the project. ### Customer Quote -> "Gitbud helped me get started on my first open source project with ease." - -### Closing and Call to Action - -> FILL_ME_IN +> "GitPal helped me get started on my first open source project with ease." diff --git a/README.md b/README.md index 43a54fc..840f28b 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,54 @@ -# GitBud +# GitPal -> GitBud is an application that allows users to connect with others who are either at the same level or higher to work on open source projects. Users can view current projects, interested users, and pair up to work on a project together. +> GitPal is an application that was forked from GitBud that allows users to connect with others who are either at the same level or higher to work on open source projects. Users can view current projects, interested users, and pair up to work on a project together. -## Team - - - __Product Owner__: Shaikat Haque - - __Scrum Master__: Francis Ngo - - __Development Team Members__: Peter Warner-Medley, Brian Kim +![Alt text](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/7b1e3bb1d52fba7f60c382df3dc03a0b-original.png) ## Table of Contents -1. [Usage](#Usage) +1. [Team](#team) +1. [Preview](#preview) + 1. [Demo](#demo) + 1. [Screenshots](#screenshots) +1. [Usage](#usage) 1. [Requirements](#requirements) 1. [Development](#development) 1. [Installing Dependencies](#installing-dependencies) 1. [Tasks](#tasks) -1. [Team](#team) + 1. [Roadmap](#roadmap) +1. [More Information](#more-information) + 1. [Server README](#server-readme) + 1. [Client README](#client-readme) +1. [GitBud Repo](#gitbud-repo) 1. [Contributing](#contributing) +## Team + + - __Product Owner__: Scott Schaefer + - __Scrum Master__: Rick Gallegos + - __Development Team Members__: Christine Zimmerman, Scott Mitchell, Sonrisa Chen + +## Preview + +### Demo + +Click [here](https://gitpal.herokuapp.com/) to try out GitPal + +### Screenshots + +[Sample projects](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/3857f3ab5dc89ba8668d64090a631d09-original.png) + +[Project page](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/2e76e08872778c681adc67e8eb0edac7-original.png) + +[User Profile](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/f8b4f7744d08fe5d166b127b7ca88fdd-original.png) + +[Navigation Drawer](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/1177499d4beb39863b539f274faf0d9a-original.png) + +[My Partners page](https://s3.amazonaws.com/poly-screenshots.angel.co/Project/c6/610187/b762fd98fafa7ec781ca2af220a948f6-original.png) + ## Usage -> __Environment Variables__ GitBud has hardcoded a username of 'neo4j' and a password of 'neo' for neo4j. You can change these in the code or override them by setting the appropriate environment variables. You will also need a GitHub Client ID and Client Secret to use the GitHub API. These, too, are set as environment variables. We have used the .env package, which allows environment variables to be set easily with the .env file in the root directory of the project. An example of the necessary variables for GitBud been provided here in this repo. +> __Environment Variables__ GitPal has hardcoded a username of 'neo4j' and a password of 'neo' for neo4j. You can change these in the code or override them by setting the appropriate environment variables. You will also need a GitHub Client ID and Client Secret to use the GitHub API. These, too, are set as environment variables. We have used the .env package, which allows environment variables to be set easily with the .env file in the root directory of the project. An example of the necessary variables for GitPal been provided here in this repo. - Fork and clone the repo - Install dependencies from the root of the repo by running @@ -63,8 +91,22 @@ npm install ### Roadmap -View the project roadmap [here](https://github.com/cranebaes/gitbud/issues) +View the project roadmap [here](https://github.com/Toucans456/GitPal/issues) + +## More Information + +### Server README + +View the GitPal server README [here](client/README.md) + +### Client README + +View the GitPal client README [here](server/README.md) + +## GitBud Repo +View the original Repo +[here](https://github.com/cranebaes/gitbud/) ## Contributing diff --git a/client/README.md b/client/README.md index 16b494e..f178089 100644 --- a/client/README.md +++ b/client/README.md @@ -1,54 +1,79 @@ +# GitPal Client + +## Table of Contents + +1. [Login/Signup](#login/signup) +1. [Questionnaire](#questionnaire) +1. [Project List](#project-list) +1. [Project Details](#project-details) +1. [User Details](#user-details) +1. [Project Status](#project-status) +1. [My Projects](#my-projects) +1. [My Partners](#my-partners) +1. [My Account](#my-account) + ## Login/Signup -- The landing page is the entry point to the app. -- Handled by the Landing react component, the user has the option to login with Github. +- The landing page is the entry point to the app. +- Handled by the Landing react component, the user has the option to login with Github. - Clicking the login button will send a GET request to the url /auth/github. - If this is the user's first time, the user will be routed to the Questionnaire component. - This logic is handled App component, where a check is made to see if the user's loggedIn state has certain properties. - If the user has logged in before, then they will be redirected to a project list page. -## Questionniare +## Questionnaire - This page asks the user for: - Experience Level - Interested Language - - Profile Descripion + - Profile Description - This info is stored in the user's node in the database and is displayed in the user's profile. - After completing the questionnaire, user is taken to the Project List page. ## Project List + - This can be considered the home page. -- The list of available projects to work on will be displated -- Clicking on a project will have 2 possibe outcomes: +- The list of available projects to work on will be displayed +- Clicking on a project will have 2 possible outcomes: - If the user is not working on the project, they will be taken to the project details page. - If the user is already working on the project with a pair, they will be taken to the Project Status page. - This logic is handled inside a 'smart' Project container. - The container checks the user's state to see whether the user is paired on a project or not, and renders the appropriate component. ## Project Details + - This page displays information about a project such description, link to GitHub repo, and a list of recommended users to pair with. - An Interest button allows user to express interest in the project. - The Project Details component uses a UserList Component to display a list of recommended users who have also expressed interest in the project. -- The list is sorted by an aglorithm on the server side that calculates the difference in coding activity by language between users. +- The list is sorted by an algorithm on the server side that calculates the difference in coding activity by language between users. - Clicking on a recommended user will route the user to the User Details component ## User Details + - This page displays user information collected from the questionnaire by a user - There are options to message the user, and pairing with the user. - Pairing with a user will establish a pairing between the 2 users, represented by a PAIRED_WITH relationship in neo4j. - The 2 users will also officially start working on the project, established by a WORKING_ON relationship in neo4j. -- The next time the user clicks the Project through the the project list page, they will be taken to the Project Status page. +- The next time the user clicks the Project through the project list page, they will be taken to the Project Status page. ## Project Status + - This page has multiple checkboxes to that allow users to track their project progress. - Clicking 'Submit Progress' after checking checkboxes will save the users progress. - The next time the user enters the Project Status component, the boxes will remain checked. +- This page also comes with a socket.io chat allowing the partnered users to collaborate on the project together in realtime. ## My Projects + - This component is accessible through the app bar. - It displays the list of projects the user is currently WORKING_ON +## My Partners + +- This component is accessible through the app bar. +- It displays the list of users the currently logged in user is partnered with. ## My Account + - This component is accessible through the app bar -- It displays the the user's information that was entered in the questionnaire \ No newline at end of file +- It displays the user's information that was entered in the questionnaire diff --git a/client/assets/octocat.bmp b/client/assets/octocat.bmp new file mode 100644 index 0000000..48d277b Binary files /dev/null and b/client/assets/octocat.bmp differ diff --git a/client/components/App.jsx b/client/components/App.jsx index 218d479..b7df2c0 100644 --- a/client/components/App.jsx +++ b/client/components/App.jsx @@ -16,12 +16,9 @@ import { connect } from 'react-redux'; import axios from 'axios'; import AppBar from 'material-ui/AppBar'; -import Paper from 'material-ui/Paper'; import ActionHome from 'material-ui/svg-icons/action/home'; import IconButton from 'material-ui/IconButton'; -import FloatingActionButton from 'material-ui/FloatingActionButton'; import { fullWhite } from 'material-ui/styles/colors'; -import SocialPartyMode from 'material-ui/svg-icons/social/party-mode'; import AppDrawer from './AppDrawer'; import Landing from './Landing'; @@ -33,37 +30,51 @@ import ProjectList from './ProjectList'; import Questionnaire from './Questionnaire'; import NotFound from './NotFound'; import MyProjects from './MyProjects'; +import MyPartners from './MyPartners'; class App extends React.Component { constructor(props) { super(props); this.state = { loggedIn: false, - drawerOpen: false, - partyMode: false, + drawerOpen: false + }; + this.checkAuthenticated(); + this.navTap = this.navTap.bind(this); + } + componentDidUpdate() { + if (this.state.loggedIn) { + this.getAllUsers(); } + } - this.checkAuthenticated(); + getAllUsers() { + axios + .get('/API/allUsers') + .then(allUsers => this.props.addAllUsers(allUsers.data)) + .catch(console.error); + } - this.navTap = this.navTap.bind(this); - this.togglePartyMode = this.togglePartyMode.bind(this); + getPairs() { + axios + .get('/API/pairs') + .then(pairs => this.props.loadPairedUsers(pairs.data)) + .catch(console.error); } - //gets list of projects + // gets list of projects getProjects() { - axios.get('/API/projects/') - .then((project) => { - this.props.addProjectsList(project.data); - }) + axios + .get('/API/projects/') + .then(project => this.props.addProjectsList(project.data)) .catch(console.error); } - //gets messages + // gets messages getMessages() { - axios.get('/API/messages') - .then((res) => { - this.props.loadMessages(res.data) - }) + axios + .get('/API/messages') + .then(res => this.props.loadMessages(res.data)) .catch(console.error); } @@ -71,31 +82,17 @@ class App extends React.Component { this.setState({ drawerOpen: !this.state.drawerOpen }); } - //gets authentication + // gets authentication checkAuthenticated() { - axios.get('/auth/authenticated') - .then((res) => { + axios.get('/auth/authenticated').then(res => { + if (res.data !== false) { this.setState({ loggedIn: res.data }); this.getMessages(); this.getProjects(); - }); - } - - //party mode - togglePartyMode() { - const colors = ['blue', 'green', 'red', 'yellow', 'lilac']; - if (this.state.partyMode) { - clearInterval(this.state.partyMode); - document.body.setAttribute('style', `background-color:white`); - this.setState({ partyMode: false }); - } else { - this.setState({partyMode: - setInterval(() => { - const randomNum = Math.floor(Math.random() * colors.length); - document.body.setAttribute('style', `background-color:${colors[randomNum]}`); - }, 200), - }); - } + this.getPairs(); + this.props.loggedInUser(res.data); + } + }); } render() { @@ -105,14 +102,32 @@ class App extends React.Component { If user is new and logged in using github auth, render questionnaire If user is not logged in (logged out) display landing page */ + if (this.state.loggedIn.language) { return (
- }/> + + + + + + } + /> {/* opens and closes side menu */} - this.setState({ drawerOpen: open }) } closeDrawer={ () => this.setState({ drawerOpen: false}) }/> + this.setState({ drawerOpen: open })} + closeDrawer={() => this.setState({ drawerOpen: false })} + /> {/* Switch renders a route exclusively. Without it, it would route inclusively @@ -125,60 +140,79 @@ class App extends React.Component { - + ( + + )} + /> {/* given this path render this component and pass down the loggedIn state as user props */} - () } /> + } + /> + - - -
); } else if (this.state.loggedIn) { return ; - } else { - return ; } + return ; } } /* Allows App component to have message and project state */ -const mapStateToProps = (state) => { - return { - message: state.message, - projects: state.projects, - }; -}; +const mapStateToProps = state => ({ + message: state.message, + projects: state.projects, + pairedUsers: state.pairedUsers +}); /* Map our dispatch to App component as props Dispatch can be found in store/reducers.js */ -const mapDispatchToProps = (dispatch) => { - return { - changeString: () => dispatch({ - type: 'CHANGE_STRING', - text: 'some other message' +const mapDispatchToProps = dispatch => ({ + addAllUsers: allUsers => + dispatch({ + type: 'LOAD_ALL_USERS', + allUsers }), - addProjectsList: projects => dispatch({ + addProjectsList: projects => + dispatch({ type: 'LIST_PROJECTS', - projects, + projects }), - loadMessages: messages => dispatch({ + loadMessages: messages => + dispatch({ type: 'MESSAGES_LOAD', - messages, + messages + }), + loadPairedUsers: pairedUsers => + dispatch({ + type: 'LOAD_PAIRING', + pairedUsers + }), + loggedInUser: loggedInUser => + dispatch({ + type: 'UPDATED_LOGGEDIN_USER', + loggedInUser }), - }; -}; + loggedOut: () => + dispatch({ + type: 'USER_LOGOUT' + }) +}); -//connects the Store to App component +// connects the Store to App component export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/client/components/AppDrawer.jsx b/client/components/AppDrawer.jsx index 38056ac..5aa51c8 100644 --- a/client/components/AppDrawer.jsx +++ b/client/components/AppDrawer.jsx @@ -5,9 +5,11 @@ import { Card, CardHeader } from 'material-ui/Card'; // menus and toolbars etc. import Drawer from 'material-ui/Drawer'; import AppBar from 'material-ui/AppBar'; -import { BottomNavigation, BottomNavigationItem } from 'material-ui/BottomNavigation'; +import { + BottomNavigation, + BottomNavigationItem +} from 'material-ui/BottomNavigation'; // buttons -import FlatButton from 'material-ui/FlatButton'; import RaisedButton from 'material-ui/RaisedButton'; // icons import ActionEject from 'material-ui/svg-icons/action/eject'; @@ -15,36 +17,74 @@ import ActionAccountCircle from 'material-ui/svg-icons/action/account-circle'; import ActionFace from 'material-ui/svg-icons/action/face'; import DeviceDeveloperMode from 'material-ui/svg-icons/device/developer-mode'; +/* +Deprecated FindUsers button +Add this to AppDrawer when user functionality expands + + }/> +*/ + function AppDrawer(props) { return ( - - - - -
- - }/> + + + + +
+ + } + /> - - + +
- - -
- }/> - + + +
+ + } + /> +
- + - }/> + } + /> - } onClick={ props.closeDrawer }/> + + } + onClick={props.closeDrawer} + /> + - ) + ); } export default AppDrawer; diff --git a/client/components/Landing.jsx b/client/components/Landing.jsx index e76ea26..26b8585 100644 --- a/client/components/Landing.jsx +++ b/client/components/Landing.jsx @@ -1,26 +1,38 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import { Card, CardMedia } from 'material-ui/Card'; import AppBar from 'material-ui/AppBar'; import RaisedButton from 'material-ui/RaisedButton'; // makes the theme available via a higher-0rder component import muiThemeable from 'material-ui/styles/muiThemeable'; +import octocat from '../assets/octocat.bmp'; const style = { - textAlign: 'center', + textAlign: 'center' }; -function Landing(props) { +function Landing() { return ( - - + + - + icon -
+ diff --git a/client/components/MyPartners.jsx b/client/components/MyPartners.jsx new file mode 100644 index 0000000..db9a739 --- /dev/null +++ b/client/components/MyPartners.jsx @@ -0,0 +1,87 @@ +/* eslint no-console:0 */ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import Paper from 'material-ui/Paper'; +import { + Table, + TableBody, + TableHeader, + TableHeaderColumn, + TableRow, + TableRowColumn +} from 'material-ui/Table'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; +import { Card } from 'material-ui/Card'; +import Subheader from 'material-ui/Subheader'; + +class MyPartners extends React.Component { + constructor(props) { + super(props); + this.state = { + isMounted: false, + userLists: [] + }; + } + + componentDidMount() { + if (this.props.pairedUsers) { + this.setState({ + userLists: this.props.pairedUsers + }); + } else { + this.setState({ + userLists: [] + }); + } + } + + render() { + const tests = this.props.pairedUsers[0] || []; + + return ( + + + + + + + + Click on a user to chat and start working! + + + + Name + Language + Experience + + + + {tests.map(user => ( + + + {user.name} + + {user.language} + {user.experience} + + ))} + +
+
+
+ ); + } +} + +const mapStateToProps = state => ({ + pairedUsers: state.pairedUsers +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(MyPartners); diff --git a/client/components/MyProjects.jsx b/client/components/MyProjects.jsx index f05b2ab..be86ed8 100644 --- a/client/components/MyProjects.jsx +++ b/client/components/MyProjects.jsx @@ -1,60 +1,58 @@ +/* eslint no-console:0 */ import React from 'react'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import Paper from 'material-ui/Paper'; -import RaisedButton from 'material-ui/RaisedButton'; +import Subheader from 'material-ui/Subheader'; import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, - TableRowColumn, + TableRowColumn } from 'material-ui/Table'; -import { - Toolbar, - ToolbarGroup, - ToolbarTitle -} from 'material-ui/Toolbar'; -import {Card, CardText } from 'material-ui/Card'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; +import { Card } from 'material-ui/Card'; -const MyProjects = (props) => { - return ( - - - - - - - - - - - Name - Language - Experience +const MyProjects = props => ( + + + + + + + + Projects you have a partner with +
+ + + Name + Language + Experience + + + + {props.projects.map(project => ( + + + {project.project} + + {project.language} + {project.experience} - - - {props.projects.map(project => - ( - { project.project } - { project.language } - { project.experience } - ) - )} - -
+ ))} + +
-
- ); -}; + +); -const mapStateToProps = (state) => { - return { - projects: state.projects.filter(project => project.paired.length > 0), - }; -}; +const mapStateToProps = state => ({ + projects: state.projects.filter(project => project.paired.length > 0) +}); -//connects the Store to MyProjects component +// connects the Store to MyProjects component export default connect(mapStateToProps)(MyProjects); diff --git a/client/components/NotFound.jsx b/client/components/NotFound.jsx index da735c1..361e8db 100644 --- a/client/components/NotFound.jsx +++ b/client/components/NotFound.jsx @@ -1,3 +1,4 @@ +/* eslint no-console:0 */ import React from 'react'; function NotFound() { diff --git a/client/components/Project.jsx b/client/components/Project.jsx index 2376cc3..7060961 100644 --- a/client/components/Project.jsx +++ b/client/components/Project.jsx @@ -9,35 +9,44 @@ class Project extends React.Component { constructor(props) { super(props); this.POSTprogress = this.POSTprogress.bind(this); - if (this.props.project.paired.length > 0 && this.props.progress.length < 1) { + + if ( + this.props.project.paired.length > 0 && + this.props.progress.length < 1 + ) { this.GETprogress(); } } - //post user's progress + // post user's progress POSTprogress() { - axios.post('/API/progress', { - projectId: this.props.project.id, - progress: this.props.progress, - }) - .catch(console.error); + axios + .post('/API/progress', { + projectId: this.props.project.id + }) + .catch(console.error); } - //gets user's progress + // gets user's progress GETprogress() { - axios.get('/API/progress') - .then(res => - this.props.loadProgress(res.data) - ) + axios + .get('/API/progress') + .then(res => this.props.loadProgress(res.data)) .catch(console.error); } render() { - if (this.props.project.paired.length > 0) { - return - } else { - return - } + if (this.props.project.paired.length > 0) { + return ( + + ); + } + return ; } } @@ -47,9 +56,10 @@ class Project extends React.Component { const mapStateToProps = (state, props) => { const projectId = Number(props.match.params.id); const project = state.projects.filter(project => project.id === projectId)[0]; + // console.log('Project.jsx line61 props.project', project); return { project, - progress: state.projectProgress[projectId] || [], + progress: state.projectProgress[projectId] || [] }; }; @@ -57,19 +67,19 @@ const mapStateToProps = (state, props) => { Map our dispatch to Project component as props Dispatch can be found in store/reducers.js */ -const mapDispatchToProps = (dispatch, props) => { - return { - dispatchProgress: (projectId, itemIndex) => dispatch({ +const mapDispatchToProps = dispatch => ({ + dispatchProgress: (projectId, itemIndex) => + dispatch({ type: 'PROGRESS_CHANGE_ITEM', projectId, itemIndex }), - loadProgress: progress => dispatch({ + loadProgress: progress => + dispatch({ type: 'PROGRESS_LOAD_ITEMS', - progress, + progress }) - }; -}; +}); -//connects the Store to Project component +// connects the Store to Project component export default connect(mapStateToProps, mapDispatchToProps)(Project); diff --git a/client/components/ProjectDetails.jsx b/client/components/ProjectDetails.jsx index e45b381..d9d49de 100644 --- a/client/components/ProjectDetails.jsx +++ b/client/components/ProjectDetails.jsx @@ -1,107 +1,206 @@ +/* eslint no-console:0 */ import React from 'react'; -import { Link } from 'react-router-dom'; +import io from 'socket.io-client'; import { connect } from 'react-redux'; import axios from 'axios'; -import { - Toolbar, - ToolbarGroup, - ToolbarTitle -} from 'material-ui/Toolbar'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; import Paper from 'material-ui/Paper'; -import {Card, CardText } from 'material-ui/Card'; +import { Card, CardText } from 'material-ui/Card'; import RaisedButton from 'material-ui/RaisedButton'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; import UserList from './UserList'; +const socket = io(); + class ProjectDetails extends React.Component { constructor(props) { super(props); - this.state = { - interest: false, + interest: this.props.project.interested, + open: false, + disableUsers: !this.props.project.interested }; this.toggleInterest = this.toggleInterest.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleClose = this.handleClose.bind(this); + this.clickHandler = this.clickHandler.bind(this); + this.handleInterest = this.handleInterest.bind(this); + this.getUsers(); } getUsers() { - axios.get('/API/users', { - params: { - projectId: this.props.project.id - } - }) - .then((users) => { + axios + .get('/API/users', { + params: { + projectId: this.props.project.id + } + }) + .then(users => { this.props.addUsers(users.data); }) .catch(console.error); } + /* dialog handler */ + handleOpen() { + this.setState({ open: true }); + } + + handleClose() { + this.setState({ open: false }); + this.handleInterest(); + } + /* dialog handler end */ + toggleInterest() { - axios.post('/API/projects', { - projectId: this.props.project.id, - }) - .then((response) => { - this.props.dispatchInterest(this.props.project.id, !this.props.project.interested); - }) - .catch((error) => { - console.log(error); - }); + // if wasnt interested, sent request for adding interests + if (!this.props.project.interested) { + // adding interest; + axios + .post('/API/projects', { + projectId: this.props.project.id + }) + .then(() => { + this.props.project.interested = !this.props.project.interested; + this.props.dispatchInterest( + this.props.project.id, + this.props.project.interested + ); + socket.emit('updateInterestList'); // REACT needs this after a POST + }) + .catch(error => { + console.log(error); + }); + } else if (this.props.project.interested) { + // deleting interest; + axios + .post('/API/deleteInterest', { + projectId: this.props.project.id + }) + .then(() => { + this.props.project.interested = !this.props.project.interested; + this.props.dispatchInterest( + this.props.project.id, + this.props.project.interested + ); + socket.emit('updateInterestList'); // REACT needs this after a POST + }) + .catch(error => { + console.log(error); + }); + } + } + + handleInterest() { + this.toggleInterest(); + } + + clickHandler() { + this.handleOpen(); } render() { + socket.on('updateInterestList', () => this.getUsers()); + + const actions = [ + , + + ]; + return ( - - + + - + - - + + - { this.props.project.description || 'This project has no description.' } + {this.props.project.description || + 'This project has no description.'} - + - - + + - + + {this.props.project.interested + ? 'Are you sure?' + : 'Choose a partner!'} + + - ) + ); } } const mapStateToProps = (state, props) => { const projectId = Number(props.routedProjectId); + const project = state.projects.filter(cur => cur.id === projectId)[0]; + const disableUsers = !project.interested; + const paringWithCur = state.pairedUsers; + console.log(paringWithCur); return { users: state.users, - project: state.projects.filter(project => project.id === projectId)[0], + project, + disableUsers }; }; -const mapDispatchToProps = (dispatch) => { - return { - addUsers: users => dispatch({ +const mapDispatchToProps = dispatch => ({ + addUsers: users => + dispatch({ type: 'USERS_ADD', - users: users + users }), - dispatchInterest: (projectId, value) => dispatch({ + dispatchInterest: (projectId, value) => + dispatch({ type: 'CHANGE_PROJECT_INTEREST', projectId, - value, + value }) - }; -}; +}); -//connects the Store to ProjectDetails component +// connects the Store to ProjectDetails component export default connect(mapStateToProps, mapDispatchToProps)(ProjectDetails); diff --git a/client/components/ProjectList.jsx b/client/components/ProjectList.jsx index 581ba7b..2f82431 100644 --- a/client/components/ProjectList.jsx +++ b/client/components/ProjectList.jsx @@ -2,60 +2,55 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import Paper from 'material-ui/Paper'; -import RaisedButton from 'material-ui/RaisedButton'; + import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, - TableRowColumn, + TableRowColumn } from 'material-ui/Table'; -import { - Toolbar, - ToolbarGroup, - ToolbarTitle -} from 'material-ui/Toolbar'; -import {Card, CardText } from 'material-ui/Card'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; +import { Card } from 'material-ui/Card'; -const ProjectList = (props) => { - return ( - - - - - - - - - - - Name - Language - Experience +const ProjectList = props => ( + + + + + + + +
+ + + Name + Language + Experience + + + + {props.projects.map(project => ( + + + {project.project} + + {project.language} + {project.experience} - - - {props.projects.map(project => - ( - { project.project } - { project.language } - { project.experience } - ) - )} - -
-
-
- ); -}; - -const mapStateToProps = (state) => { - return { - projects: state.projects, - }; -}; + ))} + + +
+
+); +const mapStateToProps = state => ({ + projects: state.projects +}); -//connects the Store to ProjectList +// connects the Store to ProjectList export default connect(mapStateToProps)(ProjectList); diff --git a/client/components/ProjectStatus.jsx b/client/components/ProjectStatus.jsx index 6135de3..00b381d 100644 --- a/client/components/ProjectStatus.jsx +++ b/client/components/ProjectStatus.jsx @@ -1,32 +1,40 @@ +/* eslint no-console:0 */ import React from 'react'; +import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; + +import io from 'socket.io-client'; + import Paper from 'material-ui/Paper'; -import { - Toolbar, - ToolbarGroup, - ToolbarTitle -} from 'material-ui/Toolbar'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; import { Card, CardHeader, CardText } from 'material-ui/Card'; +import FloatingActionButton from 'material-ui/FloatingActionButton'; import Checkbox from 'material-ui/Checkbox'; import RaisedButton from 'material-ui/RaisedButton'; import FlatButton from 'material-ui/FlatButton'; import Dialog from 'material-ui/Dialog'; +import SocialPartyMode from 'material-ui/svg-icons/social/party-mode'; + +const socket = io(); const style = { - margin: 12, + margin: 12 }; +const customContentStyle = { + width: '80%', + height: '100%', + maxWidth: 'none' +}; // renders a progress item component inside ProjectStatus -const ProgressItem = (props) => { +const ProgressItem = props => { const check = () => props.dispatchProgress(props.projectId, props.index); return (
- - { props.hint } - + {props.hint}
- ) + ); }; class ProjectStatus extends React.Component { @@ -34,82 +42,191 @@ class ProjectStatus extends React.Component { super(props); this.state = { - open: false - } + open: false, + dialogOpen: false, + chatBox: [] + }; this.handleSubmit = this.handleSubmit.bind(this); this.handleClose = this.handleClose.bind(this); + this.handleDiaLogOpen = this.handleDiaLogOpen.bind(this); + this.handleDiaLogClose = this.handleDiaLogClose.bind(this); + this.handleMessegeSubmit = this.handleMessegeSubmit.bind(this); + + socket.on('chat message', msg => this.renderMessages(msg)); + } + + /* dialog handler */ + handleDiaLogOpen() { + this.setState({ dialogOpen: true }); + } + + handleDiaLogClose() { + this.setState({ dialogOpen: false }); } + /* dialog handler end */ - //handles opening the dialog alert and submits the project's progress + // handles opening the dialog alert and submits the project's progress handleSubmit() { this.setState({ open: true }); - this.props.submitProgress() + this.props.submitProgress(); } - //handles the closing of dialog alert + // handles the closing of dialog alert handleClose() { this.setState({ open: false }); } + handleMessegeSubmit(event) { + event.preventDefault(); + + const newMessage = { + message: this._message.value, + username: this.props.loggedInUser + }; + + const myMessage = { + username: 'me: ', + message: this._message.value + }; + + const updatedChatBox = this.state.chatBox; + updatedChatBox.push(myMessage); + + this.setState({ + chatBox: updatedChatBox + }); + + socket.emit('chat message', newMessage); // send msg + } + + renderMessages(msg) { + const updatedChatBox = this.state.chatBox; + updatedChatBox.push(msg); + this.setState({ + chatBox: updatedChatBox + }); + } + render() { - return ( - - - - - - - - - - - +
+ (this._message = message)} + id="newMessage" + type="text" /> - - {this.props.project.description || 'This project has no description.' } - -
- { - this.props.progress.map((item, index) => - ( - + Send + + +
, + + ]; + return ( +
+ + + + + + + + + + - ) - ) - } -
- - - } - modal={false} - open={this.state.open} - onRequestClose={this.handleClose} + + + + + + {this.props.project.description || + 'This project has no description.'} + +
+ {this.props.progress.map((item, index) => ( + + ))} +
+ + + } + modal={false} + open={this.state.open} + onRequestClose={this.handleClose} > Congrats on your progress! - -
-
- ) + + + + + + + +
    + {this.state.chatBox.map((chat, index) => ( +
    + {chat.username} +

    {chat.message}

    +
    + ))} +
+
+ ); } } -export default ProjectStatus; +const mapStateToProps = state => { + const loggedInUser = state.loggedInUser.username; + const loggedInUserGhId = state.loggedInUser.ghId; + return { + loggedInUser, + loggedInUserGhId + }; +}; + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectStatus); diff --git a/client/components/Questionnaire.jsx b/client/components/Questionnaire.jsx index 7997fcb..8e7b7d1 100644 --- a/client/components/Questionnaire.jsx +++ b/client/components/Questionnaire.jsx @@ -1,3 +1,4 @@ +/* eslint no-console:0 */ import React from 'react'; import axios from 'axios'; import { Card } from 'material-ui/Card'; @@ -13,65 +14,87 @@ class Questionnaire extends React.Component { this.state = { selectedLanguage: 'JavaScript', selectedSkillLevel: 'Beginner', - description: '', + description: '' }; } onLanguageSelect(val) { - this.setState({ selectedLanguage: val }, () => console.log(this.state.selectedLanguage)); + this.setState({ selectedLanguage: val }); } onSkillLevelSelect(val) { - this.setState({ selectedSkillLevel: val }, () => console.log(this.state.selectedSkillLevel)); + this.setState({ selectedSkillLevel: val }); } onDescriptionChange(val) { - this.setState({ description: val }, () => console.log(this.state.description)); + this.setState({ description: val }); } onButtonClick() { - let userInfo = { + const userInfo = { language: this.state.selectedLanguage, experience: this.state.selectedSkillLevel, - description: this.state.description, + description: this.state.description }; - axios.post('/API/users', userInfo) - .then((response) => { - console.log(response); + axios + .post('/API/users', userInfo) + .then(() => { // redirect to home after successful submission window.location.href = '/projects'; }) - .catch((error) => { + .catch(error => { console.log(error); }); } render() { return ( - +

Welcome, {this.props.user.name}

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam varius quam id quam aliquot, quis varius est euismod.


-

Select your preferred language to use with other GitBud members:

- this.onLanguageSelect(val)}> - - - - +

Select your preferred language to use with other GitPal members:

+ this.onLanguageSelect(val)} + > + + + +

Select your proficieny level at the chosen language above:

- this.onSkillLevelSelect(val)}> - + this.onSkillLevelSelect(val)} + > +
-

Write a short introduction about yourself that other GitBud members can see:

- this.onDescriptionChange(val)} /> +

+ Write a short introduction about yourself that other GitPal members + can see: +

+ this.onDescriptionChange(val)} + />
- this.onButtonClick()} /> + this.onButtonClick()} + />
); } diff --git a/client/components/UserDetails.jsx b/client/components/UserDetails.jsx index e6a3e73..84ca53f 100644 --- a/client/components/UserDetails.jsx +++ b/client/components/UserDetails.jsx @@ -1,7 +1,10 @@ +/* eslint no-console:0 */ import React from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; +import chatBox from './MyPartners'; + import Paper from 'material-ui/Paper'; import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; import { Card, CardMedia, CardText, CardTitle } from 'material-ui/Card'; @@ -10,76 +13,308 @@ import RaisedButton from 'material-ui/RaisedButton'; import ActionFace from 'material-ui/svg-icons/action/face'; import ActionBuild from 'material-ui/svg-icons/action/build'; import ActionDone from 'material-ui/svg-icons/action/done'; +import ActionAdd from 'material-ui/svg-icons/social/person'; import ContentSend from 'material-ui/svg-icons/content/send'; import TextField from 'material-ui/TextField'; +/* for Dialog */ +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; -class UserDetails extends React.Component { +const customContentStyle = { + width: '80%', + height: '100%', + maxWidth: 'none' +}; +/* for Dialog */ + +import io from 'socket.io-client'; + +const socket = io(); +class UserDetails extends React.Component { constructor(props) { super(props); + console.log('this is props of UserDetails', props); this.state = { + buttonClicked: false, expanded: false, - message: '', - } - this.paired = false; + partnerName: '', + message: 'placeholder', + chatBox: [], + myMessage: 'myMessage', + receivedMessage: 'receivedMessage', + // for popUp window + open: false, + isPaired: false, + curProjectId: null, + curProjectProperty: null + }; this.expandCard = () => { this.setState({ expanded: true }); - } - this.togglePair = this.togglePair.bind(this); + }; + + socket.on('chat message', msg => this.renderMessages(msg)); + // receive messages + this.addPair = this.addPair.bind(this); + this.unPair = this.unPair.bind(this); + this.getPairs = this.getPairs.bind(this); this.pairButton = this.pairButton.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.retrieveProjectId = this.retrieveProjectId.bind(this); + // this.sendinvitations = this.sendinvitations.bind(this); + this.initialize(); + this.setMessageText = (_, text) => this.setState({ message: text }); this.sendMessage = () => { - axios.post('/API/messages', { - text: this.state.message, - recipient: this.props.user.id, - }) + axios + .post('/API/messages', { + text: this.state.message, + recipient: this.props.user.id + }) .then(() => { this.props.dispatchMessage(this.props.user.id, { text: this.state.message, - sender: true, + sender: true }); }); }; } - togglePair() { - axios.post('/API/pair', { - partnered: this.props.user.id, - project: this.props.match.params.projectId, - }) - .then((response) => { - this.props.dispatchPairing(this.props.user.id, Number(this.props.match.params.projectId)); - console.log(response); + initialize() { + return new Promise((resolve, reject) => { + this.retrieveProjectId(); + resolve(); + }).then(() => { + this.checkIfPaired(); + }); + } + getPairs() { + console.log('get pairs runninnnngnggngg'); + axios + .get('/API/pairs') + .then(pairs => { + this.props.loadPairedUsers(pairs.data); + console.log('get pairs runnin', pairs.data); + }) + .catch(console.error); + } + + checkIfPaired() { + axios + .get('/API/pairedProjects', { + params: { + userId: this.props.loggedInUserGhId, + partnerId: this.props.user.ghId + } + }) + .then(pairProjects => { + if (pairProjects.data.length > 0) { + this.setState({ + buttonClicked: true + }); + } + }) + .catch(error => { + console.log(error); + }); + } + + addPair() { + axios + .post('/API/pair', { + partnered: this.props.user.id, + project: this.state.curProjectId + }) + .then(response => { + console.log('addPair!!!!!!!', response.data); + this.props.createPairing(response.data); + this.setState({ buttonClicked: !this.state.buttonClicked }); + // window.location.reload(); // REACT needs this after a POST + socket.emit('pairInfo'); + this.getPairs(); }) - .catch((error) => { + .catch(error => { console.log(error); }); + + axios.get('/'); + } + + unPair() { + axios + .post('/API/unpair', { + partnered: this.props.user.id, + project: this.state.curProjectId + }) + .then(() => { + console.log('testGHid', this.props.user.id); + const partneredId = this.props.user.id; + this.props.removingPairing(partneredId); + this.setState({ buttonClicked: !this.state.buttonClicked }); + // window.location.reload(); // REACT needs this after a POST + socket.emit('pairInfo'); + this.getPairs(); + }) + .catch(error => { + console.log(error); + }); + + axios.get('/'); } + // sendinvitations() { + // // socketIO send invitation + // // change work with me to invitation pending + // console.log(this.props.user.id); + // console.log('userDetails', socket.id); + // let userInfo = { + // inviteeGithubId: this.props.user.id, + // inviterGithubId: this.props.loggedInUserGhId + // }; + // socket.emit('pendingInvitation', userInfo); + // // waiting for comfirmation from another user + // // change it here + // } + + /* dialog handler */ + handleOpen() { + // console.log("clicked") + this.setState({ open: true }); + } + handleClose() { + this.setState({ open: false }); + } + /* dialog handler end */ pairButton() { - if (this.props.user.paired.length > 0) { - return } - onClick={ this.togglePair } /> - } else if (this.props.match.params.projectId) { - return } - onClick={ this.togglePair } - primary={ true } /> + if (this.state.buttonClicked) { + console.log('these are the props for UserDetails', this); + return ( +
+ } + onClick={this.unPair} + /> + } + href="/my-projects" + primary + /> +
+ ); + } else if (!this.state.buttonClicked) { + return ( + } + onClick={this.addPair} + primary + /> + ); } } + handleSubmit(event) { + event.preventDefault(); + + const newMessage = { + message: this._message.value, + username: this.props.loggedInUser + }; + + const myMessage = { + username: 'me: ', + message: this._message.value + }; + + const updatedChatBox = this.state.chatBox; + updatedChatBox.push(myMessage); + + this.setState({ + chatBox: updatedChatBox + }); + + socket.emit('chat message', newMessage); // send msg + } + + getMessages() { + axios + .get('API/messages') + .then(res => { + this.props.loadMessages(res.data); + }) + .catch(console.error); + } + + renderMessages(msg) { + const updatedChatBox = this.state.chatBox; + updatedChatBox.push(msg); + this.setState({ + chatBox: updatedChatBox + }); + } + + retrieveProjectId() { + const userId = this.props.user.ghId; + axios + .get('/API/project', { + params: { + id: userId + } + }) + .then(project => { + this.state.curProjectId = project.data.id; + this.state.curProjectProperty = project.data; + }) + .catch(console.error); + } + render() { + socket.on('pairInfo', () => { + this.checkIfPaired(); + // this.getPairs(); + }); + const userInfo = { + userId: this.props.loggedInUserGhId, + socketId: socket.id + }; + // socket.emit('id myself', userInfo); + const actions = [ +
+
+ (this._message = message)} + id="newMessage" + type="text" + /> + + Send + +
+
, + + ]; return ( - - + + @@ -87,61 +322,139 @@ class UserDetails extends React.Component {
- +
- - - - project.project).join(' ')}/> + + + + project.project) + .join(' ')} + />
- { this.pairButton() } - } onClick={this.expandCard} secondary={true} /> + {this.pairButton()} + } + onClick={this.expandCard} + secondary + /> +
+ {/* dialog for message */} +
+ +
    + {this.state.chatBox.map((chat, index) => ( +
    + {chat.username} +

    {chat.message}

    +
    + ))} +
-
+ {/* dialog for message end */} + {/* should be deleted */} + +
-
- } secondary={true}/> - { this.props.messages.map((message, index) => - - { message.sender ? 'You' : this.props.user.name } - { message.text } +
+ } + secondary + /> + {this.props.messages.map((message, index) => ( + + + {message.sender ? 'You' : this.props.user.name} + + {message.text} - )} + ))}
+ {/* should be deleted end */}
); } } - const mapStateToProps = (state, props) => { const userId = Number(props.match.params.id); - const user = state.users.filter(user => user.id === userId)[0]; - const projects = state.projects.filter(project => user.projects.indexOf(project.id) > -1) + const user = state.allUsers.filter(user => user.id === userId)[0]; + const projects = state.projects.filter( + project => user.projects.indexOf(project.id) > -1 + ); + const loggedInUser = state.loggedInUser.username; + const loggedInUserGhId = state.loggedInUser.ghId; return { user, projects, messages: state.messages[userId] || [], + loggedInUser, + loggedInUserGhId }; }; - -const mapDispatchToProps = dispatch => - ({ - dispatchPairing: (userId, projectId) => dispatch({ type: 'CHANGE_USER_PAIRING', userId, projectId }), - dispatchMessage: (userId, message) => dispatch({ type: 'MESSAGE_SEND', userId, message }), - }); - +const mapDispatchToProps = dispatch => ({ + loadMessages: messages => + dispatch({ + type: 'MESSAGES_LOAD', + messages + }), + createPairing: pairs => + dispatch({ + type: 'ADD_PAIRING', + pairs + }), + removingPairing: ghId => + dispatch({ + type: 'DEL_PAIRING', + ghId + }), + loadPairedUsers: pairedUsers => + dispatch({ + type: 'LOAD_PAIRING', + pairedUsers + }), + dispatchPairing: (userId, projectId) => + dispatch({ + type: 'CHANGE_USER_PAIRING', + userId, + projectId + }), + dispatchMessage: (userId, message) => + dispatch({ + type: 'MESSAGE_SEND', + userId, + message + }) +}); export default connect(mapStateToProps, mapDispatchToProps)(UserDetails); - - - -/* }> - - */ diff --git a/client/components/UserList.jsx b/client/components/UserList.jsx index 816672f..daff94c 100644 --- a/client/components/UserList.jsx +++ b/client/components/UserList.jsx @@ -1,38 +1,34 @@ +/* eslint no-console:0 */ +/* Not currently connected to FIND PARTNER button */ +/* Used to show All Users */ import React from 'react'; import { Link } from 'react-router-dom'; import Subheader from 'material-ui/Subheader'; import Avatar from 'material-ui/Avatar'; import { List, ListItem } from 'material-ui/List'; -import { - Table, - TableBody, - TableHeader, - TableHeaderColumn, - TableRow, - TableRowColumn -} from 'material-ui/Table'; -const UserList = (props) => { - return ( - - Users interested in this project - { props.users.map((user, index) => { - return ( - } - leftAvatar={ - - } - key={ index } - primaryText={ user.name } - secondaryText={ "Rating: " + user.rating } +const UserList = props => ( + + Users interested in this project + {props.users.map((user, index) => ( + - ); - }, - )} - - ); -}; + } + leftAvatar={} + // rightAvatar={if(props.paringWithCur){return }} + key={index} + primaryText={user.name} + secondaryText={`Rating: ${user.rating}`} + /> + ))} + +); export default UserList; diff --git a/client/components/UserProfile.jsx b/client/components/UserProfile.jsx index b749ee8..cc90fec 100644 --- a/client/components/UserProfile.jsx +++ b/client/components/UserProfile.jsx @@ -1,14 +1,22 @@ +/* eslint no-console:0 */ import React from 'react'; import { connect } from 'react-redux'; import Paper from 'material-ui/Paper'; import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; -import { Card, CardMedia, CardText, CardTitle } from 'material-ui/Card'; +import { Card, CardMedia, CardTitle } from 'material-ui/Card'; function UserProfile(props) { return ( - + @@ -16,22 +24,28 @@ function UserProfile(props) {
- +
- + - - project.project).join(' ')} /> + + project.project).join(' ')} + />
); } -const mapStateToProps = (state, props) => ( - { - projects: state.projects.filter(project => props.user.projects.indexOf(project.id) > -1) - } -); +const mapStateToProps = (state, props) => ({ + projects: state.projects.filter( + project => props.user.projects.indexOf(project.id) > -1 + ) +}); export default connect(mapStateToProps)(UserProfile); diff --git a/client/store/actions.js b/client/store/actions.js index af8c37b..ff9ac9e 100644 --- a/client/store/actions.js +++ b/client/store/actions.js @@ -1,3 +1,3 @@ /* This file, you will notice, is empty. -*/ \ No newline at end of file +*/ diff --git a/client/store/index.js b/client/store/index.js index 278725f..836c372 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -1,9 +1,30 @@ -import { createStore } from 'redux'; -import reducer from './reducers'; +/* +First we load all the reducer files. In our case there is only one. +We combine these reducers into one single "file", but this is unnecessary +for this project's size. +*/ + +import * as storage from 'redux-storage'; +import reducers from './reducers'; +import { createStore, applyMiddleware, combineReducers } from 'redux'; +import createEngine from 'redux-storage-engine-localstorage'; +const reducer = storage.reducer(combineReducers(reducers)); +const engine = createEngine('my-save-key'); + +const middleware = storage.createMiddleware(engine); +const createStoreWithMiddleware = applyMiddleware(middleware)(createStore); /* - grabs all the reducer from store/reducers.js and creates a store with it. A store manages state. - */ -const store = createStore(reducer); + grabs all the reducer from store/reducers.js and creates a store with it. A store manages state. +*/ +const store = createStoreWithMiddleware( + reducers /* preloadedState, */, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() +); + +const load = storage.createLoader(engine); +load(store) + // .then(newState => console.log('Loaded state:', newState)) + .catch(() => console.log('Failed to load previous state')); export default store; diff --git a/client/store/reducers.js b/client/store/reducers.js index 58913bd..1bb056c 100644 --- a/client/store/reducers.js +++ b/client/store/reducers.js @@ -1,31 +1,99 @@ /* - Reducers - decides how the change a state after receiving an action, and thus can be considered the entrance of a state change. A reducer is comprised of functions and it changes states by taking an action as argument, in which it then returns a new state. - - The actions get sent to App component and other parent component where they can be pass through as props. + Reducers - decides how the change a state after receiving an action, + and thus can be considered the entrance of a state change. A reducer is + comprised of functions and it changes states by taking an action as argument, + in which it then returns a new state. + The actions get sent to App component and other parent component where + they can be pass through as props. */ import { combineReducers } from 'redux'; - // does nothing - implemented to test connecting Redux to React -const changeString = (state = 'some message', action) => action.type === 'CHANGE_STRING' ? action.text : state; +const changeString = (state = 'some message', action) => + action.type === 'CHANGE_STRING' ? action.text : state; /* first condition is the initial state - inside ProjectDetails component, we dispatch 'addUsers' to display users at initial load - inside UserDetails component, we dispatch 'dispatchPairing' when user select a partner to pair with + inside ProjectDetails component, we dispatch 'addUsers' to display users + at initial load inside UserDetails component, we dispatch 'dispatchPairing' + when user select a partner to pair with */ + +// Returns all users in the database +const allUsers = (state, action) => { + if (state === undefined) { + return []; + } else if (action.type === 'LOAD_ALL_USERS') { + return action.allUsers; + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('AllUsers load:', action.payload.allUsers); + return action.payload.allUsers; + } + return state; +}; + const users = (state, action) => { + // console.log('Reducer action', action); if (state === undefined) { return []; } else if (action.type === 'USERS_ADD') { + // console.log('ADDING USERS', action); return action.users; } else if (action.type === 'CHANGE_USER_PAIRING') { - return state.map((user) => { + // console.log('this is state before CHANGE_USER_PAIRING ', state); + return state.map(user => { if (user.id === action.userId) { - return Object.assign({}, user, { paired: user.paired.concat(action.projectId) }); + const object = Object.assign({}, user, { + paired: user.paired.concat(action.projectId) + }); + return object; } + // console.log('this is the user from reducers ', user); return user; }); + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('Users load:', action.payload.users); + return action.payload.users; + } + return state; +}; + +const pairedUsers = (state, action) => { + if (state === undefined) { + return []; + } else if (action.type === 'LOAD_PAIRING') { + console.log('action.pairedUsers', action.pairedUsers); + return action.pairedUsers; + } else if (action.type === 'ADD_PAIRING') { + if (state.length !== 0) { + const idCollection = state[0].map(user => user.ghId); + if (idCollection.indexOf(action.pairs.ghId) === -1) { + state[0].push(action.pairs); + } + } else { + state.push(action.pairs); + } + return state; + } else if (action.type === 'DEL_PAIRING') { + const idCollection = state[0].map(user => user.id); + const index = idCollection.indexOf(action.ghId); + if (index !== -1) { + state[0].splice(index, 1); + } + console.log('state[0]', state[0]); + return state; + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('pairedUsers load:', action.payload.pairedUsers); + return action.payload.pairedUsers; + } + return state; +}; + +const pairingStatus = (state, action) => { + if (state === undefined) { + return null; + } else if (action.type === 'ADD_PAIRING_STATUS') { + return action.isPaired; } return state; }; @@ -33,8 +101,10 @@ const users = (state, action) => { /* first condition is the initial state inside App component we dispatch 'LIST_PROJECTS' to display list of projects - inside ProjectDetails component we dispatch 'CHANGE_PROJECT_INTEREST' when user selects 'they are interested' - inside UserDetails component we dispatch 'CHANGE_USER' when user select 'they want to pair' button + inside ProjectDetails component we dispatch 'CHANGE_PROJECT_INTEREST' + when user selects 'they are interested' + inside UserDetails component we dispatch 'CHANGE_USER' when user select + 'they want to pair' button */ const projects = (state, action) => { if (state === undefined) { @@ -42,28 +112,32 @@ const projects = (state, action) => { } else if (action.type === 'LIST_PROJECTS') { return action.projects; } else if (action.type === 'CHANGE_PROJECT_INTEREST') { - return state.map((project) => { + return state.map(project => { if (project.id === action.projectId) { return Object.assign({}, project, { interested: action.value }); } return project; }); } else if (action.type === 'CHANGE_USER_PAIRING') { - return state.map((project) => { + return state.map(project => { if (project.id === action.projectId) { - return Object.assign({}, project, { paired: project.paired.concat(action.userId) }); + return Object.assign({}, project, { + paired: project.paired.concat(action.userId) + }); } return project; }); + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('Project load:', action.payload.projects); + return action.payload.projects; } return state; }; - /* first condition is the initial state - inside UserDetails component we dispatch 'MESSAGE_SEND' when user sends a message to pairing user - inside UserDetails component we dispatch 'MESSAGE_LOAD' when user refreshes page to view new incoming messages - + inside UserDetails component we dispatch 'MESSAGE_SEND' when user sends a + message to pairing user inside UserDetails component we dispatch + 'MESSAGE_LOAD' when user refreshes page to view new incoming messages THINGS TO FIX: change messaging features to appear when sent SUGGESTION: implement socket.io */ @@ -72,17 +146,22 @@ const messages = (state, action) => { return {}; } else if (action.type === 'MESSAGE_SEND') { const newMessages = {}; - newMessages[action.userId] = state[action.userId] ? [action.message].concat(state[action.userId]) : [action.message]; + newMessages[action.userId] = state[action.userId] + ? [action.message].concat(state[action.userId]) + : [action.message]; return Object.assign({}, state, newMessages); } else if (action.type === 'MESSAGES_LOAD') { return action.messages; + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('Messages load', action.payload.messages); + return action.payload.messages; } return state; }; - /* first condition is the initial state - inside Project component, we dispatch 'PROGRESS_LOAD_ITEMS' to load the user's latest progress on project + inside Project component, we dispatch 'PROGRESS_LOAD_ITEMS' + to load the user's latest progress on project inside Project component, we dispatch 'PROGRESS_CHANGE_ITEM' */ const projectProgress = (state, action) => { @@ -95,9 +174,26 @@ const projectProgress = (state, action) => { const stateProject = state[action.projectId]; newProgress[action.projectId] = stateProject.slice(); const updatedProject = newProgress[action.projectId]; - updatedProject[action.itemIndex] = Object.assign({}, stateProject[action.itemIndex]); - updatedProject[action.itemIndex].complete = !updatedProject[action.itemIndex].complete; + updatedProject[action.itemIndex] = Object.assign( + {}, + stateProject[action.itemIndex] + ); + updatedProject[action.itemIndex].complete = !updatedProject[ + action.itemIndex + ].complete; return newProgress; + } else if (action.type === 'REDUX_STORAGE_LOAD') { + // console.log('Project progress load', action.payload.projectProgress); + return action.payload.projectProgress; + } + return state; +}; + +const loggedInUser = (state, action) => { + if (state === undefined) { + return {}; + } else if (action.type === 'UPDATED_LOGGEDIN_USER') { + return action.loggedInUser; } return state; }; @@ -111,10 +207,24 @@ const projectProgress = (state, action) => { code--they don't have to worry about every other part of the state. what we are doing here is using ES6 destructuring, so key and value are named the same. */ -export default combineReducers({ + +const appReducer = combineReducers({ message: changeString, users, projects, messages, + pairedUsers, projectProgress, + loggedInUser, + pairingStatus, + allUsers }); + +const rootReducer = (state, action) => { + if (action.type === 'USER_LOGOUT') { + state = undefined; + } + return appReducer(state, action); +}; + +export default rootReducer; diff --git a/dist/index.html b/dist/index.html index fd052a5..6b38892 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,7 +6,7 @@ - GitBud + GitPal @@ -14,6 +14,7 @@
+ diff --git a/package-lock.json b/package-lock.json index 381b9d4..1093f64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "gitbud", + "name": "GitPal", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -16,7 +16,7 @@ "acorn": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", - "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==" + "integrity": "sha1-U/4WERH5EquZnuiHqQoLxSgi/XU=" }, "acorn-dynamic-import": { "version": "2.0.2", @@ -50,6 +50,11 @@ } } }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz", @@ -61,6 +66,12 @@ "json-stable-stringify": "1.0.1" } }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -72,9 +83,9 @@ } }, "ansi-escapes": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz", - "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", "dev": true }, "ansi-html": { @@ -96,7 +107,7 @@ "anymatch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=", "requires": { "micromatch": "2.3.11", "normalize-path": "2.1.1" @@ -114,7 +125,7 @@ "aria-query": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.0.tgz", - "integrity": "sha512-/r2lHl09V3o74+2MLKEdewoj37YZqiQZnfen1O4iNlrOjUgeKuu1U2yF3iKh6HJxqF+OXkLMfQv65Z/cvxD6vA==", + "integrity": "sha1-SvEKHmFXPd6gzzuZtRxSwFtCTSQ=", "dev": true, "requires": { "ast-types-flow": "0.0.7" @@ -131,7 +142,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" }, "array-find-index": { "version": "1.0.2", @@ -175,6 +186,11 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -213,7 +229,7 @@ "async": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=", "requires": { "lodash": "4.17.4" } @@ -223,6 +239,11 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "axios": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", @@ -877,15 +898,30 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.4.tgz", "integrity": "sha512-kChlV+0SXkjE0vUn9OZ7pBMWRFd8uq3mZe8x1K6jhuNcAFAtEnjchFAqB+dYEXKyd+JpT6eppRR78QAr5gTsUw==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + "integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" }, "batch": { "version": "0.6.1", @@ -893,6 +929,14 @@ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", "dev": true }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", @@ -903,10 +947,15 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.9.0.tgz", "integrity": "sha1-ZlBsFs5vTWkopbPNajPKQelB43s=" }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=" }, "body-parser": { "version": "1.17.2", @@ -1109,6 +1158,11 @@ "callsites": "0.2.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", @@ -1169,6 +1223,12 @@ "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -1188,7 +1248,7 @@ "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", "requires": { "inherits": "2.0.3", "safe-buffer": "5.1.1" @@ -1197,7 +1257,7 @@ "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=", "dev": true }, "cli-cursor": { @@ -1210,9 +1270,9 @@ } }, "cli-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", - "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, "cliui": { @@ -1243,9 +1303,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "color-convert": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "dev": true, "requires": { "color-name": "1.1.3" @@ -1260,13 +1320,28 @@ "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=" }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "compressible": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", @@ -1428,7 +1503,7 @@ "crypto-browserify": { "version": "3.11.1", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", - "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "integrity": "sha1-lIlF78Z1ekANbl5a9HGU0QBkJ58=", "requires": { "browserify-cipher": "1.0.0", "browserify-sign": "4.0.4", @@ -1656,7 +1731,7 @@ "emoji-regex": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "integrity": "sha1-m66pKbFVVlwR6kHGYm6qZc75ksI=", "dev": true }, "emojis-list": { @@ -1677,6 +1752,70 @@ "iconv-lite": "0.4.18" } }, + "engine.io": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.0.tgz", + "integrity": "sha1-XKQ4486f28kVxKIcjdnhJmcG5X4=", + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.6.8", + "engine.io-parser": "2.1.1", + "uws": "0.14.5", + "ws": "2.3.1" + } + }, + "engine.io-client": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.4.tgz", + "integrity": "sha1-T88TcLRxY70s6b4nM5ckMDUNTqE=", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.6.9", + "engine.io-parser": "2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "3.3.2", + "xmlhttprequest-ssl": "1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ws": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", + "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz", + "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg=", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary2": "1.0.2" + } + }, "enhanced-resolve": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", @@ -1707,7 +1846,7 @@ "es-abstract": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.8.0.tgz", - "integrity": "sha512-Cf9/h5MrXtExM20gSS55YFrGKCyPrRBjIVBtVyy8vmlsDfe0NPKMWj65tPLgzyfPuapWxh5whpXCtW4+AW5mRg==", + "integrity": "sha1-OwA4XoVymTK+/6kWO76hI06TKRQ=", "dev": true, "requires": { "es-to-primitive": "1.1.1", @@ -1814,33 +1953,33 @@ } }, "eslint": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.4.1.tgz", - "integrity": "sha1-mc1+r8/8ov+Zpcj18qR01jZLS9M=", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.12.1.tgz", + "integrity": "sha512-28hOYej+NZ/R5H1yMvyKa1+bPlu+fnsIAQffK6hxXgvmXnImos2bA5XfCn5dYv2k2mrKj+/U/Z4L5ICWxC7TQw==", "dev": true, "requires": { - "ajv": "5.2.2", + "ajv": "5.5.1", "babel-code-frame": "6.22.0", - "chalk": "1.1.3", + "chalk": "2.3.0", "concat-stream": "1.6.0", "cross-spawn": "5.1.0", - "debug": "2.6.8", - "doctrine": "2.0.0", + "debug": "3.1.0", + "doctrine": "2.0.2", "eslint-scope": "3.7.1", - "espree": "3.5.0", + "espree": "3.5.2", "esquery": "1.0.0", "estraverse": "4.2.0", "esutils": "2.0.2", "file-entry-cache": "2.0.0", "functional-red-black-tree": "1.0.1", "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.3", + "globals": "11.0.1", + "ignore": "3.3.7", "imurmurhash": "0.1.4", - "inquirer": "3.2.1", + "inquirer": "3.3.0", "is-resolvable": "1.0.0", - "js-yaml": "3.9.1", - "json-stable-stringify": "1.0.1", + "js-yaml": "3.10.0", + "json-stable-stringify-without-jsonify": "1.0.1", "levn": "0.3.0", "lodash": "4.17.4", "minimatch": "3.0.4", @@ -1848,19 +1987,102 @@ "natural-compare": "1.4.0", "optionator": "0.8.2", "path-is-inside": "1.0.2", - "pluralize": "4.0.0", + "pluralize": "7.0.0", "progress": "2.0.0", "require-uncached": "1.0.3", "semver": "5.4.1", + "strip-ansi": "4.0.0", "strip-json-comments": "2.0.1", - "table": "4.0.1", + "table": "4.0.2", "text-table": "0.2.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", + "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz", + "integrity": "sha512-y0tm5Pq6ywp3qSTZ1vPgVdAnbDEoeoc5wlOHXoY1c4Wug/a7JvqHIl7BTvwodaHmejWkK/9dSb3sCYfyo/om8A==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "globals": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.0.1.tgz", + "integrity": "sha1-Eqh7sBDlFUOWrMU14eQ/x1Ow5eg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } } }, "eslint-config-airbnb": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-15.1.0.tgz", - "integrity": "sha512-m0q9fiMBzDAIbirlGnpJNWToIhdhJmXXnMG+IFflYzzod9231ZhtmGKegKg8E9T8F1YuVaDSU1FnCm5b9iXVhQ==", + "integrity": "sha1-/UMpZakG4wE5ABuoMPWPc67dro4=", "dev": true, "requires": { "eslint-config-airbnb-base": "11.3.1" @@ -1877,10 +2099,27 @@ } } }, + "eslint-config-prettier": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-2.1.1.tgz", + "integrity": "sha1-qzkj+3BO6+yraWCQa30NboAc3lg=", + "dev": true, + "requires": { + "get-stdin": "5.0.1" + }, + "dependencies": { + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", + "dev": true + } + } + }, "eslint-import-resolver-node": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", - "integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", + "integrity": "sha1-RCJXTN5mqaewmZOO5NUIoZng48w=", "dev": true, "requires": { "debug": "2.6.8", @@ -1890,7 +2129,7 @@ "eslint-module-utils": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", - "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", + "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", "dev": true, "requires": { "debug": "2.6.8", @@ -1930,7 +2169,7 @@ "eslint-plugin-import": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz", - "integrity": "sha512-HGYmpU9f/zJaQiKNQOVfHUh2oLWW3STBrCgH0sHTX1xtsxYlH1zjLh8FlQGEIdZSdTbUMaV36WaZ6ImXkenGxQ==", + "integrity": "sha1-Id4zOAue+1X1720uIQ7A4H5/pp8=", "dev": true, "requires": { "builtin-modules": "1.1.1", @@ -1966,7 +2205,7 @@ "eslint-plugin-jsx-a11y": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz", - "integrity": "sha512-5I9SpoP7gT4wBFOtXT8/tXNPYohHBVfyVfO17vkbC7r9kEIxYJF12D3pKqhk8+xnk12rfxKClS3WCFpVckFTPQ==", + "integrity": "sha1-XJa7UYbKFOlNsQlf9Zs+K9lAabE=", "dev": true, "requires": { "aria-query": "0.7.0", @@ -2017,19 +2256,27 @@ } }, "espree": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz", - "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz", + "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", "dev": true, "requires": { - "acorn": "5.1.1", + "acorn": "5.2.1", "acorn-jsx": "3.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", + "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "dev": true + } } }, "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", "dev": true }, "esquery": { @@ -2182,7 +2429,7 @@ "express-session": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.5.tgz", - "integrity": "sha512-BBVy6E/XqjB507wqe5T+7Ia2N/gtur/dT/fKmvGGKQqUrzI4dcBPGJgV4t2ciX7FoxZPhZcKTDTmb4+5nCyQOw==", + "integrity": "sha1-9JoYInJjsxb2+FRNpf7iWlQCWew=", "requires": { "cookie": "0.3.1", "cookie-signature": "1.0.6", @@ -2196,14 +2443,14 @@ } }, "external-editor": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.4.tgz", - "integrity": "sha1-HtkZnanL/i7y96MbL96LDRI2iXI=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", "dev": true, "requires": { + "chardet": "0.4.2", "iconv-lite": "0.4.18", - "jschardet": "1.5.1", - "tmp": "0.0.31" + "tmp": "0.0.33" } }, "extglob": { @@ -2219,6 +2466,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -2263,10 +2516,20 @@ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", "dev": true, "requires": { - "flat-cache": "1.2.2", + "flat-cache": "1.3.0", "object-assign": "4.1.1" } }, + "file-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.5.tgz", + "integrity": "sha512-RzGHDatcVNpGISTvCpfUfOGpYuSR7HSsSg87ki+wF6rw1Hm0RALPTiAdsxAq1UwLf0RRhbe22/eHK6nhXspiOQ==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -2287,7 +2550,7 @@ "finalhandler": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz", - "integrity": "sha512-16l/r8RgzlXKmFOhZpHBztvye+lAhC5SU7hXavnerC9UfZqZxxXl3BzL8MhffPT3kF61lj9Oav2LKEzh0ei7tg==", + "integrity": "sha1-GFdPLnxLmLiuOyMMIfIB8xvbP7c=", "requires": { "debug": "2.6.8", "encodeurl": "1.0.1", @@ -2317,9 +2580,9 @@ } }, "flat-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", - "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", "dev": true, "requires": { "circular-json": "0.3.3", @@ -2331,7 +2594,7 @@ "follow-redirects": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", - "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==", + "integrity": "sha1-NV6PTRaHa0P1d7DVziZouXIyFOo=", "requires": { "debug": "2.6.8" } @@ -2378,7 +2641,7 @@ "fsevents": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "integrity": "sha1-MoK3E/s62A7eDp/PRhG1qm/AM/Q=", "optional": true, "requires": { "nan": "2.6.2", @@ -3143,14 +3406,6 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", - "requires": { - "safe-buffer": "5.0.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -3161,6 +3416,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "requires": { + "safe-buffer": "5.0.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -3304,7 +3567,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -3350,7 +3613,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=" }, "globby": { "version": "5.0.0", @@ -3394,6 +3657,26 @@ "ansi-regex": "2.1.1" } }, + "has-binary2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", + "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", @@ -3410,7 +3693,7 @@ "hash.js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "integrity": "sha1-NA3tvmKQGHFRweodd3o0SJNd+EY=", "requires": { "inherits": "2.0.3", "minimalistic-assert": "1.0.0" @@ -3455,7 +3738,7 @@ "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=" }, "hpack.js": { "version": "2.1.6", @@ -3552,9 +3835,9 @@ "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" }, "ignore": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", - "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", "dev": true }, "imurmurhash": { @@ -3601,16 +3884,16 @@ } }, "inquirer": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.2.1.tgz", - "integrity": "sha512-QgW3eiPN8gpj/K5vVpHADJJgrrF0ho/dZGylikGX7iqAdRgC9FVKYKWFLx6hZDBFcOLEoSqINYrVPeFAeG/PdA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", "dev": true, "requires": { - "ansi-escapes": "2.0.0", - "chalk": "2.1.0", + "ansi-escapes": "3.0.0", + "chalk": "2.3.0", "cli-cursor": "2.1.0", - "cli-width": "2.1.0", - "external-editor": "2.0.4", + "cli-width": "2.2.0", + "external-editor": "2.1.0", "figures": "2.0.0", "lodash": "4.17.4", "mute-stream": "0.0.7", @@ -3631,21 +3914,21 @@ "ansi-styles": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=", "dev": true, "requires": { - "color-convert": "1.9.0" + "color-convert": "1.9.1" } }, "chalk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", - "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", "dev": true, "requires": { "ansi-styles": "3.2.0", "escape-string-regexp": "1.0.5", - "supports-color": "4.2.1" + "supports-color": "4.5.0" } }, "strip-ansi": { @@ -3658,9 +3941,9 @@ } }, "supports-color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", "dev": true, "requires": { "has-flag": "2.0.0" @@ -3911,21 +4194,15 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "js-yaml": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", - "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "dev": true, "requires": { "argparse": "1.0.9", "esprima": "4.0.0" } }, - "jschardet": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.5.1.tgz", - "integrity": "sha512-vE2hT1D0HLZCLLclfBSfkfTTedhVj0fubHpJBHKwwUWX0nSbhPAfk+SG9rTX95BYNmau8rGFfCeaT6T5OW1C2A==", - "dev": true - }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", @@ -3934,7 +4211,7 @@ "json-loader": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + "integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=" }, "json-schema-traverse": { "version": "0.3.1", @@ -3949,6 +4226,12 @@ "jsonify": "0.0.0" } }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json3": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", @@ -4058,6 +4341,16 @@ "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", "dev": true }, + "lodash.isfunction": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz", + "integrity": "sha1-TbcJ/IG8So/XEnpFilNGxc3OLGs=" + }, + "lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" + }, "lodash.merge": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", @@ -4100,7 +4393,7 @@ "lru-cache": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", "requires": { "pseudomap": "1.0.2", "yallist": "2.1.2" @@ -4343,7 +4636,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "1.1.8" } @@ -4416,7 +4709,7 @@ "node-fetch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.2.tgz", - "integrity": "sha512-xZZUq2yDhKMIn/UgG5q//IZSNLJIwW2QxS14CNH5spuiXkITM2pUitjdq58yLSaU7m4M0wBNaM2Gh/ggY4YJig==", + "integrity": "sha1-xU6arFfkModSM1JfPIkcQVn/79c=", "requires": { "encoding": "0.1.12", "is-stream": "1.1.0" @@ -4468,7 +4761,7 @@ "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", @@ -4507,6 +4800,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-keys": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", @@ -4558,6 +4856,11 @@ "mimic-fn": "1.1.0" } }, + "open": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" + }, "opn": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", @@ -4616,7 +4919,7 @@ "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", "requires": { "execa": "0.7.0", "lcid": "1.0.0", @@ -4698,6 +5001,22 @@ "error-ex": "1.3.1" } }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "1.0.2" + } + }, "parseurl": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", @@ -4792,7 +5111,7 @@ "pbkdf2": { "version": "3.0.13", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.13.tgz", - "integrity": "sha512-+dCHxDH+djNtjgWmvVC/my3SYBAKpKNqKSjLkp+GtWWYe4XPE+e/PSD2aCanlEZZnqPk2uekTKNC/ccbwd2X2Q==", + "integrity": "sha1-w30pVTHnhrHaPj6tyEBCasywriU=", "requires": { "create-hash": "1.1.3", "create-hmac": "1.1.6", @@ -4830,9 +5149,9 @@ } }, "pluralize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-4.0.0.tgz", - "integrity": "sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", "dev": true }, "portfinder": { @@ -4889,7 +5208,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "requires": { "asap": "2.0.6" } @@ -4968,7 +5287,7 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", "requires": { "is-number": "3.0.0", "kind-of": "4.0.0" @@ -5005,7 +5324,7 @@ "randombytes": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "integrity": "sha1-3ACaJGuNCaF3tLegrne8Vw9LG3k=", "requires": { "safe-buffer": "5.1.1" } @@ -5074,7 +5393,7 @@ "react-redux": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", - "integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==", + "integrity": "sha1-I+06T5hjWdaLUhLqqmgeYNZXSUY=", "requires": { "hoist-non-react-statics": "2.2.2", "invariant": "2.2.2", @@ -5094,7 +5413,7 @@ "react-router-dom": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.1.2.tgz", - "integrity": "sha512-CU6pFlpfvIj/xi36rZAbUiN0x39241q+d5bAfJJLtlEqlM62F3zgyv5aERH9zesmKqyDBBp2kd85rkq9Mo/iNQ==", + "integrity": "sha1-f4p8qGjTKsrdGcoJVDtA0m347Dc=", "requires": { "history": "4.6.3", "loose-envify": "1.3.1", @@ -5152,7 +5471,7 @@ "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -5184,7 +5503,7 @@ "recompose": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.24.0.tgz", - "integrity": "sha512-7+UVym5Mfks/ukIDfcAiasrY61YGki8uIs4CmLTGU7UV2lm2ObbhOl913WrlsZKu8x8uA/sLJUOI5hxVga0dIA==", + "integrity": "sha1-Ji6T+XRDnrF+d3mCTYjM6QSSpd0=", "requires": { "change-emitter": "0.1.6", "fbjs": "0.8.14", @@ -5202,10 +5521,15 @@ "strip-indent": "1.0.1" } }, + "reduce-reducers": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.1.2.tgz", + "integrity": "sha1-+htHGLxSkqcd3R5dg5yb6pdw8Us=" + }, "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "integrity": "sha1-BrcxIyFZAdJdBlvjQusCa8HIU3s=", "requires": { "lodash": "4.17.4", "lodash-es": "4.17.4", @@ -5213,6 +5537,40 @@ "symbol-observable": "1.0.4" } }, + "redux-actions": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-0.10.1.tgz", + "integrity": "sha1-u0Qu433ZZDqUkz5AceCJ9DVYcTU=", + "requires": { + "reduce-reducers": "0.1.2" + } + }, + "redux-storage": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux-storage/-/redux-storage-4.1.2.tgz", + "integrity": "sha1-4G9L3u4mKurZEy/J9+rcZ+n5vqI=", + "requires": { + "lodash.isfunction": "3.0.8", + "lodash.isobject": "3.0.2", + "loose-envify": "1.3.1", + "redux-actions": "0.10.1", + "redux-storage-merger-simple": "1.0.5" + } + }, + "redux-storage-engine-localstorage": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/redux-storage-engine-localstorage/-/redux-storage-engine-localstorage-1.1.4.tgz", + "integrity": "sha1-KEknjXiXDww/Xz1HJ8qjwweD7Uk=" + }, + "redux-storage-merger-simple": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/redux-storage-merger-simple/-/redux-storage-merger-simple-1.0.5.tgz", + "integrity": "sha1-KaKIaw53DZtwgRrKgAqo766J+3M=", + "requires": { + "lodash.isobject": "3.0.2", + "lodash.merge": "4.6.0" + } + }, "regenerate": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", @@ -5324,7 +5682,7 @@ "resolve": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "integrity": "sha1-p1vgHFPaJdk0qY69DkxKcxL5KoY=", "dev": true, "requires": { "path-parse": "1.0.5" @@ -5404,7 +5762,16 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "5.2.2" + } }, "select-hose": { "version": "2.0.0", @@ -5424,7 +5791,7 @@ "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" }, "send": { "version": "0.15.4", @@ -5536,10 +5903,69 @@ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" }, "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0" + } + }, + "socket.io": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.3.tgz", + "integrity": "sha1-Q1nwaiSTOua9CHeYr3jGgOrjReM=", + "requires": { + "debug": "2.6.8", + "engine.io": "3.1.0", + "object-assign": "4.1.1", + "socket.io-adapter": "1.1.1", + "socket.io-client": "2.0.4", + "socket.io-parser": "3.1.2" + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, + "socket.io-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", + "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.6.8", + "engine.io-client": "3.1.4", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.1.2", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", + "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", + "requires": { + "component-emitter": "1.2.1", + "debug": "2.6.8", + "has-binary2": "1.0.2", + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } }, "sockjs": { "version": "0.3.18", @@ -5579,7 +6005,7 @@ "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=" }, "source-map": { "version": "0.5.6", @@ -5664,7 +6090,7 @@ "stream-http": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=", "requires": { "builtin-status-codes": "3.0.0", "inherits": "2.0.3", @@ -5673,18 +6099,10 @@ "xtend": "4.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "requires": { "is-fullwidth-code-point": "2.0.0", "strip-ansi": "4.0.0" @@ -5705,6 +6123,14 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -5749,34 +6175,59 @@ "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" }, "table": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.1.tgz", - "integrity": "sha1-qBFsEz+sLGH0pCCrbN9cTWHw5DU=", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", "dev": true, "requires": { - "ajv": "4.11.8", - "ajv-keywords": "1.5.1", - "chalk": "1.1.3", + "ajv": "5.5.1", + "ajv-keywords": "2.1.1", + "chalk": "2.3.0", "lodash": "4.17.4", - "slice-ansi": "0.0.4", + "slice-ansi": "1.0.0", "string-width": "2.1.1" }, "dependencies": { "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", + "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", "dev": true, "requires": { "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } } } }, @@ -5819,14 +6270,19 @@ } }, "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "requires": { "os-tmpdir": "1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -5920,7 +6376,7 @@ "uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "integrity": "sha1-Kz1cckDo/C5Y+Komnl7knAhXvTo=", "requires": { "random-bytes": "1.0.0" } @@ -5930,6 +6386,11 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" }, + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6005,6 +6466,12 @@ "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", "dev": true }, + "uws": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz", + "integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=", + "optional": true + }, "v8flags": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", @@ -6104,7 +6571,7 @@ "supports-color": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "integrity": "sha1-ZaS7JjHpDgJCDbpVVMN1pHVLuDY=", "requires": { "has-flag": "2.0.0" } @@ -6397,7 +6864,7 @@ "webpack-hot-middleware": { "version": "2.18.2", "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.18.2.tgz", - "integrity": "sha512-dB7uOnUWsojZIAC6Nwi5v3tuaQNd2i7p4vF5LsJRyoTOgr2fRYQdMKQxRZIZZaz0cTPBX8rvcWU1A6/n7JTITg==", + "integrity": "sha1-hN7mQ/A3w9WcneFCVIQwNxqo07I=", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -6409,7 +6876,7 @@ "webpack-sources": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", - "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", + "integrity": "sha1-xzVkNqTRMSO+LiQmoF0drZy+Zc8=", "requires": { "source-list-map": "2.0.0", "source-map": "0.5.6" @@ -6438,7 +6905,7 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", "requires": { "isexe": "2.0.0" } @@ -6502,6 +6969,27 @@ "mkdirp": "0.5.1" } }, + "ws": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", + "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=", + "requires": { + "safe-buffer": "5.0.1", + "ultron": "1.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + } + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz", + "integrity": "sha1-BPVgkVcks4kIhxXMDteBPpZ3v1c=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -6588,6 +7076,11 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" } } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/package.json b/package.json index e3bc35d..3c0138e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "gitbud", + "name": "GitPal", "version": "1.0.0", "description": "An application to help programmers begin working on open source projects.", "main": "server.js", @@ -7,26 +7,29 @@ "test": "echo \"Error: no test specified\" && exit 1", "webpack-dev-server": "webpack-dev-server --progress --hot --inline", "dev": "webpack -w", + "dev2": "nodemon --inspect server.js -i client/ -i dist/", "postInstall": "webpack -p", "start": "nodemon server.js -i client/ -i dist/", "seed": "node server/db/seed.js" }, "repository": { "type": "git", - "url": "git+https://github.com/cranebaes/gitbud.git" + "url": "git+https://github.com/Toucans456/GitPal.git" }, - "author": "haque-kim-ngo-warner-medley", + "author": "haque-kim-ngo-warner-medley and chen-gallegos-mitchell-schafer-zimmerman", "license": "ISC", "bugs": { - "url": "https://github.com/cranebaes/gitbud/issues" + "url": "https://github.com/Toucans456/GitPal/issues" }, - "homepage": "https://github.com/cranebaes/gitbud#readme", + "homepage": "https://github.com/Toucans456/GitPal#readme", "devDependencies": { - "eslint": "^4.4.1", + "eslint": "^4.12.1", "eslint-config-airbnb": "^15.1.0", + "eslint-config-prettier": "2.1.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-jsx-a11y": "^5.1.1", "eslint-plugin-react": "^7.2.0", + "file-loader": "^1.1.5", "webpack-dev-middleware": "^1.12.0", "webpack-dev-server": "^2.7.1", "webpack-hot-middleware": "^2.18.2" @@ -42,9 +45,11 @@ "dotenv": "^4.0.0", "express": "^4.15.4", "express-session": "^1.15.5", + "file-loader": "^1.1.6", "lodash": "^4.17.4", "material-ui": "^0.19.0", "neo4j-driver": "^1.4.0", + "open": "0.0.5", "passport": "^0.4.0", "passport-github2": "^0.1.10", "react": "^15.6.1", @@ -52,6 +57,10 @@ "react-redux": "^5.0.6", "react-router-dom": "^4.1.2", "redux": "^3.7.2", + "redux-storage": "^4.1.2", + "redux-storage-engine-localstorage": "^1.1.4", + "socket.io": "^2.0.3", + "socket.io-client": "^2.0.4", "webpack": "^3.5.3" } } diff --git a/server.js b/server.js index 87eac32..9c72da1 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,9 @@ /* * ENTRY POINT TO ALL THE SERVER SIDE CODE - * + * * Most of the server code is clearly modularised, so this * is mostly uncontroversial requires and uses. - * + * * The other server modules are: * request-handler * --Sends correct response to each URL (mostly by calling appropriate function from routes) @@ -19,31 +19,43 @@ // Allows storing of environment variables // in .env of root directory. require('dotenv').config(); +const open = require('open'); // Libraries for handling requests const express = require('express'); const path = require('path'); const bodyParser = require('body-parser'); // Libraries for authentication and sessions const session = require('express-session'); -// GitBud modules +// GitPal modules const passport = require('./server/authentication').passport; const requestHandler = require('./server/request-handler'); +const eventSockets = require('./server/sockets'); +const socketio = require('socket.io'); // Make express server const app = express(); const port = process.env.PORT || 8080; -app.listen(port, () => { - console.log(`Listening on port: ${port}`); + +const server = app.listen(port, err => { + if (err) { + console.log(err); + } else { + open(`http://localhost:${port}`); + } }); +const io = socketio(server); +eventSockets(io); // Save sessions // NOTE: This is using a bad memory store // https://www.npmjs.com/package/express-session#sessionoptions -app.use(session({ - secret: 'This is a secret', - resave: false, - saveUninitialized: true, -})); +app.use( + session({ + secret: 'This is a secret', + resave: false, + saveUninitialized: true + }) +); // Set server to use initialized passport from authentication module app.use(passport.initialize()); diff --git a/server/README.md b/server/README.md index e7bb841..9f29fa2 100644 --- a/server/README.md +++ b/server/README.md @@ -1,18 +1,18 @@ -# GitBud Server Code +# GitPal Server -> Most server code is clearly modularised and heavily (perhaps excessively) annotated for the benefit of teams wishing to inherit this codebase. On the understanding that comments can become inaccurate over time and that people may want to udnerstand the what module does what before jumping in, we're providing short notes here. -> We prefer each file to fulfill one role so modules with diverse responsibilities (_e.g._ the _routes_ module, which exports request handlers for _/API/_ and _/auth/_ routes) will generally have one file fo each responsibility and a short _index.js_ that draws together exports functionality from the module's distinct files. -> This modularisation was intended to be clear, not perfect. You may wish to reorgniase some code; for instance, the distinction between request-handler and routes is a little confusing and perhaps artificial. +> Most server code is clearly modularized and heavily (perhaps excessively) annotated for the benefit of teams wishing to inherit this codebase. On the understanding that comments can become inaccurate over time and that people may want to understand the what module does what before jumping in, we're providing short notes here. +> We prefer each file to fulfill one role so modules with diverse responsibilities (_e.g._ the _routes_ module, which exports request handlers for _/API/_ and _/auth/_ routes) will generally have one file for each responsibility and a short _index.js_ that draws together exports functionality from the module's distinct files. +> This modularization was intended to be clear, not perfect. You may wish to reorganize some code; for instance, the distinction between request-handler and routes is a little confusing and perhaps artificial. __Note:__ We've aimed to use the names of React components in this documentation, which will hopefully make it easier to cross-reference with the front-end code. Components are marked by JSX style tags (_e.g._ component). ## Table of Contents 1. [Modules](#modules) 1. [Handling A Connection](#handling-a-connection) - 1. [Order of resolution](#order-of-resolution) - 1. [Path of a request](#path-of-a-request) - 1. [New user](#new-user) - 1. [API request](#api-request) + 1. [Order of resolution](#order-of-resolution) + 1. [Path of a request](#path-of-a-request) + 1. [New user](#new-user) + 1. [API request](#api-request) ## Modules @@ -39,19 +39,19 @@ This depends heavily on the nature of the request, so we've provided two example #### New user When a new user clicks on _'Sign in with GitHub'_ on the component with which all users are greeted the following things happen: 1. The user is redirected to GitHub's OAuth page and (after successful authorisation) punted back to _/auth/github/callback_. -1. Their _req_ hits _server.js_ and is handed on (with a request and a response object) to the the handler function exported from the _request-handler_ module. +1. Their _req_ hits _server.js_ and is handed on (with a request and a response object) to the handler function exported from the _request-handler_ module. 1. This functions tests in the order above and finds that the user has requested an _/auth/_ route, checks whether or not the object exported by _/server/routes/auth.js/_ has a _github_ function and invokes it with the _req_ and _res_ objects. 1. The _github_ function verifies that this is an OAuth callback and invokes the appropriate middleware exported from the authentication module. 1. This middleware (_exports.callback_) mostly defers to other passport functionality, redirecting back to _/_ on failure and _/projects_ on successful authentication, so at this point the _req_ is handled and _res_ (a redirect) has been sent. 1. Passport provides a set of useful functions, which are invoked after (I think) the _req_ is authenticated and which we use to handle our knowledge of the user. - 1. Firstly, it provides a callback function (used to initialise the passport object), which is passed the authenticated user's OAuth token and profile from the OAuth provider. The function we provide inserts the user into the database (if not already present) and ensures that their information matches the latest from GitHub. If they are a new user. it then begins the process of scraping information from GitHub to profile their experience and compare to other users. - 1. Passport also provides _serializeUser_ which is passed the user's whole profile from the OAuth provider (here, a whole load of info from GitHub) and whose return value will be stored on the user's session in the internal ([deliberately flimsy](https://www.npmjs.com/package/express-session#sessionoptions)) store. We use this opportunity to extract the information we want and store it in the layout we want and ignore the rest. + 1. Firstly, it provides a callback function (used to initialise the passport object), which is passed the authenticated user's OAuth token and profile from the OAuth provider. The function we provide inserts the user into the database (if not already present) and ensures that their information matches the latest from GitHub. If they are a new user. it then begins the process of scraping information from GitHub to profile their experience and compare to other users. + 1. Passport also provides _serializeUser_ which is passed the user's whole profile from the OAuth provider (here, a whole load of info from GitHub) and whose return value will be stored on the user's session in the internal ([deliberately flimsy](https://www.npmjs.com/package/express-session#sessionoptions)) store. We use this opportunity to extract the information we want and store it in the layout we want and ignore the rest. 1. As this is a new user, their GitHub ID is passed into a chain of promises from the (poorly name) _profiling_ module, which: - 1. Scrapes all of their repos from GitHub's API and returns those repos which they created or have committed to; - 1. Checks the how many KBs of which languages have been written in those repos and returns that on the user's experience profile; - 1. Tots up the basic stats from their repos (stars, watchers, forks) and returns that on the user's experience profile; - 1. Stores it that information on the user's node in the db; - 1. Finally, the user's GitHub ID is passed to the _compareUser_ function, which grabs their experience profile and finds the distance between it and every other user's, so that _/API/user_ results can be returned with nearest neighbours first. + 1. Scrapes all of their repos from GitHub's API and returns those repos which they created or have committed to; + 1. Checks the how many KBs of which languages have been written in those repos and returns that on the user's experience profile; + 1. Tots up the basic stats from their repos (stars, watchers, forks) and returns that on the user's experience profile; + 1. Stores it that information on the user's node in the db; + 1. Finally, the user's GitHub ID is passed to the _compareUser_ function, which grabs their experience profile and finds the distance between it and every other user's, so that _/API/user_ results can be returned with nearest neighbours first. #### API request When an authenticated user makes a GET request to _/API/users/_, the following things happen: @@ -59,7 +59,7 @@ When an authenticated user makes a GET request to _/API/users/_, the following t 1. Handler runs through the [checklist of possible requests](#order-of-resolution), finds that this is an _/API_ request, checks that the API object exported from the _routes_ module has a request-handler for this path and invokes it with the _req_ object. 1. The request handler for _/API/users_ instantiates a database session, gets the user's GitHub ID from the user's request (info added by express-session from its memory store) and gets the project ID from the _req_ object's URL parameters. 1. It then queries the DB, combining the results from two queries: - 1. The first returns users with whom the requesting user is paired, along with the IDs of projects on which they're working and the distance between their experience and the requesting user's; - 1. The second returns the same information from users with whom the requesting user is not paired. + 1. The first returns users with whom the requesting user is paired, along with the IDs of projects on which they're working and the distance between their experience and the requesting user's; + 1. The second returns the same information from users with whom the requesting user is not paired. 1. The results are then map()ed, parsing each returned record with a DB model (a set of pseudoclassical constructors that imply a schema--post hoc--onto DB results) that returns a consistent User object. The resulting array is sorted by experience (the rating property of users at this point represent the distance between their experience-smaller numbers mean more similar experience) and resolve()d. 1. The request-handler which invoked this function has a .then which receives the resolve()d array, and sends it back to the user as JSON with an OK status code, using the _res_ object's methods. diff --git a/server/authentication/index.js b/server/authentication/index.js index 2e6f7bc..40be308 100644 --- a/server/authentication/index.js +++ b/server/authentication/index.js @@ -1,7 +1,7 @@ // Libraries for authentication const passport = require('passport'); const GitHubStrategy = require('passport-github2').Strategy; -// GitBud modules +// GitPal modules const db = require('../db'); const profiling = require('../profiling'); diff --git a/server/db/index.js b/server/db/index.js index 4eec324..3a7f391 100644 --- a/server/db/index.js +++ b/server/db/index.js @@ -3,10 +3,10 @@ * (Exports a connected DB object which is used in many places, but * primarily, you will see it in the routes module, which handles * all the endpoints that require db functionality) - * - * This module prorvides a connected db object for making queries + * + * This module provides a connected db object for making queries * and a set of models to parse the results of those queries. - * + * * We use the neo4j BOLT driver which is good (and fast), but has * its quirks. The workflow we have followed has meant creating a new * session for each query, which may well not be necessary--indeed, @@ -33,11 +33,14 @@ */ // import neo4 driver +require('dotenv').config(); const neo4j = require('neo4j-driver').v1; // set variables to connect const url = process.env.GRAPHENEDB_BOLT_URL || 'bolt://localhost'; -const username = process.env.GRAPHENEDB_BOLT_USERNAME || process.env.NEO4J_USERNAME || 'neo4j'; -const password = process.env.GRAPHENEDB_BOLT_PASSWORD || process.env.NEO4J_PASSWORD || 'neo'; +const username = + process.env.GRAPHENEDB_BOLT_USERNAME || process.env.NEO4J_USERNAME; +const password = + process.env.GRAPHENEDB_BOLT_PASSWORD || process.env.NEO4J_PASSWORD; // connect and create session exports.driver = neo4j.driver(url, neo4j.auth.basic(username, password)); diff --git a/server/db/models.js b/server/db/models.js index 5df96da..bcc9a6d 100644 --- a/server/db/models.js +++ b/server/db/models.js @@ -1,8 +1,8 @@ /* * DATABASE MODELS * (Used whenever db results are parsed--mostly - * in request-handler) - * + * in request-handler) + * * This module massages neo4j search results * into more convenient objects. The constructors are * essentially parsers. This allows us to abstract the logic @@ -24,24 +24,31 @@ exports.User = class User { this.username = user.properties.username; this.name = user.properties.name; this.avatarUrl = user.properties.avatarUrl; - this.ghId = 'ghId' in user.properties ? user.properties.ghId.toNumber() : null; - this.rating = rating ? rating.properties.difference.toNumber() : user.properties.rating.toNumber(); + this.ghId = + 'ghId' in user.properties ? user.properties.ghId.toNumber() : null; + this.rating = rating + ? rating.properties.difference.toNumber() + : user.properties.rating.toNumber(); this.paired = pairs ? pairs.map(pair => pair.toNumber()) : []; - this.projects = projects ? projects.map(project => project.toNumber()) : this.paired; + this.projects = projects + ? projects.map(project => project.toNumber()) + : this.paired; this.language = user.properties.language; this.experience = user.properties.experience; this.description = user.properties.description; } -} +}; // Includes sensitive information (e.g. OAuthToken) that should not be sent to users exports.ServerUser = class extends exports.User { constructor(user) { - super(user) + super(user); this.OAuthToken = user.properties.OAuthToken; - this.profile = user.properties.profile ? JSON.parse(user.properties.profile) : false; + this.profile = user.properties.profile + ? JSON.parse(user.properties.profile) + : false; } -} +}; exports.Project = class Project { constructor(project, pairs, interested) { @@ -54,4 +61,4 @@ exports.Project = class Project { this.paired = pairs ? pairs.map(pair => pair.toNumber()) : []; this.interested = interested; } -} +}; diff --git a/server/db/seed.js b/server/db/seed.js index d684dfe..71fdef5 100644 --- a/server/db/seed.js +++ b/server/db/seed.js @@ -9,15 +9,16 @@ const session = driver.session(); // Deletes all nodes and relationships in the graph const dropGraph = function dropGraph() { const dropGraphQueryString = 'MATCH (n) DETACH DELETE n'; - return session.run(dropGraphQueryString) - .then((result) => { - console.log('Graph dropped'); + return session + .run(dropGraphQueryString) + .then(result => { + console.log('Graph dropped'); }) - .catch((error) => { + .catch(error => { session.close(); throw error; }); -} +}; const addUsersQueryString = ` CREATE @@ -29,11 +30,12 @@ const addUsersQueryString = ` // Add user nodes function addUsers() { - return session.run(addUsersQueryString) - .then((result) => { + return session + .run(addUsersQueryString) + .then(result => { console.log('Users added'); }) - .catch((error) => { + .catch(error => { session.close(); throw error; }); @@ -49,17 +51,19 @@ const addProjectsQueryString = ` // Add project nodes const addProjects = function addProjects() { - return session.run(addProjectsQueryString) - .then((result) => { + return session + .run(addProjectsQueryString) + .then(result => { console.log('Projects added'); }) - .catch((error) => { + .catch(error => { session.close(); throw error; }); -} +}; //Create INTERESTED_IN relationships between users and projects +//Add INTERESTED_IN relationships if you addPairs const addInterestedInRelationshipsQueryString = ` MATCH (robb:User) WHERE robb.name = "Robb Stark" MATCH (arya:User) WHERE arya.name = "Arya Stark" @@ -67,6 +71,7 @@ const addInterestedInRelationshipsQueryString = ` MATCH (bran:User) WHERE bran.name = "Bran Stark" MATCH (helloGitBud:Project) WHERE helloGitBud.project = "Hello GitBud" MATCH (randomQuoteMachine:Project) WHERE randomQuoteMachine.project = "Random Quote Machine" + MATCH (ticTacToe:Project) WHERE ticTacToe.project = "Tic Tac Toe" CREATE (robb)-[:INTERESTED_IN]->(helloGitBud), (arya)-[:INTERESTED_IN]->(helloGitBud), @@ -75,15 +80,16 @@ const addInterestedInRelationshipsQueryString = ` `; const addInterestedInRelationships = function addInterestedInRelationships() { - return session.run(addInterestedInRelationshipsQueryString) - .then((result) => { + return session + .run(addInterestedInRelationshipsQueryString) + .then(result => { console.log('INTERESTED_IN relationships added'); }) - .catch((error) => { + .catch(error => { session.close(); throw error; }); -} +}; // Add pair const addPairQueryString = ` @@ -98,22 +104,23 @@ const addPairQueryString = ` `; const addPair = function addPair() { - return session.run(addPairQueryString) - .then((result) => { + return session + .run(addPairQueryString) + .then(result => { console.log('PAIRED_WITH relationships added'); }) - .catch((error) => { + .catch(error => { session.close(); throw error; }); -} +}; // Call functions that seed the db dropGraph() .then(addUsers) .then(addProjects) .then(addInterestedInRelationships) - .then(addPair) + // .then(addPair) .then(() => { session.close(); driver.close(); diff --git a/server/profiling/matching.js b/server/profiling/matching.js index d4413a5..2be66a8 100644 --- a/server/profiling/matching.js +++ b/server/profiling/matching.js @@ -24,7 +24,7 @@ const forEach = require('lodash/forEach'); const map = require('lodash/map'); const union = require('lodash/union'); -// GitBud modules +// GitPal modules const db = require('../db'); module.exports = function compareUser(dbSession, ghId) { diff --git a/server/request-handler/index.js b/server/request-handler/index.js index 20be048..b37590f 100644 --- a/server/request-handler/index.js +++ b/server/request-handler/index.js @@ -12,7 +12,7 @@ * * This order should mean that the quickest or simplest * requests are handled first. - * + * * Many of these functions make db queries. There's more information on these in the * db module, but importantly you will notice that with neo4j, we can extract the various * aliased fields of a response with the .get() method of a record. This is extremely useful @@ -22,17 +22,21 @@ // Node librares const fs = require('fs'); const path = require('path'); -// GitBud module with methods for various endpoints +// GitPal module with methods for various endpoints const routes = require('../routes'); // Cache index.html to serve quickly on React routes and invalid URLs -fs.readFile(path.join(__dirname, '../../dist/index.html'), 'utf8', (err, data) => { - if (err) { - throw err; - } else { - exports.index = data; +fs.readFile( + path.join(__dirname, '../../dist/index.html'), + 'utf8', + (err, data) => { + if (err) { + throw err; + } else { + exports.index = data; + } } -}); +); /* REACT ROUTES @@ -40,7 +44,6 @@ fs.readFile(path.join(__dirname, '../../dist/index.html'), 'utf8', (err, data) = // Serves index.html on react routes // and redirects /API request to appropriate endpoint exports.handler = function handler(req, res) { - // split URL to send to correct request handler const urlParts = req.path.split('/'); @@ -48,43 +51,50 @@ exports.handler = function handler(req, res) { if (routes.react.has(urlParts[1])) { res.send(exports.index); - /* + /* API ENDPOINTS */ - } else if (urlParts[1] === 'API' && routes.api[req.method].hasOwnProperty(urlParts[2])) { + } else if ( + urlParts[1] === 'API' && + routes.api[req.method].hasOwnProperty(urlParts[2]) + ) { // Only send meaningful response to authenticated users if (req.isAuthenticated()) { - routes.api[req.method][urlParts[2]](req) - .then((data) => { + routes.api[req.method] + [urlParts[2]](req) + .then(data => { res.statusCode = 200; res.json(data); }) - .catch((err) => { + .catch(err => { console.error(err); res.end('sorry not sorry'); }); } else { - // Unauthenticated users get 403 and empty data + // Unauthenticated users get 403 and empty data res.statusCode = 403; res.json([]); } - /* + /* AUTHENTICATION ENDPOINTS */ - // Handle authentication endpoints--these take req and res mostly for our convenience - // and to make implementation of passport easier. - // Consider refactoring similar to the above. - } else if (urlParts[1] === 'auth' && routes.auth[req.method].hasOwnProperty(urlParts[2])) { + // Handle authentication endpoints--these take req and res mostly for our convenience + // and to make implementation of passport easier. + // Consider refactoring similar to the above. + } else if ( + urlParts[1] === 'auth' && + routes.auth[req.method].hasOwnProperty(urlParts[2]) + ) { routes.auth[req.method][urlParts[2]](req, res, urlParts); - /* + /* 404 NOT FOUND */ - // If a request has still not been handled, it is using an incorrect URL. - // Send index.html -- React will render NotFound component - } else { + // If a request has still not been handled, it is using an incorrect URL. + // Send index.html -- React will render NotFound component + } else { res.statusCode = 404; res.end(exports.index); } -} +}; diff --git a/server/routes/api.js b/server/routes/api.js index fc25eeb..495a86c 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -2,9 +2,10 @@ * Request handlers for API routes. * These are stored on an object for easy lookup in the request-handler module and are indexed * first by method then by route. - * + * * THINGS TO FIX: - * There's a lot of repeated code in the db queries. You may find it helpful to create better helper functions + * There's a lot of repeated code in the db queries. You may find it helpful + * to create better helper functions * here, simply modularise the db functionality better or both. * Suggestions in the db module. */ @@ -12,8 +13,8 @@ // Required to create a db session and run a query. // For info on how to do this better, check the hints in the db module. const db = require('../db'); -const dbDriver = db.driver; +const dbDriver = db.driver; module.exports = { GET: { @@ -22,20 +23,27 @@ module.exports = { */ // Returns an object with numerical properties representing // project IDs each with a value of an array of checkpoints of the form - // { text: prompt(string), hint: hint on how to tackle prompt(string), complete: completion status of prompt(bool) } + // { text: prompt(string), hint: hint on how to tackle prompt(string), + // complete: completion status of prompt(bool) } progress: function getProgress(req) { return new Promise((resolve, reject) => { console.log('GET progress'); const dbSession = dbDriver.session(); - dbSession.run(` - MATCH (:User {ghId: ${req.user.ghInfo.id}})-->(group:Group)-->(project:Project) - RETURN group, project - `) - .then((res) => { + dbSession + .run( + ` + MATCH (:User {ghId: ${req.user.ghInfo.id}}) + -->(group:Group)-->(project:Project) + RETURN group, project + ` + ) + .then(res => { const projectProgress = {}; - res.records.forEach((record) => { + res.records.forEach(record => { const group = record.get('group').properties; - projectProgress[record.get('project').identity] = group.progress ? JSON.parse(group.progress) : []; + projectProgress[record.get('project').identity] = group.progress + ? JSON.parse(group.progress) + : []; }); resolve(projectProgress); }) @@ -50,13 +58,17 @@ module.exports = { return new Promise((resolve, reject) => { console.log('GET messages'); const dbSession = dbDriver.session(); - dbSession.run(` - MATCH (:User {ghId: ${req.user.ghInfo.id}})-[to_user]-(message:Message)--(other:User) - RETURN message, to_user, other ORDER BY message.created_at DESC - `) - .then((res) => { + dbSession + .run( + ` + MATCH (:User {ghId: ${req.user.ghInfo.id}}) + -[to_user]-(message:Message)--(other:User) + RETURN message, to_user, other ORDER BY message.created_at DESC + ` + ) + .then(res => { const messages = {}; - res.records.forEach((record) => { + res.records.forEach(record => { const text = record.get('message').properties.text; const userId = record.get('other').identity.toNumber(); const sender = record.get('to_user').type === 'SENT'; @@ -66,7 +78,7 @@ module.exports = { }); resolve(messages); }) - .catch((err) => { + .catch(err => { reject(err); dbSession.close(); }); @@ -84,30 +96,40 @@ module.exports = { console.log('GET users'); const ghId = req.user.ghInfo.id; const projectId = Number(req.query.projectId); - dbSession.run(` - MATCH (user:User {ghId: ${ghId}})<-[xp:EXPERIENCE_DIFFERENCE]->(pair:User) - WITH user, pair, xp - MATCH (pair)-->(group:Group)<--(user), - (group)-->(pairedProject:Project), - (pair)-[:INTERESTED_IN]->(project:Project) - WHERE ID(project) = ${projectId} - OPTIONAL MATCH (pair)--(:Group)--(pairsProjects:Project) - RETURN pair, COLLECT(ID(pairedProject)) as projects, COLLECT(ID(pairsProjects)) as pairsProjects, xp - UNION - MATCH (user:User {ghId: ${ghId}})<-[xp:EXPERIENCE_DIFFERENCE]->(pair:User) - WITH user, xp, pair - MATCH (pair)-[:INTERESTED_IN]->(project:Project) - WHERE ID(project) = ${projectId} AND NOT (pair)-->(:Group)<--(:User {ghId: ${ghId}}) - OPTIONAL MATCH (pair)--(:Group)--(pairsProjects:Project) - RETURN pair, false as projects, COLLECT(ID(pairsProjects)) as pairsProjects, xp - `) - .then((res) => { + dbSession + .run( + ` + MATCH (user:User {ghId: ${ghId}})<-[xp:EXPERIENCE_DIFFERENCE]->(pair:User) + WITH user, pair, xp + MATCH (pair)-->(group:Group)<--(user), + (group)-->(pairedProject:Project), + (pair)-[:INTERESTED_IN]->(project:Project) + WHERE ID(project) = ${projectId} + OPTIONAL MATCH (pair)--(:Group)--(pairsProjects:Project) + RETURN pair, COLLECT(ID(pairedProject)) as projects, COLLECT(ID(pairsProjects)) as pairsProjects, xp + UNION + MATCH (user:User {ghId: ${ghId}})<-[xp:EXPERIENCE_DIFFERENCE]->(pair:User) + WITH user, xp, pair + MATCH (pair)-[:INTERESTED_IN]->(project:Project) + WHERE ID(project) = ${projectId} AND NOT (pair)-->(:Group)<--(:User {ghId: ${ghId}}) + OPTIONAL MATCH (pair)--(:Group)--(pairsProjects:Project) + RETURN pair, false as projects, COLLECT(ID(pairsProjects)) as pairsProjects, xp + ` + ) + .then(res => { resolve( - res.records.map(user => - new db.models.User(user.get('pair'), user.get('projects'), user.get('xp'), user.get('pairsProjects')) - ) + res.records + .map( + user => + new db.models.User( + user.get('pair'), + user.get('projects'), + user.get('xp'), + user.get('pairsProjects') + ) + ) .sort((a, b) => a.rating - b.rating) - ) + ); }) .catch(reject) .then(() => dbSession.close()); @@ -121,7 +143,9 @@ module.exports = { const dbSession = dbDriver.session(); console.log('GET projects'); const ghId = req.user.ghInfo.id; - dbSession.run(` + dbSession + .run( + ` MATCH (user:User {ghId: ${ghId}})-->(group:Group)-->(project:Project) WITH user, group, project MATCH (pair:User)-->(group)-->(project) @@ -135,11 +159,79 @@ module.exports = { MATCH (user:User {ghId: ${ghId}}), (project:Project) WHERE NOT (user)-->(:Group)-->(project) AND NOT (user)-[:INTERESTED_IN]->(project) RETURN false as pairs, false as interested, project - `) - .then((res) => { - resolve(res.records.map(project => - new db.models.Project(project.get('project'), project.get('pairs'), project.get('interested')) - )); + ` + ) + .then(res => { + resolve( + res.records.map( + project => + new db.models.Project( + project.get('project'), + project.get('pairs'), + project.get('interested') + ) + ) + ); + }) + .catch(reject) + .then(() => dbSession.close()); + }); + }, + // Retrieve the project that two users share + // Returns a project + project: function getProject(req, res) { + return new Promise((resolve, reject) => { + const dbSession = dbDriver.session(); + console.log('GET project'); + const ghId = req.user.ghInfo.id; + const userId = req.query.id; + dbSession + .run( + ` + MATCH (:User {ghId:${ghId}})-[WORKING_ON]-(project:Project)--(:User {ghId:${userId}}) + RETURN project + ` + ) + .then(res => { + const project = res.records[0]; + resolve(new db.models.Project(project.get('project'))); + }) + .catch(reject) + .then(() => dbSession.close()); + }); + }, + // Retrieves all projects that a user is paired with other users + pairedProjects: function findPairProjects(req, res) { + return new Promise((resolve, reject) => { + const dbSession = dbDriver.session(); + console.log('GET paired projects'); + const userId = Number(req.query.userId); + const partnerId = Number(req.query.partnerId); + dbSession + .run( + ` + MATCH(user:User {ghId: ${userId}})-[:PAIRED_WITH]->(group)<- + [:PAIRED_WITH]-(partner:User {ghId: ${partnerId}}) + RETURN group + ` + ) + .then(res => { + resolve(res.records); + }) + .catch(reject) + .then(() => dbSession.close()); + }); + }, + // Retrieves all users + allUsers: function getAllUsers(req, res) { + return new Promise((resolve, reject) => { + const dbSession = dbDriver.session(); + dbSession + .run('MATCH (users:User) RETURN users') + .then(res => { + resolve( + res.records.map(user => new db.models.User(user.get('users'))) + ); }) .catch(reject) .then(() => dbSession.close()); @@ -153,21 +245,25 @@ module.exports = { const dbSession = dbDriver.session(); console.log('GET pairs'); const ghId = req.user.ghInfo.id; - dbSession.run(` - MATCH (pair:User)-->(group:Group)<--(user:User) - WHERE user.ghId = ${Number(ghId)} - RETURN pair - `) - .then((res) => { - resolve(res.records.map(project => - res.records.map(user => new db.models.User(user.get('pair'))) - )); + dbSession + .run( + ` + MATCH (pair:User)-->(group:Group)<--(user:User) + WHERE user.ghId = ${Number(ghId)} + RETURN pair + ` + ) + .then(res => { + resolve( + res.records.map(project => + res.records.map(user => new db.models.User(user.get('pair'))) + ) + ); }) .catch(reject) .then(() => dbSession.close()); }); } - }, /* @@ -179,15 +275,17 @@ module.exports = { return new Promise((resolve, reject) => { const dbSession = dbDriver.session(); console.log('POST projects'); - dbSession.run( - ` - MATCH (user:User) WHERE user.ghId=${Number(req.user.ghInfo.id)} - MATCH (project:Project) WHERE ID(project) = ${Number(req.body.projectId)} - MERGE (user)-[:INTERESTED_IN]->(project) - return user, project - ` - ) - .then((res) => { + dbSession + .run( + ` + MATCH (user:User) WHERE user.ghId=${Number(req.user.ghInfo.id)} + MATCH (project:Project) + WHERE ID(project) = ${Number(req.body.projectId)} + MERGE (user)-[:INTERESTED_IN]->(project) + return user, project + ` + ) + .then(res => { resolve(res); }) .catch(reject) @@ -195,46 +293,113 @@ module.exports = { }); }, + deleteInterest: function deleteInterest(req) { + return new Promise((resolve, reject) => { + const dbSession = dbDriver.session(); + console.log('298 userGhid', req.user.ghInfo.id); + console.log('299 req.body.project', req.body.projectId); + dbSession + .run( + ` + MATCH (project:Project) + WHERE ID(project) = ${req.body.projectId} + MATCH (user:User) WHERE user.ghId = ${req.user.ghInfo.id} + MATCH (user)-[r:INTERESTED_IN]->(project:Project) + DELETE r + ` + ) + .then(() => { + resolve(); + dbSession.close(); + }) + .catch(err => { + reject(err); + dbSession.close(); + }); + }); + }, + // Sets requesting user as working on the project with project ID // with the user with the given user ID pair: function addPair(req) { return new Promise((resolve, reject) => { const dbSession = dbDriver.session(); console.log('POST pair'); - dbSession.run(` - MATCH (project:Project) WHERE ID(project) = ${Number(req.body.project)} - MATCH (user:User) WHERE user.ghId = ${Number(req.user.ghInfo.id)} - MATCH (pair:User) WHERE ID(pair) = ${Number(req.body.partnered)} - MERGE (user)-[:PAIRED_WITH]->(group:Group)<-[:PAIRED_WITH]-(pair) - MERGE (group)-[:WORKING_ON]->(project) - SET group.progress = project.structure - return user, pair, group, project - `) - .then(resolve) + dbSession + .run( + ` + MATCH (project:Project) + WHERE ID(project) = ${Number(req.body.project)} + MATCH (user:User) WHERE user.ghId = ${Number(req.user.ghInfo.id)} + MATCH (pair:User) WHERE ID(pair) = ${Number(req.body.partnered)} + MERGE (user)-[:PAIRED_WITH]->(group:Group)<-[:PAIRED_WITH]-(pair) + MERGE (group)-[:WORKING_ON]->(project) + SET group.progress = project.structure + return user, pair, group, project + ` + ) + .then(res => { + const pair = res.records[0]; + resolve(new db.models.User(pair.get('pair'))); + }) .catch(reject) .then(() => dbSession.close()); }); }, + unpair: function unPair(req) { + return new Promise((resolve, reject) => { + const dbSession = dbDriver.session(); + console.log('327 userGhid', req.user.ghInfo.id); + console.log('328 partnered', req.body.partnered); + console.log('329req.body.project', req.body.project); + dbSession + .run( + ` + MATCH (project:Project) + WHERE ID(project) = ${Number(req.body.project)} + MATCH (user:User) WHERE user.ghId = ${Number(req.user.ghInfo.id)} + MATCH (pair:User) WHERE ID(pair) = ${Number(req.body.partnered)} + MERGE (user)-[:PAIRED_WITH]->(group:Group)<-[:PAIRED_WITH]-(pair) + DETACH DELETE group + ` + ) + .then(() => { + resolve(); + dbSession.close(); + }) + .catch(err => { + reject(err); + dbSession.close(); + }); + }); + }, + // Adds a new message from the requesting user to the database messages: function sendMessage(req) { return new Promise((resolve, reject) => { const dbSession = dbDriver.session(); const message = req.body; console.log('POST messages'); - dbSession.run(` - MATCH (user:User {ghId: ${ req.user.ghInfo.id }}), (recipient:User) - WHERE ID(recipient) = ${ req.body.recipient } - CREATE (user)-[:SENT]->(:Message {text: '${ req.body.text.replace('\'', '\\\'') }', created_at: TIMESTAMP()})-[:RECEIVED]->(recipient) - `) + dbSession + .run( + ` + MATCH (user:User {ghId: ${req.user.ghInfo.id}}), (recipient:User) + WHERE ID(recipient) = ${req.body.recipient} + CREATE (user)-[:SENT]-> + (:Message + {text: '${req.body.text.replace("'", "\\'")}', + created_at: TIMESTAMP()})-[:RECEIVED]->(recipient) + ` + ) .then(() => { resolve(); dbSession.close(); }) - .catch((err) => { + .catch(err => { reject(err); dbSession.close(); - }) + }); }); }, @@ -246,12 +411,17 @@ module.exports = { progress: function updateProgress(req) { return new Promise((resolve, reject) => { const dbSession = dbDriver.session(); - console.log('POST progress') - dbSession.run(` - MATCH (:User {ghId: ${req.user.ghInfo.id}})-->(group:Group)-->(project:Project) - WHERE ID(project) = ${req.body.projectId} - SET group.progress = '${JSON.stringify(req.body.progress).replace('\'', '\\\'')}' - `) + console.log('POST progress'); + dbSession + .run( + ` + MATCH (:User {ghId: ${req.user.ghInfo.id}}) + -->(group:Group)-->(project:Project) + WHERE ID(project) = ${req.body.projectId} + SET group.progress = + '${JSON.stringify(req.body.progress).replace("'", "\\'")}' + ` + ) .then(resolve) .catch(reject) .then(() => dbSession.close()); @@ -263,16 +433,18 @@ module.exports = { return new Promise((resolve, reject) => { const dbSession = dbDriver.session(); console.log('POST users'); - dbSession.run(` - MATCH (user:User) WHERE user.ghId = ${Number(req.user.ghInfo.id)} - SET user.description = '${req.body.description}' - SET user.language = '${req.body.language}' - SET user.experience = '${req.body.experience}' - `) + dbSession + .run( + ` + MATCH (user:User) WHERE user.ghId = ${Number(req.user.ghInfo.id)} + SET user.description = '${req.body.description}' + SET user.language = '${req.body.language}' + SET user.experience = '${req.body.experience}' + ` + ) .then(() => resolve()) .catch(reject); }); } - }, - + } }; diff --git a/server/routes/auth.js b/server/routes/auth.js index 97722ee..f89be55 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -1,19 +1,22 @@ /* * REQUEST HANDLERS FOR AUTHENTICATION ROUTES - * + * * These handlers deal with the response directly by convenience, not by good design. */ // Required to create a db session and run a query. // For info on how to do this better, check the hints in the db module. const db = require('../db'); -const dbDriver = db.driver; const passport = require('../authentication'); +// const processSocketIo = require('../sockets'); +const dbDriver = db.driver; module.exports = { GET: { signout: function signout(req, res) { // destroy session and redirect to home + console.log('Logging out'); + req.session.destroy(); req.logout(); res.redirect('/'); }, @@ -21,14 +24,25 @@ module.exports = { // If user signed in, send account details if (req.isAuthenticated()) { const dbSession = dbDriver.session(); - dbSession.run(` - MATCH (user:User {ghId: ${ req.user.ghInfo.id }}) + dbSession + .run( + ` + MATCH (user:User {ghId: ${req.user.ghInfo.id}}) OPTIONAL MATCH (user)--(:Group)--(project:Project) RETURN user, COLLECT(ID(project)) as projects - `) - .then((result) => { - res.json(new db.models.User(result.records[0].get('user'), false, false, result.records[0].get('projects'))); + ` + ) + .then(result => { + res.json( + new db.models.User( + result.records[0].get('user'), + false, + false, + result.records[0].get('projects') + ) + ); dbSession.close(); + // processSocketIo(); }) .catch(() => { res.send(false); diff --git a/server/sockets/index.js b/server/sockets/index.js new file mode 100644 index 0000000..3cce047 --- /dev/null +++ b/server/sockets/index.js @@ -0,0 +1,38 @@ +module.exports = io => { + let userGroup = {}; + io.on('connection', socket => { + // // when the client emits 'new message', this listens and executes + // socket.on('new message', function (data) { + // // we tell the client to execute 'new message' + // socket.broadcast.emit('new message', { + // username: socket.username, + // message: data + // }); + // }); + console.log('connect to user:', socket.id); + socket.on('id myself', obj => { + console.log('id', obj); + }); + + socket.on('pairInfo', obj => { + socket.broadcast.emit('pairInfo'); + }); + + socket.on('chat message', msg => { + console.log(msg); + socket.broadcast.emit('chat message', msg); + }); + socket.on('pendInvitation', msg => { + console.log(msg); + socket.broadcast.emit('chat message', msg); + }); + + socket.on('updateInterestList', () => { + socket.broadcast.emit('updateInterestList'); + }); + + socket.on('disconnect', () => { + console.log('user disconnected'); + }); + }); +}; diff --git a/webpack.config.js b/webpack.config.js index 9bb6cae..cc1f90c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,13 +15,21 @@ module.exports = { extensions: ['.js', '.jsx'] }, module: { - loaders: [ + rules: [ { - test: /\.jsx?$/, - loader: 'babel-loader', - exclude: /node_modules/ - } - ] + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: [ + 'babel-loader', + ], + }, + { + test: /\.(png|jpg|gif|bmp)$/, + use: [ + 'file-loader', + ], + }, + ], }, devServer: { contentBase: path.join(__dirname, '/dist'),