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 &&
+
+
+
+
+
+ }
+
+
+
+
+
+ End round
+
+
+
+
+
+ )
+ }
+}
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 (
+
+
+
+
+ Create game
+
+
+
+
+
+
+ Join game
+
+
+
+
+ )
+ }
+}
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
+
+ { draftProposal.includes(myPlayer.name) &&
+ Me
+ }
+ { draftProposal.map(playerName => (playerName !== myPlayer.name &&
+ { playerName }
+ ))}
+
+
+ )
+ }
+}
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 => (
+
+ {leader.name === myPlayer.name
+ ? Me : leader.name + ' '}
+ {leader.mustPass &&
+
+ }
+
+ ))}
+
+
+ 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 (
+
+ )
+ }
+}
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
+ }
+
+
+
+ Name
+ Actions
+
+
+
+ { players.map(player =>
+
+
+ {
+ player.name === myPlayer.name
+ ? {player.name} (me)
+ : player.name
+ }
+
+
+ { player.name === myPlayer.name &&
+
+
+
+
+
+ }
+ this.handleRemoveClick(player)
+ }>
+
+
+
+
+ ) }
+
+
+
+
+
+ End game
+
+
+
+
+
+
+ Start game
+
+
+
+
+
+
+
+ )
+ }
+}
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 (
+
+ )
+ }
+}
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 }
+ )) }
+
+
+ }
+
+ }
+
+
+
+
+
+ { this.state.showing ? 'Hide' : 'Show' } player info
+
+
+
+
+
+ )
+ }
+}
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 }
+ ))}
+
+
+
+
+ this.vote(true)}>
+ { voting.isProposal
+ ? buttonContent.proposal.yes
+ : buttonContent.missionVote.yes
+ }
+
+
+
+
+
+ this.vote(false)}>
+ { voting.isProposal
+ ? buttonContent.proposal.no
+ : buttonContent.missionVote.no
+ }
+
+
+
+
+
+ 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