diff --git a/README.md b/README.md index b6903e4..4cd288f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# resistance -Online multiplayer implementation of resistance game +# Resistance +Browser-based implementation of *The Resistance*, a great game for +playing in groups of five to ten people. One round lasts about 15 to 30 minutes. +More information about game-play can be found at +[this link](https://en.wikipedia.org/wiki/The_Resistance_(game)#Gameplay). + +## Disclaimer +*The Resistance* is designed by Don Eskridge (the Designer) and published by +Indie Boards & Cards (the Publisher). This software is an unofficial fan +project. The developers of this software are in no way affiliated with, and this +software is in no way endorsed by, the Designer or the Publisher. diff --git a/components/Create.js b/components/Create.js new file mode 100644 index 0000000..541dda2 --- /dev/null +++ b/components/Create.js @@ -0,0 +1,66 @@ +'use strict' + +import React from 'react' +import Overlay from './Overlay.js' +import Spinner from './Spinner.js' +import { Form, FormGroup, Button, Input, Label, Row, Col } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons' + +export default class Create extends React.Component { + constructor (props) { + super(props) + this.state = { + loading: false + } + this.submit = this.submit.bind(this) + } + async submit (event) { + this.setState({ + loading: true + }) + if (!(await this.props.onSubmit(event))) { + this.setState({ + loading: false + }) + } + } + render () { + const { children, onSubmit, onClickBack, ...rest } = this.props + return ( +
+
+ + + + + + + +
+ + + + +
+ +
+
+ + { this.state.loading && + +
Creating game...
+
+ +
+ } +
+ ) + } +} diff --git a/components/FontAwesomerIcon.js b/components/FontAwesomerIcon.js new file mode 100644 index 0000000..705d2ee --- /dev/null +++ b/components/FontAwesomerIcon.js @@ -0,0 +1,25 @@ +'use strict' + +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +export default class FontAwesomerIcon extends React.Component { + constructor (props) { + super(props) + this.state = { + icon: null + } + } + componentDidMount () { + this.setState({ + icon: + }) + } + render () { + return ( + + { this.state.icon } + + ) + } +} diff --git a/components/GameInProgress.js b/components/GameInProgress.js new file mode 100644 index 0000000..e93616b --- /dev/null +++ b/components/GameInProgress.js @@ -0,0 +1,97 @@ +'use strict' + +/* global confirm */ + +import React from 'react' +import TeamInfo from './TeamInfo.js' +import Mission from './Mission.js' +import Vote from './Vote.js' +import Scores from './Scores.js' +import MissionReference from './MissionReference.js' +import VoteResults from './VoteResults.js' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faTrashAlt } from '@fortawesome/free-solid-svg-icons' +import { Row, Col, Button } from 'reactstrap' + +export default class GameInProgress extends React.Component { + constructor (props) { + super(props) + this.endRound = this.endRound.bind(this) + } + endRound () { + if (this.props.gameStatus.winner || + confirm('Are you sure you want to end the round?')) { + this.props.socketEmmitter('endRound', null, 'Ending round') + } + } + render () { + const { + gameStatus, + canHideTeam, + myPlayer, + socketEmmitter, + draftProposal, + ...rest + } = this.props + const voting = gameStatus.voting + + return ( +
+ { process.env.NODE_ENV !== 'production' && +

Player name: {myPlayer.name}

+ } + +
+ +
+ + { voting && !gameStatus.winner && +
+ +
+
+ } + { canHideTeam && !gameStatus.winner && +
+ +
+ +
+ } +
+
+ + + + + +
+
+ ) + } +} diff --git a/components/Home.js b/components/Home.js new file mode 100644 index 0000000..550f297 --- /dev/null +++ b/components/Home.js @@ -0,0 +1,30 @@ +'use strict' + +import React from 'react' +import { Row, Col, Button } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faPlus, faSignInAlt } from '@fortawesome/free-solid-svg-icons' + +export default class Home extends React.Component { + render () { + const { children, onClickCreate, onClickJoin, ...rest } = this.props + return ( + + + + +
+ + + + +
+ +
+ ) + } +} diff --git a/components/Join.js b/components/Join.js new file mode 100644 index 0000000..a84c8e4 --- /dev/null +++ b/components/Join.js @@ -0,0 +1,71 @@ +'use strict' + +import React from 'react' +import Overlay from './Overlay.js' +import Spinner from './Spinner.js' +import { Form, FormGroup, Button, Input, Label, Row, Col } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons' + +export default class Create extends React.Component { + constructor (props) { + super(props) + this.state = { + loading: false + } + this.submit = this.submit.bind(this) + } + async submit (event) { + this.setState({ + loading: true + }) + if (!(await this.props.onSubmit(event))) { + this.setState({ + loading: false + }) + } + } + render () { + const { children, onSubmit, onClickBack, ...rest } = this.props + return ( +
+
+ + + + + + + + + + + +
+ + + + +
+ +
+
+ + { this.state.loading && + +
Joining game...
+
+ +
+ } +
+ ) + } +} diff --git a/components/Mission.js b/components/Mission.js new file mode 100644 index 0000000..33c86db --- /dev/null +++ b/components/Mission.js @@ -0,0 +1,95 @@ +'use strict' + +import React from 'react' +import { Card, CardBody, CardTitle } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faUserSecret } from '@fortawesome/free-solid-svg-icons' +import ProposeMissionForm from './ProposeMissionForm.js' +import MissionDraftProposal from './MissionDraftProposal.js' +import NextMissionLeaders from './NextMissionLeaders' + +export default class Mission extends React.Component { + render () { + const { + myPlayer, + gameStatus, + socketEmmitter, + draftProposal, + voting, + children, + ...rest + } = this.props + const { + players, + missions, + missionChooserIndex, + missionFailIndex, + missionNumber + } = gameStatus + const missionChooser = players[missionChooserIndex].name + const mustPass = (missionChooserIndex === missionFailIndex) + const missionSize = missions.order[missionNumber] + const starRound = (missions.includesStarRound && missionNumber === 3) + + return ( + + + Mission #{ missionNumber + 1 } + { mustPass && + + Spies earn point if + mission fails to pass! +
+
+ } + + { starRound && + + 2 must sabotage for mission to fail!
+
+ } + + { missionSize } players +
+ + Proposer: { + myPlayer.name === missionChooser + ? Me + : missionChooser + } +
+ { !voting && (myPlayer.name === missionChooser + ? ( +
+ +
+
+ ) + : (draftProposal.length > 0 && +
+ +
+
+ ) + )} + { + + } +
+
+ ) + } +} diff --git a/components/MissionDraftProposal.js b/components/MissionDraftProposal.js new file mode 100644 index 0000000..15ca19c --- /dev/null +++ b/components/MissionDraftProposal.js @@ -0,0 +1,22 @@ +'use strict' + +import React from 'react' + +export default class MissionDraftProposal extends React.Component { + render () { + const { myPlayer, draftProposal, children, ...rest } = this.props + return ( +
+
Draft Proposal
+ +
+ ) + } +} diff --git a/components/MissionReference.js b/components/MissionReference.js new file mode 100644 index 0000000..ddcb66c --- /dev/null +++ b/components/MissionReference.js @@ -0,0 +1,35 @@ +'use strict' + +import React from 'react' +import { Card, CardTitle, CardBody } from 'reactstrap' + +export default class MissionReference extends React.Component { + render () { + const { missions, numPlayers, numSpies, children, ...rest } = this.props + return ( + + + Game Reference +

+ Players: {numPlayers}, including {numSpies} spies. +

+

+ Mission Order: + { missions.order.map((numPlayers, missionNum) => ( + + { numPlayers } + { missionNum === 3 && missions.includesStarRound && '*' } + { missionNum !== missions.order.length - 1 && ', '} + + )) } +

+ { missions.includesStarRound && +

+ *In order for the spies to win this round, 2 spies must sabotage. +

+ } +
+
+ ) + } +} diff --git a/components/NextMissionLeaders.js b/components/NextMissionLeaders.js new file mode 100644 index 0000000..d0d526b --- /dev/null +++ b/components/NextMissionLeaders.js @@ -0,0 +1,48 @@ +'use strict' + +import React from 'react' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faUserSecret } from '@fortawesome/free-solid-svg-icons' + +export default class NextMissionLeaders extends React.Component { + render () { + const listSize = 3 + const { + players, + myPlayer, + missionChooserIndex, + missionFailIndex, + children, + ...rest + } = this.props + const nextLeaders = [] + let playerIndex = missionChooserIndex + for (let i = 0; i < listSize; i++) { + playerIndex = (playerIndex + 1) % players.length + nextLeaders.push({ + name: players[playerIndex].name, + mustPass: (playerIndex === missionFailIndex) + }) + } + return ( +
+
Next mission leaders
+
    + { nextLeaders.map(leader => ( +
  1. + {leader.name === myPlayer.name + ? Me : leader.name + ' '} + {leader.mustPass && + + } +
  2. + ))} +
+

+ indicates that mission must + pass, or else spies will earn a point. +

+
+ ) + } +} diff --git a/components/Overlay.js b/components/Overlay.js new file mode 100644 index 0000000..8c0d3eb --- /dev/null +++ b/components/Overlay.js @@ -0,0 +1,37 @@ +'use strict' + +import React from 'react' + +export default class Overlay extends React.Component { + render () { + const { children, ...rest } = this.props + return ( +
+
+ { children } +
+ +
+ ) + } +} diff --git a/components/PageHeader.js b/components/PageHeader.js new file mode 100644 index 0000000..a080b67 --- /dev/null +++ b/components/PageHeader.js @@ -0,0 +1,14 @@ +'use strict' + +import React from 'react' + +export default class PageHeader extends React.Component { + render () { + const { children, centering = true, ...rest } = this.props + return ( +
+ { children }

+
+ ) + } +} diff --git a/components/PageLayout.js b/components/PageLayout.js new file mode 100644 index 0000000..638cbd7 --- /dev/null +++ b/components/PageLayout.js @@ -0,0 +1,41 @@ +'use strict' + +import React from 'react' +import Head from 'next/head' +import { Container } from 'reactstrap' +import 'bootstrap/dist/css/bootstrap.min.css' + +export default class PageLayout extends React.Component { + render () { + const { title, children, ...rest } = this.props + return ( +
+ + {title} + + + + { children } + + +
+ ) + } +} diff --git a/components/PlayerLobby.js b/components/PlayerLobby.js new file mode 100644 index 0000000..4d4bde5 --- /dev/null +++ b/components/PlayerLobby.js @@ -0,0 +1,134 @@ +'use strict' + +/* global confirm */ + +import React from 'react' +import { Table, Button, Row, Col } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faTimes, + faPencilAlt, + faTrashAlt, + faArrowRight } from '@fortawesome/free-solid-svg-icons' + +export default class PlayerLobby extends React.Component { + constructor (props) { + super(props) + this.handleEditClick = this.handleEditClick.bind(this) + this.handleRemoveClick = this.handleRemoveClick.bind(this) + this.handleEndGameClick = this.handleEndGameClick.bind(this) + this.handleRoundStart = this.handleRoundStart.bind(this) + } + handleRoundStart () { + this.props.socketEmmitter('startRound', null, 'Starting game') + } + handleEndGameClick () { + const confirmed = confirm('Are you sure you want to end the game?') + if (confirmed) { + this.props.socketEmmitter('deleteGame', null, 'Ending game') + } + } + handleEditClick () { + let newName = '' + do { + newName = window.prompt('Enter a new name:') + } while (newName !== null && + (newName === '' || newName === this.props.myPlayer.name)) + + if (newName != null) { + this.props.socketEmmitter('changeName', { + newName: newName + }, 'Changing name') + } + } + handleRemoveClick (playerToRemove) { + this.props.socketEmmitter('removalRequest', playerToRemove, + 'Removing player') + } + render () { + const gameCodeLength = process.env.GAME_CODE_LENGTH + const { + children, + gameCode, + players, + myPlayer, + nameChanger, + socketEmmitter, + ...rest + } = this.props + let displayCode = gameCode + '' + while (displayCode.length < gameCodeLength) { + displayCode = '0' + displayCode + } + return ( +
+

Game code: { displayCode }

+ { players.length === 1 + ?

1 player joined

+ :

{players.length} players joined

+ } + + + + + + + + + { players.map(player => + + + + + ) } + +
NameActions
+ { + player.name === myPlayer.name + ? {player.name} (me) + : player.name + } + + { player.name === myPlayer.name && + + + + } + +
+ + + +
+ + + + +
+ +
+ + +
+ ) + } +} diff --git a/components/ProposeMissionForm.js b/components/ProposeMissionForm.js new file mode 100644 index 0000000..747f3d6 --- /dev/null +++ b/components/ProposeMissionForm.js @@ -0,0 +1,80 @@ +'use stict' + +import React from 'react' +import { + CustomInput, + Form, + FormGroup, + Label, + Row, + Col, + Button } from 'reactstrap' + +export default class ProposeMissionForm extends React.Component { + constructor (props) { + super(props) + this.submit = this.submit.bind(this) + this.handleInput = this.handleInput.bind(this) + this.state = { + checked: [] + } + } + submit (event) { + event.preventDefault() + this.props.socketEmmitter('finalProposal', this.state.checked) + } + handleInput (playerName) { + let checked = this.state.checked + let index = checked.indexOf(playerName) + if (index > -1) { + checked.splice(index, 1) + } else { + checked.push(playerName) + } + this.setState({ + checked: checked + }) + this.props.socketEmmitter('draftProposal', checked) + } + render () { + const { + players, + socketEmmitter, + myPlayer, + children, + missionSize, + ...rest + } = this.props + return ( +
+
Propose Mission
+ + +
+ Me} + id='myPlayerProposeCheckbox' + onChange={() => this.handleInput(myPlayer.name)} + checked={this.state.checked.includes(myPlayer.name)} + /> + { players.map(player => (player.name !== myPlayer.name && + this.handleInput(player.name)} + checked={this.state.checked.includes(player.name)} + /> + )) } +
+
+ + + + + + +
+ ) + } +} diff --git a/components/Scores.js b/components/Scores.js new file mode 100644 index 0000000..bf26e24 --- /dev/null +++ b/components/Scores.js @@ -0,0 +1,49 @@ +'use strict' + +import React from 'react' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faUserSecret, faFistRaised } from '@fortawesome/free-solid-svg-icons' + +export default class Scores extends React.Component { + render () { + const { gameStatus, children, ...rest } = this.props + const scores = gameStatus.scores + const spyIcon = + const resistanceIcon = + return ( +
+

+ {(() => { + switch (gameStatus.winner) { + case 'spies': + return ( + + Spies win! { spyIcon } + + ) + case 'resistance': + return ( + + Resistance wins! { resistanceIcon } + + ) + default: + return ( + + + { resistanceIcon } { scores.resistance } + + {' - '} + + { scores.spies } { spyIcon } + + + ) + } + })()} + +

+
+ ) + } +} diff --git a/components/Spinner.js b/components/Spinner.js new file mode 100644 index 0000000..52c65c1 --- /dev/null +++ b/components/Spinner.js @@ -0,0 +1,61 @@ +'use strict' + +import React from 'react' + +export default class Spinner extends React.Component { + render () { + const { + size = '100px', + children, + color = '#333333', + centered = true, + ...rest + } = this.props + + const containerClasses = `spinner-container ${centered && 'centered'}` + + return ( +
+
+
+ +
+ ) + } +} diff --git a/components/TeamInfo.js b/components/TeamInfo.js new file mode 100644 index 0000000..dca806b --- /dev/null +++ b/components/TeamInfo.js @@ -0,0 +1,79 @@ +'use strict' + +import React from 'react' +import { + Card, + CardBody, + CardTitle, + Button, + Row, + Col } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faFistRaised, faUserSecret } from '@fortawesome/free-solid-svg-icons' + +export default class TeamInfo extends React.Component { + constructor (props) { + super(props) + this.state = { + showing: true + } + this.toggle = this.toggle.bind(this) + } + toggle () { + this.setState({ + showing: !this.state.showing + }) + } + render () { + const { myPlayer, canHideTeam, gameStatus, ...rest } = this.props + return ( + + + { (this.state.showing || !canHideTeam) && +
+ + { myPlayer.isSpy + ? ( +

+ You are a + Spy + +

+ ) + : ( +

+ You are in the + Resistance + +

+ ) + } +
+ { myPlayer.isSpy && +
+ { gameStatus.spies.length > 2 + ? 'The other spies are:' : 'The other spy is:'} +
    + { gameStatus.spies.map(name => ( + name !== myPlayer.name &&
  • { name }
  • + )) } +
+
+ } +
+ } + + + + + + + +
+
+ ) + } +} diff --git a/components/Vote.js b/components/Vote.js new file mode 100644 index 0000000..2a0c544 --- /dev/null +++ b/components/Vote.js @@ -0,0 +1,135 @@ +'use strict' + +/* global confirm */ + +import React from 'react' +import { Card, CardTitle, CardBody, Button, Row, Col } from 'reactstrap' +import Spinner from './Spinner.js' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faFistRaised, + faUserSecret, + faThumbsUp, + faThumbsDown } from '@fortawesome/free-solid-svg-icons' + +export default class Vote extends React.Component { + constructor (props) { + super(props) + this.state = { + voted: false + } + } + + vote (votedYes) { + if (this.props.voting.isProposal || votedYes || this.props.myPlayer.isSpy) { + let vote + if (this.props.voting.isProposal) { + if (votedYes) { + vote = 'UPVOTE' + } else { + vote = 'DOWNVOTE' + } + } else { + if (votedYes) { + vote = 'COMPLETE MISSION' + } else { + vote = 'SABOTAGE' + } + } + if (confirm('Confirm vote: ' + vote)) { + this.props.socketEmmitter('submitVote', votedYes) + this.setState({ + voted: true + }) + } + } + } + + componentDidUpdate (prevProps) { + if (this.props.voting.voteId !== prevProps.voting.voteId) { + this.setState({ + voted: false + }) + } + } + + render () { + const { voting, socketEmmitter, children, myPlayer, ...rest } = this.props + + const buttonContent = { + proposal: { + yes: Upvote , + no: Downvote + }, + missionVote: { + yes: ( + + Complete Mission + + ), + no: Sabotage + } + } + + return ( + + + { (voting.isProposal || voting.missionList.includes(myPlayer.name)) && + !this.state.voted + ? ( +
+ Vote + { voting.isProposal ? 'Mission proposal:' : 'Mission:' } +
+
    + { voting.missionList.includes(myPlayer.name) && +
  • Me
  • + } + { voting.missionList.map(player => ( + player !== myPlayer.name && +
  • { player }
  • + ))} +
+
+ + + +
+ + + + +
+ +
+

+ Your vote is { voting.isProposal ? 'public' : 'private'}. +

+
+ ) + : ( +
+ +
+

+ Waiting for votes... +

+
+ ) + } +
+
+ ) + } +} diff --git a/components/VoteResults.js b/components/VoteResults.js new file mode 100644 index 0000000..29a8e1e --- /dev/null +++ b/components/VoteResults.js @@ -0,0 +1,163 @@ +'use strict' + +import React from 'react' +import { Alert, Row, Col } from 'reactstrap' +import FontAwesomerIcon from './FontAwesomerIcon.js' +import { faFistRaised, faUserSecret } from '@fortawesome/free-solid-svg-icons' + +export default class VoteResults extends React.Component { + constructor (props) { + super(props) + this.state = { + proposalVisible: true, + missionVisible: true + } + this.dismissProposal = this.dismissProposal.bind(this) + this.dismissMission = this.dismissMission.bind(this) + } + + dismissProposal () { + this.setState({ + proposalVisible: false + }) + } + + dismissMission () { + this.setState({ + missionVisible: false + }) + } + + componentDidUpdate (prevProps) { + const prevResults = prevProps.voteResults + const newResults = this.props.voteResults + const newResultsContainProposal = ( + newResults != null && newResults.proposal != null + ) + const proposalResultsChanged = ( + newResultsContainProposal && ( + prevResults == null || + prevResults.proposal == null || + prevResults.proposal.voteId !== newResults.proposal.voteId + ) + ) + const newResultsContainMission = ( + newResults != null && newResults.mission != null + ) + const missionResultsChanged = ( + newResultsContainMission && ( + prevResults == null || + prevResults.mission == null || + prevResults.mission.voteId !== newResults.mission.voteId + ) + ) + if (proposalResultsChanged) { + this.setState({ + proposalVisible: true + }) + } + if (missionResultsChanged) { + this.setState({ + missionVisible: true + }) + } + } + + render () { + const { + voteResults: { proposal, mission }, + myPlayer, + children, + ...rest + } = this.props + return ( +
+ { proposal && + +

+ Proposal { proposal.passed + ? `passes ${proposal.tally.yes}-${proposal.tally.no}.` + : `fails ${proposal.tally.no}-${proposal.tally.yes}.` + } +

+

+ Mission list: { proposal.missionList.map((name, index) => ( + + {name === myPlayer.name ? Me : name} + {index !== proposal.missionList.length - 1 && ', '} + + ))} +

+ { proposal.tally.yes !== 0 && proposal.tally.no !== 0 && + + +
Voted yes:
+
    + { proposal.votes.map(vote => ( + vote.vote === true && +
  • + { vote.name === myPlayer.name + ? Me + : vote.name + } +
  • + ))} +
+ + +
Voted no:
+
    + { proposal.votes.map(vote => ( + vote.vote === false && +
  • + { vote.name === myPlayer.name + ? Me + : vote.name + } +
  • + ))} +
+ +
+ } +
+ } + { mission && + +

+ Mission #{mission.missionNumber + 1} { mission.passed + ? ( + + accomplished! + + ) + : ( + + failed. + + ) + } +

+

+ Mission list: { proposal.missionList.map((name, index) => ( + + {name === myPlayer.name ? Me : name} + {index !== proposal.missionList.length - 1 && ', '} + + ))} +

+ { mission.tally.no > 0 && +

+ {mission.tally.no} { + mission.tally.no === 1 ? 'spy' : 'spies' + } sabotaged this mission. +

+ } +
+ } +
+ ) + } +} diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..ae85ccf --- /dev/null +++ b/constants.js @@ -0,0 +1,5 @@ +'use strict' + +exports.GAME_CODE_LENGTH = parseInt(process.env.GAME_CODE_LENGTH) +exports.MONGO_URL = 'mongodb://localhost:27017' +exports.GAME_TTL = 86400000 diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..740d0f4 --- /dev/null +++ b/next.config.js @@ -0,0 +1,2 @@ +const withCSS = require('@zeit/next-css') +module.exports = withCSS() diff --git a/package-lock.json b/package-lock.json index 81d1fd6..2d8ff2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -861,6 +861,36 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.8.tgz", + "integrity": "sha512-0sU7JDLdEeGQlWBSr5uEE6PZOM15YM1s9rFlpZV+WhNdX2V6Co3Sj0OW5el4F54X1Tw+nfxf4Cc3dUedudaDWg==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.8.tgz", + "integrity": "sha512-cvcMQZ5F8WSNSGMk9uWlkmZNfDzmdsWLRrTMrNwwihHcEmWRlIuSbDt+PQ/rXsnGmJnmLQJLFBT1cse/3swxbg==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.8" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.5.0.tgz", + "integrity": "sha512-VawIT2owNP97EwehZmxkvZDhoKwEevU/1HOMkln6kc4OtfE+JKYr6DpyzQnHVWXvz/eFj36QElHNe6zT8gR+Tg==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.8" + } + }, + "@fortawesome/react-fontawesome": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.3.tgz", + "integrity": "sha512-tc689l67rPZ7+ynZVUgOXY80rAt5KxvuH1qjPpJcbyJzJHzk5yhrD993BjsSEdPBLTtPqmvwynsO/XrAQqHbtg==", + "requires": { + "humps": "^2.0.1", + "prop-types": "^15.5.10" + } + }, "@webassemblyjs/ast": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz", @@ -1025,6 +1055,25 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz", "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==" }, + "@zeit/next-css": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@zeit/next-css/-/next-css-1.0.1.tgz", + "integrity": "sha512-yfHPRy/ne/5SddVClsoy+fpU7e0Cs1gkWA67/wm2uIu+9rznF45yQLxHEt5dPGF3h6IiIh7ZtIgA8VV8YKq87A==", + "requires": { + "css-loader": "1.0.0", + "extracted-loader": "1.0.4", + "find-up": "2.1.0", + "ignore-loader": "0.1.2", + "mini-css-extract-plugin": "0.4.3", + "postcss-loader": "3.0.0" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -1047,6 +1096,11 @@ "acorn": "^5.0.0" } }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", @@ -1068,6 +1122,15 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=" }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + } + }, "ansi-colors": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.1.tgz", @@ -1115,6 +1178,14 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -1173,6 +1244,11 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -1226,6 +1302,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==" + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -1248,6 +1329,45 @@ "webpack-sources": "^1.0.1" } }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -1313,6 +1433,11 @@ } } }, + "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", @@ -1368,11 +1493,29 @@ } } }, + "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.3.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, + "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.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", @@ -1383,6 +1526,11 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==" }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "bluebird": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", @@ -1446,6 +1594,26 @@ } } }, + "bootstrap": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz", + "integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w==" + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1562,6 +1730,11 @@ "node-releases": "^1.0.1" } }, + "bson": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", + "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" + }, "buffer": { "version": "4.9.1", "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -1634,11 +1807,28 @@ "unset-value": "^1.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, "caniuse-lite": { "version": "1.0.30000910", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000910.tgz", "integrity": "sha512-u/nxtHGAzCGZzIxt3dA/tpSPOcirBZFWKwz1EPz4aaupnBI2XR0Rbr74g0zc6Hzy41OEM4uMoZ38k56TpYAWjQ==" }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, "case-sensitive-paths-webpack-plugin": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz", @@ -1722,6 +1912,17 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -1762,11 +1963,21 @@ "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=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1783,6 +1994,20 @@ "typedarray": "^0.0.6" } }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "consola": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/consola/-/consola-1.4.5.tgz", @@ -1863,6 +2088,28 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -1872,6 +2119,15 @@ "elliptic": "^6.0.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, "create-hash": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -1925,6 +2181,76 @@ "randomfill": "^1.0.3" } }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "css-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz", + "integrity": "sha512-tMXlTYf3mIMt3b0dDCOQFJiVvxbocJ5Ho577WiGPYPZcqVEO218L2iU22pDXzkTZCLDE+9AmGSUkWxeh/nZReA==", + "requires": { + "babel-code-frame": "^6.26.0", + "css-selector-tokenizer": "^0.7.0", + "icss-utils": "^2.1.0", + "loader-utils": "^1.0.2", + "lodash.camelcase": "^4.3.0", + "postcss": "^6.0.23", + "postcss-modules-extract-imports": "^1.2.0", + "postcss-modules-local-by-default": "^1.2.0", + "postcss-modules-scope": "^1.1.0", + "postcss-modules-values": "^1.3.0", + "postcss-value-parser": "^3.3.0", + "source-list-map": "^2.0.0" + } + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -1948,6 +2274,12 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2035,11 +2367,44 @@ "randombytes": "^2.0.0" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.1.5.tgz", + "integrity": "sha512-xKnPpXG/pvK1B90JkwwxSGii90rQGKtzcMt2gI5G6+M0REXaq6rOHsGC2ay6/d0Uje7zzvSzjEzfR3ENhFlrfA==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + } + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", @@ -2098,6 +2463,79 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~3.3.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "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.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -2173,6 +2611,11 @@ "estraverse": "^4.1.1" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", @@ -2215,6 +2658,21 @@ "safe-buffer": "^5.1.1" } }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2430,6 +2888,11 @@ } } }, + "extracted-loader": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/extracted-loader/-/extracted-loader-1.0.4.tgz", + "integrity": "sha512-G8A0hT/WCWIjesZm7BwbWdST5dQ08GNnCpTrJT/k/FYzuiJwlV1gyWjnuoizOzAR4jpEYXG2J++JyEKN/EB26Q==" + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -2440,6 +2903,11 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -2674,8 +3142,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", @@ -2683,8 +3150,7 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -2787,8 +3253,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -2798,7 +3263,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2910,8 +3374,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -3026,7 +3489,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3095,6 +3557,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -3132,6 +3600,15 @@ } } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "globals": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", @@ -3156,6 +3633,25 @@ } } }, + "got": { + "version": "6.7.1", + "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -3177,6 +3673,26 @@ "ansi-regex": "^2.0.0" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "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": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3285,6 +3801,11 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=" + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -3293,6 +3814,19 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=" + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "requires": { + "postcss": "^6.0.1" + } + }, "ieee754": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", @@ -3303,17 +3837,50 @@ "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + "ignore-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", + "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=" }, - "inflight": { + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "requires": { + "import-from": "^2.1.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", @@ -3327,6 +3894,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3437,6 +4010,11 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -3460,6 +4038,22 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -3478,6 +4072,12 @@ } } }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -3507,6 +4107,12 @@ "isobject": "^3.0.1" } }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -3515,6 +4121,18 @@ "has": "^1.0.1" } }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, "is-symbol": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", @@ -3553,6 +4171,15 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -3588,6 +4215,15 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, "launch-editor": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", @@ -3644,11 +4280,31 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "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.tonumber": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz", + "integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk=" + }, "log-update": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", @@ -3667,6 +4323,12 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.4.tgz", @@ -3732,6 +4394,12 @@ "readable-stream": "^2.0.1" } }, + "memory-pager": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.1.0.tgz", + "integrity": "sha512-Mf9OHV/Y7h6YWDxTzX/b4ZZ4oh9NSXblQL8dtPCOomOtZciEHxePR78+uHFLLlsk01A6jVHhHsQZZ/WcIPpnzg==", + "optional": true + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -3794,6 +4462,16 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "mini-css-extract-plugin": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.3.tgz", + "integrity": "sha512-Mxs0nxzF1kxPv4TRi2NimewgXlJqh0rGE30vviCU2WHrpbta6wklnUV9dr9FUtoAHmB3p3LeXEC+ZjgHvB0Dzg==", + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + } + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -3882,6 +4560,26 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" }, + "mongodb": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", + "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", + "requires": { + "mongodb-core": "3.1.9", + "safe-buffer": "^5.1.2" + } + }, + "mongodb-core": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", + "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", + "requires": { + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -4044,6 +4742,33 @@ "semver": "^5.3.0" } }, + "nodemon": { + "version": "1.18.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.7.tgz", + "integrity": "sha512-xuC1V0F5EcEyKQ1VhHYD13owznQbUw29JKvZ8bVH7TmuvVNHvvbp9pLgE4PjTMRJVe0pJ8fGRvwR2nMiosIsPQ==", + "dev": true, + "requires": { + "chokidar": "^2.0.4", + "debug": "^3.1.0", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.2", + "semver": "^5.5.0", + "supports-color": "^5.2.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.3.0" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -4063,11 +4788,25 @@ "remove-trailing-separator": "^1.0.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, "object-assign": { "version": "4.1.1", "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-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -4166,6 +4905,12 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -4192,6 +4937,18 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -4227,6 +4984,22 @@ "error-ex": "^1.2.0" } }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "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.0" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", @@ -4262,6 +5035,12 @@ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -4325,11 +5104,116 @@ "find-up": "^2.1.0" } }, + "popper.js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.5.tgz", + "integrity": "sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-load-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", + "requires": { + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.6.tgz", + "integrity": "sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-modules-extract-imports": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", + "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==", + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "pretty-time": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", @@ -4401,6 +5285,12 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "pstree.remy": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.2.tgz", + "integrity": "sha512-vL6NLxNHzkNTjGJUpMm5PLC+94/0tTlC1vkP9bdU0pOHih+EujMjgMTwfZopZvHWRFbqJ5Y73OMoau50PewDDA==", + "dev": true + }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -4520,6 +5410,18 @@ } } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "react": { "version": "16.6.3", "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz", @@ -4547,6 +5449,46 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.0.tgz", "integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-popper": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.10.4.tgz", + "integrity": "sha1-rypBXqIike3VBGeNev2opu4ylao=", + "requires": { + "popper.js": "^1.14.1", + "prop-types": "^15.6.1" + } + }, + "react-transition-group": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.0.tgz", + "integrity": "sha512-qYB3JBF+9Y4sE4/Mg/9O6WFpdoYjeeYqx0AFb64PTazVy8RPMiE3A47CG9QmM4WJ/mzDiZYslV+Uly6O1Erlgw==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, + "reactstrap": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-6.5.0.tgz", + "integrity": "sha512-dWb3fB/wBAiQloteKlf+j9Nl2VLe6BMZgTEt6hpeTt0t9TwtkeU+2v2NBYONZaF4FZATfMiIKozhWpc2HmLW1g==", + "requires": { + "classnames": "^2.2.3", + "lodash.isfunction": "^3.0.9", + "lodash.isobject": "^3.0.2", + "lodash.tonumber": "^4.0.3", + "prop-types": "^15.5.8", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^0.10.4", + "react-transition-group": "^2.3.1" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -4685,6 +5627,25 @@ "unicode-match-property-value-ecmascript": "^1.0.2" } }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, "regjsgen": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", @@ -4720,6 +5681,27 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + }, + "dependencies": { + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + } + } + }, "resolve": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", @@ -4728,6 +5710,11 @@ "path-parse": "^1.0.5" } }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -4790,6 +5777,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saslprep": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", + "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "scheduler": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz", @@ -4814,6 +5810,15 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, "send": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", @@ -5107,6 +6112,105 @@ } } }, + "socket.io": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "requires": { + "debug": "~3.1.0", + "engine.io": "~3.2.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.1.1", + "socket.io-parser": "~3.2.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "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.2.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -5150,6 +6254,15 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "spdx-correct": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", @@ -5186,6 +6299,11 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "ssri": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", @@ -5316,6 +6434,18 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "styled-jsx": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz", @@ -5379,6 +6509,15 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz", "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==" }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + } + }, "terser": { "version": "3.10.12", "resolved": "https://registry.npmjs.org/terser/-/terser-3.10.12.tgz", @@ -5483,6 +6622,12 @@ "xtend": "~4.0.1" } }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", @@ -5491,6 +6636,11 @@ "setimmediate": "^1.0.4" } }, + "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", @@ -5539,6 +6689,15 @@ "repeat-string": "^1.6.1" } }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -5674,6 +6833,37 @@ } } }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "unfetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.0.0.tgz", @@ -5751,6 +6941,15 @@ "imurmurhash": "^0.1.4" } }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5792,11 +6991,35 @@ } } }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -5826,6 +7049,15 @@ } } }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -6038,6 +7270,15 @@ "isexe": "^2.0.0" } }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + } + }, "worker-farm": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", @@ -6075,6 +7316,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "write-file-webpack-plugin": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/write-file-webpack-plugin/-/write-file-webpack-plugin-4.3.2.tgz", @@ -6088,6 +7340,27 @@ "moment": "^2.22.1" } }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -6102,6 +7375,11 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + }, + "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 8d884ed..c834c86 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "Online multiplayer implementation of resistance game", "main": "index.js", "scripts": { - "dev": "node server.js", + "dev": "GAME_CODE_LENGTH=6 nodemon --inspect --ignore components/ --ignore pages/ --ignore .next/ server.js", "build": "next build", - "start": "NODE_ENV=production node server.js" + "start": "GAME_CODE_LENGTH=6 NODE_ENV=production node server.js" }, "repository": { "type": "git", @@ -19,9 +19,21 @@ }, "homepage": "https://github.com/szokeasaurusrex/resistance#readme", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.8", + "@fortawesome/free-solid-svg-icons": "^5.5.0", + "@fortawesome/react-fontawesome": "^0.1.3", + "@zeit/next-css": "^1.0.1", + "body-parser": "^1.18.3", + "bootstrap": "^4.1.3", "express": "^4.16.4", + "mongodb": "^3.1.10", "next": "^7.0.2", "react": "^16.6.3", - "react-dom": "^16.6.3" + "react-dom": "^16.6.3", + "reactstrap": "^6.5.0", + "socket.io": "^2.1.1" + }, + "devDependencies": { + "nodemon": "^1.18.7" } } diff --git a/pages/game.js b/pages/game.js new file mode 100644 index 0000000..58d75de --- /dev/null +++ b/pages/game.js @@ -0,0 +1,183 @@ +'use strict' + +/* global sessionStorage, alert */ + +import React from 'react' +import PageLayout from '../components/PageLayout.js' +import PageHeader from '../components/PageHeader.js' +import Overlay from '../components/Overlay.js' +import Spinner from '../components/Spinner.js' +import io from 'socket.io-client' +import Router from 'next/router' +import PlayerLobby from '../components/PlayerLobby.js' +import GameInProgress from '../components/GameInProgress.js' + +export default class Game extends React.Component { + constructor (props) { + super(props) + this.state = { + header:

Waiting for players

, + show: [], + gameInProgress: false, + gameCode: '', + gameStatus: {}, + loadMessage: '', + draftProposal: [], + player: {}, + canHideTeam: true + } + this.socketEmmitter = this.socketEmmitter.bind(this) + } + handleUnload (e) { + e.preventDefault() + e.returnValue = '' + } + socketEmmitter (event, message, loadMessage) { + this.socket.emit(event, message) + if (loadMessage) { + this.setState({ + loadMessage: loadMessage + '...' + }) + } + } + componentDidMount () { + const noTeamHidingTime = 3000 + if (sessionStorage.authKey) { + const player = JSON.parse(sessionStorage.authKey) + this.setState({ + player: player, + gameCode: player.gameCode + }) + + this.socket = io() + + this.socket.on('connect', () => { + this.socket.emit('authRequest', this.state.player) + }) + + this.socket.on('disconnect', () => { + this.setState({ + loadMessage: 'Offline. Attemting to reconnect...' + }) + }) + + this.socket.on('myError', error => { + console.error(error) + this.setState({ + loadMessage: '' + }) + alert(error.message) + if (error.type === 'authError') { + sessionStorage.removeItem('authKey') + Router.push('/') + } + }) + + this.socket.on('draftProposal', playerList => { + this.setState({ + draftProposal: playerList + }) + }) + + this.socket.on('gameStatus', status => { + if (status.playing) { + console.log(status.spies) + this.setState(prevState => ({ + gameInProgress: true, + gameStatus: status, + draftProposal: [], + player: { + ...prevState.player, + isSpy: (status.spies != null) + } + })) + } else { + this.setState({ + gameStatus: status, + gameInProgress: false + }) + } + }) + + this.socket.on('gameStarted', () => { + this.setState({ canHideTeam: false }) + setTimeout(() => { + this.setState({ canHideTeam: true }) + }, noTeamHidingTime) + }) + + this.socket.on('loading', loadMessage => { + this.setState({ + loadMessage: loadMessage + '...' + }) + }) + + this.socket.on('actionCompleted', () => this.setState({ + loadMessage: '' + })) + + this.socket.on('nameChanged', msg => { + this.setState(prevState => ({ + player: { + ...prevState.player, + name: msg.newName + } + })) + sessionStorage.authKey = JSON.stringify(this.state.player) + }) + + this.socket.on('kicked', () => { + sessionStorage.removeItem('authKey') + Router.push('/') + }) + + window.addEventListener('beforeunload', this.handleUnload) + } else { + Router.push('/') + } + } + componentWillUnmount () { + if (this.socket) { + this.socket.disconnect() + delete this.socket + } + window.removeEventListener('beforeunload', this.handleUnload) + } + render () { + return ( + + { !this.state.gameInProgress + ? ( +
+ {this.state.header} + +
+ ) + : ( + + ) + } + + { this.state.loadMessage !== '' && + +
{ this.state.loadMessage }
+
+ +
+ } + +
+ ) + } +} diff --git a/pages/index.js b/pages/index.js new file mode 100644 index 0000000..9b30fc4 --- /dev/null +++ b/pages/index.js @@ -0,0 +1,106 @@ +'use strict' + +/* global fetch, sessionStorage, alert */ + +import React from 'react' +import Router from 'next/router' +import PageLayout from '../components/PageLayout.js' +import PageHeader from '../components/PageHeader.js' +import Home from '../components/Home.js' +import Create from '../components/Create.js' +import Join from '../components/Join.js' + +export default class Index extends React.Component { + constructor (props) { + super(props) + this.state = { + header: 'Resistance', + show: ['home'] + } + this.handleCreate = this.handleCreate.bind(this) + this.handleJoin = this.handleJoin.bind(this) + this.backToHome = this.backToHome.bind(this) + } + showing (componentName) { + return this.state.show.includes(componentName) + } + handleCreate () { + this.setState({ + show: ['create'], + header: 'Create Game' + }) + } + handleJoin () { + this.setState({ + show: ['join'], + header: 'Join Game' + }) + } + async handleCreateJoinSubmit (event) { + try { + event.preventDefault() + const form = event.target + const data = { playerName: form.name.value } + if (form.code && (form.code.value >= 1000000 || form.code.value < 0)) { + throw new Error( + 'Game must be 6 digits or less, and cannot be negative.' + ) + } else if (form.code) { + data.gameCode = form.code.value + } + const response = await fetch('/join', { + method: 'post', + headers: { + 'Content-type': 'application/json; charset=UTF-8' + }, + body: JSON.stringify(data) + }) + const responseData = await response.json() + + if (responseData.error) { + throw responseData.error + } + sessionStorage.authKey = JSON.stringify(responseData) + Router.push('/game') + return true + } catch (e) { + alert('Error: ' + e.message) + if (e.name !== 'UserException') { + console.error(e) + } + return false + } + } + backToHome () { + this.setState({ + header: 'Resistance', + show: ['home'] + }) + } + render () { + return ( + + +

{ this.state.header }

+
+ { this.showing('home') && + } + + { this.showing('create') && + } + + { this.showing('join') && + } + + +
+ ) + } +} diff --git a/server.js b/server.js index cbf60b5..eea6247 100644 --- a/server.js +++ b/server.js @@ -1,25 +1,30 @@ 'use strict' -const express = require('express') -const next = require('next') +const server = require('express')() +const http = require('http').createServer(server) +const io = require('socket.io')(http) +const initDb = require('./server_modules/db.js').initDb +const handleExpressRequests = + require('./server_modules/handleExpressRequests') +const handleSocketConnections = + require('./server_modules/handleSocketConnections.js') -const dev = process.env.NODE_ENV !== 'production' -const app = next({ dev }) -const handle = app.getRequestHandler() - -async function runApp() { +async function runApp () { try { - await app.prepare() - const server = express() - server.get('*', (req, res) => { - return handle(req, res) - }) - server.listen(process.env.PORT || 3000, (err) => { + await initDb() + + await handleExpressRequests(server) + + handleSocketConnections(io) + + http.listen(process.env.PORT || 3000, err => { if (err) throw err console.log('> Ready on http://localhost:3000') }) - } catch (err) { - console.error(err.stack) + } catch (e) { + console.error(e.stack) process.exit(1) } } + +runApp() diff --git a/server_modules/UserException.js b/server_modules/UserException.js new file mode 100644 index 0000000..aee15bc --- /dev/null +++ b/server_modules/UserException.js @@ -0,0 +1,22 @@ +'use strict' + +class UserException extends Error { + constructor (message, type) { + super(message) + this.message = message + this.name = 'UserException' + this.type = type || '' + } + toString () { + return `Error: ${this.message}` + } + toJSON () { + return { + name: this.name, + message: this.message, + type: this.type + } + } +} + +module.exports = UserException diff --git a/server_modules/authUser.js b/server_modules/authUser.js new file mode 100644 index 0000000..49f75ad --- /dev/null +++ b/server_modules/authUser.js @@ -0,0 +1,45 @@ +'use strict' + +const crypto = require('crypto') +const UserException = require('./UserException.js') +const getGamesCollection = require('./db.js').getGamesCollection + +async function authUser (gameDb, socketClientId, authKey) { + const gamesCollection = getGamesCollection() + + let query = { code: parseInt(authKey.gameCode) } + if (!(await gamesCollection.findOne(query))) { + throw new UserException( + 'The game you are trying to enter does not exist', + 'authError' + ) + } + query = { + name: authKey.name + } + const player = await gameDb.collection('players').findOne(query) + if (!player) { + throw new UserException( + 'You have not yet joined the game properly', + 'authError' + ) + } + await gameDb.collection('players').updateOne(query, { + $set: { + hasConnected: true + } + }) + const hash = crypto.createHash('sha256') + hash.update(authKey.key) + if (hash.digest('hex') === player.hashedKey) { + return { + authenticated: true, + name: authKey.name, + hasConnected: player.hasConnected + } + } else { + throw new UserException('Unauthorized', 'authError') + } +} + +module.exports = authUser diff --git a/server_modules/changeName.js b/server_modules/changeName.js new file mode 100644 index 0000000..74d2be0 --- /dev/null +++ b/server_modules/changeName.js @@ -0,0 +1,28 @@ +'use strict' + +const UserException = require('./UserException.js') + +async function changeName (gameDb, currentName, newName) { + if (currentName === newName) { + throw new UserException('You entered the same name as your current name') + } else if (newName === '') { + throw new UserException('You cannot have a blank name') + } + const status = await gameDb.collection('status').findOne({}) + if (status.playing) { + throw new UserException('Cannot changle player name while game in progress') + } + const query = { name: currentName } + const mongoPlayer = await gameDb.collection('players').findOne(query) + if (!mongoPlayer) { + throw new UserException('Your current player does not exist') + } + await gameDb.collection('players').updateOne(query, { + $set: { + name: newName + } + }) + return newName +} + +module.exports = changeName diff --git a/server_modules/createGame.js b/server_modules/createGame.js new file mode 100644 index 0000000..bdf2790 --- /dev/null +++ b/server_modules/createGame.js @@ -0,0 +1,27 @@ +'use strict' + +const constants = require('../constants.js') + +async function createGame (db, gamesCollection) { + // Creates game and returns the code + const gameCodeLength = constants.GAME_CODE_LENGTH + if (await gamesCollection.countDocuments() > 100000) { + throw new Error('There are too many games in progress to start a new one.') + } + let gameCode + do { + gameCode = Math.floor(Math.random() * (10 ** gameCodeLength)) + } while (await gamesCollection.findOne({ code: gameCode })) + await Promise.all([ + db.db('game-' + gameCode).collection('status').insertOne({ + playing: false, + lastGameStart: new Date() + }), + gamesCollection.insertOne({ + code: gameCode + }) + ]) + return gameCode +} + +module.exports = createGame diff --git a/server_modules/db.js b/server_modules/db.js new file mode 100644 index 0000000..a4f194c --- /dev/null +++ b/server_modules/db.js @@ -0,0 +1,33 @@ +'use strict' + +const assert = require('assert') +const constants = require('../constants.js') +const MongoClient = require('mongodb').MongoClient + +let db, gamesCollection + +async function initDb () { + if (db) { + console.warn('Attempting to init DB again') + return db + } + db = await MongoClient.connect(constants.MONGO_URL, { + useNewUrlParser: true + }) + gamesCollection = db.db('games').collection('games') +} + +function getDb () { + assert.ok(db, 'Must init DB before trying to get DB') + return db +} + +function getGamesCollection () { + assert.ok(gamesCollection, + 'Must init DB before trying to get games collection') + return gamesCollection +} + +exports.initDb = initDb +exports.getDb = getDb +exports.getGamesCollection = getGamesCollection diff --git a/server_modules/emitStatusToTeams.js b/server_modules/emitStatusToTeams.js new file mode 100644 index 0000000..71f26af --- /dev/null +++ b/server_modules/emitStatusToTeams.js @@ -0,0 +1,15 @@ +'use strict' + +async function emitStatusToTeams (sockets, status) { + const resistanceSockets = Object.assign({}, sockets) + for (const spy of status.spies) { + sockets[spy].emit('gameStatus', status) + delete resistanceSockets[spy] + } + delete status.spies + for (const player in resistanceSockets) { + sockets[player].emit('gameStatus', status) + } +} + +module.exports = emitStatusToTeams diff --git a/server_modules/endRound.js b/server_modules/endRound.js new file mode 100644 index 0000000..e188521 --- /dev/null +++ b/server_modules/endRound.js @@ -0,0 +1,30 @@ +'use strict' + +const UserException = require('./UserException.js') + +async function endRound (gameDb) { + const gameStatus = await gameDb.collection('status').findOne({}) + if (!gameStatus.playing) { + throw new UserException('Cannot end round. No round is in progress.') + } + await Promise.all([ + gameDb.collection('status').updateOne({}, { + $set: { playing: false }, + $unset: { + voting: '', + missionChooserIndex: '', + missionFailIndex: '', + missions: '', + numPlayers: '', + scores: '', + missionNumber: '', + winner: '', + voteResults: '', + numSpies: '' + } + }), + gameDb.collection('teams').drop() + ]) +} + +module.exports = endRound diff --git a/server_modules/endVote.js b/server_modules/endVote.js new file mode 100644 index 0000000..9586b0f --- /dev/null +++ b/server_modules/endVote.js @@ -0,0 +1,91 @@ +'use strict' + +const UserException = require('./UserException.js') +const startVote = require('./startVote.js') +const startNextMission = require('./startNextMission.js') + +async function endVote (gameDb) { + const [ status, votes ] = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('votes').find({}).toArray() + ]) + if (!status.voting) { + throw new UserException('Cannot end vote. The vote is not in progress.') + } + const noVotes = votes.filter(vote => vote.vote === false).length + const yesVotes = votes.length - noVotes + const voteData = { + missionNumber: status.missionNumber, + tally: { + yes: yesVotes, + no: noVotes + }, + ...status.voting + } + let mongoCommands = [] + if (status.voting.isProposal) { + if (yesVotes > noVotes) { + const newVote = Object.assign({}, status.voting) + newVote.isProposal = false + mongoCommands.push(startVote(gameDb, newVote, true)) + } else if (status.missionChooserIndex === status.missionFailIndex) { + const newScores = { + resistance: status.scores.resistance, + spies: status.scores.spies + 1 + } + await gameDb.collection('status').updateOne({}, { + $set: { scores: newScores }, + $unset: { voting: '' } + }) + mongoCommands.push(startNextMission(gameDb, status, newScores)) + } else { + const newIndex = (status.missionChooserIndex + 1) % status.numPlayers + mongoCommands.push(gameDb.collection('status').updateOne({}, { + $set: { missionChooserIndex: newIndex }, + $unset: { voting: '' } + })) + } + mongoCommands.push( + gameDb.collection('status').updateOne({}, { + $set: { + 'voteResults.proposal': { + votes: votes, + passed: yesVotes > noVotes, + ...voteData + } + } + }) + ) + } else { + delete voteData.votes + const isStarRound = ( + status.missionNumber === 3 && status.missions.includesStarRound === true + ) + const failed = (!isStarRound && noVotes > 0) || noVotes > 1 + let newScores + if (failed) { + newScores = { + resistance: status.scores.resistance, + spies: status.scores.spies + 1 + } + } else { + newScores = { + resistance: status.scores.resistance + 1, + spies: status.scores.spies + } + } + mongoCommands.push( + gameDb.collection('status').updateOne({}, { + $set: { + scores: newScores, + 'voteResults.mission': { passed: !failed, ...voteData } + }, + $unset: { voting: '' } + }), + startNextMission(gameDb, status, newScores) + ) + } + await Promise.all(mongoCommands) +} + +module.exports = endVote diff --git a/server_modules/getGameStatus.js b/server_modules/getGameStatus.js new file mode 100644 index 0000000..8bd7032 --- /dev/null +++ b/server_modules/getGameStatus.js @@ -0,0 +1,49 @@ +'use strict' + +function cleanPlayers (players) { + // return only player properties safe to share with client + return players.map(player => ({ + name: player.name, + order: player.order, + gameCode: player.gameCode + })) +} + +async function getGameStatus (gameDb, player) { + try { + const [gameStatus, players, teams] = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('players').find({}).toArray(), + gameDb.collection('teams').findOne({}) + ]) + const cleanedPlayers = cleanPlayers(players) + if (gameStatus.playing) { + // TODO: add actions for mission proposal, voting, etc. + const { lastGameStart, ...cleanedStatus } = gameStatus + if (player != null && teams.resistance.includes(player)) { + return { + players: cleanedPlayers, + ...cleanedStatus + } + } else { + return { + spies: teams.spies, + players: cleanedPlayers, + ...cleanedStatus + } + } + } else { + return { + playing: false, + players: cleanedPlayers + } + } + } catch (e) { + console.error(e) + return { + error: new Error('An unexpected error occurred') + } + } +} + +module.exports = getGameStatus diff --git a/server_modules/handleExpressRequests.js b/server_modules/handleExpressRequests.js new file mode 100644 index 0000000..fd523c0 --- /dev/null +++ b/server_modules/handleExpressRequests.js @@ -0,0 +1,51 @@ +'use strict' + +const bodyParser = require('body-parser') +const next = require('next') +const getDb = require('./db.js').getDb +const joinGame = require('./joinGame.js') +const UserException = require('./UserException.js') + +const dev = process.env.NODE_ENV !== 'production' +const app = next({ dev }) +const handle = app.getRequestHandler() + +async function handleExpressRequests (server) { + const db = getDb() + + await app.prepare() + + server.use(bodyParser.urlencoded({ extended: false })) + server.use(bodyParser.json()) + + server.get('*', (req, res) => { + return handle(req, res) + }) + + // create or join game + server.post('/join', async (req, res) => { + try { + const { playerName } = req.body + let { gameCode } = req.body + + // Join game, send name and key to client + res.json(await joinGame(db, playerName, gameCode)) + } catch (e) { + if (e instanceof UserException || dev) { + res.json({ error: e }) + if (!(e instanceof UserException)) { + console.error(e) + } + } else { + res.status(500).json({ + error: new Error( + 'An unexpected error occurred while processing your request' + ) + }) + console.error(e) + } + } + }) +} + +module.exports = handleExpressRequests diff --git a/server_modules/handleSocketConnections.js b/server_modules/handleSocketConnections.js new file mode 100644 index 0000000..60f6239 --- /dev/null +++ b/server_modules/handleSocketConnections.js @@ -0,0 +1,206 @@ +'use strict' + +const UserException = require('./UserException.js') +const getDb = require('./db.js').getDb +const getGamesCollection = require('./db.js').getGamesCollection +const authUser = require('./authUser.js') +const getGameStatus = require('./getGameStatus.js') +const changeName = require('./changeName.js') +const removePlayer = require('./removePlayer.js') +const handleSocketError = require('./handleSocketError.js') +const startRound = require('./startRound.js') +const startVote = require('./startVote.js') +const submitVote = require('./submitVote.js') +const endRound = require('./endRound.js') +const emitStatusToTeams = require('./emitStatusToTeams.js') +const periodicallyDeleteGames = require('./periodicallyDeleteGames.js') + +function handleSocketConnections (io) { + const db = getDb() + const gamesCollection = getGamesCollection() + + const sockets = {} + const missionChoosers = {} + io.on('connection', socket => { + let player = { + authenticated: false + } + let gameDb, roomAll, gameCode, gameDashCode + + socket.on('authRequest', async authKey => { + try { + gameCode = authKey.gameCode + gameDashCode = 'game-' + gameCode + gameDb = db.db(gameDashCode) + player = await authUser(gameDb, socket.client.id, authKey) + socket.join(gameDashCode) + roomAll = gameDashCode + if (!sockets[gameCode]) { + sockets[gameCode] = {} + } + sockets[gameCode][player.name] = socket + if (player.hasConnected) { + socket.emit('gameStatus', await getGameStatus(gameDb, player.name)) + } else { + io.to(roomAll).emit('gameStatus', await getGameStatus(gameDb)) + } + socket.emit('actionCompleted') + } catch (e) { + handleSocketError(e, socket) + } + }) + + socket.on('changeName', async msg => { + if (player.authenticated) { + try { + const oldName = player.name + player.name = await changeName(gameDb, player.name, msg.newName) + sockets[gameCode][player.name] = socket + delete sockets[gameCode][oldName] + socket.emit('nameChanged', msg) + io.to(roomAll).emit('gameStatus', await getGameStatus(gameDb)) + socket.emit('actionCompleted') + } catch (e) { + handleSocketError(e, socket) + } + } + }) + + socket.on('startRound', async () => { + if (player.authenticated) { + try { + socket.broadcast.to(roomAll).emit('loading', 'Starting game') + await startRound(gameDb) + const status = await getGameStatus(gameDb) + emitStatusToTeams(sockets[gameCode], status) + missionChoosers[gameCode] = status.players[status.missionChooserIndex] + io.to(roomAll).emit('gameStarted') + } catch (e) { + handleSocketError(e, socket) + } finally { + io.to(roomAll).emit('actionCompleted') + } + } + }) + + socket.on('draftProposal', async playerList => { + if (player.authenticated) { + try { + let isMissionChooser + if (missionChoosers[gameCode] && + missionChoosers[gameCode].name === (player.name)) { + isMissionChooser = true + } else { + let status = await getGameStatus(gameDb) + const chooserName = status.players[status.missionChooserIndex].name + isMissionChooser = (chooserName === player.name) + } + if (isMissionChooser) { + socket.broadcast.to(roomAll).emit('draftProposal', playerList) + } + } catch (e) { + handleSocketError(e, socket) + } + } + }) + + socket.on('finalProposal', async playerList => { + if (player.authenticated) { + try { + let isMissionChooser + let status = await getGameStatus(gameDb) + const chooserName = status.players[status.missionChooserIndex].name + isMissionChooser = (chooserName === player.name) + if (!isMissionChooser) { + throw new UserException('You are not the mission chooser!') + } + await startVote(gameDb, { + isProposal: true, + missionList: playerList + }) + const newStatus = await getGameStatus(gameDb) + emitStatusToTeams(sockets[gameCode], newStatus) + } catch (e) { + handleSocketError(e, socket) + } + } + }) + + socket.on('submitVote', async vote => { + // vote is true or false, indicating yes or no vote + if (player.authenticated) { + try { + await submitVote(gameDb, vote, player) + emitStatusToTeams(sockets[gameCode], await getGameStatus(gameDb)) + } catch (e) { + handleSocketError(e, socket) + } + } + }) + + socket.on('endRound', async () => { + if (player.authenticated) { + try { + socket.broadcast.to(roomAll).emit('loading', 'Ending round') + await endRound(gameDb) + delete missionChoosers[gameCode] + io.to(roomAll).emit('gameStatus', await getGameStatus(gameDb)) + } catch (e) { + handleSocketError(e, socket) + } finally { + io.to(roomAll).emit('actionCompleted') + } + } + }) + + socket.on('removalRequest', async playerToRemove => { + if (player.authenticated) { + try { + const result = await removePlayer(gameDb, playerToRemove) + io.to(roomAll).emit('removedPlayer', result.playerToRemove) + const removedSocket = sockets[gameCode][playerToRemove.name] + if (removedSocket) { + removedSocket.emit('kicked') + removedSocket.disconnect() + delete sockets[gameCode][playerToRemove.name] + } + if (Object.keys(sockets[gameCode]).length === 0) { + delete sockets[gameCode] + } else { + io.to(roomAll).emit('gameStatus', await getGameStatus(gameDb)) + socket.emit('actionCompleted') + } + } catch (e) { + handleSocketError(e, socket) + } + } + }) + + socket.on('deleteGame', async () => { + if (player.authenticated) { + try { + await Promise.all([ + gameDb.dropDatabase(), + gamesCollection.removeOne({ code: gameCode }) + ]) + io.to(roomAll).emit('kicked') + for (const playerName in sockets[gameCode]) { + sockets[gameCode][playerName].disconnect() + } + delete sockets[gameCode] + } catch (e) { + handleSocketError(e, socket) + } + } + }) + }) + + periodicallyDeleteGames(deletedGames => { + for (const game of deletedGames) { + if (sockets[game]) delete sockets[game] + if (missionChoosers[game]) delete missionChoosers[game] + } + }) +} + +module.exports = handleSocketConnections diff --git a/server_modules/handleSocketError.js b/server_modules/handleSocketError.js new file mode 100644 index 0000000..072ca8e --- /dev/null +++ b/server_modules/handleSocketError.js @@ -0,0 +1,17 @@ +'use strict' + +const UserException = require('./UserException.js') + +function handleSocketError (e, socket) { + if (e instanceof UserException) { + socket.emit('myError', e) + if (e.type === 'authError') { + socket.disconnect() + } + } else { + console.error(e) + socket.emit('myError', new UserException('An unexpected error occurred')) + } +} + +module.exports = handleSocketError diff --git a/server_modules/joinGame.js b/server_modules/joinGame.js new file mode 100644 index 0000000..2bb07d3 --- /dev/null +++ b/server_modules/joinGame.js @@ -0,0 +1,82 @@ +'use strict' + +const crypto = require('crypto') +const UserException = require('./UserException.js') +const createGame = require('./createGame.js') +const constants = require('../constants.js') +const getGamesCollection = require('./db.js').getGamesCollection + +function randomBytesHexAsync (size) { + return new Promise((resolve, reject) => { + crypto.randomBytes(size, (err, buf) => { + if (err) { + reject(err) + } else { + resolve(buf.toString('hex')) + } + }) + }) +} + +async function joinGame (db, name, gameCode) { + const gamesCollection = getGamesCollection() + + // Validate name + if (name == null || name === '') { + throw new UserException('Must enter a name') + } else if (name.length > 20) { + throw new UserException('Max name length is 20 characters') + } + + if (gameCode == null) { + // Create game if doesn't exist + gameCode = await createGame(db, gamesCollection) + } else { + // Validate gameCode + gameCode = +(gameCode.replace(' ', '')) + if (('' + gameCode).length > constants.GAME_CODE_LENGTH || + !Number.isInteger(gameCode) || gameCode < 0) { + throw new UserException( + `Game code must be ${constants.GAME_CODE_LENGTH} digit number.` + ) + } + } + + const gameDb = db.db('game-' + gameCode) + + const [gameExists, gameStatus, playerInGame, playerList] = await Promise.all([ + gamesCollection.findOne({ code: gameCode }), + gameDb.collection('status').findOne({}), + gameDb.collection('players').findOne({ name: name }), + gameDb.collection('players').find({}).toArray() + ]) + if (!gameExists) { + throw new UserException(`The game ${gameCode} does not exist.`) + } else if (gameStatus.playing !== false) { + throw new UserException('Cannot join game. It is currently in progress.') + } else if (playerInGame) { + throw new UserException( + 'Your chosen name is in use by another player. Please use another name.') + } else if (playerList.length >= 10) { + throw new UserException('There are already 10 players in this game') + } + + const key = await randomBytesHexAsync(32) + const hash = crypto.createHash('sha256') + hash.update(key) + await gameDb.collection('players').insertOne({ + name: name, + gameCode: gameCode, + hasConnected: false, + order: playerList.length, + hashedKey: hash.digest('hex') + }) + + return { + gameCode: gameCode, + name: name, + key: key + } +} + +module.exports = joinGame diff --git a/server_modules/periodicallyDeleteGames.js b/server_modules/periodicallyDeleteGames.js new file mode 100644 index 0000000..d9b17a3 --- /dev/null +++ b/server_modules/periodicallyDeleteGames.js @@ -0,0 +1,49 @@ +'use strict' + +const constants = require('../constants.js') +const getDb = require('./db.js').getDb +const getGamesCollection = require('./db.js').getGamesCollection + +async function periodicallyDeleteGames (callback) { + const db = getDb() + const gamesCollection = getGamesCollection() + + try { + const games = await gamesCollection.find({}).toArray() + const gameStatuses = await Promise.all( + Array.from(games, game => new Promise(async (resolve, reject) => { + try { + const statusCollection = ( + await db.db('game-' + game.code).collection('status').findOne({}) + ) + resolve({ + code: game.code, + status: statusCollection + }) + } catch (e) { + reject(e) + } + })) + ) + const dropCommands = [] + const deletedGames = [] + const currentDate = new Date() + gameStatuses.forEach(game => { + const shouldDie = (!game.status || + currentDate - game.status.lastGameStart > constants.GAME_TTL) + if (shouldDie) { + deletedGames.push(game.code) + dropCommands.push(db.db('game-' + game.code).dropDatabase()) + dropCommands.push(gamesCollection.deleteOne({ code: game.code })) + console.log('Deleted game ' + game.code) + } + }) + await Promise.all(dropCommands) + callback(deletedGames) + } catch (e) { + console.error(e) + } + setTimeout(() => periodicallyDeleteGames(callback), constants.GAME_TTL) +} + +module.exports = periodicallyDeleteGames diff --git a/server_modules/removePlayer.js b/server_modules/removePlayer.js new file mode 100644 index 0000000..a227a2a --- /dev/null +++ b/server_modules/removePlayer.js @@ -0,0 +1,38 @@ +'use strict' + +const UserException = require('./UserException.js') +const getGamesCollection = require('./db.js').getGamesCollection + +async function removePlayer (gameDb, playerToRemove) { + const gamesCollection = getGamesCollection() + + const mongoCommands = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('players').findOne({ + name: playerToRemove.name + }) + ]) + const status = mongoCommands[0] + const playerToRemoveMongo = mongoCommands[1] + if (status.playing) { + throw new UserException('Cannot remove player while game in progress') + } else if (!playerToRemoveMongo) { + throw new UserException('The player you try to remove is not in game') + } + await gameDb.collection('players').deleteOne({ name: playerToRemove.name }) + const players = await gameDb.collection('players').find({}).toArray() + if (players.length === 0) { + await Promise.all([ + gameDb.dropDatabase(), + gamesCollection.deleteOne({ + code: playerToRemove.gameCode + }) + ]) + } + return { + playerToRemove: playerToRemove, + socketClientId: playerToRemoveMongo.socketClientId + } +} + +module.exports = removePlayer diff --git a/server_modules/startNextMission.js b/server_modules/startNextMission.js new file mode 100644 index 0000000..d19ef8c --- /dev/null +++ b/server_modules/startNextMission.js @@ -0,0 +1,26 @@ +'use strict' + +async function startNextMission (gameDb, status, newScores) { + if (newScores.spies === 3) { + await gameDb.collection('status').updateOne({}, { + $set: { winner: 'spies' } + }) + } else if (newScores.resistance === 3) { + await gameDb.collection('status').updateOne({}, { + $set: { winner: 'resistance' } + }) + } else { + let { missionChooserIndex, numPlayers, missionNumber } = status + missionChooserIndex = (missionChooserIndex + 1) % numPlayers + const missionFailIndex = (missionChooserIndex + 2) % numPlayers + await gameDb.collection('status').updateOne({}, { + $set: { + missionChooserIndex: missionChooserIndex, + missionFailIndex: missionFailIndex, + missionNumber: missionNumber + 1 + } + }) + } +} + +module.exports = startNextMission diff --git a/server_modules/startRound.js b/server_modules/startRound.js new file mode 100644 index 0000000..b5157d6 --- /dev/null +++ b/server_modules/startRound.js @@ -0,0 +1,71 @@ +'use strict' + +const UserException = require('./UserException.js') + +async function startRound (gameDb) { + const [gameStatus, playerList] = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('players').find({}).toArray() + ]) + if (gameStatus.playing) { + throw new UserException('Cannot start game. Game is in progress.') + } + + const numPlayers = playerList.length + const missions = { + order: [], + includesStarRound: false // Star rounds always occur in mission no. 3. + } + switch (numPlayers) { + case 5: + missions.order = [2, 3, 2, 3, 3] + break + case 6: + missions.order = [2, 3, 4, 3, 4] + break + case 7: + missions.order = [2, 3, 3, 4, 4] + missions.includesStarRound = true + break + case 8: + case 9: + case 10: + missions.order = [3, 4, 4, 5, 5] + missions.includesStarRound = true + break + default: + throw new UserException('Cannot start game. There must by 5-10 players.') + } + + const numSpies = Math.ceil(numPlayers / 3) + const playerNameList = playerList.map(player => player.name) + const teams = { + resistance: playerNameList, + spies: [] + } + for (let i = 0; i < numSpies; i++) { + let randIndex = Math.floor(Math.random() * teams.resistance.length) + teams.spies.push(...teams.resistance.splice(randIndex, 1)) + } + const missionChooserIndex = Math.floor(Math.random() * numPlayers) + const newStatus = { + playing: true, + lastGameStart: new Date(), + numPlayers: numPlayers, + numSpies: numSpies, + missionChooserIndex: missionChooserIndex, + missionFailIndex: (missionChooserIndex + 2) % numPlayers, + missionNumber: 0, + missions: missions, + scores: { + resistance: 0, + spies: 0 + } + } + await Promise.all([ + gameDb.collection('teams').insertOne(teams), + gameDb.collection('status').updateOne({}, { $set: newStatus }) + ]) +} + +module.exports = startRound diff --git a/server_modules/startVote.js b/server_modules/startVote.js new file mode 100644 index 0000000..0b383a7 --- /dev/null +++ b/server_modules/startVote.js @@ -0,0 +1,31 @@ +'use strict' + +const UserException = require('./UserException.js') + +async function startVote (gameDb, vote, skipVotingCheck) { + const [status, numPlayers] = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('players').countDocuments({}) + ]) + if (status.voting && skipVotingCheck !== true) { + throw new UserException('Voting is already in progress!') + } + if (vote.isProposal) { + vote.numVotesNeeded = numPlayers + } else { + vote.numVotesNeeded = vote.missionList.length + } + await Promise.all([ + gameDb.collection('status').updateOne({}, { + $set: { + voting: { + ...vote, + voteId: Math.random().toString(16).substring(2) + } + } + }), + gameDb.collection('votes').deleteMany({}) + ]) +} + +module.exports = startVote diff --git a/server_modules/submitVote.js b/server_modules/submitVote.js new file mode 100644 index 0000000..6b9d297 --- /dev/null +++ b/server_modules/submitVote.js @@ -0,0 +1,29 @@ +'use strict' + +const UserException = require('./UserException.js') +const endVote = require('./endVote.js') + +async function submitVote (gameDb, vote, player) { + const [ status, votes ] = await Promise.all([ + gameDb.collection('status').findOne({}), + gameDb.collection('votes').find({}).toArray() + ]) + if (!status.voting) { + throw new UserException('Voting is currently not in progress!') + } else if (votes.some(vote => vote.name === player.name)) { + throw new UserException('You have already voted!') + } else if (!status.voting.isProposal && + !status.voting.missionList.includes(player.name)) { + throw new UserException('You are not authorized to vote.') + } + await gameDb.collection('votes').insertOne({ + vote: vote, + name: player.name + }) + const numVotes = await gameDb.collection('votes').countDocuments({}) + if (numVotes === status.voting.numVotesNeeded) { + await endVote(gameDb) + } +} + +module.exports = submitVote