From dcdf0113045a9d0cf34cd8301dfe8586aa0e8b3b Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Sat, 22 Jun 2019 00:30:26 +0100 Subject: [PATCH] Initial v2 --- .gitignore | 35 +- .travis.yml | 32 +- README.md | 70 +- app.json | 41 - app/assets/stylesheets/config.less | 63 - app/assets/stylesheets/error.less | 165 - app/assets/stylesheets/lobby.less | 533 - app/assets/stylesheets/massivedecks.less | 183 - app/assets/stylesheets/start.less | 143 - app/assets/stylesheets/variables.less | 13 - app/massivedecks/Config.scala | 43 - app/massivedecks/CoreModule.scala | 13 - app/massivedecks/Util.scala | 20 - app/massivedecks/cardcast/CardcastAPI.scala | 91 - app/massivedecks/controllers/API.scala | 141 - .../controllers/ErrorHandler.scala | 51 - app/massivedecks/controllers/Pages.scala | 46 - .../exceptions/BadRequestException.scala | 26 - .../exceptions/ForbiddenException.scala | 26 - .../exceptions/NotFoundException.scala | 14 - .../exceptions/RequestException.scala | 8 - .../exceptions/RequestFailedException.scala | 26 - app/massivedecks/lobby/Config.scala | 35 - app/massivedecks/lobby/Deck.scala | 100 - app/massivedecks/lobby/Game.scala | 312 - app/massivedecks/lobby/Lobby.scala | 416 - app/massivedecks/lobby/Players.scala | 280 - app/massivedecks/models/Errors.scala | 69 - app/massivedecks/models/Game.scala | 118 - app/massivedecks/models/Lobby.scala | 24 - app/massivedecks/models/Player.scala | 53 - .../models/cardcast/CardcastDeck.scala | 8 - app/massivedecks/notifications/Notifier.scala | 61 - .../notifications/Notifiers.scala | 160 - app/massivedecks/stores/CachedStore.scala | 99 - app/massivedecks/stores/GameCodeManager.scala | 14 - app/massivedecks/stores/InMemoryStore.scala | 36 - app/massivedecks/stores/LobbyStore.scala | 43 - app/views/error.scala.html | 47 - app/views/head.scala.html | 23 - app/views/index.scala.html | 20 - build.sbt | 23 - client/.dockerignore | 4 + client/Dockerfile | 31 + .../assets/fonts/gothic-a1-v4-latin-700.woff2 | Bin 0 -> 14868 bytes .../fonts/gothic-a1-v4-latin-regular.woff2 | Bin 0 -> 14848 bytes client/assets/fonts/nimbus-sans-l-700.woff2 | Bin 0 -> 23716 bytes .../assets/fonts/nimbus-sans-l-regular.woff2 | Bin 0 -> 24216 bytes .../assets/fonts/roboto-v18-latin-700.woff2 | Bin 0 -> 15436 bytes .../fonts/roboto-v18-latin-700italic.woff2 | Bin 0 -> 16572 bytes .../fonts/roboto-v18-latin-italic.woff2 | Bin 0 -> 16944 bytes .../fonts/roboto-v18-latin-regular.woff2 | Bin 0 -> 15344 bytes .../source-code-pro-v8-latin-regular.woff2 | Bin 0 -> 13436 bytes client/assets/images/background.svg | 1 + client/assets/images/cardcast.png | Bin 0 -> 3274 bytes .../assets/images/certificate-thumbs-up.svg | 3 + client/assets/images/deck.svg | 6 + client/assets/images/icon.png | Bin 0 -> 1280 bytes client/assets/images/icon.svg | 4 + client/assets/images/reread-icon.svg | 4 + client/elm.json | 36 + client/nginx.conf | 54 + client/package-lock.json | 11086 ++++++++++++++++ client/package.json | 59 + client/src/MassiveDecks.elm | 25 - client/src/MassiveDecks/API.elm | 299 - client/src/MassiveDecks/API/Request.elm | 202 - client/src/MassiveDecks/Components/About.elm | 83 - .../Components/BrowserNotifications.elm | 117 - client/src/MassiveDecks/Components/Errors.elm | 121 - client/src/MassiveDecks/Components/Icon.elm | 25 - client/src/MassiveDecks/Components/Input.elm | 153 - .../src/MassiveDecks/Components/Overlay.elm | 91 - client/src/MassiveDecks/Components/QR.elm | 17 - .../src/MassiveDecks/Components/Storage.elm | 40 - client/src/MassiveDecks/Components/TTS.elm | 41 - client/src/MassiveDecks/Components/Tabs.elm | 71 - client/src/MassiveDecks/Components/Title.elm | 9 - client/src/MassiveDecks/Models.elm | 28 - client/src/MassiveDecks/Models/Card.elm | 71 - client/src/MassiveDecks/Models/Event.elm | 94 - client/src/MassiveDecks/Models/Game.elm | 64 - client/src/MassiveDecks/Models/Game/Round.elm | 93 - .../src/MassiveDecks/Models/JSON/Decode.elm | 202 - .../src/MassiveDecks/Models/JSON/Encode.elm | 46 - .../src/MassiveDecks/Models/Notification.elm | 55 - client/src/MassiveDecks/Models/Player.elm | 106 - client/src/MassiveDecks/Scenes/Config.elm | 164 - .../MassiveDecks/Scenes/Config/Messages.elm | 49 - .../src/MassiveDecks/Scenes/Config/Models.elm | 15 - client/src/MassiveDecks/Scenes/Config/UI.elm | 293 - client/src/MassiveDecks/Scenes/History.elm | 40 - .../MassiveDecks/Scenes/History/Messages.elm | 18 - .../MassiveDecks/Scenes/History/Models.elm | 10 - client/src/MassiveDecks/Scenes/History/UI.elm | 96 - client/src/MassiveDecks/Scenes/Lobby.elm | 455 - .../MassiveDecks/Scenes/Lobby/Messages.elm | 43 - .../src/MassiveDecks/Scenes/Lobby/Models.elm | 29 - .../src/MassiveDecks/Scenes/Lobby/Sidebar.elm | 50 - client/src/MassiveDecks/Scenes/Lobby/UI.elm | 431 - client/src/MassiveDecks/Scenes/Playing.elm | 341 - .../MassiveDecks/Scenes/Playing/HouseRule.elm | 23 - .../Scenes/Playing/HouseRule/Available.elm | 9 - .../Scenes/Playing/HouseRule/Id.elm | 12 - .../Scenes/Playing/HouseRule/Reboot.elm | 34 - .../MassiveDecks/Scenes/Playing/Messages.elm | 37 - .../MassiveDecks/Scenes/Playing/Models.elm | 31 - client/src/MassiveDecks/Scenes/Playing/UI.elm | 614 - .../MassiveDecks/Scenes/Playing/UI/Cards.elm | 85 - client/src/MassiveDecks/Scenes/Start.elm | 354 - .../MassiveDecks/Scenes/Start/Messages.elm | 52 - .../src/MassiveDecks/Scenes/Start/Models.elm | 28 - client/src/MassiveDecks/Scenes/Start/UI.elm | 151 - client/src/MassiveDecks/Util.elm | 237 - client/src/elm/MassiveDecks.elm | 284 + client/src/elm/MassiveDecks/Animated.elm | 127 + client/src/elm/MassiveDecks/Card.elm | 233 + client/src/elm/MassiveDecks/Card/Model.elm | 99 + client/src/elm/MassiveDecks/Card/Parts.elm | 198 + client/src/elm/MassiveDecks/Card/Play.elm | 37 + client/src/elm/MassiveDecks/Card/Source.elm | 112 + .../elm/MassiveDecks/Card/Source/Cardcast.elm | 84 + .../Card/Source/Cardcast/Model.elm | 16 + .../src/elm/MassiveDecks/Card/Source/Fake.elm | 20 + .../elm/MassiveDecks/Card/Source/Methods.elm | 19 + .../elm/MassiveDecks/Card/Source/Model.elm | 46 + client/src/elm/MassiveDecks/Cast.elm | 114 + client/src/elm/MassiveDecks/Cast/Client.elm | 32 + client/src/elm/MassiveDecks/Cast/Model.elm | 24 + client/src/elm/MassiveDecks/Components.elm | 160 + .../src/elm/MassiveDecks/Components/Menu.elm | 102 + client/src/elm/MassiveDecks/Error.elm | 146 + .../src/elm/MassiveDecks/Error/Messages.elm | 8 + client/src/elm/MassiveDecks/Error/Model.elm | 24 + client/src/elm/MassiveDecks/Error/Overlay.elm | 63 + client/src/elm/MassiveDecks/Game.elm | 454 + client/src/elm/MassiveDecks/Game/Action.elm | 59 + .../elm/MassiveDecks/Game/Action/Model.elm | 8 + client/src/elm/MassiveDecks/Game/Messages.elm | 17 + client/src/elm/MassiveDecks/Game/Model.elm | 79 + client/src/elm/MassiveDecks/Game/Player.elm | 77 + client/src/elm/MassiveDecks/Game/Round.elm | 160 + .../elm/MassiveDecks/Game/Round/Complete.elm | 63 + .../elm/MassiveDecks/Game/Round/Judging.elm | 68 + .../elm/MassiveDecks/Game/Round/Playing.elm | 146 + .../src/elm/MassiveDecks/Game/Round/Plays.elm | 44 + .../elm/MassiveDecks/Game/Round/Revealing.elm | 63 + client/src/elm/MassiveDecks/Game/Rules.elm | 115 + client/src/elm/MassiveDecks/LocalStorage.elm | 20 + client/src/elm/MassiveDecks/Messages.elm | 30 + client/src/elm/MassiveDecks/Model.elm | 29 + .../src/elm/MassiveDecks/Models/Decoders.elm | 937 ++ .../src/elm/MassiveDecks/Models/Encoders.elm | 110 + .../src/elm/MassiveDecks/Models/MdError.elm | 42 + client/src/elm/MassiveDecks/Pages.elm | 93 + client/src/elm/MassiveDecks/Pages/Lobby.elm | 617 + .../elm/MassiveDecks/Pages/Lobby/Actions.elm | 95 + .../MassiveDecks/Pages/Lobby/Configure.elm | 797 ++ .../Pages/Lobby/Configure/Messages.elm | 24 + .../Pages/Lobby/Configure/Model.elm | 49 + .../elm/MassiveDecks/Pages/Lobby/Events.elm | 73 + .../elm/MassiveDecks/Pages/Lobby/GameCode.elm | 49 + .../elm/MassiveDecks/Pages/Lobby/Invite.elm | 144 + .../elm/MassiveDecks/Pages/Lobby/Messages.elm | 19 + .../elm/MassiveDecks/Pages/Lobby/Model.elm | 108 + .../elm/MassiveDecks/Pages/Lobby/Route.elm | 43 + .../elm/MassiveDecks/Pages/Lobby/Token.elm | 39 + client/src/elm/MassiveDecks/Pages/Model.elm | 15 + client/src/elm/MassiveDecks/Pages/Route.elm | 104 + .../src/elm/MassiveDecks/Pages/Spectate.elm | 304 + .../MassiveDecks/Pages/Spectate/Messages.elm | 8 + .../elm/MassiveDecks/Pages/Spectate/Model.elm | 67 + .../elm/MassiveDecks/Pages/Spectate/Route.elm | 52 + client/src/elm/MassiveDecks/Pages/Start.elm | 456 + .../MassiveDecks/Pages/Start/LobbyBrowser.elm | 170 + .../Pages/Start/LobbyBrowser/Messages.elm | 8 + .../Pages/Start/LobbyBrowser/Model.elm | 33 + .../elm/MassiveDecks/Pages/Start/Messages.elm | 13 + .../elm/MassiveDecks/Pages/Start/Model.elm | 30 + .../elm/MassiveDecks/Pages/Start/Route.elm | 108 + client/src/elm/MassiveDecks/Pages/Unknown.elm | 50 + .../elm/MassiveDecks/Pages/Unknown/Model.elm | 9 + .../elm/MassiveDecks/Pages/Unknown/Route.elm | 25 + client/src/elm/MassiveDecks/Ports.elm | 28 + client/src/elm/MassiveDecks/Requests/Api.elm | 80 + .../elm/MassiveDecks/Requests/HttpData.elm | 195 + .../Requests/HttpData/Messages.elm | 10 + .../MassiveDecks/Requests/HttpData/Model.elm | 21 + .../src/elm/MassiveDecks/Requests/Request.elm | 14 + .../src/elm/MassiveDecks/ServerConnection.elm | 58 + client/src/elm/MassiveDecks/Settings.elm | 289 + .../elm/MassiveDecks/Settings/Messages.elm | 13 + .../src/elm/MassiveDecks/Settings/Model.elm | 27 + client/src/elm/MassiveDecks/Strings.elm | 321 + .../elm/MassiveDecks/Strings/Languages.elm | 166 + .../elm/MassiveDecks/Strings/Languages/En.elm | 746 ++ .../MassiveDecks/Strings/Languages/Model.elm | 20 + .../src/elm/MassiveDecks/Strings/Render.elm | 216 + .../elm/MassiveDecks/Strings/Translation.elm | 30 + client/src/elm/MassiveDecks/User.elm | 64 + client/src/elm/MassiveDecks/Util.elm | 29 + client/src/elm/MassiveDecks/Util/Cmd.elm | 14 + client/src/elm/MassiveDecks/Util/Html.elm | 25 + .../elm/MassiveDecks/Util/Html/Attributes.elm | 12 + client/src/elm/MassiveDecks/Util/List.elm | 78 + client/src/elm/MassiveDecks/Util/Maybe.elm | 100 + client/src/elm/MassiveDecks/Util/Random.elm | 18 + client/src/elm/MassiveDecks/Util/Result.elm | 47 + client/src/elm/MassiveDecks/Util/String.elm | 16 + client/src/elm/MassiveDecks/Util/Url.elm | 25 + client/src/elm/MassiveDecks/Version.elm | 11 + client/src/elm/MassiveDecks/index.d.ts | 70 + client/src/elm/Reread.elm | 13 + client/src/elm/Weightless.elm | 89 + client/src/elm/Weightless/Attributes.elm | 420 + client/src/html/cast.html | 23 + client/src/html/index.html | 24 + client/src/scss/animation.scss | 12 + client/src/scss/card/sources.scss | 10 + client/src/scss/card/variables.scss | 33 + client/src/scss/cards.scss | 194 + client/src/scss/cast.scss | 9 + client/src/scss/colors.scss | 22 + client/src/scss/error.scss | 55 + client/src/scss/fonts.scss | 56 + client/src/scss/game.scss | 262 + client/src/scss/massive-decks.scss | 239 + client/src/scss/pages/lobby.scss | 248 + client/src/scss/pages/lobby/configure.scss | 129 + client/src/scss/pages/lobby/invite.scss | 49 + client/src/scss/pages/spectate.scss | 79 + client/src/scss/pages/start.scss | 143 + .../src/scss/pages/start/lobby-browser.scss | 17 + client/src/scss/pages/unknown-page.scss | 3 + client/src/scss/settings.scss | 50 + client/src/scss/variables.scss | 1 + client/src/scss/weightless.scss | 80 + client/src/ts/cast.ts | 10 + client/src/ts/chromecast.ts | 150 + client/src/ts/index.ts | 74 + client/src/ts/server-connection.ts | 151 + client/src/ts/weightless.ts | 15 + client/src/webmanifest/manifest.webmanifest | 17 + client/tsconfig.json | 21 + client/webpack.config.js | 261 + conf/application.conf | 39 - conf/lobbies.routes | 11 - conf/logback.xml | 22 - conf/routes | 14 - docker-compose.yml | 19 + docker_push | 3 + elm-package.json | 20 - gulpfile.js | 62 - package.json | 11 - project/build.properties | 1 - project/plugins.sbt | 10 - public/apple-touch-icon-precomposed.png | Bin 9484 -> 0 bytes public/apple-touch-icon.png | Bin 8197 -> 0 bytes public/favicon.ico | Bin 15086 -> 0 bytes public/icon.png | Bin 10900 -> 0 bytes public/images/bgnoise_lg.png | Bin 5425 -> 0 bytes public/images/icon.svg | 112 - public/images/icons/fast-forward.png | Bin 413 -> 0 bytes public/images/icons/gavel.png | Bin 485 -> 0 bytes public/images/icons/hourglass.png | Bin 393 -> 0 bytes public/javascripts/mui.min.js | 1 - public/javascripts/qrcode.min.js | 1 - public/javascripts/util.js | 64 - public/mstile.png | Bin 7994 -> 0 bytes public/safari-pinned-tab.svg | 49 - public/stylesheets/mui.min.css | 1 - server/.dockerignore | 4 + server/.eslintrc.json | 37 + server/Dockerfile | 34 + server/config.json5 | 91 + server/package-lock.json | 4785 +++++++ server/package.json | 61 + server/src/ts/action.ts | 39 + server/src/ts/action/authenticate.ts | 61 + server/src/ts/action/game-action.ts | 88 + server/src/ts/action/game-action/czar.ts | 38 + .../src/ts/action/game-action/czar/judge.ts | 59 + .../src/ts/action/game-action/czar/reveal.ts | 56 + server/src/ts/action/game-action/player.ts | 37 + .../ts/action/game-action/player/submit.ts | 86 + .../ts/action/game-action/player/take-back.ts | 37 + server/src/ts/action/game-action/redraw.ts | 48 + server/src/ts/action/handler.ts | 21 + server/src/ts/action/initial/check-alive.ts | 19 + server/src/ts/action/initial/create-lobby.ts | 26 + server/src/ts/action/initial/register-user.ts | 26 + server/src/ts/action/privileged.ts | 36 + server/src/ts/action/privileged/configure.ts | 69 + .../privileged/configure/change-decks.ts | 72 + .../privileged/configure/change-house-rule.ts | 47 + .../privileged/configure/set-hand-size.ts | 47 + .../privileged/configure/set-password.ts | 50 + .../privileged/configure/set-score-limit.ts | 50 + server/src/ts/action/privileged/start-game.ts | 27 + server/src/ts/action/validation.ts | 9 + server/src/ts/action/validation.validator.ts | 529 + server/src/ts/cache.ts | 157 + server/src/ts/caches.ts | 13 + server/src/ts/caches/in-memory.ts | 61 + server/src/ts/config.ts | 105 + server/src/ts/errors.ts | 15 + .../src/ts/errors/action-execution-error.ts | 170 + server/src/ts/errors/authentication.ts | 82 + server/src/ts/errors/game-state-error.ts | 24 + server/src/ts/errors/lobby.ts | 51 + server/src/ts/errors/validation.ts | 22 + server/src/ts/event.ts | 170 + server/src/ts/events/game-event.ts | 18 + .../src/ts/events/game-event/game-started.ts | 20 + .../src/ts/events/game-event/hand-redrawn.ts | 25 + .../src/ts/events/game-event/play-revealed.ts | 17 + .../ts/events/game-event/play-submitted.ts | 14 + .../ts/events/game-event/play-taken-back.ts | 14 + .../ts/events/game-event/round-finished.ts | 18 + .../src/ts/events/game-event/round-started.ts | 30 + .../ts/events/game-event/start-revealing.ts | 15 + server/src/ts/events/lobby-event.ts | 5 + .../src/ts/events/lobby-event/configured.ts | 23 + .../lobby-event/configured/decks-changed.ts | 23 + .../lobby-event/configured/hand-size-set.ts | 9 + .../configured/house-rule-changed.ts | 20 + .../lobby-event/configured/password-set.ts | 15 + .../lobby-event/configured/score-limit-set.ts | 9 + .../events/lobby-event/connection-changed.ts | 34 + .../ts/events/lobby-event/presence-changed.ts | 25 + server/src/ts/events/user-event.ts | 6 + server/src/ts/events/user-event/sync.ts | 24 + server/src/ts/games/cards/card.ts | 73 + server/src/ts/games/cards/decks.ts | 105 + server/src/ts/games/cards/hand.ts | 6 + server/src/ts/games/cards/play.ts | 29 + server/src/ts/games/cards/source.ts | 248 + server/src/ts/games/cards/sources.ts | 39 + server/src/ts/games/cards/sources/cardcast.ts | 211 + server/src/ts/games/cards/sources/custom.ts | 15 + server/src/ts/games/game.ts | 86 + server/src/ts/games/game/round.ts | 164 + server/src/ts/games/game/round/public.ts | 35 + server/src/ts/games/player.ts | 53 + server/src/ts/games/rules.ts | 86 + server/src/ts/index.ts | 198 + server/src/ts/lobby.ts | 131 + server/src/ts/lobby/change.ts | 122 + server/src/ts/lobby/config.ts | 75 + server/src/ts/lobby/game-code.ts | 34 + server/src/ts/logging.ts | 43 + server/src/ts/secret.ts | 3 + server/src/ts/server-state.ts | 30 + server/src/ts/socket-manager.ts | 150 + server/src/ts/store.ts | 114 + server/src/ts/store/in-memory.ts | 157 + server/src/ts/store/postgres.ts | 146 + server/src/ts/store/stores.ts | 13 + server/src/ts/task.ts | 35 + server/src/ts/task/load-deck-summary.ts | 61 + server/src/ts/task/start-game.ts | 50 + server/src/ts/task/tasks.ts | 56 + server/src/ts/timeout.ts | 63 + server/src/ts/timeout/finished-playing.ts | 51 + server/src/ts/timeout/round-start.ts | 69 + server/src/ts/timeout/user-disconnect.ts | 40 + server/src/ts/user.ts | 94 + server/src/ts/user/token.ts | 64 + server/src/ts/util.ts | 134 + server/tsconfig.json | 64 + test/massivedecks/lobby/ConfigSpec.scala | 63 - test/massivedecks/lobby/DeckSpec.scala | 47 - test/massivedecks/lobby/GameSpec.scala | 15 - test/massivedecks/lobby/LobbySpec.scala | 15 - test/massivedecks/lobby/PlayersSpec.scala | 83 - test/massivedecks/matchers/Matchers.scala | 19 - .../notifications/FakeWebSocketClient.scala | 124 - .../notifications/NotifierSpec.scala | 77 - .../notifications/NotifiersSpec.scala | 212 - 379 files changed, 36123 insertions(+), 10481 deletions(-) delete mode 100644 app.json delete mode 100644 app/assets/stylesheets/config.less delete mode 100644 app/assets/stylesheets/error.less delete mode 100644 app/assets/stylesheets/lobby.less delete mode 100644 app/assets/stylesheets/massivedecks.less delete mode 100644 app/assets/stylesheets/start.less delete mode 100644 app/assets/stylesheets/variables.less delete mode 100644 app/massivedecks/Config.scala delete mode 100644 app/massivedecks/CoreModule.scala delete mode 100644 app/massivedecks/Util.scala delete mode 100644 app/massivedecks/cardcast/CardcastAPI.scala delete mode 100644 app/massivedecks/controllers/API.scala delete mode 100644 app/massivedecks/controllers/ErrorHandler.scala delete mode 100644 app/massivedecks/controllers/Pages.scala delete mode 100644 app/massivedecks/exceptions/BadRequestException.scala delete mode 100644 app/massivedecks/exceptions/ForbiddenException.scala delete mode 100644 app/massivedecks/exceptions/NotFoundException.scala delete mode 100644 app/massivedecks/exceptions/RequestException.scala delete mode 100644 app/massivedecks/exceptions/RequestFailedException.scala delete mode 100644 app/massivedecks/lobby/Config.scala delete mode 100644 app/massivedecks/lobby/Deck.scala delete mode 100644 app/massivedecks/lobby/Game.scala delete mode 100644 app/massivedecks/lobby/Lobby.scala delete mode 100644 app/massivedecks/lobby/Players.scala delete mode 100644 app/massivedecks/models/Errors.scala delete mode 100644 app/massivedecks/models/Game.scala delete mode 100644 app/massivedecks/models/Lobby.scala delete mode 100644 app/massivedecks/models/Player.scala delete mode 100644 app/massivedecks/models/cardcast/CardcastDeck.scala delete mode 100644 app/massivedecks/notifications/Notifier.scala delete mode 100644 app/massivedecks/notifications/Notifiers.scala delete mode 100644 app/massivedecks/stores/CachedStore.scala delete mode 100644 app/massivedecks/stores/GameCodeManager.scala delete mode 100644 app/massivedecks/stores/InMemoryStore.scala delete mode 100644 app/massivedecks/stores/LobbyStore.scala delete mode 100644 app/views/error.scala.html delete mode 100644 app/views/head.scala.html delete mode 100644 app/views/index.scala.html delete mode 100644 build.sbt create mode 100644 client/.dockerignore create mode 100644 client/Dockerfile create mode 100644 client/assets/fonts/gothic-a1-v4-latin-700.woff2 create mode 100644 client/assets/fonts/gothic-a1-v4-latin-regular.woff2 create mode 100644 client/assets/fonts/nimbus-sans-l-700.woff2 create mode 100644 client/assets/fonts/nimbus-sans-l-regular.woff2 create mode 100644 client/assets/fonts/roboto-v18-latin-700.woff2 create mode 100644 client/assets/fonts/roboto-v18-latin-700italic.woff2 create mode 100644 client/assets/fonts/roboto-v18-latin-italic.woff2 create mode 100644 client/assets/fonts/roboto-v18-latin-regular.woff2 create mode 100644 client/assets/fonts/source-code-pro-v8-latin-regular.woff2 create mode 100644 client/assets/images/background.svg create mode 100644 client/assets/images/cardcast.png create mode 100644 client/assets/images/certificate-thumbs-up.svg create mode 100644 client/assets/images/deck.svg create mode 100644 client/assets/images/icon.png create mode 100644 client/assets/images/icon.svg create mode 100644 client/assets/images/reread-icon.svg create mode 100644 client/elm.json create mode 100644 client/nginx.conf create mode 100644 client/package-lock.json create mode 100644 client/package.json delete mode 100644 client/src/MassiveDecks.elm delete mode 100644 client/src/MassiveDecks/API.elm delete mode 100644 client/src/MassiveDecks/API/Request.elm delete mode 100644 client/src/MassiveDecks/Components/About.elm delete mode 100644 client/src/MassiveDecks/Components/BrowserNotifications.elm delete mode 100644 client/src/MassiveDecks/Components/Errors.elm delete mode 100644 client/src/MassiveDecks/Components/Icon.elm delete mode 100644 client/src/MassiveDecks/Components/Input.elm delete mode 100644 client/src/MassiveDecks/Components/Overlay.elm delete mode 100644 client/src/MassiveDecks/Components/QR.elm delete mode 100644 client/src/MassiveDecks/Components/Storage.elm delete mode 100644 client/src/MassiveDecks/Components/TTS.elm delete mode 100644 client/src/MassiveDecks/Components/Tabs.elm delete mode 100644 client/src/MassiveDecks/Components/Title.elm delete mode 100644 client/src/MassiveDecks/Models.elm delete mode 100644 client/src/MassiveDecks/Models/Card.elm delete mode 100644 client/src/MassiveDecks/Models/Event.elm delete mode 100644 client/src/MassiveDecks/Models/Game.elm delete mode 100644 client/src/MassiveDecks/Models/Game/Round.elm delete mode 100644 client/src/MassiveDecks/Models/JSON/Decode.elm delete mode 100644 client/src/MassiveDecks/Models/JSON/Encode.elm delete mode 100644 client/src/MassiveDecks/Models/Notification.elm delete mode 100644 client/src/MassiveDecks/Models/Player.elm delete mode 100644 client/src/MassiveDecks/Scenes/Config.elm delete mode 100644 client/src/MassiveDecks/Scenes/Config/Messages.elm delete mode 100644 client/src/MassiveDecks/Scenes/Config/Models.elm delete mode 100644 client/src/MassiveDecks/Scenes/Config/UI.elm delete mode 100644 client/src/MassiveDecks/Scenes/History.elm delete mode 100644 client/src/MassiveDecks/Scenes/History/Messages.elm delete mode 100644 client/src/MassiveDecks/Scenes/History/Models.elm delete mode 100644 client/src/MassiveDecks/Scenes/History/UI.elm delete mode 100644 client/src/MassiveDecks/Scenes/Lobby.elm delete mode 100644 client/src/MassiveDecks/Scenes/Lobby/Messages.elm delete mode 100644 client/src/MassiveDecks/Scenes/Lobby/Models.elm delete mode 100644 client/src/MassiveDecks/Scenes/Lobby/Sidebar.elm delete mode 100644 client/src/MassiveDecks/Scenes/Lobby/UI.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/HouseRule.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/Messages.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/Models.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/UI.elm delete mode 100644 client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm delete mode 100644 client/src/MassiveDecks/Scenes/Start.elm delete mode 100644 client/src/MassiveDecks/Scenes/Start/Messages.elm delete mode 100644 client/src/MassiveDecks/Scenes/Start/Models.elm delete mode 100644 client/src/MassiveDecks/Scenes/Start/UI.elm delete mode 100644 client/src/MassiveDecks/Util.elm create mode 100644 client/src/elm/MassiveDecks.elm create mode 100644 client/src/elm/MassiveDecks/Animated.elm create mode 100644 client/src/elm/MassiveDecks/Card.elm create mode 100644 client/src/elm/MassiveDecks/Card/Model.elm create mode 100644 client/src/elm/MassiveDecks/Card/Parts.elm create mode 100644 client/src/elm/MassiveDecks/Card/Play.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source/Cardcast.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source/Cardcast/Model.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source/Fake.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source/Methods.elm create mode 100644 client/src/elm/MassiveDecks/Card/Source/Model.elm create mode 100644 client/src/elm/MassiveDecks/Cast.elm create mode 100644 client/src/elm/MassiveDecks/Cast/Client.elm create mode 100644 client/src/elm/MassiveDecks/Cast/Model.elm create mode 100644 client/src/elm/MassiveDecks/Components.elm create mode 100644 client/src/elm/MassiveDecks/Components/Menu.elm create mode 100644 client/src/elm/MassiveDecks/Error.elm create mode 100644 client/src/elm/MassiveDecks/Error/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Error/Model.elm create mode 100644 client/src/elm/MassiveDecks/Error/Overlay.elm create mode 100644 client/src/elm/MassiveDecks/Game.elm create mode 100644 client/src/elm/MassiveDecks/Game/Action.elm create mode 100644 client/src/elm/MassiveDecks/Game/Action/Model.elm create mode 100644 client/src/elm/MassiveDecks/Game/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Game/Model.elm create mode 100644 client/src/elm/MassiveDecks/Game/Player.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round/Complete.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round/Judging.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round/Playing.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round/Plays.elm create mode 100644 client/src/elm/MassiveDecks/Game/Round/Revealing.elm create mode 100644 client/src/elm/MassiveDecks/Game/Rules.elm create mode 100644 client/src/elm/MassiveDecks/LocalStorage.elm create mode 100644 client/src/elm/MassiveDecks/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Model.elm create mode 100644 client/src/elm/MassiveDecks/Models/Decoders.elm create mode 100644 client/src/elm/MassiveDecks/Models/Encoders.elm create mode 100644 client/src/elm/MassiveDecks/Models/MdError.elm create mode 100644 client/src/elm/MassiveDecks/Pages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Configure/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Events.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/GameCode.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Invite.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Route.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Lobby/Token.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Route.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Spectate.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Spectate/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Spectate/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Spectate/Route.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Start/Route.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Unknown.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Unknown/Model.elm create mode 100644 client/src/elm/MassiveDecks/Pages/Unknown/Route.elm create mode 100644 client/src/elm/MassiveDecks/Ports.elm create mode 100644 client/src/elm/MassiveDecks/Requests/Api.elm create mode 100644 client/src/elm/MassiveDecks/Requests/HttpData.elm create mode 100644 client/src/elm/MassiveDecks/Requests/HttpData/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Requests/HttpData/Model.elm create mode 100644 client/src/elm/MassiveDecks/Requests/Request.elm create mode 100644 client/src/elm/MassiveDecks/ServerConnection.elm create mode 100644 client/src/elm/MassiveDecks/Settings.elm create mode 100644 client/src/elm/MassiveDecks/Settings/Messages.elm create mode 100644 client/src/elm/MassiveDecks/Settings/Model.elm create mode 100644 client/src/elm/MassiveDecks/Strings.elm create mode 100644 client/src/elm/MassiveDecks/Strings/Languages.elm create mode 100644 client/src/elm/MassiveDecks/Strings/Languages/En.elm create mode 100644 client/src/elm/MassiveDecks/Strings/Languages/Model.elm create mode 100644 client/src/elm/MassiveDecks/Strings/Render.elm create mode 100644 client/src/elm/MassiveDecks/Strings/Translation.elm create mode 100644 client/src/elm/MassiveDecks/User.elm create mode 100644 client/src/elm/MassiveDecks/Util.elm create mode 100644 client/src/elm/MassiveDecks/Util/Cmd.elm create mode 100644 client/src/elm/MassiveDecks/Util/Html.elm create mode 100644 client/src/elm/MassiveDecks/Util/Html/Attributes.elm create mode 100644 client/src/elm/MassiveDecks/Util/List.elm create mode 100644 client/src/elm/MassiveDecks/Util/Maybe.elm create mode 100644 client/src/elm/MassiveDecks/Util/Random.elm create mode 100644 client/src/elm/MassiveDecks/Util/Result.elm create mode 100644 client/src/elm/MassiveDecks/Util/String.elm create mode 100644 client/src/elm/MassiveDecks/Util/Url.elm create mode 100644 client/src/elm/MassiveDecks/Version.elm create mode 100644 client/src/elm/MassiveDecks/index.d.ts create mode 100644 client/src/elm/Reread.elm create mode 100644 client/src/elm/Weightless.elm create mode 100644 client/src/elm/Weightless/Attributes.elm create mode 100644 client/src/html/cast.html create mode 100644 client/src/html/index.html create mode 100644 client/src/scss/animation.scss create mode 100644 client/src/scss/card/sources.scss create mode 100644 client/src/scss/card/variables.scss create mode 100644 client/src/scss/cards.scss create mode 100644 client/src/scss/cast.scss create mode 100644 client/src/scss/colors.scss create mode 100644 client/src/scss/error.scss create mode 100644 client/src/scss/fonts.scss create mode 100644 client/src/scss/game.scss create mode 100644 client/src/scss/massive-decks.scss create mode 100644 client/src/scss/pages/lobby.scss create mode 100644 client/src/scss/pages/lobby/configure.scss create mode 100644 client/src/scss/pages/lobby/invite.scss create mode 100644 client/src/scss/pages/spectate.scss create mode 100644 client/src/scss/pages/start.scss create mode 100644 client/src/scss/pages/start/lobby-browser.scss create mode 100644 client/src/scss/pages/unknown-page.scss create mode 100644 client/src/scss/settings.scss create mode 100644 client/src/scss/variables.scss create mode 100644 client/src/scss/weightless.scss create mode 100644 client/src/ts/cast.ts create mode 100644 client/src/ts/chromecast.ts create mode 100644 client/src/ts/index.ts create mode 100644 client/src/ts/server-connection.ts create mode 100644 client/src/ts/weightless.ts create mode 100644 client/src/webmanifest/manifest.webmanifest create mode 100644 client/tsconfig.json create mode 100644 client/webpack.config.js delete mode 100644 conf/application.conf delete mode 100644 conf/lobbies.routes delete mode 100644 conf/logback.xml delete mode 100644 conf/routes create mode 100644 docker-compose.yml create mode 100644 docker_push delete mode 100644 elm-package.json delete mode 100644 gulpfile.js delete mode 100644 package.json delete mode 100644 project/build.properties delete mode 100644 project/plugins.sbt delete mode 100644 public/apple-touch-icon-precomposed.png delete mode 100644 public/apple-touch-icon.png delete mode 100644 public/favicon.ico delete mode 100644 public/icon.png delete mode 100644 public/images/bgnoise_lg.png delete mode 100644 public/images/icon.svg delete mode 100644 public/images/icons/fast-forward.png delete mode 100644 public/images/icons/gavel.png delete mode 100644 public/images/icons/hourglass.png delete mode 100644 public/javascripts/mui.min.js delete mode 100644 public/javascripts/qrcode.min.js delete mode 100644 public/javascripts/util.js delete mode 100644 public/mstile.png delete mode 100644 public/safari-pinned-tab.svg delete mode 100644 public/stylesheets/mui.min.css create mode 100644 server/.dockerignore create mode 100644 server/.eslintrc.json create mode 100644 server/Dockerfile create mode 100644 server/config.json5 create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/src/ts/action.ts create mode 100644 server/src/ts/action/authenticate.ts create mode 100644 server/src/ts/action/game-action.ts create mode 100644 server/src/ts/action/game-action/czar.ts create mode 100644 server/src/ts/action/game-action/czar/judge.ts create mode 100644 server/src/ts/action/game-action/czar/reveal.ts create mode 100644 server/src/ts/action/game-action/player.ts create mode 100644 server/src/ts/action/game-action/player/submit.ts create mode 100644 server/src/ts/action/game-action/player/take-back.ts create mode 100644 server/src/ts/action/game-action/redraw.ts create mode 100644 server/src/ts/action/handler.ts create mode 100644 server/src/ts/action/initial/check-alive.ts create mode 100644 server/src/ts/action/initial/create-lobby.ts create mode 100644 server/src/ts/action/initial/register-user.ts create mode 100644 server/src/ts/action/privileged.ts create mode 100644 server/src/ts/action/privileged/configure.ts create mode 100644 server/src/ts/action/privileged/configure/change-decks.ts create mode 100644 server/src/ts/action/privileged/configure/change-house-rule.ts create mode 100644 server/src/ts/action/privileged/configure/set-hand-size.ts create mode 100644 server/src/ts/action/privileged/configure/set-password.ts create mode 100644 server/src/ts/action/privileged/configure/set-score-limit.ts create mode 100644 server/src/ts/action/privileged/start-game.ts create mode 100644 server/src/ts/action/validation.ts create mode 100644 server/src/ts/action/validation.validator.ts create mode 100644 server/src/ts/cache.ts create mode 100644 server/src/ts/caches.ts create mode 100644 server/src/ts/caches/in-memory.ts create mode 100644 server/src/ts/config.ts create mode 100644 server/src/ts/errors.ts create mode 100644 server/src/ts/errors/action-execution-error.ts create mode 100644 server/src/ts/errors/authentication.ts create mode 100644 server/src/ts/errors/game-state-error.ts create mode 100644 server/src/ts/errors/lobby.ts create mode 100644 server/src/ts/errors/validation.ts create mode 100644 server/src/ts/event.ts create mode 100644 server/src/ts/events/game-event.ts create mode 100644 server/src/ts/events/game-event/game-started.ts create mode 100644 server/src/ts/events/game-event/hand-redrawn.ts create mode 100644 server/src/ts/events/game-event/play-revealed.ts create mode 100644 server/src/ts/events/game-event/play-submitted.ts create mode 100644 server/src/ts/events/game-event/play-taken-back.ts create mode 100644 server/src/ts/events/game-event/round-finished.ts create mode 100644 server/src/ts/events/game-event/round-started.ts create mode 100644 server/src/ts/events/game-event/start-revealing.ts create mode 100644 server/src/ts/events/lobby-event.ts create mode 100644 server/src/ts/events/lobby-event/configured.ts create mode 100644 server/src/ts/events/lobby-event/configured/decks-changed.ts create mode 100644 server/src/ts/events/lobby-event/configured/hand-size-set.ts create mode 100644 server/src/ts/events/lobby-event/configured/house-rule-changed.ts create mode 100644 server/src/ts/events/lobby-event/configured/password-set.ts create mode 100644 server/src/ts/events/lobby-event/configured/score-limit-set.ts create mode 100644 server/src/ts/events/lobby-event/connection-changed.ts create mode 100644 server/src/ts/events/lobby-event/presence-changed.ts create mode 100644 server/src/ts/events/user-event.ts create mode 100644 server/src/ts/events/user-event/sync.ts create mode 100644 server/src/ts/games/cards/card.ts create mode 100644 server/src/ts/games/cards/decks.ts create mode 100644 server/src/ts/games/cards/hand.ts create mode 100644 server/src/ts/games/cards/play.ts create mode 100644 server/src/ts/games/cards/source.ts create mode 100644 server/src/ts/games/cards/sources.ts create mode 100644 server/src/ts/games/cards/sources/cardcast.ts create mode 100644 server/src/ts/games/cards/sources/custom.ts create mode 100644 server/src/ts/games/game.ts create mode 100644 server/src/ts/games/game/round.ts create mode 100644 server/src/ts/games/game/round/public.ts create mode 100644 server/src/ts/games/player.ts create mode 100644 server/src/ts/games/rules.ts create mode 100644 server/src/ts/index.ts create mode 100644 server/src/ts/lobby.ts create mode 100644 server/src/ts/lobby/change.ts create mode 100644 server/src/ts/lobby/config.ts create mode 100644 server/src/ts/lobby/game-code.ts create mode 100644 server/src/ts/logging.ts create mode 100644 server/src/ts/secret.ts create mode 100644 server/src/ts/server-state.ts create mode 100644 server/src/ts/socket-manager.ts create mode 100644 server/src/ts/store.ts create mode 100644 server/src/ts/store/in-memory.ts create mode 100644 server/src/ts/store/postgres.ts create mode 100644 server/src/ts/store/stores.ts create mode 100644 server/src/ts/task.ts create mode 100644 server/src/ts/task/load-deck-summary.ts create mode 100644 server/src/ts/task/start-game.ts create mode 100644 server/src/ts/task/tasks.ts create mode 100644 server/src/ts/timeout.ts create mode 100644 server/src/ts/timeout/finished-playing.ts create mode 100644 server/src/ts/timeout/round-start.ts create mode 100644 server/src/ts/timeout/user-disconnect.ts create mode 100644 server/src/ts/user.ts create mode 100644 server/src/ts/user/token.ts create mode 100644 server/src/ts/util.ts create mode 100644 server/tsconfig.json delete mode 100644 test/massivedecks/lobby/ConfigSpec.scala delete mode 100644 test/massivedecks/lobby/DeckSpec.scala delete mode 100644 test/massivedecks/lobby/GameSpec.scala delete mode 100644 test/massivedecks/lobby/LobbySpec.scala delete mode 100644 test/massivedecks/lobby/PlayersSpec.scala delete mode 100644 test/massivedecks/matchers/Matchers.scala delete mode 100644 test/massivedecks/notifications/FakeWebSocketClient.scala delete mode 100644 test/massivedecks/notifications/NotifierSpec.scala delete mode 100644 test/massivedecks/notifications/NotifiersSpec.scala diff --git a/.gitignore b/.gitignore index 730e1cf..7b60546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,9 @@ -# Elm -/elm-stuff/ +# IDE files. +.idea/ -# NodeJS -/node_modules/ +# Generated files. +dist/ -# Activator -/logs/ -/target/ -/project/project/target -/project/target/ -/project/sbt-ui.sbt -/project/play-fork-run.sbt -/RUNNING_PID - -# IntelliJ -/.idea/ - -# Eclipse -/.classpath -/.project -/.settings - -# SBT -/.sbtserver -/project/.sbtserver -/project/.sbtserver.lock - -# Compiled from elm. -/public/javascripts/MassiveDecks.js +# Package management cruft. +elm-stuff/ +node_modules/ diff --git a/.travis.yml b/.travis.yml index d1d78a0..56e46d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,27 @@ -language: scala -scala: - - 2.11.8 -jdk: - - oraclejdk8 +language: sh + +services: + - docker + +sudo: required + +env: + global: + - VERSION=${TRAVIS_TAG:-${TRAVIS_COMMIT}-dev} + matrix: + - COMPONENT=server + - COMPONENT=client + +before_install: + - cd ${COMPONENT} + +install: true + +script: + - docker build -t massivedecks/${COMPONENT}:${VERSION} --build-arg VCS_REF="${TRAVIS_COMMIT}" --build-arg BUILD_DATE="$(date --rfc-3339=seconds)" --build-arg VERSION="${VERSION}" . + +deploy: + provider: script + script: bash ../docker_push "${COMPONENT}" "${VERSION}" + on: + branch: v2 diff --git a/README.md b/README.md index ce66c9b..2eb735e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,42 @@ -# Massive Decks [![Build Status](https://travis-ci.org/Lattyware/massivedecks.svg?branch=master)](https://travis-ci.org/Lattyware/massivedecks) +# Massive Decks [![Build Status](https://travis-ci.org/Lattyware/massivedecks.svg?branch=v2)](https://travis-ci.org/Lattyware/massivedecks) -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Lattyware/massivedecks) +Massive Decks is a comedy party game based on [Cards against Humanity][cah]. The game is open source software available +under [the AGPLv3 license][license]. -Massive Decks is a web game based on the excellent [Cards against Humanity][cah] - a party game where you play white cards to try and produce the most amusing outcome when combined with the given black card. The game can be played on phone or computer and uses [Cardcast decks][cardcast]. - -Play the game at: -**[https://massivedecks.herokuapp.com/][massivedecks]** - -(This is hosted on a service that will automatically sleep if it's unused for a while - it may take a little time to load in those cases, but should be fine after the initial load.) - -[![A screenshot of the game.](https://cloud.githubusercontent.com/assets/1239492/16138236/8299ee32-3433-11e6-8ca2-36993bb83d58.png)][massivedecks] - -If you have any problems with the game, please [raise an issue][issue]. If you would like to help develop the game, check [the wiki][wiki] for how to get set up and help. - -Massive Decks is also inspired by: -* [CardCast](https://www.cardcastgame.com/) - a great app that allows you to play on a ChromeCast. -* [Pretend You're Xyzzy](http://pretendyoure.xyz/zy/) - a web game where you can jump in with people you don't know. - -This is an open source game developed in [Elm][elm] for the client and [Scala][scala] for the server. - -We also use: -* [CardCast](https://www.cardcastgame.com/)'s APIs for getting decks of cards (you can go there to make your own!). -* [The Play framework](https://www.playframework.com/) -* [Less](http://lesscss.org/) -* [Font Awesome](https://fortawesome.github.io/Font-Awesome/) -* [MUI](https://www.muicss.com/) - -Massive Decks is under [the AGPLv3 license][license]. The game concept 'Cards against Humanity' is used under a -[Creative Commons BY-NC-SA 2.0 license][cah-license] granted by [Cards against Humanity][cah]. - -[massivedecks]: https://massivedecks.herokuapp.com/ [cah]: https://cardsagainsthumanity.com/ -[cardcast]: https://www.cardcastgame.com/browse +[license]: https://github.com/Lattyware/massivedecks/blob/master/LICENSE + +## About + +This is a bleeding-edge development version of the game, a complete rewrite from the old version. It is not currently +ready for release, and unless you are interested in developing the game, you should stick with v1 for now. + +## Contributing + +If you have any problems with the game, please [raise an issue][issue]. If you would like to help develop the game, +check [the wiki][wiki] for how to get started. + [issue]: https://github.com/Lattyware/massivedecks/issues/new [wiki]: https://github.com/Lattyware/massivedecks/wiki -[elm]: http://elm-lang.org -[scala]: http://www.scala-lang.org/ -[license]: https://github.com/Lattyware/massivedecks/blob/master/LICENSE + +## Credits + +### Maintainers + +Massive Decks is maintained by [Reread Games][reread]. + +[reread]: https://www.rereadgames.com/ + +### Inspiration + +The 'Cards against Humanity' game concept is used under a [Creative Commons BY-NC-SA 2.0 license][cah-license] granted +by [Cards against Humanity][cah]. + [cah-license]: https://creativecommons.org/licenses/by-nc-sa/2.0/ + +Massive Decks is also inspired by: +* [CardCast][cardcast] - an app you to play on a ChromeCast. +* [Pretend You're Xyzzy][xyzzy] - a web game where you can jump in with people you don't know. + +[cardcast]: https://www.cardcastgame.com/ +[xyzzy]: http://pretendyoure.xyz/zy/ diff --git a/app.json b/app.json deleted file mode 100644 index 3874f9f..0000000 --- a/app.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "Massive Decks", - "description": "A party game where you play white cards to try and produce the most amusing outcome when combined with the given black card.", - "keywrods": [ - "game", - "party", - "comedy" - ], - "repository": "https://github.com/Lattyware/massivedecks", - "env": { - "MD_HOST": { - "description": "The hostname Massive Decks will be accessible at.", - "value": "yourappname.herokuapp.com" - }, - "MD_PATH": { - "description": "The path Massive Decks will be accessible at.", - "value": "/" - }, - "MD_PROTOCOL": { - "description": "The protocol Massive Decks will be accessible on.", - "value": "https" - }, - "APPLICATION_SECRET": { - "description": "A random secret for security confirmation in the application.", - "generator": "secret" - }, - "ELM_VERSION": "0.18", - "ELM_COMPILE": "elm make client/src/MassiveDecks.elm --output public/javascripts/MassiveDecks.js" - }, - "buildpacks": [ - { - "url": "https://github.com/ianpurvis/heroku-buildpack-version" - }, - { - "url": "https://github.com/srid/heroku-buildpack-elm" - }, - { - "url": "https://github.com/heroku/heroku-buildpack-scala" - } - ] -} diff --git a/app/assets/stylesheets/config.less b/app/assets/stylesheets/config.less deleted file mode 100644 index ff2d1bc..0000000 --- a/app/assets/stylesheets/config.less +++ /dev/null @@ -1,63 +0,0 @@ -@import "variables.less"; - -.game-code { - font-family: monospace; -} - -h2 { - margin: 0px; -} - -#config { - width: 100%; -} - -#config-content { - max-width: 500px; - margin: 15px auto; - - h1 { - text-align: center; - } - - .decks { - margin-bottom: 0px; - - table { - td { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } - - .input-with-button { - display: flex; - - & > .mui-textfield { - margin: 0px; - flex-grow: 1; - } - } - - .house-rule { - div { - width: 100%; - justify-content: space-between; - align-items: baseline; - display: flex; - } - } - - #start-game { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - & > span { - margin-top: 10px; - } - } -} diff --git a/app/assets/stylesheets/error.less b/app/assets/stylesheets/error.less deleted file mode 100644 index d31eaed..0000000 --- a/app/assets/stylesheets/error.less +++ /dev/null @@ -1,165 +0,0 @@ -@import "variables.less"; - -body { - background-image: url("/images/bgnoise_lg.png"); - background-color: @background-color; - width: 100%; - height: auto; - - padding: 0px; - margin: 0px; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.card { - width: @card-width; - height: @card-height; - - @media (min-width: @enhance-width) { - width: @card-width-enhanced; - height: @card-width-enhanced; - } - - margin: 10px; - padding: 15px; - font-weight: bold; - word-wrap: break-word; - border-radius: 10px; - - & > div { - margin: 0px; - padding: 0px; - overflow-x: hidden; - overflow-y: auto; - height: 100%; - width: 100%; - } -} - -.play-area { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - position: relative; - - .action-menu { - position: absolute; - top: 0px; - right: 5px; - } - - .round-area { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 100%; - - .others-picked { - .card { - position: absolute; - z-index: -10; - transition: transform 1s, left 1s, right 1s, top 1s ease-out - } - } - } -} - -#advice { - max-width: 500px; - margin: 0px 25px 0px 25px; - text-align: center; -} - -#cards { - display: flex; - justify-content: center; - margin: 25px; - width: 100%; -} - -#logo { - display: block; - margin: 25px auto 0px auto; - width: 150px; - - img { - width: 150px; - } -} - -.call { - background-color: #000000; - color: #ffffff; - flex-shrink: 0; - - transform: rotate(4deg); - - & > .call-text { - & > .slot { - text-decoration: underline; - - &:empty { - display: inline-block; - height: 1.2em; - border-bottom: 1px solid #ffffff; - min-width: 40px; - max-width: 80px; - } - } - } -} - -.response { - background-color: #ffffff; - color: #000000; - - &.picked { - opacity: 0.5; - } -} - -.empty-spot { - opacity: 0.25; -} - -ol { - margin: 0px; - padding: 0px; - list-style-type: none; - flex-shrink: 0; - white-space: nowrap; - - & > li { - display: inline-block; - white-space: normal; - - .card { - margin: 10px 5px 10px 5px; - } - - &:not(:first-child) { - margin-left: -(@card-width * @card-overlap); - @media (min-width: @enhance-width) { - margin-left: -(@card-width-enhanced * @card-overlap); - } - } - } -} - -#status-card { - transform: rotate(-7deg); -} - -#message-card { - transform: rotate(12deg); -} - -#version { - margin: 20px; -} diff --git a/app/assets/stylesheets/lobby.less b/app/assets/stylesheets/lobby.less deleted file mode 100644 index fa2c731..0000000 --- a/app/assets/stylesheets/lobby.less +++ /dev/null @@ -1,533 +0,0 @@ -@import "variables.less"; - -.card { - width: @card-width; - height: @card-height; - - @media (min-width: @enhance-width) { - width: @card-width-enhanced; - height: @card-width-enhanced; - } - - margin: 10px; - padding: 15px; - font-weight: bold; - word-wrap: break-word; - border-radius: 10px; - - & > div { - margin: 0px; - padding: 0px; - overflow-x: hidden; - overflow-y: auto; - height: 100%; - width: 100%; - } -} - -.play-area { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - position: relative; - - .action-menu { - position: absolute; - top: 0px; - right: 5px; - } - - .round-area { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 100%; - - .others-picked { - .card { - position: absolute; - z-index: -10; - transition: transform 1s, left 1s, right 1s, top 1s ease-out - } - } - } -} - -.call { - background-color: #000000; - color: #ffffff; - flex-shrink: 0; - - & > .call-text { - & > .slot { - text-decoration: underline; - - &:empty { - display: inline-block; - height: 1.2em; - border-bottom: 1px solid #ffffff; - min-width: 40px; - max-width: 80px; - } - } - } -} - -.response { - background-color: #ffffff; - color: #000000; - - &.picked { - opacity: 0.5; - } -} - -.empty-spot { - opacity: 0.25; -} - -.winner { - max-width: 500px; - margin: 15px auto; - padding: 5px 25px 5px 25px; - - & > h1 { - margin: 5px; - - & > .fa { - width: 100%; - text-align: center; - color: #FFD700; - } - } - - & > h2 { - margin: 5px; - - &::first-letter { - text-transform: uppercase; - } - } - - & > h3 { - margin: 5px; - width: 100%; - text-align: right; - font-style: italic; - } -} - -#next-round-button { - display: block; - margin: 10px auto; -} - -.picked { - ol { - display: inline; - margin: 0px; - padding: 0px; - list-style-type: none; - white-space: nowrap; - - & > li { - display: inline-block; - white-space: normal; - - .card { - margin: 10px 5px 10px 5px; - } - - & > .response { - display: inline-block; - position: relative; - - & > .withdraw-button { - position: absolute; - left: -4px; - bottom: -9px; - } - } - - &:not(:first-child) { - margin-left: -(@card-width * @card-overlap); - @media (min-width: @enhance-width) { - margin-left: -(@card-width-enhanced * @card-overlap); - } - } - } - } - - .play-button { - display: inline; - margin-left: -45px; - } -} - -ol.considering { - margin: 0px; - padding: 0px; - list-style-type: none; - flex-shrink: 0; - white-space: nowrap; - - & > li { - display: inline-block; - white-space: normal; - - .card { - margin: 10px 5px 10px 5px; - } - - &:not(:first-child) { - margin-left: -(@card-width * @card-overlap); - @media (min-width: @enhance-width) { - margin-left: -(@card-width-enhanced * @card-overlap); - } - } - } -} - -.choose-button { - margin-left: -45px; -} - -ol.played { - margin: 0px; - padding: 0px; - list-style-type: none; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - - & > li { - display: inline-block; - - & > ol { - margin: 0px 10px 0px 10px; - padding: 0px; - list-style-type: none; - display: inline-block; - position: relative; - white-space: nowrap; - - & > li { - white-space: normal; - display: inline-block; - - .card { - margin: 10px 5px 10px 5px; - } - } - - & > li > .response { - display: inline-block; - } - } - } -} - -.hand { - padding: 0; - list-style-type: none; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - width: 100%; - - &.disabled { - opacity: 0.5; - } -} - -.scores-toggle { - flex: none; - width: 50px; - font-size: 20px; - margin: 0px 5px; - padding: 0px; -} - -.badge { - display: inline-block; - - color: #ffffff; - background-color: #555555; - - padding: 5px; - border-radius: 2px; - - line-height: normal; - - position: relative; - - transition: opacity 1s; - - white-space: nowrap; - - &.hide { - opacity: 0; - } - - &:after { - right: 100%; - top: 50%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border-color: rgba(85, 85, 85, 0); - border-right-color: #555555; - border-width: 10px; - margin-top: -10px; - } - - @media (min-width: @enhance-width) { - display: none !important; - } -} - -.hide-scores { - #scores { - transform: translate(0px); - } - - #warning-drawer { - @media (min-width: @enhance-width) { - left: 0px; - } - } - - .badge { - display: inline-block !important; - } -} - -#scores { - position: fixed; - top: 0; - bottom: 0; - width: @sidebar-width; - left: -@sidebar-width; - overflow: auto; - z-index: 2; - background-color: #fff; - transition: transform @sidebar-transition-time; - @media (min-width: @enhance-width) { - transform: translate(@sidebar-width); - } - - &.shownAsOverlay { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - } - - &-title { - display: flex; - justify-content: space-between; - padding: 0px 5px 0px 5px; - } - - &.active { - transform: translate(@sidebar-width) !important; - } - - & > table { - table-layout: fixed; - - & > thead > tr > th { - text-align: center; - padding: 5px; - - &.state, &.score { - width: 30px; - } - } - - & > tbody > tr { - padding: 0px; - - &.left { - opacity: 0.3; - } - - &.you { - font-style: italic; - } - - &.skipping { - opacity: 0.6; - } - - &.czar > td.name { - font-weight: bold; - } - - & > td { - padding: 0px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &.state, &.score { - text-align: center; - } - } - } - } -} - -#info-bar { - width: 100%; - background-color: #cccccc; - padding: 3px 10px 3px 10px; - text-align: center; -} - -#warning-drawer { - position: fixed; - bottom: 0px; - left: 0; - right: 0px; - - z-index: 100; - - transition: opacity 1s, left @sidebar-transition-time; - - max-height: 75%; - - & > .contents { - overflow: auto; - } - - & > .toggle { - float: right; - margin: 5px; - background-color: #FFCC00; - opacity: 0.75; - } - - & > .top { - clear: both; - width: 100%; - height: 0px; - background-color: #FFCC00; - } - - &.shut { - & > .top { - height: 3px; - } - - & > .contents { - display: none; - } - } - - &.hidden { - opacity: 0; - } - - @media (min-width: @enhance-width) { - left: @sidebar-width; - } - - .notice { - display: flex; - align-items: center; - align-content: center; - justify-content: space-between; - - background-color: #FFCC00; - padding: 2px; - - & > h3 { - margin: 2px 5px; - } - - & > .actions { - display: flex; - flex-wrap: nowrap; - - & > button { - margin: 2px; - } - } - } -} - -#history { - position: relative; - margin: 0px; - padding: 0px; - - h1 { - margin: 10px; - } - - ul { - display: block; - margin: 0px; - padding: 0px; - } - - li { - display: block; - } - - button { - position: absolute; - top: 0px; - right: 5px; - } - - .round { - margin: 0px 0px 20px 10px; - - div { - display: flex; - flex-direction: column; - margin: 0px 10px 0px 0px; - } - - .card { - margin: 0px 5px 5px 5px; - } - - .who { - display: block; - width: @card-width; - text-align: center; - margin: 0px 5px 0px 5px; - height: 25px; - line-height: 25px; - - @media (min-width: @enhance-width) { - width: @card-width-enhanced; - } - } - - .who.won { - font-weight: bold; - } - } - - .responses { - background-color: #dddddd; - margin: 0px 10px 0px 0px; - border-radius: 10px; - } - - .round, .plays, .responses { - display: flex; - } -} diff --git a/app/assets/stylesheets/massivedecks.less b/app/assets/stylesheets/massivedecks.less deleted file mode 100644 index ee8e532..0000000 --- a/app/assets/stylesheets/massivedecks.less +++ /dev/null @@ -1,183 +0,0 @@ -@import "variables.less"; -@import "start.less"; -@import "config.less"; -@import "lobby.less"; - -body { - background-image: url("/images/bgnoise_lg.png"); - background-color: @background-color; - width: 100%; - height: auto; - padding: 0px; - margin: 0px; - - font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif; -} - -#content-wrapper { - min-height: 100%; - - margin-left: 0px; - transition: margin-left @sidebar-transition-time; - - @media (min-width: @enhance-width) { - margin-left: @sidebar-width; - } -} - -.link { - cursor: pointer; -} - -.link.disabled { - opacity: 0.5; -} - -header { - position: fixed; - top: 0; - left: 0; - @media (min-width: @enhance-width) { - left: @sidebar-width; - } - right: 0; - z-index: 2; - transition: left @sidebar-transition-time; - - & > div { - width: 100%; - - & > div { - width: 100%; - display: flex; - justify-content: space-between; - align-items: baseline; - padding: 0px; - - & > .score-buttons { - position: relative; - - & > .badge { - position:absolute; - top: 15px; - } - } - } - } -} - -#title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.menu { - button { - flex: none; - width: 50px; - font-size: 20px; - margin: 0px 5px; - padding: 0px; - } -} - -.hide-scores { - @media (min-width: @enhance-width) { - header { - left: 0; - } - - #content-wrapper { - margin-left: 0; - } - } -} - -#error-panel { - display: block; - margin: 0px; - padding: 0px; - list-style-type: none; - position: fixed; - bottom: 10px; - right: 10px; - max-height: 50%; - overflow: auto; - z-index: 10; -} - -.error { - position: relative; - display: block; - border: 1px solid #ff0000; - background-color: #ffaaaa; - border-radius: 10px; - padding: 10px; - width: 200px; - text-align: center; - margin: 5px; - - .mui-divider { - margin: 10px; - } - - h5 { - margin: 0px; - } - - & > div > a { - display: block; - position: absolute; - top: 5px; - right: 10px; - text-align: center; - font-size: 20px; - } - - p { - text-align: left; - margin: 0px; - } - - .mui-divider { - margin: 2px; - } -} - -#mui-overlay { - display: flex; - justify-content: center; - align-content: flex-start; - align-items: flex-start; - - .overlay { - display: block; - } - - .close-link { - text-align: right; - } -} - -.overlay { - display: none; - - max-width: 500px; - margin: 20px; - - & input { - width: 100%; - } -} - -.clear { - clear: both; -} - -.input-error { - display: block; - color: #ff0000; - width: 90%; - margin: 0px auto 0px auto; -} diff --git a/app/assets/stylesheets/start.less b/app/assets/stylesheets/start.less deleted file mode 100644 index f235df9..0000000 --- a/app/assets/stylesheets/start.less +++ /dev/null @@ -1,143 +0,0 @@ -@import "variables.less"; -@start-width: 300px; -@degrade-fork-banner: (@start-width + 100px); - -#start-screen { - width: 100%; -} - -#start-screen-content { - max-width: @start-width; - margin: 15px auto; - - h1 { - width: 100%; - margin: 0px; - text-align: center; - font-weight: bold; - padding: 0px 0px 10px 0px; - margin: 0px 0px 10px 0px; - } - - button { - width: 100%; - margin: 15px auto 0px auto; - } - - .rejoin { - text-align: center; - padding: 0px 0px 10px 0px; - - ul { - display: inline; - list-style: none; - padding: 0px; - - li { - display: inline; - - &:not(:last-of-type)::after { - content: ", " - } - - &:nth-last-of-type(2)::after { - content: " or " - } - } - } - } - - .mui-textfield { - width: 90%; - margin: 10px auto 0px auto; - } - - & > .mui-divider { - width: 200px; - } - - .about-link { - display: inline-block; - width: 100%; - text-align: center; - margin: 20px 0px 0px 0px; - padding: 10px 0px 0px 0px; - } -} - -.nickname-entry { - margin-bottom: 0px; -} - -#forkongithub { - margin-top: 5px; - overflow: hidden; - pointer-events: none; - - @media (min-width: @degrade-fork-banner) { - margin-top: 0px; - position: absolute; - top: 0; - right: 0; - z-index: 100; - height: 150px; - width: 150px; - } - - div { - padding: 2px 0; - background-color: #333333; - background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); - box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); - font: 700 13px "Helvetica Neue", Helvetica, Arial, sans-serif; - z-index: 9999; - pointer-events: auto; - - @media (min-width: @degrade-fork-banner) { - position: absolute; - top: 42px; - right: -43px; - transform: rotate(45deg); - } - - a { - color: #fff; - text-decoration: none; - text-shadow: 0 -1px rgba(0, 0, 0, 0.5); - text-align: center; - - width: 100%; - line-height: 20px; - - display: inline-block; - padding: 2px 0; - - border-width: 1px 0; - border-style: dotted; - border-color: #fff; - border-color: rgba(255, 255, 255, 0.7); - - @media (min-width: @degrade-fork-banner) { - width: 200px; - } - } - } -} - -footer { - max-width: @start-width; - margin: 30px auto; - padding: 10px; - - text-align: center; - font-size: 0.8em; - - img { - height: 50px; - margin: 0px 0px 10px 0px; - } - - p:last-child { - margin: 0px; - } -} diff --git a/app/assets/stylesheets/variables.less b/app/assets/stylesheets/variables.less deleted file mode 100644 index e643153..0000000 --- a/app/assets/stylesheets/variables.less +++ /dev/null @@ -1,13 +0,0 @@ -@background-color: #e5e5e5; - -@enhance-width: 768px; // If changed, should be changed in the elm (Lobby.elm) too. -@sidebar-width: 200px; -@sidebar-transition-time: 0.2s; - -@card-width: 135px; -@card-height: 160px; - -@card-width-enhanced: 180px; -@card-height-enhanced: 180px; - -@card-overlap: 0.80; diff --git a/app/massivedecks/Config.scala b/app/massivedecks/Config.scala deleted file mode 100644 index cf83b80..0000000 --- a/app/massivedecks/Config.scala +++ /dev/null @@ -1,43 +0,0 @@ -package massivedecks - -import javax.inject.Inject - -import play.api.Configuration -import play.api.mvc.RequestHeader - -object Config { - /** - * Factory for dependency injection. - */ - class Factory @Inject()(base: Configuration) { - def apply(request: RequestHeader) = new Config(base, request) - } -} -class Config(base: Configuration, request: RequestHeader) { - def getString(key: String) = base.getString(key) - - def requestProtocol = request.headers.get("X-Forwarded-Proto") match { - case Some(proto) => proto - case None => if (request.secure) { "https" } else { "http" } - } - - def protocol = canonicalProtocol.getOrElse(requestProtocol) - def host = canonicalHost.getOrElse(request.host) - def path = canonicalPath.getOrElse("/") - - def url = s"$protocol://$host$path" - - def canonicalProtocol = getString("md_protocol") - def canonicalHost = getString("md_host") - def canonicalPath = getString("md_path") - - def requestIsCanonical = (canonicalProtocol.forall(proto => proto == requestProtocol) - && canonicalHost.forall(host => host == request.host) - && canonicalPath.forall(path => request.path.startsWith(path))) - - def canonicalUrlTo(path: String) = s"$protocol://$host$path" - - def version = getString("md_release_version").getOrElse { - getString("md_git_version").map(v => s"${v.take(7)}-dev").getOrElse("") - } -} diff --git a/app/massivedecks/CoreModule.scala b/app/massivedecks/CoreModule.scala deleted file mode 100644 index e71e990..0000000 --- a/app/massivedecks/CoreModule.scala +++ /dev/null @@ -1,13 +0,0 @@ -package massivedecks - -import com.google.inject.AbstractModule -import massivedecks.stores.{InMemoryStore, LobbyStore} - -/** - * Guice module for the application. - */ -class CoreModule extends AbstractModule { - def configure = { - bind(classOf[LobbyStore]).to(classOf[InMemoryStore]) - } -} diff --git a/app/massivedecks/Util.scala b/app/massivedecks/Util.scala deleted file mode 100644 index 2ab7d7a..0000000 --- a/app/massivedecks/Util.scala +++ /dev/null @@ -1,20 +0,0 @@ -package massivedecks - -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{Await, Future, Promise} -import scala.util.Try - -/** - * Utility functions. - */ -object Util { - - /** - * Wait for the given amount of time. - * - * @param duration The time to wait for. - * @return A future to wait on. - */ - def wait(duration: FiniteDuration): Try[Future[Nothing]] = Try(Await.ready(Promise().future, duration)) - -} diff --git a/app/massivedecks/cardcast/CardcastAPI.scala b/app/massivedecks/cardcast/CardcastAPI.scala deleted file mode 100644 index ff2cb51..0000000 --- a/app/massivedecks/cardcast/CardcastAPI.scala +++ /dev/null @@ -1,91 +0,0 @@ -package massivedecks.cardcast - -import javax.inject.Inject - -import scala.collection.mutable.ListBuffer -import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext, Future, Promise} -import scala.util.Try - -import play.api.cache._ -import play.api.libs.json.JsValue -import play.api.libs.ws.WSClient -import massivedecks.exceptions.BadRequestException -import massivedecks.models.{Errors, Game} -import massivedecks.models.cardcast.CardcastDeck - -class CardcastAPI @Inject() (@NamedCache("cardcast") cache: CacheApi, ws: WSClient) (implicit ec: ExecutionContext) { - private val apiUrl: String = "https://api.cardcastgame.com/v1" - - private def deckUrl(id: String): String = { - BadRequestException.verify(id.length > 0, Errors.DeckNotFound()) - s"$apiUrl/decks/$id" - } - private def cardsUrl(id: String): String = s"${deckUrl(id)}/cards" - - /** - * Get a future that either returns the deck or blows up after 10 seconds of waiting on Cardcast. - * - * @param id The id of the deck to get. - * @return See above. - */ - def deck(id: String): Future[CardcastDeck] = cache.get(id).map(Future.successful).getOrElse { - requestDeck(id) - } - - private def requestDeck(id: String): Future[CardcastDeck] = { - val deckInfo = requestJson(deckUrl(id)).map(parseInfo) - val cards = requestJson(cardsUrl(id)).map(parseCards) - - val futureDeck = for { - name <- deckInfo - (calls, responses) <- cards - } yield CardcastDeck(id, name, calls, responses) - - futureDeck.onSuccess { case deck => - cache.set(id, deck, CardCastAPI.cacheDuration) - } - - futureDeck - } - - private def requestJson(url: String): Future[JsValue] = - ws.url(url).get().map(response => response.json) - - private def parseInfo(deckInfo: JsValue): String = { - Try( - (deckInfo \ "name").validate[String].get - ).getOrElse { - parseError(deckInfo) - } - } - - private def parseCards(cards: JsValue): (List[Game.Call], List[Game.Response]) = { - Try({ - val calls = ListBuffer[Game.Call]() - val responses = ListBuffer[Game.Response]() - for (call: JsValue <- (cards \ "calls").validate[List[JsValue]].get) { - calls += Game.Call((call \ "id").validate[String].get, (call \ "text").validate[List[String]].get) - } - for (response: JsValue <- (cards \ "responses").validate[List[JsValue]].get) { - responses += Game.Response((response \ "id").validate[String].get, (response \ "text").validate[List[String]].get.head) - } - (calls.toList, responses.toList) - }).getOrElse { - parseError(cards) - } - } - - private def parseError[T](error: JsValue): T = { - (error \ "id").validate[String].asOpt match { - case Some("not_found") => throw BadRequestException(Errors.DeckNotFound()) - case Some(errorName) => throw new Exception(s"Cardcast gave an unknown error ('$errorName') when trying to retrieve the deck.") - case None => throw new Exception(s"Cardcast gave an error that couldn't be parsed when trying to retrieve the deck.") - } - } -} -object CardCastAPI { - val cacheDuration = 15.minutes - - def wait(duration: FiniteDuration): Try[Future[Nothing]] = Try(Await.ready(Promise().future, duration)) -} diff --git a/app/massivedecks/controllers/API.scala b/app/massivedecks/controllers/API.scala deleted file mode 100644 index 61efbab..0000000 --- a/app/massivedecks/controllers/API.scala +++ /dev/null @@ -1,141 +0,0 @@ -package massivedecks.controllers - -import javax.inject.Inject - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -import akka.stream.scaladsl.{Flow, Sink, Source} -import play.api.libs.json.{JsResult, JsValue, Json} -import play.api.mvc._ -import massivedecks.exceptions._ -import massivedecks.models.Errors -import massivedecks.models.Game.Formatters._ -import massivedecks.models.Lobby.Formatters._ -import massivedecks.models.Lobby.GameCodeAndSecret -import massivedecks.models.Player -import massivedecks.models.Player.Formatters._ -import massivedecks.stores.LobbyStore -import play.api.libs.streams.Streams - -class API @Inject()(store: LobbyStore) (implicit context: ExecutionContext) extends Controller { - - def createLobby() = Action(parse.json) { request: Request[JsValue] => - wrap { - val name = (request.body \ "name").validate[String].getOrElse(throw BadRequestException(Errors.InvalidName())) - val gameCode = store.newLobby(name) - store.performInLobby(gameCode) { lobby => - Json.toJson(GameCodeAndSecret(lobby.gameCode, lobby.owner)) - } - } - } - - def notifications(gameCode: String) = WebSocket.acceptOrResult[String, String] { requestHeader => Future { - Try { - store.performInLobby(gameCode) { lobby => - lobby.register() - } - } match { - case Success((iteratee, enumerator)) => - val (sub, _) = Streams.iterateeToSubscriber(iteratee) - val pub = Streams.enumeratorToPublisher(enumerator) - Right(Flow.fromSinkAndSource(Sink.fromSubscriber(sub), Source.fromPublisher(pub))) - - case Failure(error) => - error match { - case exception: RequestException => - Left(Status(exception.statusCode)(exception.details.toJson())) - case _ => - throw error - } - } - } } - - def getLobby(gameCode: String) = Action { - wrap(store.readFromLobby(gameCode) { lobby => - Json.toJson(lobby.lobby) - }) - } - - def command(gameCode: String) = Action(parse.json) { request: Request[JsValue] => - wrap { - store.performInLobby(gameCode) { lobby => - val secret = (request.body \ "secret").validate[Player.Secret].getOrElse(throw ForbiddenException(Errors.BadlyFormedSecret())) - extractCommandArgument((request.body \ "command").validate[String]) match { - case "addDeck" => lobby.addDeck(secret, extractCommandArgument((request.body \ "playCode").validate[String])) - case "newGame" => lobby.newGame(secret) - case "play" => lobby.play(secret, extractCommandArgument((request.body \ "ids").validate[List[String]])) - case "choose" => lobby.choose(secret, extractCommandArgument((request.body \ "winner").validate[Int])) - case "getLobbyAndHand" => lobby.getLobbyAndHand(secret) - case "skip" => lobby.skip(secret, extractCommandArgument((request.body \ "players").validate[Set[Player.Id]])) - case "back" => lobby.back(secret) - case "enableRule" => lobby.enableRule(secret, extractCommandArgument((request.body \ "rule").validate[String])) - case "disableRule" => lobby.disableRule(secret, extractCommandArgument((request.body \ "rule").validate[String])) - case "redraw" => lobby.redraw(secret) - case "setPassword" => lobby.setPassword(secret, (request.body \ "password").asOpt[String]) - case unknown => throw BadRequestException(Errors.InvalidCommand(unknown)) - } - } - } - } - - def newPlayer(gameCode: String) = Action(parse.json) { request: Request[JsValue] => - wrap { - val name = (request.body \ "name").validate[String].getOrElse(throw BadRequestException(Errors.InvalidName())) - store.performInLobby(gameCode) { lobby => - Json.toJson(lobby.newPlayer(name, (request.body \ "password").asOpt[String])) - } - } - } - - def getPlayer(gameCode: String, playerId: Int) = Action(parse.json) { request: Request[JsValue] => - wrap(store.readFromLobby(gameCode) { lobby => - val secret = extractSecret(request) - Json.toJson(lobby.getHand(secret)) - }) - } - - def getHistory(gameCode: String) = Action { request => - wrap(store.readFromLobby(gameCode) { lobby => - Json.toJson(lobby.gameHistory()) - }) - } - - def newAi(gameCode: String) = Action(parse.json) { request: Request[JsValue] => - wrap(store.performInLobby(gameCode) { lobby => - val secret = extractSecret(request) - lobby.newAi(secret) - Json.toJson("") - }) - } - - def leave(gameCode: String, playerId: Int) = Action(parse.json) { request: Request[JsValue] => - wrap(store.performInLobby(gameCode) { lobby => - val secret = extractSecret(request) - lobby.leave(secret) - Json.toJson("") - }) - } - - private def extractSecret(request: Request[JsValue]) = - request.body.validate[Player.Secret].getOrElse(throw ForbiddenException(Errors.BadlyFormedSecret())) - - private def extractCommandArgument[T](value: JsResult[T]) = - value.getOrElse(throw BadRequestException(Errors.InvalidCommand(""))) - - private def wrap(attempt: => JsValue) = { - (Try(attempt) match { - case Success(result) => - Ok(result) - - case Failure(error) => - error match { - case exception: RequestException => - Status(exception.statusCode)(exception.details.toJson()) - case _ => - throw error - } - }).as(JSON) - } - -} diff --git a/app/massivedecks/controllers/ErrorHandler.scala b/app/massivedecks/controllers/ErrorHandler.scala deleted file mode 100644 index 70687da..0000000 --- a/app/massivedecks/controllers/ErrorHandler.scala +++ /dev/null @@ -1,51 +0,0 @@ -package massivedecks.controllers - -import java.net.URLEncoder -import java.nio.charset.StandardCharsets -import javax.inject.{Inject, Singleton} - -import scala.concurrent.Future - -import play.api.http.ContentTypes._ -import play.api.http.HttpErrorHandler -import play.api.mvc.Results._ -import play.api.mvc._ -import play.api.Logger -import massivedecks.Config - -@Singleton -class ErrorHandler @Inject()(getConfig: Config.Factory) extends HttpErrorHandler { - - def onClientError(request: RequestHeader, statusCode: Int, rawMessage: String) = Future.successful { - val config = getConfig(request) - val (message, description) = statusCode match { - case 400 => ("Bad Request", "there was a problem with the request you made") - case 403 => ("Forbidden", ",you are not able to access the resource you requested") - case 404 => ("Not Found", "the page you tried to access doesn't seem to exist") - case _ => ("Client Error", "there was a problem with the request you made") - } - NotFound(views.html.error( - config.url, config.version, statusCode, message, description, - bugReportBody(config, request.path, s"$statusCode", rawMessage))).as(HTML) - } - - def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = Future.successful { - val config = getConfig(request) - val description = "the server had a problem trying to handle your request" - Logger.error(exception.getMessage) - NotFound(views.html.error( - config.url, config.version, 500, "Server Error", description, - bugReportBody(config, request.path, exception.getClass.getSimpleName, exception.getMessage))).as(HTML) - } - - def bugReportBody(config: Config, path: String, error: String, detail: String) = { - val extra = if (detail == null || detail.isEmpty) "" else s"\n\nDetail:\n\t${detail.take(1000)}" - val v = config.version - val version = if (v.isEmpty) "Not Specified" else v - val text = - (s"I was [a short explanation of what you were doing] when I got sent to '$path' and got a '$error' error.$extra" - ++ s"\n\nApplication Info:\n\tVersion: $version\n\tURL: ${config.url}") - URLEncoder.encode(text, StandardCharsets.UTF_8.name()) - } - -} diff --git a/app/massivedecks/controllers/Pages.scala b/app/massivedecks/controllers/Pages.scala deleted file mode 100644 index ea93cdf..0000000 --- a/app/massivedecks/controllers/Pages.scala +++ /dev/null @@ -1,46 +0,0 @@ -package massivedecks.controllers - -import javax.inject.Inject - -import play.api.libs.json.Json -import play.api.mvc.{Action, Controller} -import play.api.cache.Cached -import play.api.Play.current - -import massivedecks.Config - -class Pages @Inject() (cached: Cached, getConfig: Config.Factory) extends Controller { - - def index() = Action { request => - val config = getConfig(request) - if (!config.requestIsCanonical) { - Redirect(config.canonicalUrlTo(request.path)) - } else { - Ok(views.html.index(config.url, config.version)).as(HTML) - } - } - - def manifest() = cached("manifest")(Action { request => - val config = getConfig(request) - val json = Json.obj( - "name" -> "Massive Decks", - "description" -> "An online party game inspired by Cards Against Humanity.", - "icons" -> Json.arr( - Json.obj( - "src" -> controllers.routes.Assets.versioned("icon.png").url, - "sizes" -> "192x192", - "type" -> "image/png", - "density" -> 4.0 - ) - ), - "lang" -> "en", - "start_url" -> config.path, - "display" -> "standalone", - "orientation" -> "portrait-primary", - "background_color" -> "#e5e5e5", - "theme_color" -> "#2196F3" - ) - Ok(json.toString).as(withCharset("application/manifest+json")) - }) - -} diff --git a/app/massivedecks/exceptions/BadRequestException.scala b/app/massivedecks/exceptions/BadRequestException.scala deleted file mode 100644 index ffba3a4..0000000 --- a/app/massivedecks/exceptions/BadRequestException.scala +++ /dev/null @@ -1,26 +0,0 @@ -package massivedecks.exceptions - -import massivedecks.models.Errors.ErrorDetails -import play.api.http.Status - -/** - * An exception representing a request that failed due to the request being faulty. - * Represents a 400 error. - * - * @param details The details of the error to be given to the requester. - */ -case class BadRequestException(details: ErrorDetails) extends RequestException { - val statusCode = Status.BAD_REQUEST -} -object BadRequestException { - /** - * Verify the given requirement - if not met, throw a BadRequestException. - * @param requirement The requirement to verify - * @param details The details. - */ - def verify(requirement: Boolean, details: ErrorDetails): Unit = { - if (!requirement) { - throw BadRequestException(details) - } - } -} diff --git a/app/massivedecks/exceptions/ForbiddenException.scala b/app/massivedecks/exceptions/ForbiddenException.scala deleted file mode 100644 index b03418b..0000000 --- a/app/massivedecks/exceptions/ForbiddenException.scala +++ /dev/null @@ -1,26 +0,0 @@ -package massivedecks.exceptions - -import massivedecks.models.Errors.ErrorDetails -import play.api.http.Status - -/** - * An exception representing a request that failed due to the request not being authorized. - * Represents a 403 error. - * - * @param details The details of the error to be given to the requester. - */ -case class ForbiddenException(details: ErrorDetails) extends RequestException { - val statusCode = Status.FORBIDDEN -} -object ForbiddenException { - /** - * Verify the given requirement - if not met, throw a ForbiddenException. - * @param requirement The requirement to verify - * @param details The details. - */ - def verify(requirement: Boolean, details: ErrorDetails): Unit = { - if (!requirement) { - throw ForbiddenException(details) - } - } -} diff --git a/app/massivedecks/exceptions/NotFoundException.scala b/app/massivedecks/exceptions/NotFoundException.scala deleted file mode 100644 index 5b75f85..0000000 --- a/app/massivedecks/exceptions/NotFoundException.scala +++ /dev/null @@ -1,14 +0,0 @@ -package massivedecks.exceptions - -import massivedecks.models.Errors.ErrorDetails -import play.api.http.Status - -/** - * An exception representing a request that failed due to the requested resource not being found. - * Represents a 404 error. - * - * @param details The details of the error to be given to the requester. - */ -case class NotFoundException(details: ErrorDetails) extends RequestException { - val statusCode = Status.NOT_FOUND -} diff --git a/app/massivedecks/exceptions/RequestException.scala b/app/massivedecks/exceptions/RequestException.scala deleted file mode 100644 index 155cd37..0000000 --- a/app/massivedecks/exceptions/RequestException.scala +++ /dev/null @@ -1,8 +0,0 @@ -package massivedecks.exceptions - -import massivedecks.models.Errors.ErrorDetails - -trait RequestException extends Exception { - def statusCode: Int - def details: ErrorDetails -} diff --git a/app/massivedecks/exceptions/RequestFailedException.scala b/app/massivedecks/exceptions/RequestFailedException.scala deleted file mode 100644 index 332379b..0000000 --- a/app/massivedecks/exceptions/RequestFailedException.scala +++ /dev/null @@ -1,26 +0,0 @@ -package massivedecks.exceptions - -import massivedecks.models.Errors.ErrorDetails -import play.api.http.Status - -/** - * An exception representing a request that failed due to a request being made by the server failing. - * Represents a 502 error. - * - * @param details The details of the error to be given to the requester. - */ -case class RequestFailedException(details: ErrorDetails) extends RequestException { - val statusCode = Status.BAD_GATEWAY -} -object RequestFailedException { - /** - * Verify the given requirement - if not met, throw a RequestFailedException. - * @param requirement The requirement to verify - * @param details The details. - */ - def verify(requirement: Boolean, details: ErrorDetails): Unit = { - if (!requirement) { - throw RequestFailedException(details) - } - } -} diff --git a/app/massivedecks/lobby/Config.scala b/app/massivedecks/lobby/Config.scala deleted file mode 100644 index 4bbd427..0000000 --- a/app/massivedecks/lobby/Config.scala +++ /dev/null @@ -1,35 +0,0 @@ -package massivedecks.lobby - -import massivedecks.models.Game.{Config => ConfigModel} -import massivedecks.models.cardcast.CardcastDeck -import massivedecks.notifications.Notifiers - -class Config(notifiers: Notifiers) { - - var decks: List[CardcastDeck] = List() - var houseRules: Set[String] = Set() - var password: Option[String] = None - - def config = ConfigModel(decks.map(deck => deck.info), houseRules, password) - - def addDeck(deck: CardcastDeck): Unit = { - decks = decks :+ deck - notifiers.configChange(config) - } - - def addHouseRule(rule: String): Unit = { - houseRules = houseRules + rule - notifiers.configChange(config) - } - - def removeHouseRule(rule: String): Unit = { - houseRules = houseRules - rule - notifiers.configChange(config) - } - - def setPassword(newPass: Option[String]) = { - password = newPass.flatMap(pass => if (pass.isEmpty) None else newPass) - notifiers.configChange(config) - } - -} diff --git a/app/massivedecks/lobby/Deck.scala b/app/massivedecks/lobby/Deck.scala deleted file mode 100644 index f35164a..0000000 --- a/app/massivedecks/lobby/Deck.scala +++ /dev/null @@ -1,100 +0,0 @@ -package massivedecks.lobby - -import java.util.UUID - -import scala.util.Random - -import massivedecks.exceptions.BadRequestException -import massivedecks.models.Errors -import massivedecks.models.Game.{Call, Response} -import massivedecks.models.cardcast.CardcastDeck - -/** - * A live deck of cards in a game, constructed from a collection of cardcast decks. - * - * @param decks The cardcast decks. - * @throws BadRequestException with key "invalid-deck-configuration" if there are no calls or responses. Has value - * "reason" explaining the issue. - */ -case class Deck(decks: List[CardcastDeck]) { - /** - * The current calls in the deck. - */ - var calls: List[Call] = List() - - /** - * The current responses in the deck. - */ - var responses: List[Response] = List() - - resetCalls() - resetResponses() - BadRequestException.verify(calls.nonEmpty, Errors.InvalidDeckConfiguration("The decks for the game have no calls.")) - BadRequestException.verify(responses.nonEmpty, Errors.InvalidDeckConfiguration("The decks for the game have no responses.")) - - /** - * Draw a call from the deck, reshuffling if the deck runs out. - * - * @return The drawn call. - */ - def drawCall(): Call = { - if (calls.isEmpty) { - resetCalls() - } - val drawn = calls.head - calls = calls.tail - drawn - } - - /** - * Draw the given number of responses, reshuffling if the deck runs out. - * - * @param count The number of responses to draw. - * @return The drawn responses. - */ - def drawResponses(count: Int): List[Response] = { - if (responses.length < count) { - val partial = responses.take(count) - resetResponses() - partial ++ drawResponses(count - partial.length) - } else { - val drawn = responses.take(count) - responses = responses.drop(count) - drawn - } - } - - /** - * Populate the active deck with all calls, shuffling them into a random order. - */ - def resetCalls(): Unit = { - calls = decks.flatMap(deck => deck.calls).map(call => call.copy(id = UUID.randomUUID().toString)) - shuffleCalls() - } - - /** - * Populate the active deck with all responses, shuffling them into a random order. - */ - def resetResponses(): Unit = { - responses = decks.flatMap(deck => deck.responses).map(response => response.copy(id = UUID.randomUUID().toString)) - shuffleResponses() - } - - /** - * Shuffle the active deck of calls and responses. - */ - def shuffle(): Unit = { - shuffleCalls() - shuffleResponses() - } - - /** - * Shuffle the active deck of calls. - */ - def shuffleCalls(): Unit = calls = Random.shuffle(calls) - - /** - * Shuffle the active deck of responses. - */ - def shuffleResponses(): Unit = responses = Random.shuffle(responses) -} diff --git a/app/massivedecks/lobby/Game.scala b/app/massivedecks/lobby/Game.scala deleted file mode 100644 index f560b57..0000000 --- a/app/massivedecks/lobby/Game.scala +++ /dev/null @@ -1,312 +0,0 @@ -package massivedecks.lobby - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Random - -import massivedecks.models.Game._ -import massivedecks.exceptions.BadRequestException -import massivedecks.models.{Errors, Player} -import massivedecks.notifications.Notifiers -import massivedecks.Util - -/** - * The state of the game. - * - * @param players The players in the lobby the game is in. - * @param config The configuration for the lobby the game is in. - * @param notifiers The notifiers for the lobby the game is in. - */ -class Game(players: Players, config: Config, notifiers: Notifiers) (implicit context: ExecutionContext) { - - import Game._ - - /** - * The history of the game. - */ - var history: List[Round.Finished] = List() - - private val deck: Deck = Deck(config.decks) - private var czarIndex = 0 - private var hands: Map[Player.Id, Hand] = players.ids.map(id => { - val hand = new Hand(deck.drawResponses(Hand.size)) - notifiers.handChange(id, hand) - id -> hand - }).toMap - private var roundProgress: RoundProgress = Playing - private var roundTimedOut: Boolean = false - - // Initial values overwritten by beginRound() call below, just keeping the compiler happy. - private var czar: Player.Id = Player.Id(0) - private var call: Call = deck.drawCall() - private var playedCards: Map[Player.Id, Option[List[Response]]] = Map() - beginRound(true) - - /** - * @return The ids of the players that are taking part in the current round. - */ - def playersInRound = playedCards.keySet - - /** - * @return The model representing the current round. - */ - def round: Round = roundProgress match { - case Playing => - Round(czar, call, Round.State.Playing(roundTimedOut, numberOfPlayersWhoHavePlayed)) - case Judging(shuffled, _) => - Round(czar, call, Round.State.Judging(roundTimedOut, shuffled)) - case Finished(shuffled, playedOrder, winner) => - Round(czar, call, Round.State.Finished(shuffled, PlayedByAndWinner(playedOrder, winner))) - } - - /** - * Add a player to the game, giving them a hand. - * - * @param playerId The id of the player to add. - */ - def addPlayer(playerId: Player.Id): Unit = { - hands += (playerId -> Hand(deck.drawResponses(Hand.size))) - } - - /** - * Remove a player from the game, removing their hand and any played cards for the round. - * Note this may invalidate the round (if the player is the czar) or finish the round. - * - * @param playerId The id of the player to remove. - */ - def playerLeft(playerId: Player.Id): Unit = { - hands = hands.filterKeys(id => id != playerId) - playedCards = playedCards.filterKeys(id => id != playerId) - if (playerId == round.czar) { - invalidateRound() - } else { - checkIfFinishedPlaying() - } - } - - /** - * Begin the round. - * - * @param firstRound If this is the first round in the game. - * @throws BadRequestException with key "not-enough-players" if there are not enough players in the game to begin a - * round. The value "required" gives the minimum number of players needed for the request - * to succeed. - */ - def beginRound(firstRound: Boolean = false) = { - if (players.amount < Players.minimum) { - throw BadRequestException(Errors.NotEnoughPlayers(Players.minimum)) - } - - czar = nextCzar() - call = deck.drawCall() - - if (firstRound) { - notifiers.gameStart(czar, call) - } else { - notifiers.roundStart(czar, call) - } - - players.updatePlayers(players.setPlayerStatus(Player.NotPlayed)) - players.updatePlayer(czar, players.setPlayerStatus(Player.Czar)) - - playedCards = (for (player <- players.players if !Player.Status.notInRound.contains(player.status) && !player.left) yield player.id -> None).toMap - roundProgress = Playing - - val slots = call.slots - if (slots > Hand.extraDrawAfter) { - val toDraw = slots - Hand.extraDrawAfter + 1 - for (player <- playersInRound) { - val hand = hands(player).hand - val newHand = Hand(hand ++ deck.drawResponses(toDraw)) - hands += (player -> newHand) - notifiers.handChange(player, newHand) - } - } - - for (ai <- players.ais) { - play(ai.id, getIdsForFirstXCardsInHand(ai.id, call.slots)) - } - - startRoundTimer(round.state.getClass, call) - checkIfFinishedPlaying() - } - - private def getIdsForFirstXCardsInHand(playerId: Player.Id, amount: Int): List[String] = { - val hand = hands(playerId).hand - (0 until amount).map(i => hand(i).id).toList - } - - private def startRoundTimer(state: Class[_ <: Round.State], call: Call) = Future { - roundTimedOut = false - Util.wait(Game.roundTimeLimit) - if (state.isInstance(round.state) && round.call == call) { - roundTimedOut = true - notifiers.roundTimeLimitHit() - } - } - - /** - * Play the given cards into the round. - * - * @param playerId The player the play is for. - * @param cardIds The ids of the responses to play. - * @throws BadRequestException with key "not-in-round" if the player is not in the round. - * @throws BadRequestException with key "already-played" if the player has already played into the round. - * @throws BadRequestException with key "already-judging" if the round is already in it's judging state. - * @throws BadRequestException with key "wrong-number-of-cards-played" if the wrong number of responses were played. - * The value "got" is the number of cards played, the value "expected" is the number - * required for the request to succeed - * @throws BadRequestException with key "invalid-card-id-given" if any of the card ids are not in the given player's - * hand. - */ - def play(playerId: Player.Id, cardIds: List[String]): Unit = { - BadRequestException.verify(playersInRound.contains(playerId), Errors.NotInRound()) - BadRequestException.verify(playedCards(playerId).isEmpty, Errors.AlreadyPlayed()) - BadRequestException.verify(roundProgress == Playing, Errors.AlreadyJudging()) - BadRequestException.verify(cardIds.length == call.slots, Errors.WrongNumberOfCardsPlayed(cardIds.length, call.slots)) - val hand = hands(playerId).hand - val toPlay: List[Response] = cardIds.flatMap(id => hand.find(response => response.id == id)) - BadRequestException.verify(toPlay.length == cardIds.length, Errors.InvalidCardId()) - val toDraw = Hand.size - (hand.length - toPlay.length) - val newHand = Hand(hand.filter(response => !toPlay.contains(response)) ++ deck.drawResponses(toDraw)) - hands += (playerId -> newHand) - playedCards += (playerId -> Some(toPlay)) - players.updatePlayer(playerId, players.setPlayerStatus(Player.Played)) - notifiers.roundPlayed(numberOfPlayersWhoHavePlayed) - checkIfFinishedPlaying() - } - - /** - * Choose the winning play for the round. - * - * @param playerId The player who is choosing the winner. - * @param winner The index of the played responses being chosen. - * @throws BadRequestException with key "not-czar" if the current player is not the czar. - * @throws BadRequestException with key "not-judging" if the round is not yet in the judging phase. - * @throws BadRequestException with key "no-such-played-cards" if the index does not exist. - * @throws BadRequestException with key "already-judged" if the round is already finished. - */ - def choose(playerId: Player.Id, winner: Int): Unit = { - BadRequestException.verify(playerId == czar, Errors.NotCzar()) - roundProgress match { - case Playing => - throw BadRequestException(Errors.NotJudging()) - case Judging(responses, playedOrder) => - playedOrder.lift(winner) match { - case Some(winnerId) => - players.updatePlayer(winnerId, players.modifyPlayerScore(1)) - roundProgress = Finished(responses, playedOrder, winnerId) - val finishedRound = Round.Finished(round.czar, round.call, Round.State.Finished(responses, PlayedByAndWinner(playedOrder, winnerId))) - history = finishedRound :: history - notifiers.roundEnd(finishedRound) - - case None => - throw BadRequestException(Errors.NoSuchPlayedCards()) - } - case Finished(_, _, _) => - throw BadRequestException(Errors.AlreadyJudged()) - } - } - - /** - * Get the hand of the given player. - * - * @param playerId The id of the player. - * @return The hand for the player. - */ - def getHand(playerId: Player.Id): Hand = hands(playerId) - - /** - * Redraw the hand of the given player. - * - * @param playerId The id of the player. - * @throws BadRequestException with the key "not-enough-points" if the player doesn't have enough points. - */ - def redraw(playerId: Player.Id): Unit = { - val player = players.getPlayer(playerId) - BadRequestException.verify(player.score > 0, Errors.NotEnoughPoints()) - players.updatePlayer(playerId, players.modifyPlayerScore(-1)) - hands += (playerId -> Hand(deck.drawResponses(Hand.size))) - } - - /** - * Start skipping the given players. - * - * @param playerId The player requesting the skipping. - * @param playerIds The players to start skipping. - */ - def skip(playerId: Player.Id, playerIds: Set[Player.Id]): Unit = { - for (id <- playerIds) { - players.updatePlayer(id, players.setPlayerStatus(Player.Skipping)) - playedCards = playedCards.filterKeys(pId => pId != id) - } - if (playerIds.contains(round.czar)) { - invalidateRound() - } else { - checkIfFinishedPlaying() - } - } - - private def invalidateRound(): Unit = { - beginRound() - } - - private def checkIfFinishedPlaying(): Unit = { - if (roundProgress == Playing && playedCards.values.forall(item => item.isDefined)) { - roundTimedOut = false - startRoundTimer(round.state.getClass, round.call) - val shuffled = Random.shuffle(playedCards) - val shuffledPlayedCards = shuffled.map(item => item._2.get).toList - roundProgress = Judging(shuffledPlayedCards, shuffled.map(item => item._1).toList) - notifiers.roundJudging(shuffledPlayedCards) - } - } - - private def numberOfPlayersWhoHavePlayed = { - playedCards.values.count(played => played.isDefined) - } - - private def nextCzar(): Player.Id = { - if (czarIndex >= players.amount) { - czarIndex = 0 - } - val player = players.players(czarIndex) - czarIndex += 1 - if (players.canBeCzar(player)) { - nextCzar() - } else { - player.id - } - } - -} -object Game { - val roundTimeLimit = 120.seconds - - /** - * Represents the progress through a round. - */ - sealed trait RoundProgress - - /** - * Indicates the game is being played. - */ - case object Playing extends RoundProgress - - /** - * Indicates the round is being judged. - * - * @param shuffled The shuffled list of played responses. - * @param playedOrder A list of player ids for the players who played the responses matching the order shuffled. - */ - case class Judging(shuffled: List[List[Response]], playedOrder: List[Player.Id]) extends RoundProgress - - /** - * Indicates the round is done. - * - * @param shuffled The shuffled list of played responses. - * @param playedOrder A list of player ids for the players who played the responses matching the order shuffled. - * @param winner The id of the winner of the round. - */ - case class Finished(shuffled: List[List[Response]], playedOrder: List[Player.Id], winner: Player.Id) extends RoundProgress -} diff --git a/app/massivedecks/lobby/Lobby.scala b/app/massivedecks/lobby/Lobby.scala deleted file mode 100644 index d6c1a45..0000000 --- a/app/massivedecks/lobby/Lobby.scala +++ /dev/null @@ -1,416 +0,0 @@ -package massivedecks.lobby - -import javax.inject.Inject - -import scala.concurrent.{Await, ExecutionContext, Future} -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -import massivedecks.exceptions.{BadRequestException, ForbiddenException, RequestFailedException} -import massivedecks.notifications.Notifiers -import massivedecks.models.{Errors, Player, Game => GameModel, Lobby => LobbyModel} -import massivedecks.models.Game.Formatters._ -import massivedecks.models.Lobby.Formatters._ -import play.api.libs.iteratee.{Enumerator, Iteratee} -import play.api.libs.json.{JsValue, JsObject, Json} -import massivedecks.Util -import massivedecks.cardcast.CardcastAPI - -object Lobby { - - /** - * Factory for dependency injection. - */ - class Factory @Inject() (cardcast: CardcastAPI) (implicit context: ExecutionContext) { - def build(gameCode: String, ownerName: String) = new Lobby(cardcast, gameCode, ownerName) - } - - /** - * How long to wait after a player disconnects to allow for them to reconnect without treating them as disconnected. - */ - val disconnectGracePeriod: FiniteDuration = 5.seconds - /** - * How long to wait for calls to Cardcast to complete. - */ - val cardCastWaitPeriod: FiniteDuration = 10.seconds -} -/** - * Represents a game lobby. - * - * @param cardcast The cardcast api. - * @param gameCode The game code for the lobby. - */ -class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(implicit context: ExecutionContext) { - - /** - * The game in progress if there is one. - */ - var game: Option[Game] = None - - /** - * Notifiers for the lobby. - */ - val notifiers: Notifiers = new Notifiers() - - /** - * Configuration for the lobby. - */ - var config = new Config(notifiers) - - /** - * The players in the lobby. - */ - val players: Players = new Players(notifiers) - - def gameState = game match { - case None => GameModel.State.Configuring() - case Some(g) => GameModel.State.Playing(g.round) - } - - val owner = newPlayer(ownerName, None) - - /** - * @return The model for the lobby. - */ - def lobby = LobbyModel.Lobby(gameCode, owner.id, config.config, players.players, gameState) - - /** - * Add a new player to the lobby. - * - * @param name The name for the player. - * @return The secret for the player. - * @throws BadRequestException with key "name-in-use" if there is a player in the lobby with the same name. - * @throws ForbiddenException with key "password-wrong" if the password is required and wrong. - */ - def newPlayer(name: String, password: Option[String]): Player.Secret = { - ForbiddenException.verify(config.password.isEmpty || config.password == password, Errors.PasswordWrong()) - val secret = players.addPlayer(name) - if (game.isDefined) { - game.get.addPlayer(secret.id) - } - setPlayerDisconnectedAfterGracePeriod(secret.id) - secret - } - - /** - * Try to add the deck to the lobby. - * - * @param secret The secret for the player making the request. - * @param playCode The cardcast play code for the deck. - * @return An empty response. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws ForbiddenException with key "not-owner" if the requester is not the owner. - * @throws RequestFailedException with the key "cardcast-timeout" if the request to cardcast doesn't complete. - */ - def addDeck(secret: Player.Secret, playCode: String): JsValue = { - players.validateSecret(secret) - validateIsOwner(secret) - Try(Await.ready({ - cardcast.deck(playCode).map { deck => - config.addDeck(deck) - } - }, Lobby.cardCastWaitPeriod)) match { - case Success(result) => - result.value.get.get - case Failure(exception) => - throw RequestFailedException(Errors.CardcastTimeout()) - } - EmptyResponse - } - - /** - * Add a new ai to the lobby. - * - * @param secret The secret of the player making the request. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws ForbiddenException with key "not-owner" if the requester is not the owner. - */ - def newAi(secret: Player.Secret): Unit = { - players.validateSecret(secret) - validateIsOwner(secret) - players.addAi() - } - - /** - * Start a new game in the lobby. - * - * @param secret The secret of the player making the request. - * @return The hand of the player making the request in the new game. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with the key "game-in-progress" if the game has already started. - */ - def newGame(secret: Player.Secret): JsValue = { - players.validateSecret(secret) - if (game.isDefined) { - throw BadRequestException(Errors.GameInProgress()) - } - val current = new Game(players, config, notifiers) - game = Some(current) - Json.toJson(getHand(secret)) - } - - /** - * Play the given cards into the round. - * - * @param secret The secret of the player making the request. - * @param cardIds The ids of the responses to play. - * @return The hand of the player making the request after the cards have been played and replacements drawn. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - * @throws BadRequestException with key "not-in-round" if the player is not in the round. - * @throws BadRequestException with key "already-played" if the player has already played into the round. - * @throws BadRequestException with key "already-judging" if the round is already in it's judging state. - * @throws BadRequestException with key "wrong-number-of-cards-played" if the wrong number of responses were played. - * The value "got" is the number of cards played, the value "expected" is the number - * required for the request to succeed - * @throws BadRequestException with key "invalid-card-id-given" if any of the card ids are not in the given player's - * hand. - */ - def play(secret: Player.Secret, cardIds: List[String]): JsValue = { - players.validateSecret(secret) - validateInGame().play(secret.id, cardIds) - Json.toJson(getHand(secret)) - } - - /** - * Choose the winning play for the round. - * - * @param secret The secret of the player making the request. - * @param winner The index of the played responses being chosen. - * @return An empty response. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - * @throws BadRequestException with key "not-czar" if the current player is not the czar. - * @throws BadRequestException with key "not-judging" if the round is not yet in the judging phase. - * @throws BadRequestException with key "no-such-played-cards" if the index does not exist. - * @throws BadRequestException with key "already-judged" if the round is already finished. - */ - def choose(secret: Player.Secret, winner: Int): JsValue = { - players.validateSecret(secret) - val game = validateInGame() - game.choose(secret.id, winner) - beginRound() - EmptyResponse - } - - /** - * Get the hand of the player. - * - * @param secret The secret of the player making the request. - * @return The hand of the player. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - */ - def getHand(secret: Player.Secret): GameModel.Hand = { - players.validateSecret(secret) - validateInGame().getHand(secret.id) - } - - /** - * @return The history of the current game. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - */ - def gameHistory() : List[GameModel.Round.Finished] = { - validateInGame().history - } - - /** - * Get the lobby and hand models for the lobby. - * - * @param secret The secret of the player making the request. - * @return The lobby and the hand of the player. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - */ - def getLobbyAndHand(secret: Player.Secret): JsValue = { - players.validateSecret(secret) - Json.toJson(lobbyAndHand(secret)) - } - - private def lobbyAndHand(secret: Player.Secret): LobbyModel.LobbyAndHand = { - val hand = game match { - case Some(_) => - getHand(secret) - case None => - GameModel.Hand(List()) - } - LobbyModel.LobbyAndHand(lobby, hand) - } - - /** - * Mark the given player as having left the game. - * - * @param secret The secret of the player making the request. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - */ - def leave(secret: Player.Secret): Unit = { - players.validateSecret(secret) - players.leave(secret.id) - game.foreach { current => - if (players.activePlayers.length < Players.minimum) { - endGame() - } - current.playerLeft(secret.id) - } - } - - private def endGame(): Unit = { - game = None - players.updatePlayers(players.setPlayerStatus(Player.Neutral)) - notifiers.gameEnd() - } - - /** - * Start skipping the given players. - * - * @param secret The secret of the player making the request. - * @param playerIds The players to start skipping. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - * @throws BadRequestException with key "not-enough-players" if the game would end by skipping the given - * players. - * @throws BadRequestException with key "players-must-be-skippable" if the players are not skippable. - * @return An empty response. - */ - def skip(secret: Player.Secret, playerIds: Set[Player.Id]): JsValue = { - players.validateSecret(secret) - val game = validateInGame() - BadRequestException.verify((players.activePlayers.length - playerIds.size) >= Players.minimum, Errors.NotEnoughPlayers(Players.minimum)) - val requestedPlayers = players.players.filter(player => playerIds.contains(player.id)) - if (!game.round.state.afterTimeLimit) { - BadRequestException.verify(requestedPlayers.forall(player => player.disconnected), Errors.PlayersMustBeSkippable()) - } else { - if (game.round.state.isPlaying) { - BadRequestException.verify(requestedPlayers.forall(player => player.status == Player.Czar), Errors.PlayersMustBeSkippable()) - } else { - BadRequestException.verify(requestedPlayers.forall(player => player.status == Player.NotPlayed), Errors.PlayersMustBeSkippable()) - } - } - game.skip(secret.id, playerIds) - EmptyResponse - } - - /** - * Mark the given player as back into the game. - * - * @param secret The secret of the player making the request. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "not-being-skipped" if the player was not being skipped. - * @return An empty response. - */ - def back(secret: Player.Secret): JsValue = { - players.validateSecret(secret) - players.back(secret.id) - EmptyResponse - } - - /** - * Redraw the hand of the given player. - * - * @param secret The secret of the player making the request. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws BadRequestException with key "rule-not-enabled" if the rule is not enabled. - * @throws BadRequestException with key "no-game-in-progress" if there is not a game underway. - * @throws BadRequestException with the key "not-enough-points-to-redraw" if the player doesn't have enough points. - * @return The hand of the player. - */ - def redraw(secret: Player.Secret): JsValue = { - players.validateSecret(secret) - BadRequestException.verify(config.houseRules.contains("reboot"), Errors.RuleNotEnabled()) - validateInGame().redraw(secret.id) - Json.toJson(getHand(secret)) - } - - /** - * Enable the given rule. - * - * @param secret The secret of the player making the request. - * @param rule The rule to enable. - * @return An empty response. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws ForbiddenException with key "not-owner" if the requester is not the owner. - */ - def enableRule(secret: Player.Secret, rule: String): JsValue = { - players.validateSecret(secret) - validateIsOwner(secret) - config.addHouseRule(rule) - EmptyResponse - } - - /** - * Set the password - * - * @param secret The secret of the player making the request. - * @param password The password to set, empty means no password. - * @return An empty response. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws ForbiddenException with key "not-owner" if the requester is not the owner. - */ - def setPassword(secret: Player.Secret, password: Option[String]): JsValue = { - players.validateSecret(secret) - validateIsOwner(secret) - config.setPassword(password) - EmptyResponse - } - - /** - * Disable the given rule. - * - * @param secret The secret of the player making the request. - * @param rule The rule to disable. - * @return An empty response. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - * @throws ForbiddenException with key "not-owner" if the requester is not the owner. - */ - def disableRule(secret: Player.Secret, rule: String): JsValue = { - players.validateSecret(secret) - validateIsOwner(secret) - config.removeHouseRule(rule) - EmptyResponse - } - - def EmptyResponse: JsValue = JsObject(Seq()) - - /** - * Register a websocket connection. - * - * @return The iteratee and enumerator for the websocket. - */ - def register(): (Iteratee[String, Unit], Enumerator[String]) = { - notifiers.openedSocket(register, unregister) - } - - private def register(secret: Player.Secret): LobbyModel.LobbyAndHand = { - players.validateSecret(secret) - players.register(secret.id) - lobbyAndHand(secret) - } - - private def unregister(playerId: Player.Id): Unit = { - players.unregister(playerId) - setPlayerDisconnectedAfterGracePeriod(playerId) - } - - private def setPlayerDisconnectedAfterGracePeriod(playerId: Player.Id): Unit = { - Future { - Util.wait(Lobby.disconnectGracePeriod) - if (!players.connected.contains(playerId)) { - players.updatePlayer(playerId, players.setPlayerDisconnected(true)) - } - } - } - - private def beginRound(): Unit = { - val game = validateInGame() - game.beginRound() - } - - private def validateInGame(): Game = game match { - case Some(state) => state - case None => throw BadRequestException(Errors.NoGameInProgress()) - } - - private def validateIsOwner(secret: Player.Secret) = { - ForbiddenException.verify(owner == secret, Errors.NotOwner()) - } - -} diff --git a/app/massivedecks/lobby/Players.scala b/app/massivedecks/lobby/Players.scala deleted file mode 100644 index a52e8e5..0000000 --- a/app/massivedecks/lobby/Players.scala +++ /dev/null @@ -1,280 +0,0 @@ -package massivedecks.lobby - -import scala.util.Random - -import massivedecks.exceptions.{BadRequestException, ForbiddenException} -import massivedecks.models.{Errors, Player} -import massivedecks.notifications.Notifiers - -/** - * Players in the game. - */ -class Players(notifiers: Notifiers) { - - /** - * All players that have been in the game. - */ - var players: List[Player] = List() - - /** - * Secrets for all the ais in the game. - */ - var ais: Set[Player.Secret] = Set() - - /** - * Any players registered as connected to the game. - */ - var connected: Set[Player.Id] = Set() - - private var secrets: Map[Player.Id, Player.Secret] = Map() - private var nextId: Int = 0 - private var aiNames: List[String] = Players.aiName :: Random.shuffle(Players.aiNames) - private var nameIteration = 0 - - /** - * Validate the given secret as valid. - * @param secret The secret to check. - * @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid. - */ - def validateSecret(secret: Player.Secret): Unit = { - ForbiddenException.verify(secrets.values.exists(s => s == secret), Errors.SecretWrongOrNotAPlayer()) - } - - /** - * Add a player to the game, marking them as the owner if they are the first player in the game. - * (Notifies all clients). - * @param name The name of the new player. - * @return The secret for the new player. - * @throws BadRequestException with key "name-in-use" if there is a player in the lobby with the same name. - */ - def addPlayer(name: String): Player.Secret = { - BadRequestException.verify(players.forall(player => player.name != name), Errors.NameInUse()) - val id = newId() - val player = Player(id, name) - players = players :+ player - val secret = Player.Secret(id) - secrets = secrets + (id -> secret) - notifiers.playerJoin(player) - secret - } - - /** - * Adds an Ai to the game, generating a name for it. (Notifies all clients). - */ - def addAi(): Unit = { - val secret = addPlayer(generateAiName()) - updatePlayer(secret.id, setPlayerStatus(Player.Ai, Set())) - connected += secret.id - ais += secret - } - - private def generateAiName(): String = { - val name :: rest = aiNames - aiNames = rest - if (rest.isEmpty) { - nameIteration += 1 - aiNames = Random.shuffle(Players.aiNames).map(name => name + " " + nameIteration) - } - if (players.exists(player => player.name == name)) { - generateAiName() - } else { - name - } - } - - /** - * Update all players using the given updater. - * Note that you will need to notify clients about the changes if the updater doesn't. - * @param update The player updater. - */ - def updatePlayers(update: (Player => Player)): Unit = { - players = players.map(player => update(player)) - } - - /** - * Update the given player using the given updater. - * Note that you will need to notify clients about the changes if the updater doesn't. - * @param update The player updater. - * @throws BadRequestException with key "not-a-player" if the player doesn't exist. - */ - def updatePlayer(playerId: Player.Id, update: (Player => Player)): Unit = { - val updated = update(getPlayer(playerId)) - players = players.map(player => if (player.id == playerId) updated else player) - } - - /** - * Get the given player. - * @param playerId The ID of the player to get. - * @return The player. - * @throws BadRequestException with the key "not-a-player" if the player doesn't exist. - */ - def getPlayer(playerId: Player.Id): Player = { - players.find(player => player.id == playerId).getOrElse(throw BadRequestException(Errors.NotAPlayer())) - } - - /** - * Mark the given player as back into the game. (Notifies all clients) - * @param playerId The player to mark as back. - * @throws BadRequestException with key "not-a-player" if the player doesn't exist. - * @throws BadRequestException with key "not-being-skipped" if the player was not being skipped. - */ - def back(playerId: Player.Id): Unit = { - val player = getPlayer(playerId) - BadRequestException.verify(player.status == Player.Skipping, Errors.NotBeingSkipped()) - updatePlayer(playerId, setPlayerStatus(Player.Neutral, Player.Status.sticky - Player.Skipping)) - } - - /** - * Mark the given player as having left the game. (Notifies all clients) - * @param playerId The player to mark as having left the game. - * @throws BadRequestException with key "not-a-player" if the player doesn't exist. - */ - def leave(playerId: Player.Id): Unit = { - updatePlayer(playerId, setPlayerLeft()) - } - - /** - * Register the player as connected to the game, and mark them as such. (Notifies all clients) - * @param playerId The player to mark as connected. - * @throws BadRequestException with key "already-left-game" if the player is marked as having left the game. - * @throws BadRequestException with key "not-a-player" if the player doesn't exist. - */ - def register(playerId: Player.Id): Unit = { - val player = getPlayer(playerId) - BadRequestException.verify(!player.left, Errors.AlreadyLeftGame()) - connected += playerId - updatePlayer(playerId, setPlayerDisconnected(false)) - if (player.status == Player.Skipping) { - back(playerId) - } - } - - /** - * Remove the registration of the player as connected to the game. - * @param playerId The player to remove registration for. - */ - def unregister(playerId: Player.Id): Unit = { - connected -= playerId - } - - private def newId() = { - val id = Player.Id(nextId) - nextId += 1 - id - } - - /** - * Check if the given player can be the card czar (is human, hasn't left, isn't being skipped). - * @param player The player to check. - * @return If the player can be the card czar. - */ - def canBeCzar(player: Player): Boolean = { - ais.exists(ai => ai.id == player.id) || player.left || player.status == Player.Skipping - } - - /** - * @return The ids of every player that has been in the game. - */ - def ids = players.map(player => player.id) - - /** - * @return The number of players that have been in the game. - */ - def amount = players.length - - /** - * @return Players that are actively in the game (not left or being skipped) - */ - def activePlayers = players.filter(player => !player.left && player.status != Player.Skipping) - - /** - * An updater for players that changes their status. See updatePlayers()/updaterPlayer(). - * @param newStatus The status to set the player to. - * @param ignoring If the player has a status in this set, do not change it. (Defaults to the sticky statuses). - * @return An updater function for a player. (Notifies all clients) - */ - def setPlayerStatus(newStatus: Player.Status, ignoring: Set[Player.Status] = Player.Status.sticky): (Player => Player) = { - player => - if (!player.left && !ignoring.contains(player.status) && player.status != newStatus) { - notifiers.playerStatus(player.id, newStatus) - player.copy(status = newStatus) - } else { - player - } - } - - /** - * An updater for players that changes their score. See updatePlayers()/updaterPlayer(). - * @param by The amount to change the score by. - * @return An updater function for a player. (Notifies all clients) - */ - def modifyPlayerScore(by: Int): (Player => Player) = { - player => - val newScore = player.score + by - notifiers.playerScoreChange(player.id, newScore) - player.copy(score = newScore) - } - - /** - * An updater for players that marks them as having left the game. See updatePlayers()/updaterPlayer(). - * @return An updater function for a player. (Notifies all clients) - */ - def setPlayerLeft(): (Player => Player) = { - player => - notifiers.playerLeft(player.id) - if (player.status != Player.Neutral) { - notifiers.playerStatus(player.id, Player.Neutral) - } - player.copy(status = Player.Neutral, left = true) - } - - /** - * An updater for players that marks them as being disconnected from the game. See updatePlayers()/updaterPlayer(). - * Note that we generally leave a grace period for clients to reconnect before marking them as disconnected. - * @param disconnected If the player is connected or disconnected. - * @return An updater function for a player. (Notifies all clients) - */ - def setPlayerDisconnected(disconnected: Boolean): (Player => Player) = { - player => - if (!player.left && player.status != Player.Ai) { - if (disconnected) { - notifiers.playerDisconnect(player.id) - } else { - notifiers.playerReconnect(player.id) - } - player.copy(disconnected = disconnected) - } else { - player - } - } -} -object Players { - /** - * The minimum number of players that can be active in a game for it to start or continue. - */ - val minimum = 2 - - /** - * The name of the first ai. - */ - val aiName = "Rando Cardrissian" - - /** - * Names for additional ais. - */ - val aiNames = List( - "HAL 9000", - "GLaDOS", - "Wheatley", - "TEC-XX", - "EDI", - "343 Guilty Spark", - "Jarvis", - "Deep Thought", - "Gibson", - "Skynet", - "AI", - "Computer", - "Real Human" - ) -} diff --git a/app/massivedecks/models/Errors.scala b/app/massivedecks/models/Errors.scala deleted file mode 100644 index 60bc4e0..0000000 --- a/app/massivedecks/models/Errors.scala +++ /dev/null @@ -1,69 +0,0 @@ -package massivedecks.models - -import play.api.libs.json.Json - -/** - * Errors models. - */ -object Errors { - trait ErrorDetails { - def error: String - def toJson(): String - } - - trait SimpleErrorDetails extends ErrorDetails { - def toJson() = Json.obj("error" -> error).toString() - } - - case class LobbyNotFound(error: String = "lobby-not-found") extends SimpleErrorDetails - case class InvalidCommand(command: String, error: String = "invalid-command") extends ErrorDetails { - def toJson() = Json.obj( - "error" -> error, - "command" -> command - ).toString() - } - case class InvalidName(error: String = "invalid-name") extends SimpleErrorDetails - case class NameInUse(error: String = "name-in-use") extends SimpleErrorDetails - case class BadlyFormedSecret(error: String = "badly-formed-secret") extends SimpleErrorDetails - case class SecretWrongOrNotAPlayer(error: String = "secret-wrong-or-not-a-player") extends SimpleErrorDetails - case class PasswordWrong(error: String = "password-wrong") extends SimpleErrorDetails - case class NotAPlayer(error: String = "not-a-player") extends SimpleErrorDetails - case class NotBeingSkipped(error: String = "not-being-skipped") extends SimpleErrorDetails - case class AlreadyLeftGame(error: String = "already-left-game") extends SimpleErrorDetails - case class CardcastTimeout(error: String = "cardcast-timeout") extends SimpleErrorDetails - case class GameInProgress(error: String = "game-in-progress") extends SimpleErrorDetails - case class NotEnoughPlayers(required: Int, error: String = "not-enough-players") extends ErrorDetails { - def toJson() = Json.obj( - "error" -> error, - "required" -> required - ).toString() - } - case class PlayersMustBeSkippable(error: String = "players-must-be-skippable") extends SimpleErrorDetails - case class RuleNotEnabled(error: String = "rule-not-enabled") extends SimpleErrorDetails - case class NoGameInProgress(error: String = "no-game-in-progress") extends SimpleErrorDetails - case class NotInRound(error: String = "not-in-round") extends SimpleErrorDetails - case class AlreadyPlayed(error: String = "already-played") extends SimpleErrorDetails - case class AlreadyJudging(error: String = "already-judging") extends SimpleErrorDetails - case class InvalidCardId(error: String = "invalid-card-id") extends SimpleErrorDetails - case class WrongNumberOfCardsPlayed(got: Int, expected: Int, error: String = "wrong-number-of-cards-played") extends ErrorDetails { - def toJson() = Json.obj( - "error" -> error, - "got" -> got, - "expected" -> expected - ).toString() - } - case class NotCzar(error: String = "not-czar") extends SimpleErrorDetails - case class NotJudging(error: String = "not-judging") extends SimpleErrorDetails - case class NoSuchPlayedCards(error: String = "no-such-played-cards") extends SimpleErrorDetails - case class AlreadyJudged(error: String = "already-judged") extends SimpleErrorDetails - case class NotEnoughPoints(error: String = "not-enough-points") extends SimpleErrorDetails - case class InvalidDeckConfiguration(reason: String, error: String = "invalid-deck-configuration") extends ErrorDetails { - def toJson() = Json.obj( - "error" -> error, - "reason" -> reason - ).toString() - } - case class DeckNotFound(error: String = "deck-not-found") extends SimpleErrorDetails - case class NotOwner(error: String = "not-owner") extends SimpleErrorDetails - -} diff --git a/app/massivedecks/models/Game.scala b/app/massivedecks/models/Game.scala deleted file mode 100644 index 431279b..0000000 --- a/app/massivedecks/models/Game.scala +++ /dev/null @@ -1,118 +0,0 @@ -package massivedecks.models - -import play.api.libs.json._ -import massivedecks.models.Player.Formatters._ - -/** - * Created by gareth on 30/06/16. - */ -object Game { - - sealed trait Card - - case class Call(id: String, parts: List[String]) extends Card { - require(parts.length > 1, "A call must have at least one slot.") - - def slots: Int = parts.length - 1 - - def withResponses(responses: List[Response]): String = - intersperse(parts, responses.map(response => response.text)).mkString("") - } - - case class Response(id: String, text: String) extends Card - - case class DeckInfo(id: String, name: String, calls: Int, responses: Int) - - case class Config(decks: List[DeckInfo], houseRules: Set[String], password: Option[String]) - - sealed trait State { - def gameState: String - } - object State { - case class Configuring() extends State { - override val gameState = "configuring" - } - case class Playing(round: Round) extends State { - override val gameState = "playing" - } - case class Finished() extends State { - override val gameState = "finished" - } - } - - case class Round(czar: Player.Id, call: Call, state: Round.State) - object Round { - sealed trait State { - def afterTimeLimit: Boolean - def roundState: String - - def isPlaying: Boolean = this.isInstanceOf[State.Playing] - def isJudging: Boolean = this.isInstanceOf[State.Judging] - def isFinished: Boolean = this.isInstanceOf[State.Finished] - } - object State { - case class Playing(override val afterTimeLimit: Boolean, numberPlayed: Int) extends State { - override val roundState = "playing" - } - - case class Judging(override val afterTimeLimit: Boolean, cards: List[List[Response]]) extends State { - override val roundState = "judging" - } - - case class Finished(cards: List[List[Response]], playedByAndWinner: PlayedByAndWinner) extends State { - override val roundState = "finished" - override val afterTimeLimit = false - } - } - case class Finished(czar: Player.Id, call: Call, state: Round.State.Finished) - } - - case class PlayedByAndWinner(playedBy: List[Player.Id], winner: Player.Id) - - case class Hand(hand: List[Response]) - object Hand { - val size: Int = 10 - val extraDrawAfter: Int = 2 - } - - object Formatters { - implicit val callFormat: Format[Call] = Json.format[Call] - implicit val responseFormat: Format[Response] = Json.format[Response] - implicit val playedByAndWinnerFormat: Format[PlayedByAndWinner] = Json.format[PlayedByAndWinner] - implicit val deckInfoFormat: Format[DeckInfo] = Json.format[DeckInfo] - implicit val configFormat: Format[Config] = Json.format[Config] - implicit val handFormat: Format[Hand] = Json.format[Hand] - - private val playingRoundStateFormat: Format[Round.State.Playing] = Json.format[Round.State.Playing] - private val judgingRoundStateFormat: Format[Round.State.Judging] = Json.format[Round.State.Judging] - private val finishedRoundStateFormat: Format[Round.State.Finished] = Json.format[Round.State.Finished] - implicit val roundStateWrites: Writes[Round.State] = Writes { state: Round.State => - (state match { - case state: Round.State.Playing => - playingRoundStateFormat.writes(state) - - case state: Round.State.Judging => - judgingRoundStateFormat.writes(state) - - case state: Round.State.Finished => - finishedRoundStateFormat.writes(state) - }).as[JsObject] + ("roundState" -> Json.toJson(state.roundState)) - } - implicit val roundWrites: Writes[Round] = Json.writes[Round] - implicit val finishedRoundWrites: Writes[Round.Finished] = Json.writes[Round.Finished] - - private val playingGameStateFormat: Writes[State.Playing] = Json.writes[State.Playing] - implicit val gameStateWrites: Writes[State] = Writes { state: State => - (state match { - case state: State.Configuring => JsObject(List()) - case state: State.Playing => playingGameStateFormat.writes(state) - case state: State.Finished => JsObject(List()) - }).as[JsObject] + ("gameState" -> Json.toJson(state.gameState)) - } - } - - private def intersperse[A](a : List[A], b : List[A]): List[A] = a match { - case first :: rest => first :: intersperse(b, rest) - case _ => b - } -} diff --git a/app/massivedecks/models/Lobby.scala b/app/massivedecks/models/Lobby.scala deleted file mode 100644 index d8689f2..0000000 --- a/app/massivedecks/models/Lobby.scala +++ /dev/null @@ -1,24 +0,0 @@ -package massivedecks.models - -import play.api.libs.json.{Json, Writes} -import massivedecks.models.Game.Formatters._ -import massivedecks.models.Player.Formatters._ - -/** - * Created by gareth on 30/06/16. - */ -object Lobby { - - case class Lobby(gameCode: String, owner: Player.Id, config: Game.Config, players: List[Player], state: Game.State) - - case class LobbyAndHand(lobby: Lobby, hand: Game.Hand) - - case class GameCodeAndSecret(gameCode: String, secret: Player.Secret) - - object Formatters { - implicit val lobbyWrites: Writes[Lobby] = Json.writes[Lobby] - implicit val lobbyAndHandWrites: Writes[LobbyAndHand] = Json.writes[LobbyAndHand] - implicit val gameCodeAndSecretWrites: Writes[GameCodeAndSecret] = Json.writes[GameCodeAndSecret] - } - -} diff --git a/app/massivedecks/models/Player.scala b/app/massivedecks/models/Player.scala deleted file mode 100644 index 27bf973..0000000 --- a/app/massivedecks/models/Player.scala +++ /dev/null @@ -1,53 +0,0 @@ -package massivedecks.models - -import java.util.UUID - -import play.api.libs.json._ - -case class Player(id: Player.Id, name: String, status: Player.Status = Player.Neutral, score: Int = 0, disconnected: Boolean = false, left: Boolean = false) { - require(!name.isEmpty, "Name can't be empty!") -} -object Player { - - case class Id(id: Int) extends AnyVal - - sealed trait Status { - val name: String - } - object Status { - private val types = List(NotPlayed, Played, Czar, Ai, Neutral, Skipping) - val fromName: Map[String, Status] = (for (status <- types) yield status.name -> status).toMap - val notInRound: Set[Status] = Set(Skipping, Czar) - val sticky: Set[Status] = Set(Ai, Skipping) - } - case object NotPlayed extends Status { - val name = "not-played" - } - case object Played extends Status { - val name = "played" - } - case object Czar extends Status { - val name = "czar" - } - case object Ai extends Status { - val name = "ai" - } - case object Neutral extends Status { - val name = "neutral" - } - case object Skipping extends Status { - val name = "skipping" - } - - case class Secret(id: Player.Id, secret: String = UUID.randomUUID().toString) - - object Formatters { - implicit val idFormat: Format[Id] = Format( - Reads(num => num.validate[Int].map(Id)), Writes(id => JsNumber(id.id))) - implicit val statusFormat : Format[Status] = Format( - Reads(str => str.validate[String].map(Status.fromName)), - Writes(status => JsString(status.name))) - implicit val secretFormat: Format[Secret] = Json.format[Secret] - implicit val playerFormat: Format[Player] = Json.format[Player] - } -} diff --git a/app/massivedecks/models/cardcast/CardcastDeck.scala b/app/massivedecks/models/cardcast/CardcastDeck.scala deleted file mode 100644 index 6250989..0000000 --- a/app/massivedecks/models/cardcast/CardcastDeck.scala +++ /dev/null @@ -1,8 +0,0 @@ -package massivedecks.models.cardcast - -import massivedecks.models.Game -import Game.{Call, DeckInfo, Response} - -case class CardcastDeck(id: String, name: String, calls: List[Call], responses: List[Response]) { - def info: DeckInfo = DeckInfo(id, name, calls.length, responses.length) -} diff --git a/app/massivedecks/notifications/Notifier.scala b/app/massivedecks/notifications/Notifier.scala deleted file mode 100644 index 13da36e..0000000 --- a/app/massivedecks/notifications/Notifier.scala +++ /dev/null @@ -1,61 +0,0 @@ -package massivedecks.notifications - -import scala.concurrent.ExecutionContext - -import massivedecks.models.Player -import Player.Formatters._ -import play.api.libs.iteratee.Concurrent.Channel -import play.api.libs.iteratee._ -import play.api.libs.json.Json - -/** - * Handles websocket notifications. - */ -class Notifier (implicit context: ExecutionContext) { - private var unicastChannel: Option[Channel[String]] = None - - /** - * Call when a socket has been opened for the notifier. - * @param onConnect The callback when the socket connects. - * @param onIdentify The callback when the client identifies down the socket. - * @param onClose The callback when the socket closes. - * @return The iteratee and enumerator for the socket. - */ - def openedSocket(onConnect: () => Unit, onIdentify: (Player.Secret) => Unit, onClose: () => Unit): (Iteratee[String, Unit], Enumerator[String]) = { - val iteratee = Notifier.forEachAndOnClose[String]( - message => onIdentify(Json.parse(message).validate[Player.Secret].get), - onClose - ) - val enumerator = Concurrent.unicast[String]( - channel => { - unicastChannel = Some(channel) - onConnect() - } - ) - (iteratee, enumerator) - } - - /** - * Notify the client down the socket. - * @param message The message to send. - */ - def notify(message: String) = - unicastChannel.foreach { channel => - channel.push(message) - } -} -object Notifier { - def forEachAndOnClose[E](onMessage: (E) => Unit, onClose: () => Unit): Iteratee[E, Unit] = { - def cont: Iteratee[E, Unit] = Cont { - case Input.EOF => - onClose() - Done((), Input.EOF) - case Input.El(message) => - onMessage(message) - cont - case Input.Empty => - cont - } - cont - } -} diff --git a/app/massivedecks/notifications/Notifiers.scala b/app/massivedecks/notifications/Notifiers.scala deleted file mode 100644 index 6e3fda2..0000000 --- a/app/massivedecks/notifications/Notifiers.scala +++ /dev/null @@ -1,160 +0,0 @@ -package massivedecks.notifications - -import scala.concurrent.ExecutionContext - -import massivedecks.models.{Game, Lobby, Player} -import Game.Formatters._ -import Player.Formatters._ -import Lobby.Formatters._ -import play.api.libs.iteratee.{Concurrent, Enumerator, Iteratee} -import play.api.libs.json.{JsValue, Json} - -/** - * Manages open sockets. - */ -class Notifiers (implicit context: ExecutionContext) { - - private val (broadcastEnumerator, broadcastChannel) = Concurrent.broadcast[String] - private var identified: Map[Player.Id, Notifier] = Map() - - /** - * Sets up notifications for a new websocket. - * - * @param onIdentify When we get an identification message from the client, this will be run. This should validate - * the secret and throw a ForbiddenException if it isn't correct. - * @param onClose When a client that has previously identified disconnects, this will be run. - * @return The websocket iteratee and enumerator. - */ - def openedSocket(onIdentify: Player.Secret => Lobby.LobbyAndHand, onClose: (Player.Id => Unit)): (Iteratee[String, Unit], Enumerator[String]) = { - val notifier = new Notifier() - val (unicastIteratee, unicastEnumerator) = notifier.openedSocket( - () => notifier.notify("identify"), - (secret) => { - val lobbyAndHand = onIdentify(secret) - identified += (secret.id -> notifier) - sync(secret.id, lobbyAndHand) - }, - () => { - identified.find(item => notifier == item._2).foreach { item => - val (id, _) = item - identified -= id - onClose(id) - } - } - ) - (unicastIteratee, unicastEnumerator.interleave(broadcastEnumerator)) - } - - - def sync(playerId: Player.Id, lobbyAndHand: Lobby.LobbyAndHand): Unit = - notify(playerId, Json.obj( - "event" -> "Sync", - "lobbyAndHand" -> Json.toJson(lobbyAndHand) - )) - - - def playerJoin(player: Player): Unit = - notifyAll(Json.obj( - "event" -> "PlayerJoin", - "player" -> Json.toJson(player) - )) - - def playerStatus(playerId: Player.Id, status: Player.Status): Unit = - notifyAll(Json.obj( - "event" -> "PlayerStatus", - "player" -> Json.toJson(playerId), - "status" -> Json.toJson(status) - )) - - def playerLeft(playerId: Player.Id): Unit = - notifyAll(Json.obj( - "event" -> "PlayerLeft", - "player" -> Json.toJson(playerId) - )) - - def playerDisconnect(playerId: Player.Id): Unit = - notifyAll(Json.obj( - "event" -> "PlayerDisconnect", - "player" -> Json.toJson(playerId) - )) - - def playerReconnect(playerId: Player.Id): Unit = - notifyAll(Json.obj( - "event" -> "PlayerReconnect", - "player" -> Json.toJson(playerId) - )) - - def playerScoreChange(playerId: Player.Id, score: Int): Unit = - notifyAll(Json.obj( - "event" -> "PlayerScoreChange", - "player" -> Json.toJson(playerId), - "score" -> Json.toJson(score) - )) - - - def handChange(playerId: Player.Id, hand: Game.Hand): Unit = - notify(playerId, Json.obj( - "event" -> "HandChange", - "hand" -> Json.toJson(hand) - )) - - - def roundStart(czar: Player.Id, call: Game.Call): Unit = - notifyAll(Json.obj( - "event" -> "RoundStart", - "czar" -> Json.toJson(czar), - "call" -> Json.toJson(call) - )) - - def roundPlayed(playedCards: Int): Unit = - notifyAll(Json.obj( - "event" -> "RoundPlayed", - "playedCards" -> Json.toJson(playedCards) - )) - - def roundJudging(playedCards: List[List[Game.Response]]): Unit = - notifyAll(Json.obj( - "event" -> "RoundJudging", - "playedCards" -> Json.toJson(playedCards) - )) - - def roundEnd(finishedRound: Game.Round.Finished): Unit = - notifyAll(Json.obj( - "event" -> "RoundEnd", - "finishedRound" -> Json.toJson(finishedRound) - )) - - - def gameStart(czar: Player.Id, call: Game.Call): Unit = - notifyAll(Json.obj( - "event" -> "GameStart", - "czar" -> Json.toJson(czar), - "call" -> Json.toJson(call) - )) - - def gameEnd(): Unit = - notifyAll(Json.obj( - "event" -> "GameEnd" - )) - - - def configChange(config: Game.Config): Unit = - notifyAll(Json.obj( - "event" -> "ConfigChange", - "config" -> Json.toJson(config) - )) - - - def roundTimeLimitHit(): Unit = - notifyAll(Json.obj( - "event" -> "RoundTimeLimitHit" - )) - - - private def notify(player: Player.Id, event: JsValue) = - identified.get(player).foreach { notifier => notifier.notify(event.toString) } - - private def notifyAll(event: JsValue) = - broadcastChannel.push(event.toString) - -} diff --git a/app/massivedecks/stores/CachedStore.scala b/app/massivedecks/stores/CachedStore.scala deleted file mode 100644 index fe2f624..0000000 --- a/app/massivedecks/stores/CachedStore.scala +++ /dev/null @@ -1,99 +0,0 @@ -package massivedecks.stores - -import scala.concurrent.duration._ -import java.util.concurrent.locks.{Lock, ReentrantLock} - -import massivedecks.exceptions.NotFoundException -import massivedecks.lobby.Lobby -import massivedecks.models.Errors -import play.api.cache.CacheApi - -/** - * This trait provides a framework for a LobbyStore with caching and locking. - */ -trait CachedStore extends LobbyStore { - - protected val lobbyFactory: Lobby.Factory - protected val gameCodeManager: GameCodeManager - protected val cache: CacheApi - - protected var locks: Map[String, Lock] = Map() - - /** - * Get the lobby for the given game code, or None if it doesn't exist. - * - * @param gameCode The game code for the lobby. - * @return The lobby or None. - */ - protected def getLobby(gameCode: String): Option[Lobby] - - /** - * Save the lobby into storage - overwriting existing ones if found. - * - * @param lobby The lobby to save. - */ - protected def storeLobby(lobby: Lobby): Unit - - override def newLobby(ownerName: String): String = { - val gameCode = gameCodeManager.generate() - val lock = lockFor(gameCode) - try { - val lobby = lobbyFactory.build(gameCode, ownerName) - saveLobby(lobby) - } finally { - lock.unlock() - } - gameCode - } - - private def saveLobby(lobby: Lobby): Unit = { - cache.set(lobby.gameCode, lobby, CachedStore.cacheTimeout) - storeLobby(lobby) - } - - override def readFromLobby[Result](gameCode: String)(readAction: Lobby => Result): Result = - performInLobby(gameCode)(readAction) - - override def performInLobby[Result](gameCode: String)(action: Lobby => Result): Result = { - val lock = lockFor(gameCode) - cache.get(gameCode).orElse { getLobby(gameCode) } match { - case Some(lobby) => - try { - try { - action(lobby) - } finally { - saveLobby(lobby) - } - } finally { - lock.unlock() - } - - case None => - lock.unlock() - throw NotFoundException(Errors.LobbyNotFound()) - } - } - - private def lockFor(gameCode: String): Lock = { - locks.get(gameCode) match { - case Some(lock) => - lock.lock() - lock - - case None => - val lock = new ReentrantLock() - lock.lock() - locks += gameCode -> lock - lock - } - } - -} -object CachedStore { - - /** - * How long after being saved lobbies will fall out of the cache. - */ - val cacheTimeout = 1.minutes - -} diff --git a/app/massivedecks/stores/GameCodeManager.scala b/app/massivedecks/stores/GameCodeManager.scala deleted file mode 100644 index c396b4f..0000000 --- a/app/massivedecks/stores/GameCodeManager.scala +++ /dev/null @@ -1,14 +0,0 @@ -package massivedecks.stores - -import java.util.concurrent.atomic.AtomicLong - -import org.hashids.Hashids - -/** - * A generator for game codes. - */ -class GameCodeManager(state: AtomicLong) { - private val gameCodeEncoder = Hashids.reference("massivedecks", 0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - - def generate() = gameCodeEncoder.encode(state.incrementAndGet()) -} diff --git a/app/massivedecks/stores/InMemoryStore.scala b/app/massivedecks/stores/InMemoryStore.scala deleted file mode 100644 index f621deb..0000000 --- a/app/massivedecks/stores/InMemoryStore.scala +++ /dev/null @@ -1,36 +0,0 @@ -package massivedecks.stores - -import java.util.concurrent.atomic.AtomicLong -import javax.inject.Inject - -import massivedecks.lobby.Lobby -import play.api.cache.CacheApi - -case class InMemoryStore @Inject() (lobbyFactory: Lobby.Factory, cache: CacheApi) extends CachedStore { - override protected val gameCodeManager: GameCodeManager = new GameCodeManager(new AtomicLong(0)) - - private var lobbies: Map[String, Lobby] = Map() - - /** - * Save the lobby into storage - overwriting existing ones if found. - * - * @param lobby The lobby to save. - */ - override protected def storeLobby(lobby: Lobby): Unit = lobbies += lobby.gameCode -> lobby - - /** - * Get the lobby for the given game code, or None if it doesn't exist. - * - * @param gameCode The game code for the lobby. - * @return The lobby or None. - */ - override protected def getLobby(gameCode: String): Option[Lobby] = lobbies.get(gameCode) - - /** - * Remove the given lobby from storage. - * - * @param gameCode The game code for the lobby. - */ - override def removeLobby(gameCode: String): Unit = lobbies -= gameCode - -} diff --git a/app/massivedecks/stores/LobbyStore.scala b/app/massivedecks/stores/LobbyStore.scala deleted file mode 100644 index 4fb110a..0000000 --- a/app/massivedecks/stores/LobbyStore.scala +++ /dev/null @@ -1,43 +0,0 @@ -package massivedecks.stores - -import massivedecks.lobby.Lobby - -/** - * A lobby store is used to store the game data from lobbies. - */ -trait LobbyStore { - - /** - * Make a new lobby. - * @param ownerName The name of the owner (the player making the lobby). - * @return The game code for the new lobby. - */ - def newLobby(ownerName: String): String - - /** - * Perform a read-only action in the lobby. - * This should lock the lobby so other reads can't happen. - * @param gameCode The game code for the lobby. - * @param readAction The action to read from the lobby. - * @tparam Result The type of the result of the read. - * @return The result of the read. - */ - def readFromLobby[Result](gameCode: String)(readAction: Lobby => Result): Result - - /** - * Perform an action inside the lobby, saving the resulting lobby. - * This should lock the lobby so other writes and reads can't happen. - * @param gameCode The game code for the lobby. - * @param action The action to perform in the lobby. - * @tparam Result The type of the result of the action. - * @return The result of the action. - */ - def performInLobby[Result](gameCode: String)(action: Lobby => Result): Result - - /** - * Remove the given lobby from storage. - * @param gameCode The game code for the lobby. - */ - def removeLobby(gameCode: String): Unit - -} diff --git a/app/views/error.scala.html b/app/views/error.scala.html deleted file mode 100644 index da3911a..0000000 --- a/app/views/error.scala.html +++ /dev/null @@ -1,47 +0,0 @@ -@(url: String, version: String, statusCode: Int, errorMessage: String, errorDescription: String, bugReportBody: String) - - - @head(s"$errorMessage - Massive Decks", url) - - - -
-
-
- @url - -
-
-
    -
  1. -
    -
    - @statusCode. -
    -
    -
  2. -
  3. -
    -
    - @errorMessage. -
    -
    -
  4. -
-
-
-

@errorMessage

-

- Unfortunately, @errorDescription. You probably want to go back to the main page. -

-

- If you think this is a bug in Massive Decks, please - submit a bug report. -

-
- - @if(version.nonEmpty) { -

Version @version

- } - - diff --git a/app/views/head.scala.html b/app/views/head.scala.html deleted file mode 100644 index ee646dc..0000000 --- a/app/views/head.scala.html +++ /dev/null @@ -1,23 +0,0 @@ -@(title: String, url: String) - - @title - - - - - - - - - - - - - - - - - - - - diff --git a/app/views/index.scala.html b/app/views/index.scala.html deleted file mode 100644 index c570894..0000000 --- a/app/views/index.scala.html +++ /dev/null @@ -1,20 +0,0 @@ -@(url: String, version: String) - - - @head("Massive Decks", url) - - - - - - - - - - - - - diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f3c796d..0000000 --- a/build.sbt +++ /dev/null @@ -1,23 +0,0 @@ -name := "massivedecks" - -version := "dev" - -lazy val root = (project in file(".")).enablePlugins(PlayScala) - -scalaVersion := "2.11.8" - -resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" -resolvers += "dl-john-ky" at "http://dl.john-ky.io/maven/releases" - -libraryDependencies ++= Seq( - cache, - ws, - - "io.john-ky" %% "hashids-scala" % "1.1.1-7d841a8", - - specs2 % Test -) - -includeFilter in (Assets, LessKeys.less) := "massivedecks.less" | "error.less" - -routesGenerator := InjectedRoutesGenerator diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..10a6944 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,4 @@ +.idea/ +dist/ +elm-stuff/ +node_modules/ diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..0d44a69 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,31 @@ +FROM node:11-alpine + +WORKDIR /md + +COPY ["./src", "./src"] +COPY ["./assets", "./assets"] +COPY ["./package.json", "./package-lock.json", "./tsconfig.json", "./webpack.config.js", "./elm.json", "./"] +RUN ["npm", "ci"] +RUN ["npm", "run", "build"] + +FROM nginx:mainline-alpine +LABEL maintainer="team@rereadgames.com" + +ARG BUILD_DATE +ARG VCS_REF +ARG VERSION + +EXPOSE 8080 + +LABEL org.label-schema.name="massivedecks/client" +LABEL org.label-schema.description="The web client for Massive Decks, a comedy party game." +LABEL org.label-schema.url="https://github.com/Lattyware/massivedecks" +LABEL org.label-schema.vcs-url="https://github.com/Lattyware/massivedecks" +LABEL org.label-schema.vendor="Reread Games" +LABEL org.label-schema.schema-version="1.0" +LABEL org.label-schema.build-date=$BUILD_DATE +LABEL org.label-schema.vcs-ref=$VCS_REF +LABEL org.label-schema.version=$VERSION + +COPY --from=0 ["/md/dist", "/usr/share/nginx/html/"] +COPY ["./nginx.conf", "/etc/nginx/nginx.conf"] diff --git a/client/assets/fonts/gothic-a1-v4-latin-700.woff2 b/client/assets/fonts/gothic-a1-v4-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ff40f0bcb41bbba84fdd223409e3cfc7413c8f65 GIT binary patch literal 14868 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

KPL`$HQc*ZD<=(gn-Z)h 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 j8tM$M6533ugk{!2t18MtDRYm@Po+0`_>M724 literal 0 HcmV?d00001 diff --git a/client/assets/fonts/gothic-a1-v4-latin-regular.woff2 b/client/assets/fonts/gothic-a1-v4-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1686b3585ddc15e2b9dc145a9609b18fdcfe870b GIT binary patch literal 14848 zcmY+r1CS+K6D{1`)3z~f+qP|c+O}=mwr$%sr!{Tc#@}=A{oeoHiWQaToT#eUXXnmU znN?XXvLXxsfB@g%002PvYwO_w0NAts{q67f{}(?wFe9w71VD8G{4BxgAcBk$z#;ux z*!**$paPTt09k;DeuNMKAG+XI0qpm1Ln!}9A!PglKwLC{5%0LUy2wH6B`@~;I7Ly`MF@26ibX~Y%P zG?n~a!^4pe53wDD4D+*2(HQs~pzJ_VAN_F*l%PS+I@YVKTd10gS^mKt-`+TuCVT{Q% zGpfYh_;_(jsgfsEkbbcN=!id=Y=I5E?=Hj4sIABb<21hF4xkR||yL#e&Vv6$? z$m;PWM@<-_S_(2mNBU$IL?d^u9pN{v-QM1xsZn41GG@3axj%6zlL6qSDou$AcU}=e zlJvvgYHn~Wx2xN9n8>!**R1$@Kf<$)_NIsi{HMbolm!w%c1s`uYRKZ=_|qJn^^UxG zLMM&<|G{f@8-g4C5j5IB_&A<)dkbQho9cDzvBh&86S>Rv&!4 z%U4yKTGSs7U+ltMMjol){Jt28c;!k6hnDh%SY!@|qEyhh{6ul4^n?0C3A-{lt6Zdh z0HMg%j!LmBOWMg&W zAYwn&UV7-uo~6*mZ2L+QAk}~l$YSGwaVIQ%UTfS&CzQa&G?Qq`;3#p-K>N__`U*dQ zm~v&bljEg|2)(vvc2(mkex$rQa8{q8G=gV{C;}D1D<$QnOX2|`rS^V_+5i9mUeS=r zwUFkrPi0$A(CM!gJZS)_2Z()y3kS@nz}#{X_T#kwQ6h(l&l-J1I`4!Ao%{^vkFVvL9Uvega_e@? zR{KQudCg8pn`t0r5>aoKgI*`JCO3drr|30*PW{?_ybic;WqI|*XkPXDW=PHi49~zi zdgClH2g&tmzqnAhvG{LR5fB@P z%9Sr;0?ceS3H*_3LJ`O;&>{s!!=E%x{!+?xlgY4_&=Oc95S~*Q!2Iygc2odgbk0dR z#;;FCKZH?~mE=W**#V}@?;p*Zb5AltevXd+QEyZor8p@S65oDl+-lQc(G0|@|MOB@0h0v_+(>!O4eo+7~CM^=VW(dI5QsbFH`o(^R^ zpkhax&VV|wK9b@{8|F3h(7q54XA%t=A*&sad*(CB!YrFUu>}n|n{?w|*_)7OZm#~! z5n*_rcoVryOo(Q;>_z?Hs=@kYes44xv5Yq5dz$a{y04}PWd=!vV7xQC=>CI5NGUJT z-Un#c13)kTjo{R1+Y~N;_Ffd6V&%x!ttuh~g({N>yhm(h{zOK;lSa6OtYo_OvErRx zmA83uW?OkFZajQsd`r zt>4D%&Pb$om=k@;1_at-A1i5A2Kumrz!IQpZ*IyJhIiVajb2ovb> zZ=KAK8FsDIuKU=QzEDy?YR%U204b3*qRD*GS(v7y@FRmPV8q?54na|I

d)on0Tm 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 CT*12l literal 0 HcmV?d00001 diff --git a/client/assets/fonts/nimbus-sans-l-700.woff2 b/client/assets/fonts/nimbus-sans-l-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d5acffb28511f7c05526bb5e00bd0326f9cb090e GIT binary patch literal 23716 zcmV)KK)SzoPew8T0RR9109>R15&!@I0O*ha09-r(0RR9100000000000000000000 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ⅈ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 TKp0K?$q$MS${(hbVr;xIDOA<| literal 0 HcmV?d00001 diff --git a/client/assets/fonts/nimbus-sans-l-regular.woff2 b/client/assets/fonts/nimbus-sans-l-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..55a50a44dc700017f176e9be4bf67defb94424e9 GIT binary patch literal 24216 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 literal 0 HcmV?d00001 diff --git a/client/assets/fonts/roboto-v18-latin-700.woff2 b/client/assets/fonts/roboto-v18-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e327dc95b6a3b75d2cfecfc611b84526dd360cb7 GIT binary patch literal 15436 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 CPzPE7 literal 0 HcmV?d00001 diff --git a/client/assets/fonts/roboto-v18-latin-700italic.woff2 b/client/assets/fonts/roboto-v18-latin-700italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d3239a3b13ce5412e32dfc58c68f12a122dcf08f GIT binary patch literal 16572 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 HBnkikn^Nxw literal 0 HcmV?d00001 diff --git a/client/assets/fonts/roboto-v18-latin-regular.woff2 b/client/assets/fonts/roboto-v18-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7e854e669b7517d21df4c0b3c51047be86555015 GIT binary patch literal 15344 zcmV1bO#^| zf=L@_d=+e)hO^s20I9$1Vm6|JjRO)4`_iH)+oY$-{{NOhWyrc+(+=P&Itq;~moSmb za;PcG#b{HklW6bNgyv3$DXT=*jnciMxUi7driA)p(OvuU@bLMU?p_zpvvfc4ZF}nQ z5FR2pG=>k&Im?~B3$Nk{W8DbtKiv#+GjE^n2#tM`%fnM$kAE(zdSXuiuHr9x1<-rr z^$^YNeq&?w*pSG9f=Q%+&?BWswnUDuu>m70Wzw5Ll%isRqR?VsVKq?XH1(wv*`Lx2 z3JpyVe+uq)G`7-aNh4VFdMuMH6CL+GVRm``5k?{}pvb$}5}K(d39T?VjApX$;Rl-gZG!mrQa(rhG+ zTG?9cz4Q87bG|gh0^kGsfCa!HcmM{#0QmmD_xeVXKf5Cp5K4BaVw>Qr<= zP$nterAtxmzFm2>{|o36@15buuqTti1UeaHg@7v*gk*=#7a~LDq-cn`aOpA#O`Eo5 zEXpcXP=(__fpG+fHv|+!fI$Dt)GYn4u>hCgLfmfoAaW+i8W5WGtQF$4(SgXk9w1n=t= z9R>t30dzuh-REw^Fn|K&Ig>_PV(l|{x!B6TDkQ4E`r}ln*HBH>S}li8!g5nTNzObt zWl|@-GrG1iHw&6SJCDLoOlB&nQfkq%K!b1fSE>x>wHm6Qs^#*v-P&q?K~zENtAYs< z8+KTdPCDhZGh|Hxaug|1rb3mv3p8obX2O&iOV(@<*mLB}g&PlL%2lc|fsWy-Yp%Ot z(v(L}o_+fA?Z+<@CxnD0B&DQfWR)tQC#tP+P{-o}3{AWqY7>GD2NNAH40CG zaXbv#;_WS;0zU*n0wurNOOw4Z)oZiL@w~UL;#Cc`HhT6La22k>b+}=B)D{Q`Fc6?N z)>|9|gdhw_NI@DhkhSG{BM${A+Dg4vj#{}iy+F#{9Y>rpYJ#BJs)64Ywy_hsY#6A3YN$n*;2pe!cM?Dcqj3W> zIF?|QpHMo7in(C({I2f3e(2450d$YRYoV; zjI}+yS~$ChnQkhZYHKDEVigMhZQ5~D?s~+kJQ|284zP7YOaS7RfiQl{ zYK#qzp0{tua%(zz8;yL?>Z1Nl2ku(em&N=Bj=n~O#>*X+R;!mi2T1396*%hA@Bj4S z{RlkX9ugK zWOFO9Viw)GSG4H1yWcv6HNV$H%SCmSzY^tdVax3v z8_pg|@B8PvGv2){Vxm#g`wscdZx_)AJ<6ei@^T?Zv3=tJI?|_?RsXN`JyKnCvi1dl zPeyb_$Au-eH1ChNq2fQS;wZA|$U0&3mRB}gLuy$8k*1L{)jpS%(f6&k&9;EE@+c?k z+=JHj?9;71-q&w@1sC&S`;EqZ^G*IT6$6geEq8Uqq#DbiOm+ZFHW2il!eC0nNjimvqAp;R z%@RJsM}$B~A_^l#1)QYZPEp8diXcN-$x=3Ql$RofC{Z4&l!F>&rA`H1p!_r_nkHqZ zMY$MIBty!~h#hmQ8VZ<98%zkGaJG!u#cEh!kw7urC<}MWiOiX&cm|&eaG|N5!KWHj zNG%mI;RvC-0cNsi+@{PzxaBU$vIkf_RF3Q(S%dV{@t9{eq3k6xdF~ZBdgUyHn{XRE zxWRZatj5EU%z!yd6h?;fQKT@+6y`kRM64PsVIju|mT(a^f<~-N6OfC>|?y~y|?3JGpOCeq!=F4tvfCE9M`Zl(4fT4}<5DjxnGisr0Pi-vC z4O<(WUhF7Rq>s!5k=ZO9U>A;1z+fz=KqVp?QybquCX@)Qv`hdB zOnxc>0`Ao(NHI|3wc@0}RfJmC1OUZ#-Z(6ew@oN7+*}J7^ZZQ}kA<|sK`d>u6uRi?-^0fp6*Yu!7QKHdY zF=W_?Q7{l5B7o{AMKJ_0UN#hgIO?slzRUmT<*;SLV}KL&y~0Yk#tkfufDUlXzDV#N zxcgF@00(+CKwmOwaAq_dSn$K$Qr`sDUdKH##dQG}!C}G=*h8vj`&aLz1^2vk=m>9u zkPG=NH?}>TAWjq~fs?|iT)(2naCC$e;CPGUGf#P^%c{X@vwnf&#|dv*t#tIg zlsgS&3~i+Prpf($iLWc-uLs%RuKqD^fLe@a9?;xoxPqp!-ShN z*LA-m%d&qugK1BWM<4_vr~`R3+BB!=(@MXbS9#8g^ zQo}Vvip&A_-|@tgb#Nt)tuMjTh0ueBXYxB1ow(-|&iPYFfcH3e9ojBdx4aV2vu-sJ zN2jtyuIvK8!|kPMac+w<2*lIE^K@FCdrzD>TAEhpwuZElyxPX`xD$@NC11;zyfwrH zc=^&=@=dPZDL?Y9WN|cvTL$82Y;P$}yVOZ2jrVhMQkSN+3eo}ctOYu6FxH!&$_L&`hiU$+OY&j+LrVYG}uN5TlYBko=cZA;(+rlubd3z_mx%|`X-{l)~5dA zA0qLKrg5kCcpY%S2>{fufP5fu?pq*$=W5SG5a6Ip*c-qYT)#xx7J>(!Ne5QgmW2SO zy=WgPvI{Mj4YGp5)_^JPeG_b5x^n#R7Xr_K96tvfVAtm6xqCt(S>@qek85O7)K*wsx><6 zu-UP&CQ2VVlbCC@4*KrSsyU!BwbIFA>kdw7V_SW-_Jfpog*7&3ylG@GyT!3|vb3gf zDP+OcAWk5g(Lx z?r>8=m@|cj8ZGQHWS4XfDNXb;ZTz^oq$pO*f)r9t|KaZ>;>fr;ZK&9x2qaJx;_YG|>+Dp1ivyU0y_PDTv>%STSskt(Agay9CHD?r0MRpTjLh+tz5(X+zv5vxl zU4Ed`aiHFq71-32QmlD)-1Sf@KO;anR5>$b5O=bJ_4L^X$^sIY&ShMYl2rdM=u;M4 z0!55RmrhFZI3lYYM5Q-ZfbCS5kQbz%5rb76@124O1yWYhJ;QknA;8IE;WutqFUVwB z<(5WxMRQ82s$Cjl;$XA8o+Fp%C}X9ueU-C+vgYUA-zvA*Vf#q@$65R!$kA=Sy8X+Y zyKbJLa7eG%Cs=GRw$Zd?`_7%lzKu=+OY#`)W{Q}oD42F>jwz)8G1G(v?`HhKg_!=W zq9x7*V#2l5OAai{3e*eE-9w4|5wxX2<4)F=5IAgPOQZGWqJ_N>mBr$?1OOAbw}vBcDjCpq)+pm#WJi86%oCf!bL z48$>VQP9%-xl%pV&t+_T!uXbGPXpB!&?`60uVi_)`x=ZV;JU|v=#3SUShyyYJf)7M z8C~Vd?owDiBOFk~6W0ibC(MMj+RQB;Z*I|ale{VWt3pAE$`KD%U==AdD+q-Qme^j{ zq}ZW_Eb$HUc#A?hCDE8yKGESiD#@JkB#yd;Sb=a1(yk9l%$V|U$Ksq78l}X@*_q4C zaE^)hDp;}I=fpD$kMn@VT_JpgoRk`XrD51h(cVfwPJ{YJA zq9c|ga@mYhAkZu9`?pK!jz$6mEY9Mjz(6GhVJ}o`LYa_@f>mpoOEHqPDlgH1P+VT| zMhHV*n@Ux!M#V%5?$l@eUpA3r;OXSTpjU?TQ-X;d1ieOr_xCZg|4q0;Nrn*Q8(I`f;eITaZ4GP(v(hORGI6WFfYy#)fvVlQ z#DY?+59o{SG}oFJ$pn%*I#UXj``jQtNQ?$?Y(v`oI!+p1v)kZ%gVAJV3uk+@q1dHK z1LRW;NlG(BNTx5`ECmv$Olbn!(RSDw9fa*$WSN#K@~4eQWrYJi#q=W_L;3kGqHfrZ zaE=@Ho~*5kq0*=}c281iEd(!-@gpumkGuy&lHNUhOIOc8(X@N@Z zDd|1W%NII?q^m@Vo#em<@uVWG%o~8@^_g!8Zu>@;>IWPc{O5h@uY6$V<#LYOQsu0> zrenqXrK|2p)XkrZYAxC3mavr#$+87qhq{=ESQ*;qfqKaaGOae1lmA3Jt1`sh?kP>n zsh!^(Kw1&Dx$#I=V%|MQ<+vm1Yks*t)qNKEoLS1B1QADuqsZy^w_n=b7A=OV0bY-q%jy6_J>KMy?qxL2na3h@sNx6 zwA(gV>R4qIt#Ra;fA3~EIiA=^;XP1sv-_LmS?Kt28EiLry-)XYz6LLxkyffhY-AuY zCmLKfkB9ArW4Jhw&ZfB|KGhG zH{P{w6wkFjc~G$H8|!`P!ILLnZAB-ikE|%cjiWRDYJm3z9C^u<0)J~zmNnEvuETs5 zycdijV%0?xj?6sRODt=qy!?%tFttKF5emvbB)%fqaLEc7$= zGn%@NqON;Cw>*g^x(oPAtI+Oa)3Stc?{4PK#LU?0LfG*ao-x!I8+}=Mdo6t*M>89b zusp8S5}xo3uvLN1w%ChrIwYhUNG6noGj5}Vfo*1ikZn)2hh#*yN&LWD0*eZMJG%O# zQ?*%^2GIY;GSNSSyDMuv^%E=56 z3x#RPO;SkdMyyV2sKMricL&=_aD^Ybo9ExZ-8=_Vn_z#N^_W_eu?lUTdPmx!o*B8)l!G1iw{dVQb6Yf(vo$HqTJ_?jf zs3M05j)EdmBz@rT{_OI>(;1J%D}iU@Br^xo=8n>}U~`NSRgs^XTS&=DtII2eC`qbb z2TN5;HVm7p&{MOY=1+cEoL_P3UJ zexzkRA*E=%qOy3xTF=f{a@2T6vVxzddQDMlPAQbw<0Ts@mwh$v`v2|w5C865i%q{C zDd#C0-V-wM_Q~|ryC*DB9*MOzzbmVLmKJ7ZmKAaP3Wnz9Mu%sUw*0qEKE+pYFI^4a zOz=!)B>C--O2ZCrCy~~E>hY=cSmxR=ov1C8U(`2v?KU#oVrYv$IH6PZP|Ya~)D zQ$pe?Ayi>U^NeM|m!iKvz*Hht3T_uGva}Dr=7aI>PxB9S)>Tg2=lWClex7frxwlwo z*hp=Tx*Ny48kNCKw`Kh}mFk#**E`j=73XKR)fK^fEv+y; zk87lj@nHc1E zC(*kt{0esERVAEQu$VTTJyYFQ-IDaUBHGs^2D1VXm`Q-ZJZkipHSM5nfx7^)&Lw^* z1VOtyH=Z>PTyAOWA81_HB>xv=y7Ewbp4KZqN_(KXwoDEa&n4 zY2IVBLCl;aZy0YFHd=C!7eSkHI;EfKD!q66!Q;(+zw?*QKYy}$=fQ5U?@H3MXJS4j zVpC%A&&CaT)q35A6UFlTncR&(3Jr0iuO|VLG+CVhm?siWz;b9bDWo(F3o6qS0rNy+ z^)x_|rcVmCU%kHDf97%8>BoI1)zYiVtICB%+MhK+lH12m9kJXfJ!iQ2&IqLkFb+oS zyrPJaJ~=){3~OTLoI~u@M+|t|=L$&&^w$E1IwuLEtrFWshbXUPJs9`T=jW#Gh8Lrf zdjS%B(F`Y(cTU46n68)H%^$byS+VUI$tk<*ubJsn7RT%~$mAN+HRlty$s`5D8l%3g z<&}?pe9Am9$Q)3V8(3JoG^>lE2={Ox_5~P@)4Qc#Vn*%UihiPVwgia-!;9 zF3qaplTCF$sp^5h8FmAU`z+Ct_;;fys;`gQ7k-(3y;o<4rQ)@+vsZvO@fGz|pPz^1zk@T=3t<#gTo3Ny&rc6y zwtdv?J;Gv3`|~QR$Zlb2Q#ano(?5?_+|L`dZv0?d+nZPQAeV)2W0L#2hzXPHJ z!o!{OJS5HGbbth3Ozr6E^hHtW(2x}8q>x~3o)PtNRUu9J@*hfCI(mew7}0ZK$}0O4 zT`tnUM#U#f6mRj4wes0*pIwc48e_NhksyL?S7(Zgr7S(S9#A&#_lWgH#IMqqq63L3 zm+}%>FD*0X-@O{stL3Gu$74s-5qU{>R^8TFsww&3Rz7tTE&rdRG5+@srE~)= z|Cf#$>3mvH*s-xV)V)!V-}Tf$caQtWuJrVLL^2B!g75A9!D{IXKA^7%eAc6nz=L6Uf1 zPjKmZr(>tni)9$*uC9VUM+HFMsK8==FX|T~vu=v8#$XLraPE+bGGdX_7f)M213^kA6rI=B)2b+IQo4H4M;CuSI!3xg| zdxcrhy+IUBGJVV9nH>iaDf2g}0tfUrL*k1<8^>2}3DaAVT*Q4Qwt@|lnaig}}>p&12V{QCapjm<|_CWBLx!UHpEQc!U1hBW-gh{mi3 z_Zl;D@-lMC1()iUmcv_1D>L#+D;nZv=NjY7tFlOyHKDDy=lmLrs9AaHu%sm7quGSL z*~Ey4Nyjh2x%i04**M}OUGZT7#md#Ndx#W#2R<3EH?60Ytkt2F!W?#QEwYq@kHL47 z@twhMR5Ey-Zi|;Q%Jc1pFYHI$Ja3>%j<`#QGKVYcNgdIiPK5#YHAiJ9!(3~_8KVxm za)x=nove#nui?9Q=_y^cl%i&$x50VKTpdx%ZlefKwRF*5Cq7d_ON^*5}q zpA;D%obT@HO7Q!i)!%di)1kxN(R8A}w?Q1!+0u+{Z#+)ySzTZRs;lA zRQx}tVq&PKZepr(-qa$*)I|PNnSDrANMl!HMpZ^b7Z1-D{c3&V-dcx{vjQQ02J(M3 zE*f4`Yan)(p=wgY0)m1F7K6o9dvv+Kt*McQob-XAnyJF+2JhCyRl+JYi@6u zmGNpD)*I_wl|`(HY0fq%U&iH@revq3zm8{MNpAJmiCIW=YDyi z#B%gL)HXOhQBC)b#^^G)`!rsisja&@)oI$qm{z(z-O-Bh^^CM7xD)*>d^{rw)*hjd zryd%OzSsoWD%QaY+)RwtCUF;FtytI zRYzXe*q3STSzAPXd3`c~Fh%P1co2ERI(EBmg;XcLnNXYSY6>Ew*IT2aDvy${bcmN%GTr_tl@m&ms~>})25M*P^2G6 zQ3z+q!1O+}toCT;O{J$)bNs(&q@w(r(HcHFezU}gdk6pCMlU0K+U9bv#RZ$Bkg~qz z-3iz7(ez%3$lYL;{Xc;kZ+b$>Z+g5u*1Sf}YgyI1|JbLZ^;kZqU<#?CM$Y#GYfVK@ zc~1@dH?9aJiI{YUq-Yn{#F+lXgo(riQ#)BbeR(;3U2i?Syn?R58O0YHA&EJzr-wZ? z`-M+8$|oU%<0)>(qde5a#m=hgOY5LtVgrp_Q(>i~iD-Y%MRAsL-T))l3>dX|JQMhM zE{GQh1{gc1!b)Q4zJA`f#0t&@7&((*)pUA$i1)({?aV{SXb<+hFOONL9KNFo}f@OGsZg>pN`O-X&J4 z$G27u6jJK?KvPj$?Es~)st>BKM^2G*;KRc+lx+BjF>0wx{y$~3<(c5j;GTw_ID}t3 zq9y5ITb#`_JX5}8E8Fc@kB+cudJ!ohxv4lep)&>HTgT#^p=)G{*D^OVHR<)0$P&%s zW`lU28JQ7dbBp7m8*8#DogGz*_cx#4VM-}cG&{>BVQXWnt!ZwNYhzhhNmFIC2*q*lg|=9SFVw6smtl$0;3rCwB{9G>5o zlX(^K%JqB7H*LI*Uu;2sYC>yMMt)s8tW2}M0PC4nndOz(;C|XrgJ5B)ap59CU)RLk z4U0u%(E`}T+Nr9kv^jsnshYq4Zgp^jpNP$!-M@i6yAU7Wv&vIH((f$Iw|fFoF#iD1 z0<-?8C?}J!9TQ0C-NANn-G6?}y1=jhGg`j~FzX`-m@+%p^{m!~8-gDy7CreG$X0l# zusjj0$tE0-+QwqScy2)o9L(M;B_Dvo5=z0S1zq^h}TDAeUvd{;r%6D%m5ABR2 zrU5t+Cg{SFw;X|5^Z@zt1YPhFYmc?p+Gp*z4v3eBBoenoT+*t^_>c>g$T=n^or~Ps zxY&HCA34ac{WfY?g|2W?U`vRH>ko2zGTxui1Lv3^I~OUh4Khcho$+gza|~tYqE)9h zXpQhH*hv0(6#N$a-=ZM@SvTf{Ck_E%ND zHC(E?;%k&0FrGUGSSk1_gy;VKxT-BFU04!IROPs}vI1H@uTCtD=FQv*zzf!Iz3c&% ztjnu|#wrW5VNB#uO$WH>T>yKtZ;VlwpaU%RrWw;DOR|DqDv~Ld9Qh%_z&K)2xw1$! z%|GzcDG|4W@3k?f2k5I8J&0vqKOi2P3$#Gq2$kyrTn($?+bjDt> z7)M@&ioCG0Z^LQu@!VIAIR!xddc5O;Q_)HjK;Q9hz&z4W)W!sI&&IoR@vHD?3P`$e zay%N1U*uRIFGLW%TrXsBaZ41r>Fo3W8$bnwHjwD82>jI|iAu7mzqu{q$BVKqkeP@MvNyLDtf8R&+_F*}y2>8kYtX_k{nrF&gy z^^DDA_x*_@Ymo@2Zr!@sECsB=uwi4*q-Ode1825ZIo<8|bnDY}bi$=8*|+JWt(V4PPhG08u)T=DhB3&$(amXACWf8q=?b(0K80)Hvf^Jagc;W~HGXp|q>icLM*5zR z#CSnuF|qZRS(4brR1Jru;2Rztx?P|(eSZM(_6IQd4m$GmOkekC`84hRqsMxCH{!)!CF4c`zwo;SjIk1_06^@bJCi(~Z3`vc-!M?!VDI zn{)puBX5->Qnxng13CjiJE@aZG077drym65^Tk>0?SM*Ej&;l}vlpWuyn`P*V`Q48 zqlbz6%sX&tI?D zzY>Q(A$kp~1F?fiZ!5RGg|{^J@sAtO1DL9&^!0{?0)JtP2%$t%*@_gix&ll{QzYz| zMlm&Z%vBph2<%iQf(=Uz7#Q1GieJRCb49Vci~+>^?;1B}Iqu99-#SEGfQhj-khNh3 zGPdl+R>Z>G=6$2db=D_SFywt(iE*=u{o)zvbN;_6NO89Hp#Przemb6pF5MCUAeoX2 zF#8W{@V^eEANtotM!sp7N3s9Ao@06u4j;n^2@cLpl=pa4C@QBg4?-Q0o9oDR%RW^-TOWlISX#XqoNGZkqQ2Q|WSCev zCuP)npH#g=*2tigGA!?vGu>yfEUyQK$4LCdYS@EUeqZX4v{lH1Z?84%4+19RQj3v< zufdow8AzE^sz*+i&-ecQ^xl?A!l3(T0d8b6;^po%BzMqJZBE=JF_zUV2LTC4n3E$7 zTBQpPOQD-W2PgG_=%bW3O}Q-|;6rxCZ5lQn$ZgprxIgpV%IzUMhmT-^_;`(cPrUNQ z4pX9;o1{HKwo6CZtsF)gsWt``C28@8(=MZM6v4&#-+@&rL^=GJlHe&tVNa)t$t)Dj z0|Lo}##>jph%NwNy@6+mnh`IJ2?tfrO_4TbLHj|P!&8TUH)}OAG;^CIAb^4<9geUM zj0?S2rN^+6SA_OLwr%TsCUk;Ff1OqarIYO&_<1BOS4H3Q>k= z2-s!Yb8=gM-dDRcBevyf#?MH3GWHDGmq9E|G9uOu%n88|FHdM4|wb?bC zfe76K6db16n~HFonlxNeQV>>m>Iq+ThvOLH+o<5=!c#1SoV;)UshP{3Gxc!#$zP%L z!=D2CN52Kg@UQbC2PHQk;E?*t>8=Nqhs!q@{9s?2dFA;7E18|LR=vCxY^Y5@8RRTv zy%1Pg+O2kz4d#2P2tW$WTpBWAd$(QS1r<}BprOmUYtf#auJRh{!pITErw^uSP*7qh zMM(7l&NMcii7pP7I>4AURWQIY6-0X$^<;Rw$wT8b6*C$ z)!s&m1oi~1on3)yn?Jx5d0V9v)?J6B$}!?VhHpH~4Y=c=SdKaC9V|Cq=4mpJ4C&cj z|7p;dvx`yobshrb^nMOY#;`DJHwz!m5Z?|bK$cpQRa0c>*tzCm)T6Vr7#%oKWF_=5 z7^#-H?HsFCToa%|G0{b{GGhu&o(-$DgI_69gsPMfsb;A3*?-kmJxFeJq*s`{?6UN% z>uVSAbpthgI~|OCmeGnVg>oNqQ!_Q>X*s^ww5G@BY=KL=gGLx72>>G0H>K3Rp8bxC9pNZYh z1MIiowxgX`FCMyZ8;^${S%!^_i#pTXM%ca z22+0Bg}vGp=&`z6#9!VWjTB1wqIk}AmIJ#PM^wAij*^*{9Z|I=m~*pgw01PI z<9$(LY4YaDWZj~cqE0GJL}&!Sei-cGnyO#TW=vs?A_`>BE(iTD!n!}EDCfY`;J~Q) zH$VPMX{cbp--=@q)uOIS=fWsVO5PgN%=pr>wK8X-#+U0?dXj=jk$R7__r#A1!20y~ z%ovu-sLu`|6+1elS*hpE$SPQB&0y^3)2a@a9v^}zwc6dRtmI)Nb<|~lcW%vgS8gD~ z*k6$ff2a!}-g$0r$A|~tt-A0gSP|_oNT4w+*d^AM*u1qFS2+HfkxYKTHnQEq%A|Xb zL+vm|7dlB}JT2ijN)H0O^lm=DToQjzhfia0*}`zv{vk3?K}4{A;RJkG)jVt$0TcV2 z+|zCuO52qV_*Jsu=)za`-p zq?m0Q0=SjUohV))PbGom$X*10+jlXo{v1AI8*?;>zXxBiy!xCnF;Y+m^G$P*l(A1q zDiWQLRdR$kYQ)o7ElR)$7Lt+|1Sm252vlgGa5jW@d~#*bE*Q+a14;RyD$ECc3vEo( z;XzFp!}>YFIG~wdwv|F|SVna=_))>0|@p?&%*^2SP27}9IztKhyyWr90 z<9}K%wxEdft@yE)FUbq|kg=C33X!7Q@|0KeXbx4~NL- z=8F;@bX-%8=ZhOA<-WJtcqHtjMjaR)*2an)c|eD8+qlI|;|AN|?G1LdS66mE-}G8U zWcb<8wLouhXdG}}K8Nao9ZPm1#?Xztn5tI`KIu%?X&=aA3d9&~5urn+%YOgMf;GcL zLxz^oIJdrnj)K_zxbyGgBPNoTX$3 zatVz4Mo+tKY3j!tm-Z#njj2LnOf1%Q)IM8ZP=}!&;Tq1O6;YE|QxdrwF;G2%*6CWL zmU9j~qA>%zd*yp}?DwMhdG*-;?<~T>7FtL&x0D!7E>3u$5oOpf zhl1H>qLbZiU&s#(@br(qmm)5+>D`US(3zsas@;DSi;xWN*uvyyg z^jt2CGOJ~dT;-?=Mdmb~ee=$KZKTa@i^`vLXYg$iCEg|Iqwid;0Ri~aE-VdXX3eq8tm=`OJ(k@E8lUXVL>JvHr616b zw>I%b+1XuUzHY|3`FgxYFI_*D^+wN(a%Mj{meyP4o|{?S%R*Zl_)*{uxyz=j=VD85 zch{`ay60w7aBk+rKrT&tAeTno?ePBzdF;OJH=M z_HR|oZK43XfB^_#x$h1;0J~i4KSKXacaZ~uEBw9+_!`H5Z~m*fm*%7*^?PxhDr{^B z>|gxC&!xO3CvTD}BH_+w5v4>@nPdtnE;E=}lJqD*FSvm_C_^n|gE>U3Y~T1OH?BC{ z+>F~-b<4=DMHd%YK|_^r|Iss)#SLveU7&=J9*ii7V?<9%8}A^>gGmxB0`o5r5=Oe% z!T$F64ej^3dy-WzkRNgi1|Q)?faYML*2}p1YwS}@X&F0}Vp!^ybgCXxK8jj93+)sI zEyuO9c(f+BT%^${JUc)^d{P((>tvNvVSG}*z)?C#K^Jq8F$;L)C;->f1}uFqq}m}q z;m+0%r|TA8<>;8BbmEIa6FVYh-Sbh}Q($*;86)`lrHCtjtE^vhE=PXTgiDnvt7-ln zCT*H>u3km?Pi7nP%0qg2NGmfXHA!t9&`(}v@kewXN~|`CHN}5yu$#)Prke1&1-~S& zpBv9F8a&YnS;(=3`MwM!T^iA&e3h~Mg#76A*MSIz0k0SUDrY3OL3+KQuwXVj33 zDkpM9K(5osX(w_Bc{nRieu%VWedHaKlWcv``iU>gM)e(!0myr4I%8@ zH-zC7lnLLL$pjlBNQvJNNm%3EH_iFv5ASAR2RGFNqPts|)rvq9}np%pAx}R&?G4<4d2H z%_hA-gR#*fsFU53I8%m~2p8)kfFBvsi56syKt2}2(7yb#%$U$;zycw1Y118Y;Ff!| zPWdVsHI4l&4V(o-5E^zC8sdhi2jkr8a*{%5fI17tA>#`&;yky6ahRjA>M3pqx6xOU zLgy=0{(Xvbn}pd!U%#~SN-QAMPa zEz8$J#E$*x5ZOn=j)Moy&Vsb#)KEAn%x%Y|qjXw?X9U_x*P=PV5QdHUEDijjp!gwe zK`3g-<5^4-WNV^_EX8n=$=khbU{rWstK(0d5tp&(WhJ>(%$#e3qBpj{I) O6y7R}gQBj@I4BA`2c0zl literal 0 HcmV?d00001 diff --git a/client/assets/fonts/source-code-pro-v8-latin-regular.woff2 b/client/assets/fonts/source-code-pro-v8-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..acf1f053a9a9e6918a06808412095bb26dd756f4 GIT binary patch literal 13436 zcmV-?G=s}`Pew8T0RR9105p665dZ)H0C|`I05lx{0RR9100000000000000000000 z0000QIvZphgFXge0E89@A_;3TnC9k3_%<2 zXcgm_Rlvpp0JF=)jG{K|QJyGjYg<3%68+D6&Co_NOO;M3L_&rhIGWSx3OPmK zHlqMY*;db%@skoHT;#jI1QY-O)r|+Y2Vq5RXSwg&vZ^6k1rak=70#u*3wG`uA(Okg zygB(4h2P!ZsIEyAJTB}kQT`q`O@GohZNmeDK%t*F;m{Uq!3dI#UnBWHn`y%HXS_ZL z=}fHWM{s^AnhkQ(`1OI&%=m6+M><#;cC|*GeFoMU1O}Z^0mKTruoN_cLVy)kKpk+= zv{V6hf#*v#ZTbF^2ZzMrg_?u0n;KFsor=y)gIoN;3b$CU0_70vSh-CE7Sd1zrQDs{ z*t&I9+H9H5iYrM)E-VWf1k-Q7y=<+f!Q)~|%Z)LzkufF}Frj>$=w2h36`U^eFj<^a{e9IhFd6Ac6AFa86JfI8FZMXX+H^MVo& z-K(Psga(DQlFWyMZ>|R@k*NSwLIOas=sIcd?f*9d6MW(qkM-{;d z$@V?Gxe-KTl7Az@#vpb_&(6j+khCXIPXtmRXgMj(@t779i=y}0n@uJ1qSMYe>zwm0 zxagA023>Q-RcYw|B0p@ueg_OV=#ayXIO>FxjyWy@bFxW5+-#M28@L$@NEO^Es%p?= zI(NS&90<%XAiGX%#EUMc@7`+3>DPPB!%jM%cpPL{lMaRjW^92}p|(1h)DK4mI=)ui zr!yZbxh31No^ccv2L*x+Rtqadhwu)XLnQcO2LZ7OyOP=+VE`aE(pn^8CqS-O6!*%C zyVd|w1NEOG-c?Z$?<-|b5cnEEpwIm?Aj*+blr0B?paQ7)w0kILNI@!?ShENTIp-6f!^%lHd_NER9K7Tjd zV^4~@q?+;`&$^|zytU+Nz@Bh$yGI)a#DnlUe5cX(YcIGN!h(W}%(l~APYgH9 zFXiUF-`bNbG0}RHP4UtTuguVAwT;$zV}*ZMYlC&(mcYdxy62AjHre7c+g!KEJ4Z>t{P*yV=Be)XH*&E#WdVU^3q?imM{JZ>Idel3QoQL9e9292I;(yC3n z4xNVS;?%6$9P`aJ&(GlW{6I7-5%Kz#crPX1rxWihiTA6-KMH8_qo0^9cibuP7DJ$C2x1D;$0hFX1}%BJ)aG5Jxz6Z<&*Kqi;QzM!sk2g&)s^m6)!is_{h zs0mf{B~;dTnW1v}PCTFw&r!>hdJkgX@_fu`TxEWK7N;L57D?y^rJg_?3aRr&`EAmU zoV*DwWze<^<^}uC)MNUV>+>;0B4Fry*40JNI>h{ZXK55!pX@uj7D*e1lt)dB{%9m) zn`|tPgUJ70EEqs0_MMq4G1EuoS0~iZvH6Y)q^X1bpjHj)ntqMj^0F}wCZ?oETFUY< z-*kFTKQWUx@dOQegQokN0|KGy2Lq@J2wVcu`8glA0nqDZy+2e z4l}3f920|c^-09-YX9N6vZ-`afV&c@M!STuGqh>~bu^7yJqg)buO6(ZLps4H?d6cc zEk)ZTkk(!A2Jd^vq2D4}2EsYn!5XY@Rz{oeU-UEx=UN7(7Ne}*TN@l7LI{xxvx;xB ziI*nUi!YO<^rI@%TU>HmSeMLY2xV0I?5@lG()3!y#x$97E>ZW&abugXSFiI;8F_!U zv31i;n}c7)-fjKLX4YDs?7?mri~6Z*g0H}!&QuwS||OK{{Gc^cM|)OT)$#qEm!^M&sQtxQj(i(8 zhQ3v{2z{qrbD3|6{Tu4Mn!cq!__vnnFMM_jzfq`~gP_3o_1_#6Hu0s1^0{TY?EXN|gr#0Rgb+cK^sm5WjMh5?b8jT$>R1=F+ zTg)_aFx-IMVXXCdycRk9C%n8uQn7bp8g^O5sYd~I%vr5Qa9p$4O3Ko)cbc`N6d6Ou zB5xNaiO_kDtqkiibPjDZsX2hX?QW1prf)to(tl1)ZdN@rTW}im_l{*T%d-FC0Qf5V zz`=?<5pJ%dn@wFAoLXtKiir;NkVUiAk5F+b;^pYvaBhd`AC*dHtNa3ERl46A<|Ke@ zHoIM48WxLSin&HS!i7MyD8W_=i$hE_E)!lhmHpbs5)q%mdOl&5kj>xO;yO;@dOkxN zg*C_Ovg|n?T|-C1t72sLus)Vu%_s6}g=29t$4h&Px3Ef))9x5n^90r2R3Pz!7=oWd z5R`O#W=4(&#~+i>){q)MdEg?S(PStC zHykI;XCn%Qb-(xbjGZwCvM4|(m|nGBv_hNmd4pD(R-v{cBdT<}6Z>;WIgG=t!OFDH zKtn}Pcv^8**^${W@w=I0BG|+6`%=s}jx2~!$#9-y|)}2OSWZ=%Q>UPy%+w7q%zcA*R#Z|B8 z<8eXSBP)YP0qT}FDm-02n^1&-fglwb%X0fW>npWYZ~OSjkmv9yG0`FI0NY#~2`3&S zduPPhbDD3+g>{D&19?j4>T5u}L47E}UDs~>2bh;UyS8fKHhXANwWfBK!eV>TWAs5)dktqgdOc3YKj^L`9La}M9x)C-322U2wL zLP#Gk!H~~NK_bGEG|Blmi`~{B5O=wLLiCv43L2KLU1eweu&Ej|4tLVg@!TLeDy8M| zYmZ0QtO4-fo>FuYYWJvN)kGk&+8ydo>)Wq7z8#Lb`L9?jJl~J_A1bW&c|_I7@T}wAEVfIvrn$-NFd7$p!IC$d@Y0v~ zJAh20ahl`HVpGfT4fX2jUK z-}eqCC=q4H^&iJF!d!lXAaa240qx{jLV=l^3GRxJt???X2Kqd~!jL)6Z&T);`!s#@ zWTF9iC-2!*&yV2FL!RCQ2T#P-(ouDlN)iwT(PfL}kd(bQ38^whERn**&u_bZ6!pj3LS+S}MHQw^G2qolwbvdoFMUp->nC@%9<-Cq`ShLI_%{bd z_zU+8s~D+Ghe=IhvxxBm)EL)HT z8&Iz=VOS~hGO+1L^J!NIiJhmd{lLrl2uiooU5<%rP?6yqZZCud&h*{gD~lp(#4ixU zZ@N(#M?93U&JA9v}uRr2L=nl=#@WO3)XV4Gs zlcD$E-jnGF5E!~wJCNr}dWD2mtoDXM3}o^2C-1s#Xxdw_y{AGVVG2dWi`SWP3`kb8u55EojzGgC!ZFC!Vnyi z8b-=weA%8iI&$Eb`88-HjIt0byH06xI7u!xjVLYm9lT1n8z*h&GjydLw$Wsx<|M2m zNhwb7_V@3|ybbtp7MKQ-N*g<|pCQ7?jedsIZ9vaXXRa2zmK$D1nSu52X?;bhrvi8D z#YW_*b-_$uwnJj*+wOwNhjzEExN{Nus-x^210vBSjtyZNWl(w?rUwlC(nPVD&1QzZ zohZk1)lmatKU*?2An&Xr`|ItaepIyIBPJlK&J$-BR z$Vw9s-sg15|CQ1H z@Bou9!Jgj$FoPld@URWBtvqqR4OV}aaHdfq2p+To>vHl_8f&?C)$hKq2WMGwJ=B)F z6&bEgcfU0(@302OVUc#_sSa@m^Yg$A8tKS2Q5`W|qg?w4o2AB#XBr57Cl2g|BfBTV&7Klw?9gYGh*V#}z-J?=Y#>v8p~8v^i@;JzuU3aG)9vVfh;V zS8k45Y;B=QSI;J8J$G(3c|UX+ivlX*CV!0T09(?%-+Qos{#jC_t+7YKCZ`PF$S)^j zCC;?DnRj9CE}t|>7ENxE*P9&fM<~6DWITzXOXb4aWG5Kble?eF{7lsayf{EhYY_=d z5mZJdpqZKn`u-*`+~K-QQfyt8yW2L@$B67RLK9}Kod59WhsQi)pI^zpGUj<6$~-|- z1(j4;CQLB%i#1)Zzv%_V%9*kF@+d;Xf6)Jm5p|Bft9_?B>wDN;Lch{MO;z_Ep*v+= zJWG)2=!KeMopwE+Y*Oo{b{WV?#<6HY_pNs638=`sq_cNRXqd;n@7FF>-JB$^}a|^f+iEa=p!L z%i5*7mZoW=M>kDqF-ObWx)&HVSCULg?{ZBm?326a8dbj}7!zn7o!I1Ljm&zyaA#Ryq7=-^QXQ@@yoLqm#7`vm}n+i+y&hbiM60fDi zxCXMuxRiNfnwDxN&%0>KpqbRxytY(h)lqJmi{&OMGiyXhNvPXvfux#CuKfROLGfX4 z>%>8C{iJTU#b8>*a&;FKr31B4LTzDs(Hv7NJdH+4g)nF2f923EV9kI)#L_f)tI_W6 z_TB8!Oo~X3t`mEmbIxQ1i>?v{ocXM{%tD?vNEzdFQbuY0g`7-T5m!=O=5X3{ltmFw zYNp;|8WCZY5)oqRZc=Hdm^wsAsbb0x-Dyc>_jzf4>Kl?pDkkNA3?ijnP|smgz^yi# zyi#9%q-l~>nO@E`o}p3CFpcTuN@aSOsXv8e=v6kWv46|b0}TPB_PI?e_2W8CPVy*? zzmS_LYn1;VL>}!hV%tT2tL$8ig(*s!G$D6iu+i>PsWmQXfp?~P=3EkbU>Au7 zk=Rm~Rd{b~aQ3WJb?R7cM> z7t$X%YN{x7f^L@&D>V-jU9x~Jca2cvU8`5*==LrZ!Hw2bJU+DmZWJxeTWkVCy)F!& zpNH8FgiIo4cd#}Tiz!DzXa4p3(@`}c-_`DrC}xgFybKlXU_ds)os0D!v-KaRz+D@m zj3G{jV41UQPLUPXaL@)RIylM%E3p6X-lr%k+;C>ZSLj3QzWfcred?2rqHy1x)d|o? z^Uj9)g2mvM57q_4C#Ynr3@W4MQcI5eAFO-jRjUO}NT+eHuo=;5y03JN&S>zmCSi() zL_ZjWkof@${9jCT`9q+htf7#pi_$>4!yGYb*NJ&fS{n95O&ZfhnK#ZJS(El|;kcr+ z?i1i*qMyzZm@3C@p7 zW{E`;Cdw54jE58Nsc|%>0S(6JnM||}q(?J3R?YEkp>dK~ z932rGk$=#APTvgq#m(mMZA6(GSL{^=ts%WxZT^(_5&-JPA#@Y;9~BuTdQFhT6XyXl za2`=YG;&nb_4x9AcXT@L@J9?lo2gZ>FX=9= z$y+l+H65CfUs7Lb)?xg!T{IWZ!PF&xnUG4Z&MZyS{zcko#Nwt@L9=cV5QDp*huGI-pT`A@~q+-rUVug7U{I4utA#^Lg^ zFFqijIH}fWc3d{i1iQR-`7A8u9tZ_vmeCC86FpcL);)Ydf7-RL^| zA4|$*Go<#oIShv@5cqAqB>Snl8Ilsiw5E7`$_$azW+`&|{1UX6TBZ#i8CraKIM)FB~r^no_gBcgoak2^(D;MYXHh0b{CTq1{0IZ%BObcpJuzP6SJcb7J4l4`lMl zC73v8GDBj*u7z1R_WyNJUZ~YCkq3>3JW)celBr|P&WpeP!l$0*)IZS16VMY#;~&kb z&T~(T6hoeRvykC9jRqP@(6(lMF=l^hOVG4JL$Mln7Sd7sv|RpoI8NUIEsfJnll>L$ z`0vZ+mxZ>vljnbj-Z*&DToc&(;lN)7-sV3N{(?Te|IFDOtn^HpR~{$&jQ#z$XP&P3 z$9T(c&=Y8LUv0eOR>BP>R*lM_97t9d8dLtyxf3to+RXQe>y;IJ6+3=JiZ0d&U1-(51%Rb_0QmpsB}ALE(oPm50Td&WBp)VWM6 z6j=51(hWU5OSjFv6aN}K?B;cU^Hg+uo4Ta=sc%VUshDK@7(__B7J84|3%GMkCa=U- z50xj6vyAkQghkTyJhU3w{QQQKz~OJWH%2eyQ_F2BPEH~U8xfJ4U6hUe)pmNE8byuecHbO}%Wk;>C6WKy+~PtvhcT%5!x!beE1yhw^JQ=Tr{MCO|^{mkZK z+DEP$T>>p73`O5f=kVN-rdfd~E+$uE!SWbq7whq~J(zwp^6`LW_f0xbxnUobX+ZoH zvs;K`ULoLaUCKQ6kWA{s9s#I*Oqt4>`^89pu{S}lsJOGhRx?32uP@otX30QJ*4NZ; zzvN!vJ`eU2eRq-$&;@c8r7V_`ONt*!x<|=?*f{WHAwy8dDZ9 zVGMHqCwK9g{fv^kwiLCfQ*>B8hZR>peCx_p!#C7Rd&8=gdM!#zn@Puzcj*8eas}6&m-5N;A$&ou!2=yqQ zQIgGZlE3b5t7NMGA|&r79xaGXP4#w!Qz>b z?Cn%ereh>@`J*%viSo9`i;_wGs3S=Mq=f3B^~4mrOHDY4SVz$6^}!2W8Vr@D$B5O0 zYOt-ch$Uc~k$mDFqIF$erZ^wixD-(zn)@c&I}+&~jrNvAdW$@@@`2KUQICrRZqlg# z035bHJ6JT#jgR|Inu@~>BN2z=aMMl$-)JgKLuYCZyIsSLkvZUE7q~bkkjY+0-iXI< zBvUrw@tZh#`E~j0Dftv~7TAx^JqXVJswHSufz`W z-b6w*KwbjSx$@lHE*&ZE?yUc>5G5C=YPb|&5OO=Ub2)XxwRlB%%-S*QLUDOtq!BiJ zcgkz*B=np66H*C$Ge3dVhWAeHBoaF(d-2@d;xKyC6H`Lpj%wGi-{;P4g{xiXE4(FV z$Bw0Z+VgS4Ckc||o_KM;o`myne7(VkBgEBTuk+zWa6aM(3yG&3v^MS8f%d23^6V!o0Whdy$%7E}Z6X@?u> z5eeLFxdpt)0v6f>BG8|gE{-nZYZ->(Z~#og`X#fruCT%=w^apBW~rMhUf z*aoeGsncfGuU%#Mwq6>2i2tQW<6DPz?Vh2%pBFi=?dUfui8N!-jQ#lDOdw8wV#avt z<_{+`y=ct*w`Sa{r?z|^?5&&Qj6!STfecW;cV(Hl40B0EciCKkpTUs@ggVxvs9700c(Vktub< z{fr?t;0*=lh~&7rBtwQVLt6=NNl79iusS7qjfvfgc0y53R;x)Wn-*0AH4ZRM1JJ0N zY0`5)%A{YVWgtZ!v0PXSEi`7(F~1V|c>5Il*rY^CQqBxICoeS^ChOxBDbt2%s*#3 zEX=&<0-wX<>=>Ma0KfR^_vEFAr88m-BLU%cKHnghG7oOj%c$6gD8yj`oG0|{Y`T2^ zA)J-P0FGlsVrE+8n;ocZyUXPF78vk5ImU14@#+!sF`FcRQa+6t6Qe_DXfK@bIxKoB zJn^?tFQHHU+1dXm!Omh(w^P#hw>AP5%b<<#FfjbHWeA~~OT~gZpiA{c7!Pq7gMBoF zpm=r@afkrt5)Hf2n78P@`>)|lWG3%8T0~*8zlhq;#hpm|x;IzalrngU9?s5Y`_kiN z!-}gHZ<_o#Z!jhP8!dea9KIwyjd|*!|8!iS;gTa$PNk*wr=s^zI6~bq*7~5NQq~Zg z@PLjJ7ftUqp&2;}Q_e}TKB8?Jz}oo^nT|rN4ZB^@T8ezuT1M~Y$pA2UU^as>dw_eQ zZ0^<2B}pt+l0?i`vsipJg0&TO98lr#D&lbz>RO5ek8>nnn?>p&Q+r6n9xAy9As_4U zMrDx6=qR`EkU#&g92FF||B%NQ#Uzt6qCDP1fnaz%UKQyNI{(H4f38L%SN{p%;WsGa z#GCj(@Iz6etu*ZKMqD1wnDLqmOn#w8m(Nqo?I>rlYYGY&a>X$k;e~LlewsIxT#yD3PN_b&A7~9X5c8(>2>re3h4Z^?TJerdF>O1I!(VGq)As zBosplG%#&+;bst^B|%|CZF1pe_ko<%y#1Q}{QbS|-o5<2n!UVL{W%R1S79Mv6`ni} z)~RN#iH5~QClASH{{`!uIk8R@U=JoPSxK|zhZ6iW0V^soIyN;L!vB`w4U`&KYV!3) z`NL5;0j*9In0Yk%o{1Q)7S)`xkZo9xM2vSATF49PMAbV#R<;XH&kcUe(iCBA`wk|o zzwnN*_`WCFxs-W!o$L$HU^q|5%YMuj#KiLUsrDVw9uWg~YWJ#F(&~rwm>2(K*6iR< zgg)tUnas4@G?c+4rQu(8*QUiAwJFm~Rt?D$1Mw4t?ycDl)}Cxgus!vVbt1hyH5jN9 zDm>tD#FZfYM;sjzI!wpW#1_cEE-{b1Hiwp-3d4EYIf>Cp_Xf`G*Y9WJ-g#JB1~NLC zYVFhS%Pu)BeP>=$IdPdZu79u~e|}C}?0jKKpxF?Yp~#8p3*wZ=4T0v8xadJ+*~Fcq z4MVlz9MqcQrOmF=W}Q|C)#^|^TIW^gu`R&Fg-_$F>NEL?_=$rNN{2t*;_MEGGe>M%SV3Vj&C8pu#(Fml3ImXnd;XiptLAr7X(4cs~ z`c#DNOZp&1YCH{nya0WWf~4f9St_i#9wtB^`jix@NYEn%=z|mlON89w~n^wuZ( zoYV-A-t6tZO_@+(nJL9)JGPm>za^- za6J{9enaIN<)8|Wz@xPv1+M)VkoGt{9N1J=+f>_>Y$`x|LVHqsN_$#+2A+kw_MG-S zdBM$io2Gham-9*dIQH#G*`Wzch^fgnTin0ZkJo5?78}XW`M#g*i0AT4DbN4qYdGuWoahneNJV%?H!a>qW3;XwS<4WzPAKTWRz<7 zx3Gt}|8$B_-b8iji)Dxon)ic-=rr{)SieGk$#%K<4gQ<0ZPi+*Uk66xu1c-_p5_G` zj0PJ_12ros!2u?cn+W>kHiG@Ine3nx*j9A?#*rp?0k@zb^LIsPXNCBSg;mI|H~6Xn zJD3d@x`hRd!Yys>R(g5|KwjoCuo&oayRdvd#|NUf+0-r_^DeGcqER;T5lrm*z$n$| zYrKXPuo3zmz24DjzsKnMjLR1&z+UgM!w&Bj6uQ9^Hj!IMqGUz{aE3ES>r)iGzP(rvprf`| zTk8ZV(#lN1CP_7BdWL)4`zCxVV0{Chf?{SVVU2Y)J4j6jBfAK|#SjYS zU`T~nZH3yRnt3bWm2O2mtW*Updn*k3LuBY+f2nSPA~*up51QR^A%+~&1ED6gUJdF)O?J2r2{8rpqk*)0Hf)KX52jUcP$lltgbu6@a2uw`T1nq-HX^A1GcDlFF;zmYG%Rp9Gj*Y`?r^-bawSYhwVhD&U7{rUW znin2CWv5GxRS%Y`WL;cK(>7M((Uh^tFwHs;S@9pkU6PfO$_>ZgGy_1>Rwje;=VB>E zv5*IOsDm`;9uzMX%lG2tr0}qzOmGp45K$5s^t-`aO~N!p5r%ua>m-^+8Qa@BmQ3w5 z26RR<8NQC#IGTG3cN+s22f|JK#<~e5peAC3t92LX%^EVgBb&8%iJpK<~oZDz=YDr#Kcv>Gh#7P6)mnyC$c%&5VGIi|B>DH?x}o!{^ugc z&OXNyt@FfQ|N6ttYZYI8rf1m;71}&=(=~fG2X^MjX7)|O`O@y`j%lh|5M2E&i!KZT zMG!p2fKa8*vohFp`!3i{Sj~!n7&@USc0%{1+s!FGEhgT^z~jB(>MriIZ zFE9~ZY8VkIN}mI^EWXc`tW?WFMon;Fnq?@nz^-7^wP89cihCc2T`IcrNB|fX$darQ zlbj?VbHqbwQjR;0jA?O{k(uV5QRetG8_T49rbE+#0Bk_MN?%s_GudwvM>vl9c2Ghs}vo^$4VO3JybE?lmV>Yy9tRUiQ9QjrlV=J6(E#4c3%kW=v_!*s}V zgHy5-{{TYqngx8l84KS1KfmLJpnV0BjNZn>8^%(fER2Urs{*YB#uU$e6V2p#t zY?G1$4_T%w+ARrA2cWE5yVVR0Y1tOHa&FJvb5Dz3IA>k2$tVF{j`G-+Yqqu-vjZ8o zQgJqKAdm7h3Da#O=i*sL87vz}5fc;Gcq-SED>1L7eJ&j%vpR#>rN)}(qDbn$j{XO@2|`%6S}cc?10!!Qc*}vzRsoNQtRx<9 zJx*;4imy!Vu4!VrtJX!%z%K3NVr2-co&1<8@}H3*|MR~rj_hXoRC4lUDBo7AGfnmP zZ0mZ?^R?xq&t?sZmH1qwvfJ9xGWv92QKfXh1X8t(5@?gKVQkwK&oNrve-LBdh4Cr- z?mq}>-~Fe@uQg~01oY_@T>l;m4hHY*74YmG^Z@Z>Zq&Ycz<6!{<&I`tFXjb}FHh|J zzIkr#?a~yX>RBK-SsPg4X0U3%qHccM&%5CBK#)+E6`@zY=&SOFeX01uXEWql=d@pZ zfp=O?u8Ilmg2eo-!nJv8KCF~-6W34d4{MTjORTlX`nF#9WbnFL1i=8>4-Y)&suaYb zxia?zZtdfgZ#J_)5p#RvYGI|f@CG&CK&g#59fArG<+qhe8O(-zA0BvVIB)Rk+2OvT zq6_NJlXjZ)W6VgNZoa1*6AUU995$gg{v<1g)NRT+j3IRp>XF=T> zZrmm{+fZO}do2m^7*}&AH=I5|KS#ZlopLHqaKvetXXM_?UGOcrl5{f4`R@O*;| z&@ru15OO=ilhzDZy%SY&CzAMV?}m_-&Za;A94C9E2j*rvL{C|JOFcs~=NYFYTTL)P zX(AYX^$u+s!U3=~J$CwF3rc02|+5lvuSQtz?FEX(uC-)w$kDLFcfEnc(FI3^F8{Jntp)^fOZpYp3#*W(nD>?q?tm&*Bl%+KF%qq-W*YI42~^e zrRO2!zC-~o0O(hsBE03R-^Q_Awcm=jNaOMAtgyMk(x&-?i6=b4YPF2{&^*CoBmZu& z*e9vd`6x8dBm_9Uvd=5e9(-CLIN<#7B~$c0HTXXWI2{GND^4O({8B+)4F1;2Zzfhb z@kyEg?``=+o%aTZdXP>K@AuvV@{W#N3z@L`-iTos6_6B%XIrV7mQTAWB%Dg+37agB z2VWI|3OYf})FGRhx^e)&Q>I2E{mlf| z+#{#jJ&HZ;29FRQt+)%^1B)mO8sRtPhACIx5LmWt6*9wX*)IqL4BBoh_?&;0E4tQ;& zUJLDX)gw!8D)Rp#*A`ethp>*8(RW-2@}?=&#V=W7NK6TJFghkAwEGXWfxMY9E}RU z6IMy>dJNaDLpI8523RyH*Q`y;LWSzx3f!i$;nN@B)}q=4vQo-Qf1qn2J=?OgsB$fg znyTO&m$m|KS?x(|5Yk3~TNcP<57W~!glZ^73$ZvL4v2=zxm124PPQ&0gsxWSYT46#c$x+FXW)F34L0X2j$in-Oq5Zi+ zqocTFoJPDVhkKz?HK0I9aTl^tiF|f(srHT#D2s95ytN=%K`4IM)UCKfghE}mz6 zz9M20QZkw3vSd?G(lanJ{ZIehJ2<7U{D&D3l&?UcBE_~j@2u^@%FR*XqDtpnaMdN3 zT~XzRLD%e1ZLU7I-E>Qh*Is(7POW;48Z=p{S*sRp+I8yCWvDxb8DY4QMjK_7ou(OU zjB$SQ#(-DeiD3}9H0cnKP|z^2QY0}P-WE`9L$X>dDG%FkkG)Pg?TDi`*(^CI_uK8H zDYW8fvXokhGBBlbypX{8rkY{8naHA)DpRgPHPzKnQ!TaCQCBH~DOGT#DP0I5g%VmA zVTBW31OXz7B(f;<=#45G6C5^#^DKXSVwu`Q;D$)&zQiCe#ZxkzN!K zLm;zWpBc~;I!&%nCW>O9GeLgri;Q?Dj}zY%K6ABwY`X-;C+(V9j{U2pOd=cOHx2HSUrx##^3YrbwrpL1Y literal 0 HcmV?d00001 diff --git a/client/assets/images/background.svg b/client/assets/images/background.svg new file mode 100644 index 0000000..0fe5918 --- /dev/null +++ b/client/assets/images/background.svg @@ -0,0 +1 @@ + diff --git a/client/assets/images/cardcast.png b/client/assets/images/cardcast.png new file mode 100644 index 0000000000000000000000000000000000000000..49eedc37d2cea600a52475bb8e18b11d402943c5 GIT binary patch literal 3274 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 + + diff --git a/client/assets/images/deck.svg b/client/assets/images/deck.svg new file mode 100644 index 0000000..e7e96b1 --- /dev/null +++ b/client/assets/images/deck.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/assets/images/icon.png b/client/assets/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..48565141c012ec2c49daae5ee74a6cd24389cbe6 GIT binary patch literal 1280 zcmV+b1^@bqP)oU|Kxx5x|EzUt4ZzF&( zLKusFuuC`U^re`T*5StjzQfws1fL@{Ll8y;A&ikVtlN)-lhosTT#R4gi!6bLaqyZF zOvFamA7|iJynwH;1f9@CBLYyKd~_j+;vD>}5Z;1@8#!m&*s3~220U-qw5b{$2!5mLOp7GCAmj|HXDC8m|py59dKVAbA{5)S=161C1 z3UlxeK+c{6tnOto<~bMr(7PaG7?oON3{$Ckg42)(_xfK{0_4$!ZuQfUEahF z*aI({O~7Crhb>Tv(a3hH?2ek`3jB z=%ez79Zw*RNA;5(7L|vu}7>=;6fDrb? zVtl5XfO41LcXRBB)et})ye5czG@#2(0W;9;VhVnf^;B8gbrUch1(4IgHmli?|JE<3MbR>FDqD6CrLsCKpE;ta85YIsb1A#Rm8pQ)Df~M<_!OPVzH?7$xO7 zNh6k`Ml8d+*bMjLIJX5ngMDF@=W!9%!1air8U2tCubG7TD8Oh05wcjVimJnUSm?HZ z3veY=WH-DDr!gvOD4Ov!{_%1O?1a~$A{XNdWI5Jj8ilG%t0Ip{3QDY&|n>f1$>Hb6v9c8n2+BP6Tf3Fdf=<%!AUMOc{!;^RK)N* q5@^K=mvwLsPQn%#iRC=K>COLkSVr};z!Pf#0000 + + + diff --git a/client/assets/images/reread-icon.svg b/client/assets/images/reread-icon.svg new file mode 100644 index 0000000..91d2de5 --- /dev/null +++ b/client/assets/images/reread-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/elm.json b/client/elm.json new file mode 100644 index 0000000..a77287a --- /dev/null +++ b/client/elm.json @@ -0,0 +1,36 @@ +{ + "type": "application", + "source-directories": ["./src/elm"], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.1", + "elm/core": "1.0.2", + "elm/html": "1.0.0", + "elm/http": "2.0.0", + "elm/json": "1.1.3", + "elm/random": "1.0.0", + "elm/svg": "1.0.1", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm-community/dict-extra": "2.4.0", + "elm-community/random-extra": "3.0.0", + "fapian/elm-html-aria": "1.4.0", + "lattyware/elm-fontawesome": "2.3.0", + "myrho/elm-round": "1.0.4", + "pablohirafuji/elm-qrcode": "3.1.1", + "truqu/elm-base64": "2.0.4" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/file": "1.0.5", + "elm/regex": "1.0.0", + "elm/virtual-dom": "1.0.2", + "elm-community/list-extra": "8.1.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/client/nginx.conf b/client/nginx.conf new file mode 100644 index 0000000..1c964af --- /dev/null +++ b/client/nginx.conf @@ -0,0 +1,54 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + resolver 127.0.0.11; + + types { + application/manifest+json webmanifest; + } + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 65; + + gzip on; + + upstream server { + server server:8081; + } + + server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name _; + + location / { + root "/usr/share/nginx/html"; + } + + location /api { + proxy_pass http://server; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + } +} diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..81289b8 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,11086 @@ +{ + "name": "massivedecks", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@a11y/focus-trap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@a11y/focus-trap/-/focus-trap-1.0.2.tgz", + "integrity": "sha512-5/Wp7UwuUxi5/3nNB+V8bj76BWsxfl9D4Y5+k8SQk/279Yrd0omxHdiU4DMhfnNowPMKWVmWnDD6b7NMqrKf4w==" + }, + "@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@csstools/convert-colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", + "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", + "dev": true + }, + "@fortawesome/fontawesome-free": { + "version": "5.9.0", + "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz", + "integrity": "sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg==" + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, + "@types/chrome": { + "version": "0.0.86", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.86.tgz", + "integrity": "sha512-7ehebPf/5IR64SYdD2Vig0q0oUCNbWMQ29kASlNJaHVVtA5/J/DnrxnYVPCID70o7LgHdYsav6I4/XdqAxjTLQ==", + "dev": true, + "requires": { + "@types/filesystem": "*" + } + }, + "@types/chromecast-caf-receiver": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/chromecast-caf-receiver/-/chromecast-caf-receiver-3.0.8.tgz", + "integrity": "sha512-Zdl05s8nutvcXFri2ie5U16Sn8hT4I5SbkPW0Dsc84n3VJTQkEra3zroD/rDF0xAPL2fY1BU4w3nJA3X91KAVw==", + "dev": true + }, + "@types/chromecast-caf-sender": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.1.tgz", + "integrity": "sha512-/JuG+zrS+KCPwEiOrK9O7WrIMyiUEF7Ev9ywbzXcCOPkXin9tLX7w9zxCmxtnOPdgH9lZbtOvgo5IA4cEJknRg==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/filesystem": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.29.tgz", + "integrity": "sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz", + "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz", + "integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@types/tapable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", + "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", + "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack": { + "version": "4.4.33", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.33.tgz", + "integrity": "sha512-MU5Lokm30QHF9Wn0+yq/hnToPlEBRQVa72mgn1/W8OpJu63aRDKy0aEdMI+pCk8F6zwQJfSFaVzx1JN1MLXx8A==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "source-map": "^0.6.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==", + "dev": true + } + } + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webcomponents/webcomponentsjs": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.2.10.tgz", + "integrity": "sha512-5dzhUhP+h0qMiK0IWb7VNb0OGBoXO3AuI6Qi8t9PoKT50s5L1jv0xnwnLq+cFgPuTB8FLTNP8xIDmyoOsKBy9Q==" + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "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.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "app-manifest-loader": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/app-manifest-loader/-/app-manifest-loader-2.4.0.tgz", + "integrity": "sha512-Yao4pAd3/oy+h7NHcPMXC8YrAZo4u+8RdQDYq/Kvyv3eXMyNzBH28noO2weeHX1vNeCu3MdeONPBCYcLANRXgQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "core-js": "^3.1.4", + "loader-utils": "^1.2.3", + "xml-js": "^1.6.11" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "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", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "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==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.0.tgz", + "integrity": "sha512-kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ==", + "dev": true, + "requires": { + "browserslist": "^4.6.1", + "caniuse-lite": "^1.0.30000971", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.16", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "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==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "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==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" + }, + "binwrap": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/binwrap/-/binwrap-0.1.4.tgz", + "integrity": "sha1-yh94cDAiElGPoksHcm+cUKFcdVk=", + "dev": true, + "requires": { + "request": "^2.81.0", + "request-promise": "^4.2.0", + "tar": "^2.2.1", + "unzip": "^0.1.11" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.3.tgz", + "integrity": "sha512-CNBqTCq22RKM8wKJNowcqihHJ4SkI8CGeK7KOR9tPboXUuS5Zk5lQgzzTbs4oxD8x+6HUshZUa2OyNI9lR93bQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000975", + "electron-to-chromium": "^1.3.164", + "node-releases": "^1.1.23" + } + }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", + "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30000976", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000976.tgz", + "integrity": "sha512-tleNB1IwPRqZiod6nUNum63xQCMN96BUO2JTeiwuRM7p9d616EHsMBjBWJMudX39qCaPuWY8KEWzMZq7A9XQMQ==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dev": true, + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://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" + } + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "chromecast-device-emulator": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/chromecast-device-emulator/-/chromecast-device-emulator-1.2.7.tgz", + "integrity": "sha512-A93dyExP1j/Ho/n5Sqrm3uqvzFBsI5jmFrJRnqXt6nHAKDvK/mg+722R3V+OaGieWXiZmgVpi4snF6PrnNpVhw==", + "dev": true, + "requires": { + "ajv": "^4.11.8", + "chalk": "^2.4.1", + "commander": "^2.15.1", + "ws": "^5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "requires": { + "chalk": "^1.1.3" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "dev": true, + "requires": { + "source-map": "~0.6.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==", + "dev": true + } + } + }, + "clean-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "dev": true, + "requires": { + "@types/webpack": "^4.4.31", + "del": "^4.1.1" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.0", + "shallow-clone": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "requires": { + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "dev": true, + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", + "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.0.tgz", + "integrity": "sha1-glR3SrR4a4xbPPTfumbOVjkywlI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-blank-pseudo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", + "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-has-pseudo": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", + "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0-rc.4" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "css-hot-loader": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/css-hot-loader/-/css-hot-loader-1.4.4.tgz", + "integrity": "sha512-J/qXHz+r7FOT92qMIJfxUk0LC9fecQNZVr0MswQ4FOpKLyOCBjofVMfc6R268bh/5ktkTShrweMr0wWqerC92g==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "lodash": "^4.17.5", + "normalize-url": "^1.9.1" + } + }, + "css-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.0.0.tgz", + "integrity": "sha512-WR6KZuCkFbnMhRrGPlkwAA7SSCtwqPwpyXJAPhotYkYsc0mKU9n/fu5wufy4jl2WhBw9Ia8gUQMIp/1w98DuPw==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.17", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.0", + "schema-utils": "^1.0.0" + } + }, + "css-prefers-color-scheme": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", + "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.28", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz", + "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-url-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz", + "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=", + "dev": true + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + }, + "cssdb": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", + "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", + "integrity": "sha1-F4tDpEYhIhwndWCG9THgL0KQDug=", + "requires": { + "clap": "^1.0.9", + "source-map": "^0.5.3" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "deasync": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.15.tgz", + "integrity": "sha512-pxMaCYu8cQIbGkA4Y1R0PLSooPIpH1WgFBLeJ+zLxQgHfkZG86ViJSmZmONSjZJ/R3NjwkMcIWZAzpLB2G9/CA==", + "requires": { + "bindings": "~1.2.1", + "node-addon-api": "^1.6.0" + } + }, + "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" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "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==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "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" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.167", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.167.tgz", + "integrity": "sha512-84IjpeRudjP43Q0+K7tlS7ESoHOl0W6CIdzs5reS9p+sAjCQEDiaAyiXN2v1qLUdL+Of6ZSaH4Cq6bl+sfzy8A==", + "dev": true + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "elm": { + "version": "0.19.0-no-deps", + "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.0-no-deps.tgz", + "integrity": "sha512-tOlSeo5Bt0Rkrt/aBjL+/a+TmAFdfeqjrOlThrhgKOoGeHt1hU7/hJ+HFMlHiODf09AQh7Anl64o7V6thzmRHw==", + "dev": true + }, + "elm-format": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.1.tgz", + "integrity": "sha512-uLcDICdmEoRBaCp3PobcvHtUej2RJFHRYfMd7i3CSH+RsajJYTdiPkviKIq823Kd4gfjQMpNajEyM1lLFez7+A==", + "dev": true, + "requires": { + "binwrap": "^0.2.0" + }, + "dependencies": { + "binwrap": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/binwrap/-/binwrap-0.2.1.tgz", + "integrity": "sha512-kILc2+zMfFEv66/NLfO2GIpmWRPE8hL68fv+o5A94OlN9AIIG4zernpgn9bpPAImb5t4QwFxnqAGSyP1+tGKrA==", + "dev": true, + "requires": { + "mustache": "^2.3.0", + "request": "^2.87.0", + "request-promise": "^4.2.0", + "tar": "^4.4.8", + "unzip-stream": "^0.3.0" + } + }, + "tar": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } + }, + "elm-hot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/elm-hot/-/elm-hot-1.1.0.tgz", + "integrity": "sha512-9vA9qE2IcJGNV5N7Ga3FdNs5GKY81ocBbliD2rxJELHnVW4agH95gbpROSoSd43JL8DRmKIQvZ6sX0h+xztTmA==", + "dev": true + }, + "elm-hot-webpack-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/elm-hot-webpack-loader/-/elm-hot-webpack-loader-1.0.2.tgz", + "integrity": "sha512-lkfBivr4VRIPBh9lqITI2gPndQZ8WhtpF3gDmCjndtIEOWNzJ1GaFMQI8P96mn3siY0oOHhTk6qUf9rntmJxwQ==", + "dev": true, + "requires": { + "elm-hot": "^1.0.1" + } + }, + "elm-webpack-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/elm-webpack-loader/-/elm-webpack-loader-5.0.0.tgz", + "integrity": "sha512-d1a06wvnQzXsAAakkI+lB2uQ9m3BREAqRFk/foxdznQ2Ge29ygkBUGQZ3spNwOy8hLo55ULKd9n1eifW9HJ5ag==", + "dev": true, + "requires": { + "elm": "^0.19.0", + "glob": "^7.1.1", + "loader-utils": "^1.0.2", + "node-elm-compiler": "^5.0.0", + "yargs": "^6.5.0" + }, + "dependencies": { + "elm": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.0.tgz", + "integrity": "sha512-CYgewByRByMOilPk5/yrW1mtflaS/vp+026gwb0EEX6aqUl+TGYoTSTW+uf44XB/FOKgUauV3TDH3Bl0IHZ8Ag==", + "dev": true, + "requires": { + "binwrap": "0.1.4" + } + } + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-templates": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", + "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", + "dev": true, + "requires": { + "recast": "~0.11.12", + "through": "~2.3.6" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "dev": true + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.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" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extract-loader": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/extract-loader/-/extract-loader-3.1.0.tgz", + "integrity": "sha512-baiz/xalbyQJOTHwcMJKXYsHbhEHGWQ2loK26vqZVoqO6eeinrnSrMx9681pNZgGRqz2L/PsyNxz+PVDiSmNPg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "btoa": "^1.2.1", + "loader-utils": "^1.1.0", + "resolve": "^1.8.1" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "file-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.0.0.tgz", + "integrity": "sha512-roAbL6IdSGczwfXxhMi6Zq+jD4IfUpL0jWHD7fvmjdOVb7xBfdRUHe4LpBgO23VtVK5AW1OlWZo0p34Jvx3iWg==", + "dev": true, + "requires": { + "loader-utils": "^1.2.2", + "schema-utils": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + } + } + }, + "find-elm-dependencies": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.1.tgz", + "integrity": "sha512-KBxPjc7J1CLw90jVateMKvMYMROZRTR7/QT2I3MxT+7I6KuUQUMNUFuS/eQXQnMnyElGKQ1iyPbe7GnEuYnFXw==", + "dev": true, + "requires": { + "firstline": "1.2.0", + "lodash": "4.17.11" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "firstline": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/firstline/-/firstline-1.2.0.tgz", + "integrity": "sha1-yfSIbn9/vwr8EtcZQdzgaxkq6gU=", + "dev": true + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "dev": true, + "requires": { + "debug": "^3.2.6" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-minipass": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", + "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", + "dev": true, + "requires": { + "es6-templates": "^0.2.3", + "fastparse": "^1.1.1", + "html-minifier": "^3.5.8", + "loader-utils": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz", + "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "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-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lit-element": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.2.0.tgz", + "integrity": "sha512-Mzs3H7IO4wAnpzqreHw6dQqp9IG+h/oN8X9pgNbMZbE7x6B0aNOwP5Nveox/5HE+65ZfW2PeULEjoHkrwpTnuQ==", + "requires": { + "lit-html": "^1.0.0" + } + }, + "lit-html": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.1.0.tgz", + "integrity": "sha512-ZDJHpJi09yknMpjwPI8fuSl5sUG7+pF+eE5WciFtgyX7zebvgMDBgSLq4knXa7grxM00RkQ7PBd7UZQiruA78Q==" + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "loglevel": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.3.tgz", + "integrity": "sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA==", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "match-stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz", + "integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "readable-stream": "~1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-svg-data-uri": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.0.3.tgz", + "integrity": "sha512-YBaqsh6GE+4jQhLNkFYEagH2o4bVTlGGpEvkuwtwc+1NBGXqpcVCnsUGkGp75ovPXxtF2GsDYzUwyhfC0hntiA==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natives": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", + "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-addon-api": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.3.tgz", + "integrity": "sha512-FXWH6mqjWgU8ewuahp4spec8LkroFZK2NicOv6bNwZC3kcwZUI8LeZdG80UzTSLLhK4T7MsgNwlYDVRlDdfTDg==" + }, + "node-elm-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.3.tgz", + "integrity": "sha512-I3CWm/ExYYQ/a9bjB0OL9VsGa3Lmgbb8QOs4y2kEiB/DTkTqkcTaCr/lVyOYjRpgR25TsmOBATscsg6H6aC9Hg==", + "dev": true, + "requires": { + "cross-spawn": "4.0.0", + "find-elm-dependencies": "2.0.1", + "lodash": "4.17.11", + "temp": "^0.8.3" + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + } + } + }, + "node-releases": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.23.tgz", + "integrity": "sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "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" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "over": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz", + "integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "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-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "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", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + } + } + }, + "portfinder": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz", + "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + } + }, + "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=", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-attribute-case-insensitive": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz", + "integrity": "sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.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==", + "dev": true + } + } + }, + "postcss-color-functional-notation": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", + "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-gray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", + "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-hex-alpha": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", + "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "dev": true, + "requires": { + "postcss": "^7.0.14", + "postcss-values-parser": "^2.0.1" + } + }, + "postcss-color-mod-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", + "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", + "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-custom-media": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", + "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "postcss-custom-properties": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", + "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", + "dev": true, + "requires": { + "postcss": "^7.0.17", + "postcss-values-parser": "^2.0.1" + } + }, + "postcss-custom-selectors": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", + "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-dir-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", + "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-double-position-gradients": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", + "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", + "dev": true, + "requires": { + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-env-function": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", + "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-focus-visible": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", + "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-focus-within": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", + "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-font-variant": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz", + "integrity": "sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-gap-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", + "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-image-set-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", + "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-import": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", + "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-initial": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.0.tgz", + "integrity": "sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q==", + "dev": true, + "requires": { + "lodash.template": "^4.2.4", + "postcss": "^7.0.2" + } + }, + "postcss-lab-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", + "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "requires": { + "cosmiconfig": "^5.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==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + } + }, + "postcss-logical": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", + "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-media-minmax": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", + "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", + "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-nesting": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.0.tgz", + "integrity": "sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "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==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-overflow-shorthand": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", + "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-page-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", + "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-place": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", + "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-preset-env": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz", + "integrity": "sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA==", + "dev": true, + "requires": { + "autoprefixer": "^9.4.9", + "browserslist": "^4.4.2", + "caniuse-lite": "^1.0.30000939", + "css-blank-pseudo": "^0.1.4", + "css-has-pseudo": "^0.10.0", + "css-prefers-color-scheme": "^3.1.1", + "cssdb": "^4.3.0", + "postcss": "^7.0.14", + "postcss-attribute-case-insensitive": "^4.0.1", + "postcss-color-functional-notation": "^2.0.1", + "postcss-color-gray": "^5.0.0", + "postcss-color-hex-alpha": "^5.0.2", + "postcss-color-mod-function": "^3.0.3", + "postcss-color-rebeccapurple": "^4.0.1", + "postcss-custom-media": "^7.0.7", + "postcss-custom-properties": "^8.0.9", + "postcss-custom-selectors": "^5.1.2", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-double-position-gradients": "^1.0.0", + "postcss-env-function": "^2.0.2", + "postcss-focus-visible": "^4.0.0", + "postcss-focus-within": "^3.0.0", + "postcss-font-variant": "^4.0.0", + "postcss-gap-properties": "^2.0.0", + "postcss-image-set-function": "^3.0.1", + "postcss-initial": "^3.0.0", + "postcss-lab-function": "^2.0.1", + "postcss-logical": "^3.0.0", + "postcss-media-minmax": "^4.0.0", + "postcss-nesting": "^7.0.0", + "postcss-overflow-shorthand": "^2.0.0", + "postcss-page-break": "^2.0.0", + "postcss-place": "^4.0.1", + "postcss-pseudo-class-any-link": "^6.0.0", + "postcss-replace-overflow-wrap": "^3.0.0", + "postcss-selector-matches": "^4.0.0", + "postcss-selector-not": "^4.0.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", + "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "postcss-replace-overflow-wrap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", + "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "postcss-selector-not": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz", + "integrity": "sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + } + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "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==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "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==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.2.tgz", + "integrity": "sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.28", + "css-url-regex": "^1.1.0", + "csso": "^3.5.1", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz", + "integrity": "sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==", + "dev": true + }, + "postcss-values-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", + "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "prettier": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "dev": true + }, + "prettier-plugin-elm": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-elm/-/prettier-plugin-elm-0.4.2.tgz", + "integrity": "sha512-ux3JcE+YGoLRpceRCmAt44j1qknhRg8/UieGK8LA4j6rohaJeKh4RT3ubaIijI1gspxExP4duiVAevqrZ1AsZA==", + "dev": true, + "requires": { + "elm-format": "^0.8.1", + "execa": "^1.0.0", + "make-dir": "^1.3.0", + "object-hash": "^1.3.1", + "serialize-error": "^3.0.0", + "temp-dir": "^1.0.0" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", + "integrity": "sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pullstream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz", + "integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=", + "dev": true, + "requires": { + "over": ">= 0.0.5 < 1", + "readable-stream": "~1.0.31", + "setimmediate": ">= 1.0.2 < 2", + "slice-stream": ">= 1.0.0 < 2" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + } + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "dev": true, + "requires": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz", + "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, + "sass-inline-svg": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sass-inline-svg/-/sass-inline-svg-1.2.0.tgz", + "integrity": "sha512-4fBlpj2QWo/zk6a23Twz4O7yroAs5vrN7l34X4LQREL2kGlyjeObHc4uGRkBUjuwO57ZsUJNCJAq3fYm7I/b4Q==", + "requires": { + "css-select": "^1.2.0", + "deasync": "^0.1.7", + "dom-serializer": "^0.1.0", + "htmlparser2": "^3.9.0", + "mini-svg-data-uri": "^1.0.0", + "object-assign": "^4.0.1", + "svgo": "^0.6.6" + } + }, + "sass-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "dev": true, + "requires": { + "clone-deep": "^2.0.1", + "loader-utils": "^1.0.1", + "lodash.tail": "^4.1.1", + "neo-async": "^2.5.0", + "pify": "^3.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz", + "integrity": "sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw==", + "dev": true, + "requires": { + "node-forge": "0.7.5" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-3.0.0.tgz", + "integrity": "sha512-+y3nkkG/go1Vdw+2f/+XUXM1DXX1XcxTl99FfiD/OEPUNw4uo0i6FKABfTAN5ZcgGtjTRZcEbxcE/jtXbEY19A==", + "dev": true + }, + "serialize-javascript": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz", + "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slice-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz", + "integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=", + "dev": true, + "requires": { + "readable-stream": "~1.0.31" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "sockjs-client": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz", + "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "spdy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", + "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "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=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "style-loader": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", + "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "svgo": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", + "integrity": "sha1-s0CIkDbyD5tEdUMHfQ9Vc+0ETAg=", + "requires": { + "coa": "~1.0.1", + "colors": "~1.1.2", + "csso": "~2.0.0", + "js-yaml": "~3.6.0", + "mkdirp": "~0.5.1", + "sax": "~1.2.1", + "whet.extend": "~0.9.9" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "temp": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", + "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "dev": true, + "requires": { + "os-tmpdir": "^1.0.0", + "rimraf": "~2.2.6" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "dev": true + }, + "terser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.0.0.tgz", + "integrity": "sha512-dOapGTU0hETFl1tCo4t56FN+2jffoKyER9qBGoUFyZ6y7WLoKT0bF+lAYi6B6YsILcGF3q1C2FBh8QcKSCgkgA==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.10" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-W2YWmxPjjkUcOWa4pBEv4OP4er1aeQJlSo2UhtCFQCuRXEHjOFscO8VyWHj9JLlA0RzQb8Y2/Ta78XZvT54uGg==", + "dev": true, + "requires": { + "cacache": "^11.3.2", + "find-cache-dir": "^2.0.0", + "is-wsl": "^1.1.0", + "loader-utils": "^1.2.3", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "terser": "^4.0.0", + "webpack-sources": "^1.3.0", + "worker-farm": "^1.7.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==", + "dev": true + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "thunky": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", + "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "ts-loader": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.4.tgz", + "integrity": "sha512-p2zJYe7OtwR+49kv4gs7v4dMrfYD1IPpOtqiSPCbe8oR+4zEBtdHwzM7A7M91F+suReqgzZrlClk4LRSSp882g==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "dev": true + }, + "uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "requires": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.1.3.tgz", + "integrity": "sha512-/lRkCaFbI6pT3CxsQHDhBcqB6tocOnqba0vJqJ2DzSWFLRgOIiip8q0nVFydyXk+n8UtF7ZuS6hvWopcYH5FuA==", + "dev": true, + "requires": { + "cacache": "^11.3.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "uglify-js": "^3.5.12", + "webpack-sources": "^1.3.0", + "worker-farm": "^1.7.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==", + "dev": true + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "dev": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + } + } + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "unzip": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/unzip/-/unzip-0.1.11.tgz", + "integrity": "sha1-iXScY7BY19kNYZ+GuYqhU107l/A=", + "dev": true, + "requires": { + "binary": ">= 0.3.0 < 1", + "fstream": ">= 0.1.30 < 1", + "match-stream": ">= 0.0.2 < 1", + "pullstream": ">= 0.4.1 < 1", + "readable-stream": "~1.0.31", + "setimmediate": ">= 1.0.1 < 2" + }, + "dependencies": { + "fstream": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", + "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", + "dev": true, + "requires": { + "graceful-fs": "~3.0.2", + "inherits": "~2.0.0", + "mkdirp": "0.5", + "rimraf": "2" + } + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "dev": true, + "requires": { + "natives": "^1.1.0" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "unzip-stream": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.0.tgz", + "integrity": "sha512-NG1h/MdGIX3HzyqMjyj1laBCmlPYhcO4xEy7gEqqzGiSLw7XqDQCnY4nYSn5XSaH8mQ6TFkaujrO8d/PIZN85A==", + "dev": true, + "requires": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", + "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webpack": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.35.0.tgz", + "integrity": "sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.0.5", + "acorn-dynamic-import": "^4.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^1.0.0", + "tapable": "^1.1.0", + "terser-webpack-plugin": "^1.1.0", + "watchpack": "^1.5.0", + "webpack-sources": "^1.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack-cli": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.4.tgz", + "integrity": "sha512-ubJGQEKMtBSpT+LiL5hXvn2GIOWiRWItR1DGUqJRhwRBeGhpRXjvF5f0erqdRJLErkfqS5/Ldkkedh4AL5Q1ZQ==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.0", + "findup-sync": "^2.0.0", + "global-modules": "^1.0.0", + "import-local": "^2.0.0", + "interpret": "^1.1.0", + "loader-utils": "^1.1.0", + "prettier": "^1.17.0", + "supports-color": "^5.5.0", + "v8-compile-cache": "^2.0.2", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", + "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.2", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.7.2.tgz", + "integrity": "sha512-mjWtrKJW2T9SsjJ4/dxDC2fkFVUw8jlpemDERqV0ZJIkjjjamR2AbQlr3oz+j4JLhYCHImHnXZK5H06P2wvUew==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.6", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.2.1", + "http-proxy-middleware": "^0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "killable": "^1.0.1", + "loglevel": "^1.6.3", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.20", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.4", + "semver": "^6.1.1", + "serve-index": "^1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.3.0", + "spdy": "^4.0.0", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.0", + "webpack-log": "^2.0.0", + "yargs": "12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "weightless": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/weightless/-/weightless-0.0.34.tgz", + "integrity": "sha512-4DtRAW8/6g3NUp/8tHH4yuhbZdSP90r48HFSpd4rwAcAYjiva+xV+jz+KrIEBSF9k3ii216wGbfhqvijFPYjwQ==", + "requires": { + "@a11y/focus-trap": "1.0.2", + "lit-element": "^2.1.0", + "lit-html": "^1.1.0" + } + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "requires": { + "sax": "^1.2.4" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..3a6773f --- /dev/null +++ b/client/package.json @@ -0,0 +1,59 @@ +{ + "name": "massivedecks", + "version": "2.0.0", + "description": "A web party game.", + "main": "src/ts/index.ts", + "author": "Gareth Latty ", + "license": "AGPL-3.0", + "private": true, + "dependencies": { + "@fortawesome/fontawesome-free": "^5.9.0", + "@webcomponents/webcomponentsjs": "^2.2.10", + "sass-inline-svg": "^1.2.0", + "weightless": "0.0.34" + }, + "devDependencies": { + "@types/chrome": "0.0.86", + "@types/chromecast-caf-receiver": "^3.0.8", + "@types/chromecast-caf-sender": "^1.0.1", + "app-manifest-loader": "^2.3.1", + "chromecast-device-emulator": "^1.2.7", + "clean-webpack-plugin": "^3.0.0", + "css-hot-loader": "^1.4.4", + "css-loader": "^3.0.0", + "cssnano": "^4.1.10", + "elm": "^0.19.0-no-deps", + "elm-hot-webpack-loader": "^1.0.2", + "elm-webpack-loader": "^5.0.0", + "extract-loader": "^3.1.0", + "file-loader": "^4.0.0", + "html-loader": "^0.5.5", + "node-sass": "^4.12.0", + "postcss-import": "^12.0.1", + "postcss-loader": "^3.0.0", + "postcss-preset-env": "^6.6.0", + "prettier": "1.18.2", + "prettier-plugin-elm": "^0.4.2", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", + "terser-webpack-plugin": "^1.3.0", + "ts-loader": "^6.0.4", + "typescript": "^3.5.2", + "uglifyjs-webpack-plugin": "^2.1.3", + "webpack": "^4.35.0", + "webpack-cli": "^3.3.4", + "webpack-dev-server": "^3.7.2" + }, + "browserslist": [ + "last 2 version", + "not dead", + "Firefox ESR", + "> 0.2%" + ], + "scripts": { + "build": "npx webpack --mode=production", + "watch": "npx webpack --mode=development --watch", + "dev": "npx webpack-dev-server --mode=development", + "errors": "npx webpack --mode=development --display-error-details" + } +} diff --git a/client/src/MassiveDecks.elm b/client/src/MassiveDecks.elm deleted file mode 100644 index d34fb81..0000000 --- a/client/src/MassiveDecks.elm +++ /dev/null @@ -1,25 +0,0 @@ -module MassiveDecks exposing (main) - -import Navigation -import MassiveDecks.Models exposing (Init, Path, pathFromLocation) -import MassiveDecks.Scenes.Start as Start -import MassiveDecks.Scenes.Start.Messages as Start -import MassiveDecks.Scenes.Start.Models as Start - - -{-| The main application loop setup. --} -main : Program Init Start.Model Start.Message -main = - Navigation.programWithFlags - locationToMessage - { init = Start.init - , update = Start.update - , subscriptions = Start.subscriptions - , view = Start.view - } - - -locationToMessage : Navigation.Location -> Start.Message -locationToMessage location = - pathFromLocation location |> Start.PathChange diff --git a/client/src/MassiveDecks/API.elm b/client/src/MassiveDecks/API.elm deleted file mode 100644 index 264c6de..0000000 --- a/client/src/MassiveDecks/API.elm +++ /dev/null @@ -1,299 +0,0 @@ -module MassiveDecks.API exposing (..) - -import Json.Decode as Decode -import Json.Encode as Encode -import MassiveDecks.API.Request exposing (..) -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Game.Round as Round -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Models.JSON.Decode exposing (..) -import MassiveDecks.Models.JSON.Encode exposing (..) - - -{-| Makes a request to create a new game lobby to the server. On success, returns that lobby. --} -createLobby : String -> Request Never Game.GameCodeAndSecret -createLobby name = - request "POST" "/api/lobbies" (Just (encodeName name)) [] gameCodeAndSecretDecoder - - -{-| Errors specific to new player requests. -* `NameInUse` - The request was to create a new player with a name already being uses in that lobby. -* `LobbyNotFound` - The given lobby does not exist. --} -type NewPlayerError - = NameInUse - | PasswordWrong - | NewPlayerLobbyNotFound - - -{-| Makes a request to add a new player to the given lobby. On success, returns a `Secret` for that player. --} -newPlayer : String -> String -> String -> Request NewPlayerError Player.Secret -newPlayer gameCode name password = - request - "POST" - ("/api/lobbies/" ++ gameCode ++ "/players") - (Just (encodeNameAndPassword name password)) - [ ( ( 400, "name-in-use" ), Decode.succeed NameInUse ) - , ( ( 403, "password-wrong" ), Decode.succeed PasswordWrong ) - , ( ( 404, "lobby-not-found" ), Decode.succeed NewPlayerLobbyNotFound ) - ] - playerSecretDecoder - - -{-| Errors specific to getting the lobby and hand. -* `LobbyNotFound` - The given lobby does not exist. --} -type GetLobbyAndHandError - = LobbyNotFound - | SecretWrongOrNotAPlayer - - -{-| Get the lobby and the hand for the player with the given secret (using it to authenticate). --} -getLobbyAndHand : String -> Player.Secret -> Request GetLobbyAndHandError Game.LobbyAndHand -getLobbyAndHand = - commandRequest - "getLobbyAndHand" - [] - [ ( ( 404, "lobby-not-found" ), Decode.succeed LobbyNotFound ) - , ( ( 403, "secret-wrong-or-not-a-player" ), Decode.succeed SecretWrongOrNotAPlayer ) - ] - lobbyAndHandDecoder - - -{-| Get the hand of the player with the given secret (using it to authenticate). --} -getHand : String -> Player.Secret -> Request Never Card.Hand -getHand gameCode secret = - request - "POST" - ("/api/lobbies/" ++ gameCode ++ "/players/" ++ (toString secret.id)) - (Just (encodePlayerSecret secret)) - [] - handDecoder - - -{-| Get the history of the given game. --} -getHistory : String -> Request Never (List Round.FinishedRound) -getHistory gameCode = - request - "GET" - ("/api/lobbies/" ++ gameCode ++ "/history") - Nothing - [] - (Decode.list finishedRoundDecoder) - - -{-| Errors specific to add deck requests. -* `CardcastTimeout` - The server timed out trying to retrieve the deck from Cardcast. -* `DeckNotFound` - The given play code does not resolve to a Cardcast deck. --} -type AddDeckError - = CardcastTimeout - | DeckNotFound - - -{-| Makes a request to add the deck for the given play code to the game configuration, using the given secret to -authenticate. --} -addDeck : String -> Player.Secret -> String -> Request AddDeckError () -addDeck gameCode secret playCode = - commandRequest - "addDeck" - [ ( "playCode", Encode.string playCode ) ] - [ ( ( 502, "cardcast-timeout" ), Decode.succeed CardcastTimeout ) - , ( ( 400, "deck-not-found" ), Decode.succeed DeckNotFound ) - ] - (Decode.succeed ()) - gameCode - secret - - -{-| Makes a request to the server to add a new AI player to the game. --} -newAi : String -> Player.Secret -> Request Never () -newAi gameCode secret = - request - "POST" - ("/api/lobbies/" ++ gameCode ++ "/players/newAi") - (Just (encodePlayerSecret secret)) - [] - (Decode.succeed ()) - - -{-| Errors specific to starting a new game. -* `NotEnoughPlayers` - There are not enough players in the lobby to start the game. The required number is given. -* `GameInProgress` - There is already a game in progress. --} -type NewGameError - = NotEnoughPlayers Int - | GameInProgress - - -{-| Makes a request to the server to start a new game in the given lobby, using the given secret to authenticate. --} -newGame : String -> Player.Secret -> Request NewGameError Card.Hand -newGame gameCode secret = - commandRequest - "newGame" - [] - [ ( ( 400, "game-in-progress" ), Decode.succeed GameInProgress ) - , ( ( 400, "not-enough-players" ), Decode.map NotEnoughPlayers (Decode.field "required" Decode.int) ) - ] - handDecoder - gameCode - secret - - -{-| Errors specific to choosing a winner for the round. -* `NotCzar` - The player is not the card czar. --} -type ChooseError - = NotCzar - - -{-| Make a request to choose the given (by index) winning response for round for the given lobby, using the given secret -to authenticate. --} -choose : String -> Player.Secret -> Int -> Request ChooseError () -choose gameCode secret winner = - commandRequest - "choose" - [ ( "winner", Encode.int winner ) ] - [ ( ( 400, "not-czar" ), Decode.succeed NotCzar ) ] - (Decode.succeed ()) - gameCode - secret - - -{-| Errors specific to playing responses into the round. -* `NotInRound` - The player is not in the round, and therefore can't play (i.e.: They are card czar, joined mid-round -* or were skipped.) -* `AlreadyPlayed` - The player has already played into the round. -* `AlreadyJudging` - The round is in the judging phase, no more cards can be played. -* `WrongNumberOfCards` - The wrong number of cards were played, with the number got, and the number expected. --} -type PlayError - = NotInRound - | AlreadyPlayed - | AlreadyJudging - | WrongNumberOfCards Int Int - - -{-| Make a request to play the given (by index) cards from the player's hand into the round for the given lobby, using -the given secret to authenticate. --} -play : String -> Player.Secret -> List String -> Request PlayError Card.Hand -play gameCode secret ids = - commandRequest - "play" - [ ( "ids", Encode.list (List.map Encode.string ids) ) ] - [ ( ( 400, "not-in-round" ), Decode.succeed NotInRound ) - , ( ( 400, "already-played" ), Decode.succeed AlreadyPlayed ) - , ( ( 400, "already-judging" ), Decode.succeed AlreadyJudging ) - , ( ( 400, "wrong-number-of-cards-played" ), Decode.map2 WrongNumberOfCards (Decode.field "got" Decode.int) (Decode.field "expected" Decode.int) ) - ] - handDecoder - gameCode - secret - - -{-| Errors specific to skipping a player in the lobby. -* `NotEnoughPlayers` - The number of active players would drop below the minimum if the given players were -* skipped. -* `PlayersNotSkippable` - One of the given players was not in a state where they could be skipped (i.e.: not -* disconnected or timed out). --} -type SkipError - = NotEnoughPlayersToSkip Int - | PlayersNotSkippable - - -{-| Make a request to skip the given players in the given lobby using the given secret to authenticate. --} -skip : String -> Player.Secret -> List Player.Id -> Request SkipError () -skip gameCode secret players = - commandRequest - "skip" - [ ( "players", Encode.list (List.map Encode.int players) ) ] - [ ( ( 400, "not-enough-players" ), Decode.map NotEnoughPlayersToSkip (Decode.field "required" Decode.int) ) - , ( ( 400, "players-must-be-skippable" ), Decode.succeed PlayersNotSkippable ) - ] - (Decode.succeed ()) - gameCode - secret - - -{-| Make a request to stop being skipped. --} -back : String -> Player.Secret -> Request Never () -back = - commandRequest "back" [] [] (Decode.succeed ()) - - -{-| Make a request to leave the game. --} -leave : String -> Player.Secret -> Request Never () -leave gameCode secret = - request - "POST" - ("/api/lobbies/" ++ gameCode ++ "/players/" ++ (toString secret.id) ++ "/leave") - (Just (encodePlayerSecret secret)) - [] - (Decode.succeed ()) - - -{-| Errors specific to redrawing. -* `NotEnoughPoints` - The player does not have enough points to redraw. --} -type RedrawError - = NotEnoughPoints - - -{-| Make a request to redraw the players hand, losing a point. --} -redraw : String -> Player.Secret -> Request RedrawError Card.Hand -redraw = - commandRequest - "redraw" - [] - [ ( ( 400, "not-enough-points" ), Decode.succeed NotEnoughPoints ) - ] - handDecoder - - -{-| Make a request to enable a house rule. --} -enableRule : HouseRule.Id -> String -> Player.Secret -> Request Never () -enableRule rule gameCode secret = - commandRequest "enableRule" [ ( "rule", rule |> HouseRule.toString |> Encode.string ) ] [] (Decode.succeed ()) gameCode secret - - -{-| Make a request to disable a house rule. --} -disableRule : HouseRule.Id -> String -> Player.Secret -> Request Never () -disableRule rule gameCode secret = - commandRequest "disableRule" [ ( "rule", rule |> HouseRule.toString |> Encode.string ) ] [] (Decode.succeed ()) gameCode secret - - -{-| Make a request to set the lobby password --} -setPassword : String -> Player.Secret -> String -> Request Never () -setPassword gameCode secret password = - commandRequest "setPassword" [ ( "password", password |> Encode.string ) ] [] (Decode.succeed ()) gameCode secret - - - -{- Private -} - - -{-| Construct a request for a command. --} -commandRequest : String -> List ( String, Encode.Value ) -> List (KnownError a) -> Decode.Decoder b -> String -> Player.Secret -> Request a b -commandRequest name args errors decoder gameCode secret = - request "POST" ("/api/lobbies/" ++ gameCode) (Just (encodeCommand name secret args)) errors decoder diff --git a/client/src/MassiveDecks/API/Request.elm b/client/src/MassiveDecks/API/Request.elm deleted file mode 100644 index feef740..0000000 --- a/client/src/MassiveDecks/API/Request.elm +++ /dev/null @@ -1,202 +0,0 @@ -module MassiveDecks.API.Request exposing (send, send_, request, Request, KnownError, Error(..)) - -import Json.Decode as Json -import Dict exposing (Dict) -import Http -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Util as Util - - -{-| Send a request to the server. - -request - The request to the server. See MassiveDecks.API. -onSpecificError - How to handle any errors specific to the request. -onGeneralError - How to handle any general errors. -onSuccess - How to handle success. - -If the request has no specific errors, use `send_` instead. --} -send : Request specificError result -> (specificError -> message) -> (Errors.Message -> message) -> (result -> message) -> Cmd message -send request onSpecificError onGeneralError onSuccess = - let - req = - Http.request - { method = request.method - , headers = [] - , url = request.url - , body = - case request.body of - Just json -> - Http.jsonBody json - - Nothing -> - Http.emptyBody - , expect = Http.expectStringResponse (handleResponse request.resultDecoder request.errors) - , timeout = Nothing - , withCredentials = False - } - in - Http.send - (\result -> - case result of - Result.Ok errorOrResult -> - case errorOrResult of - Error error -> - handleErrors onSpecificError onGeneralError error - - Result result -> - onSuccess result - - Result.Err error -> - (case error of - Http.BadStatus response -> - (Json.decodeString - (Json.maybe errorKeyDecoder - |> Json.andThen - (\n -> - case n of - Just errorName -> - errorDecoder response errorName request.errors - - Nothing -> - Json.succeed (General error) - ) - ) - response.body - ) - |> Result.withDefault (General error) - - _ -> - General error - ) - |> handleErrors onSpecificError onGeneralError - ) - req - - -handleErrors : (specificError -> message) -> (Errors.Message -> message) -> Error specificError -> message -handleErrors onSpecificError onGeneralError error = - case error of - Known specificError -> - onSpecificError specificError - - _ -> - onGeneralError (genericErrorHandler error) - - -handleResponse : Json.Decoder result -> KnownErrors specificError -> Http.Response String -> Result String (ErrorOrResult specificError result) -handleResponse resultDecoder knownErrors response = - Json.decodeString (resultOrErrorDecoder response resultDecoder knownErrors) response.body - - -{-| Same as 'send', but for requests with no known errors. --} -send_ : Request Never result -> (Errors.Message -> message) -> (result -> message) -> Cmd message -send_ request onGeneralError onSuccess = - send request Util.impossible onGeneralError onSuccess - - -{-| A request to the API. --} -type alias Request specificError result = - { method : String - , url : String - , body : Maybe Json.Value - , errors : KnownErrors specificError - , resultDecoder : Json.Decoder result - } - - -{-| A convinience method to make the errors dictionary from a list. --} -request : String -> String -> Maybe Json.Value -> List (KnownError specificError) -> Json.Decoder result -> Request specificError result -request verb url body errors resultDecoder = - Request verb url body (Dict.fromList errors) resultDecoder - - -{-| Specifies an error the client understands and can present to the user nicely. --} -type alias KnownError specificError = - ( ( Int, String ), Json.Decoder specificError ) - - -{-| The dictionary of KnownErrors. --} -type alias KnownErrors specificError = - Dict ( Int, String ) (Json.Decoder specificError) - - -{-| The top level of potential errors from an API request. --} -type Error specificError - = General Http.Error - | Known specificError - | Unknown (Http.Response String) - - -type ErrorOrResult specificError result - = Error (Error specificError) - | Result result - - -{-| Convert errors to generic error messages for display in the errors component. Generally you want to use this as a -fallback after handling all known errors. --} -genericErrorHandler : Error a -> Errors.Message -genericErrorHandler error = - case error of - Known specificError -> - Errors.New ("An error was not correctly handled: " ++ (toString specificError)) True - - Unknown response -> - Errors.New ("An error was not not recognised (status " ++ (toString response.status.code) ++ "): " ++ response.body) True - - General (Http.Timeout) -> - Errors.New "Timed out trying to connect to the server." False - - General (Http.NetworkError) -> - Errors.New "There was a network error trying to connect to the server." False - - General (Http.BadUrl url) -> - Errors.New ("The URL '" ++ url ++ "' was invalid.") True - - General (Http.BadStatus response) -> - Errors.New ("Recieved an unexpected response (" ++ (toString response.status.code) ++ ") from the server: " ++ response.status.message) True - - General (Http.BadPayload explanation response) -> - Errors.New ("The response recieved from the server wasn't what we expected: " ++ explanation) True - - -{-| Decode the error key from --} -errorKeyDecoder : Json.Decoder String -errorKeyDecoder = - Json.at [ "error" ] Json.string - - -errorDecoder : Http.Response String -> String -> KnownErrors specificError -> Json.Decoder (Error specificError) -errorDecoder response errorName knownErrors = - let - decoder = - Dict.get ( response.status.code, errorName ) knownErrors - in - case decoder of - Just decoder -> - Json.map Known decoder - - Nothing -> - Json.succeed (Unknown response) - - -resultOrErrorDecoder : Http.Response String -> Json.Decoder result -> KnownErrors specificError -> Json.Decoder (ErrorOrResult specificError result) -resultOrErrorDecoder response resultDecoder knownErrors = - Json.maybe errorKeyDecoder - |> Json.andThen - (\error -> - case error of - Just errorName -> - Json.map (Error) (errorDecoder response errorName knownErrors) - - Nothing -> - Json.map Result resultDecoder - ) diff --git a/client/src/MassiveDecks/Components/About.elm b/client/src/MassiveDecks/Components/About.elm deleted file mode 100644 index 66e9826..0000000 --- a/client/src/MassiveDecks/Components/About.elm +++ /dev/null @@ -1,83 +0,0 @@ -module MassiveDecks.Components.About exposing (show) - -import String -import Html exposing (..) -import Html.Attributes exposing (..) -import MassiveDecks.Components.Overlay as Overlay exposing (Overlay) - - -show : String -> Overlay.Message a -show version = - Overlay.Show (Overlay icon title (contents version)) - - -icon : String -icon = - "info-circle" - - -title : String -title = - "About" - - -contents : String -> List (Html a) -contents version = - ([ p [] - [ text "Massive Decks is a web game based on the excellent " - , a [ href "https://cardsagainsthumanity.com/", target "_blank", rel "noopener" ] [ text "Cards against Humanity" ] - , text " - a party game where you play white cards to try and produce the most amusing outcome when " - , text "combined with the given black card." - ] - , p [] - [ text "Massive Decks is also inspired by: " - , ul [] - [ li [] - [ a [ href "https://www.cardcastgame.com/", target "_blank", rel "noopener" ] [ text "Cardcast" ] - , text " - an app that allows you to play on a ChromeCast." - ] - , li [] - [ a [ href "http://pretendyoure.xyz/zy/", target "_blank", rel "noopener" ] [ text "Pretend You're Xyzzy" ] - , text " - a web game where you can jump in with people you don't know." - ] - ] - ] - , p [] - [ text "This is an open source game developed in " - , a [ href "http://elm-lang.org/", target "_blank", rel "noopener" ] [ text "Elm" ] - , text " for the client and " - , a [ href "http://www.scala-lang.org/", target "_blank", rel "noopener" ] [ text "Scala" ] - , text " for the server." - ] - , p [] - [ text "We also use: " - , ul [] - [ li [] - [ a [ href "https://www.cardcastgame.com/", target "_blank", rel "noopener" ] [ text "Cardcast" ] - , text "'s APIs for getting decks of cards (you can go there to make your own!)." - ] - , li [] - [ text "The " - , a [ href "https://www.playframework.com/", target "_blank", rel "noopener" ] [ text "Play framework" ] - ] - , li [] [ a [ href "http://lesscss.org/", target "_blank", rel "noopener" ] [ text "Less" ] ] - , li [] [ a [ href "https://fortawesome.github.io/Font-Awesome/", target "_blank", rel "noopener" ] [ text "Font Awesome" ] ] - , li [] [ a [ href "https://www.muicss.com", target "_blank", rel "noopener" ] [ text "MUI" ] ] - ] - ] - , p [] - [ text "Bug reports and contributions are welcome on the " - , a [ href "https://github.com/Lattyware/massivedecks", target "_blank", rel "noopener" ] [ text "GitHub repository" ] - , text ", where you can find the complete source to the game, under the AGPLv3 license. The game concept " - , text "'Cards against Humanity' is used under a " - , a [ href "https://creativecommons.org/licenses/by-nc-sa/2.0/", target "_blank", rel "noopener" ] [ text "Creative Commons BY-NC-SA 2.0 license" ] - , text " granted by " - , a [ href "https://cardsagainsthumanity.com/", target "_blank", rel "noopener" ] [ text "Cards against Humanity" ] - ] - ] - ) - ++ (if String.isEmpty version then - [] - else - [ p [] [ text ("This server is running version " ++ version ++ ".") ] ] - ) diff --git a/client/src/MassiveDecks/Components/BrowserNotifications.elm b/client/src/MassiveDecks/Components/BrowserNotifications.elm deleted file mode 100644 index 9473f3d..0000000 --- a/client/src/MassiveDecks/Components/BrowserNotifications.elm +++ /dev/null @@ -1,117 +0,0 @@ -port module MassiveDecks.Components.BrowserNotifications exposing (Model, Message, ConsumerMessage(..), Permission(..), init, update, subscriptions, notify, enable, disable) - -import Maybe -import MassiveDecks.Util as Util - - -init : Bool -> Bool -> Model -init supported enabled = - { supported = supported - , enabled = enabled - , permission = Nothing - } - - -type alias Model = - { supported : Bool - , enabled : Bool - , permission : Maybe Permission - } - - -type alias Notification = - { title : String - , icon : Maybe String - } - - -type Permission - = Granted - | Denied - | Default - - -update : Message -> Model -> ( Model, Cmd Message, Cmd ConsumerMessage ) -update message model = - case message of - PermissionGiven permission -> - ( { model | permission = Just permission }, Cmd.none, Util.cmd (PermissionChanged permission) ) - - SendNotification notification -> - if model.supported && model.enabled then - ( model, notifications notification, Cmd.none ) - else - ( model, Cmd.none, Cmd.none ) - - EnableNotifications -> - ( { model | enabled = True }, requestPermission (), Cmd.none ) - - DisableNotifications -> - ( { model | enabled = False }, Cmd.none, Cmd.none ) - - -type ConsumerMessage - = PermissionChanged Permission - - -type Message - = PermissionGiven Permission - | SendNotification Notification - | EnableNotifications - | DisableNotifications - - -subscriptions : Model -> Sub Message -subscriptions model = - if model.supported && model.enabled && model.permission == Nothing then - permissions permission - else - Sub.none - - -port permissions : (String -> msg) -> Sub msg - - -permission : String -> Message -permission name = - let - permission = - case name of - "granted" -> - Granted - - "denied" -> - Denied - - "default" -> - Default - - _ -> - let - _ = - Debug.log "Unexpected permission for browser notifications, assuming denied" name - in - Denied - in - PermissionGiven permission - - -notify : Notification -> Message -notify notification = - SendNotification notification - - -port notifications : Notification -> Cmd msg - - -enable : Message -enable = - EnableNotifications - - -disable : Message -disable = - DisableNotifications - - -port requestPermission : () -> Cmd msg diff --git a/client/src/MassiveDecks/Components/Errors.elm b/client/src/MassiveDecks/Components/Errors.elm deleted file mode 100644 index 5b58020..0000000 --- a/client/src/MassiveDecks/Components/Errors.elm +++ /dev/null @@ -1,121 +0,0 @@ -module MassiveDecks.Components.Errors exposing (Message(..), Model, ApplicationInfo, view, update, init, reportUrl) - -import String -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Util as Util - - -type Message - = New String Bool - | Remove Int - - -type alias Model = - { currentId : Int - , errors : List Error - } - - -type alias ApplicationInfo = - { url : String - , version : String - } - - -{-| A generic error message to be displayed when something goes wrong. Should only be used where there isn't a good way -to avoid the error altogether or display the error closer to it's source. --} -type alias Error = - { id : Int - , message : String - , bugReport : Bool - } - - -init : Model -init = - { currentId = 0 - , errors = [] - } - - -view : ApplicationInfo -> Model -> Html Message -view applicationInfo model = - ol [ id "error-panel" ] (List.map (errorMessage applicationInfo) model.errors) - - -update : Message -> Model -> ( Model, Cmd Message ) -update message model = - case message of - New message bugReport -> - let - new = - { id = model.currentId, message = message, bugReport = bugReport } - in - ( { model - | errors = model.errors ++ [ new ] - , currentId = model.currentId + 1 - } - , Cmd.none - ) - - Remove id -> - ( { model | errors = List.filter (\error -> error.id /= id) model.errors }, Cmd.none ) - - -reportText : String -> String -reportText message = - ("I was [a short explanation of what you were doing] when I got the following error: \n\n" ++ message) - - -reportUrl : ApplicationInfo -> String -> String -reportUrl applicationInfo message = - let - version = - if String.isEmpty applicationInfo.version then - "Not Specified" - else - applicationInfo.version - - full = - message ++ "\n\nApplication Info:\n\tVersion: " ++ version ++ "\n\tURL: " ++ applicationInfo.url - in - "https://github.com/Lattyware/massivedecks/issues/new?body=" ++ full - - -errorMessage : ApplicationInfo -> Error -> Html Message -errorMessage applicationInfo error = - let - url = - reportUrl applicationInfo (reportText error.message) - - bugReportLink = - if error.bugReport then - Just (p [] [ a [ href url, target "_blank", rel "noopener" ] [ Icon.icon "bug", text " Report this as a bug." ] ]) - else - Nothing - in - li - [ class "error" ] - [ div - [] - ([ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (Remove error.id) - ] - [ Icon.icon "times" ] - , h5 [] - [ Icon.icon "exclamation-triangle" - , text " Error" - ] - , div [ class "mui-divider" ] [] - , p [] [ text error.message ] - ] - |> Util.andMaybe bugReportLink - ) - ] diff --git a/client/src/MassiveDecks/Components/Icon.elm b/client/src/MassiveDecks/Components/Icon.elm deleted file mode 100644 index 2a2a416..0000000 --- a/client/src/MassiveDecks/Components/Icon.elm +++ /dev/null @@ -1,25 +0,0 @@ -module MassiveDecks.Components.Icon exposing (..) - -import Html exposing (..) -import Html.Attributes exposing (..) - - -{-| A FointAwesome icon by name. --} -icon : String -> Html a -icon name = - i [ class ("fa fa-" ++ name) ] [] - - -{-| A full width FointAwesome icon by name. --} -fwIcon : String -> Html a -fwIcon name = - i [ class ("fa fa-fw fa-" ++ name) ] [] - - -{-| A loading spinner. --} -spinner : Html a -spinner = - i [ class "fa fa-circle-o-notch fa-spin" ] [] diff --git a/client/src/MassiveDecks/Components/Input.elm b/client/src/MassiveDecks/Components/Input.elm deleted file mode 100644 index b1947fe..0000000 --- a/client/src/MassiveDecks/Components/Input.elm +++ /dev/null @@ -1,153 +0,0 @@ -module MassiveDecks.Components.Input exposing (Message, Model, Change(..), init, initWithExtra, subscriptions, view, update) - -import Json.Decode as Json -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import MassiveDecks.Util as Util -import MassiveDecks.Components.Icon as Icon - - -{-| Messages for changes to the input. --} -type alias Message id = - ( id, Change ) - - -{-| Changes to the input. --} -type Change - = Changed String - | Error (Maybe String) - | Submit - | SetEnabled Bool - | SetDefaultValue String - | NoOp - - -{-| The state of the input. --} -type alias Model id msg = - { identity : id - , class : String - , label : List (Html msg) - , placeholder : String - , value : String - , error : Maybe String - , extra : String -> List (Html msg) - , embedMethod : Message id -> msg - , submit : Cmd msg - , enabled : Bool - } - - -{-| Create the initial model. - -The identity is unique to the input, used to differentiate between different inputs. -The class is the class of the wrapping div. -The label is any HTML that will be inserted into the lable after the info icon. -The value is the initial value of the input. If the input update is run, this will always contain the value currently -shown in the textbox - however, changing it will not change the value of the textbox. -The placeholder is a value to show if no value is entered. -The error is a message that will appear next to the input if set. -The extra is a method to produce anything to be added to the input (e.g: a submit button). It will be given the -current value. -The embedMethod is how to wrap the input message for the surrounding message type. --} -init : id -> String -> List (Html msg) -> String -> String -> Cmd msg -> (Message id -> msg) -> Model id msg -init identity class label value placeholder submit embedMethod = - initWithExtra identity class label value placeholder (\_ -> []) submit embedMethod - - -{-| Create the initial model with some extra content. See init for most of how this works. - -The extra is a method to produce anything to be added to the input (e.g: a submit button). It will be given the -current value. --} -initWithExtra : id -> String -> List (Html msg) -> String -> String -> (String -> List (Html msg)) -> Cmd msg -> (Message id -> msg) -> Model id msg -initWithExtra identity class label value placeholder extra submit embedMethod = - { identity = identity - , class = class - , label = label - , value = value - , placeholder = placeholder - , error = Nothing - , extra = extra - , embedMethod = embedMethod - , submit = submit - , enabled = True - } - - -{-| Subscriptions for the input. --} -subscriptions : Model id msg -> Sub (Message id) -subscriptions model = - Sub.none - - -{-| Render the input. --} -view : Model id msg -> Html msg -view model = - div [ class model.class ] - ([ div [ class "mui-textfield" ] - ([ input - [ type_ "text" - , defaultValue model.value - , placeholder model.placeholder - , disabled (not model.enabled) - , on "input" (Json.map (\value -> (model.embedMethod ( model.identity, Changed value ))) targetValue) - , Util.onKeyDown "Enter" (model.embedMethod ( model.identity, Submit )) (model.embedMethod ( model.identity, NoOp )) - ] - [] - , label [] (List.append [ Icon.icon "info-circle", text " " ] model.label) - ] - |> Util.andMaybe (error model.error) - ) - ] - ++ model.extra model.value - ) - - -{-| Render an error message for the input. --} -error : Maybe String -> Maybe (Html msg) -error message = - Maybe.map (\error -> span [ class "input-error" ] [ Icon.icon "exclamation", text " ", text error ]) message - - -{-| Handles messages and alters the model as appropriate. --} -update : Message id -> Model id msg -> ( Model id msg, Cmd msg ) -update message model = - let - ( identity, change ) = - message - in - if (identity == model.identity) then - case change of - Changed value -> - ( { model - | value = value - , error = Nothing - } - , Cmd.none - ) - - Error error -> - ( { model | error = error }, Cmd.none ) - - Submit -> - ( { model | error = Nothing }, model.submit ) - - SetEnabled enabled -> - ( { model | enabled = enabled }, Cmd.none ) - - SetDefaultValue value -> - ( { model | value = value }, Cmd.none ) - - NoOp -> - ( model, Cmd.none ) - else - ( { model | error = Nothing }, Cmd.none ) diff --git a/client/src/MassiveDecks/Components/Overlay.elm b/client/src/MassiveDecks/Components/Overlay.elm deleted file mode 100644 index b3d1382..0000000 --- a/client/src/MassiveDecks/Components/Overlay.elm +++ /dev/null @@ -1,91 +0,0 @@ -port module MassiveDecks.Components.Overlay exposing (Overlay, Model, Message(..), init, view, update, map) - -import Json.Decode as Json -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Util as Util - - -type alias Model a = - { overlay : Maybe (Overlay a) - , wrap : Message a -> a - } - - -type alias Overlay a = - { icon : String - , title : String - , contents : List (Html a) - } - - -type Message a - = Show (Overlay a) - | Hide - | NoOp - - -init : (Message a -> a) -> Model a -init wrap = - { overlay = Nothing - , wrap = wrap - } - - -map : (a -> b) -> Message a -> Message b -map mapper message = - case message of - Show overlay -> - Show (Overlay overlay.icon overlay.title (List.map (Html.map mapper) overlay.contents)) - - Hide -> - Hide - - NoOp -> - NoOp - - -update : Message a -> Model a -> Model a -update message model = - case message of - Show overlay -> - { model | overlay = Just overlay } - - Hide -> - { model | overlay = Nothing } - - NoOp -> - model - - -view : Model a -> List (Html a) -view model = - case model.overlay of - Just overlay -> - [ div - [ id "mui-overlay" - , Util.onClickIfId "mui-overlay" (model.wrap Hide) (model.wrap NoOp) - , Util.onKeyDown "Escape" (model.wrap Hide) (model.wrap NoOp) - , tabindex 0 - ] - [ div [ class "overlay mui-panel" ] - ([ h1 [] [ Icon.icon overlay.icon, text " ", text overlay.title ] ] - ++ overlay.contents - ++ [ p [ class "close-link" ] - [ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (model.wrap Hide) - ] - [ Icon.icon "times", text " Close" ] - ] - ] - ) - ] - ] - - Nothing -> - [] diff --git a/client/src/MassiveDecks/Components/QR.elm b/client/src/MassiveDecks/Components/QR.elm deleted file mode 100644 index 06cc41c..0000000 --- a/client/src/MassiveDecks/Components/QR.elm +++ /dev/null @@ -1,17 +0,0 @@ -port module MassiveDecks.Components.QR exposing (view, encodeAndRender) - -import Html exposing (Html) -import Html.Attributes as Html - - -port qr : { id : String, value : String } -> Cmd msg - - -view : String -> Html msg -view containerId = - Html.div [ Html.id containerId ] [] - - -encodeAndRender : String -> String -> Cmd msg -encodeAndRender containerId value = - qr { id = containerId, value = value } diff --git a/client/src/MassiveDecks/Components/Storage.elm b/client/src/MassiveDecks/Components/Storage.elm deleted file mode 100644 index 17fd320..0000000 --- a/client/src/MassiveDecks/Components/Storage.elm +++ /dev/null @@ -1,40 +0,0 @@ -port module MassiveDecks.Components.Storage exposing (Model, Message(..), update, join, leave) - -import MassiveDecks.Models.Game as Game - - -type alias Model = - List Game.GameCodeAndSecret - - -type Message - = Store - | Clear - - -port store : Model -> Cmd msg - - -update : Message -> Model -> ( Model, Cmd Message ) -update message model = - case message of - Store -> - ( model, store model ) - - Clear -> - ( [], store [] ) - - -join : Game.GameCodeAndSecret -> Model -> Model -join gameCodeAndSecret model = - gameCodeAndSecret :: List.filter (different gameCodeAndSecret) model - - -leave : Game.GameCodeAndSecret -> Model -> Model -leave gameCodeAndSecret = - List.filter (different gameCodeAndSecret) - - -different : Game.GameCodeAndSecret -> Game.GameCodeAndSecret -> Bool -different check existing = - check.gameCode /= existing.gameCode diff --git a/client/src/MassiveDecks/Components/TTS.elm b/client/src/MassiveDecks/Components/TTS.elm deleted file mode 100644 index dc72326..0000000 --- a/client/src/MassiveDecks/Components/TTS.elm +++ /dev/null @@ -1,41 +0,0 @@ -port module MassiveDecks.Components.TTS exposing (Model, Message(..), update, init) - -import MassiveDecks.Util as Util - - -type alias Model = - { enabled : Bool - } - - -type Message - = Say String - | Enabled Bool - - -init : Model -init = - { enabled = False } - - -update : Message -> Model -> ( Model, Cmd Message ) -update message model = - case message of - Say text -> - ( model - , if model.enabled then - say text - else - Cmd.none - ) - - Enabled enabled -> - ( { model | enabled = enabled } - , if enabled then - Cmd.none - else - Say "" |> Util.cmd - ) - - -port say : String -> Cmd msg diff --git a/client/src/MassiveDecks/Components/Tabs.elm b/client/src/MassiveDecks/Components/Tabs.elm deleted file mode 100644 index 4f28ca4..0000000 --- a/client/src/MassiveDecks/Components/Tabs.elm +++ /dev/null @@ -1,71 +0,0 @@ -port module MassiveDecks.Components.Tabs exposing (Message(..), Model, Tab, init, update, view) - -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) - - -{-| Messages for the tab system. --} -type Message tabId - = SetTab tabId - - -{-| The model for the tabs. --} -type alias Model tabId msg = - { tabs : List (Tab tabId msg) - , current : tabId - , tagger : Message tabId -> msg - } - - -{-| An individual tab. --} -type alias Tab tabId msg = - { id : tabId - , title : List (Html msg) - } - - -{-| Initialise the model. --} -init : List (Tab tabId msg) -> tabId -> (Message tabId -> msg) -> Model tabId msg -init = - Model - - -{-| Given a message, update the model to fit. --} -update : Message tabId -> Model tabId msg -> Model tabId msg -update message model = - case message of - SetTab tabId -> - { model | current = tabId } - - -{-| Given a model, render it. - -The first argument is a rendering function for the content of the tab. This allows the caller of the view to control the -content easily. --} -view : (tabId -> List (Html msg)) -> Model tabId msg -> List (Html msg) -view renderer model = - [ ul [ class "mui-tabs__bar mui-tabs__bar--justified" ] (List.map (viewTab model.tagger model.current) model.tabs) - ] - ++ (List.map (viewPane model.current renderer) model.tabs) - - -viewTab : (Message tabId -> msg) -> tabId -> Tab tabId msg -> Html msg -viewTab tagger current model = - li [ classList [ ( "mui--is-active", current == model.id ) ] ] - [ a - [ onClick (tagger <| SetTab model.id) - ] - model.title - ] - - -viewPane : tabId -> (tabId -> List (Html msg)) -> Tab tabId msg -> Html msg -viewPane current renderer model = - div [ classList [ ( "mui-tabs__pane", True ), ( "mui--is-active", current == model.id ) ] ] (renderer model.id) diff --git a/client/src/MassiveDecks/Components/Title.elm b/client/src/MassiveDecks/Components/Title.elm deleted file mode 100644 index 6c3e27c..0000000 --- a/client/src/MassiveDecks/Components/Title.elm +++ /dev/null @@ -1,9 +0,0 @@ -port module MassiveDecks.Components.Title exposing (set) - - -port title : String -> Cmd msg - - -set : String -> Cmd msg -set = - title diff --git a/client/src/MassiveDecks/Models.elm b/client/src/MassiveDecks/Models.elm deleted file mode 100644 index b306e32..0000000 --- a/client/src/MassiveDecks/Models.elm +++ /dev/null @@ -1,28 +0,0 @@ -module MassiveDecks.Models exposing (..) - -import Navigation -import MassiveDecks.Models.Game exposing (GameCodeAndSecret) - - -{-| Data required to create the initial application state. --} -type alias Init = - { version : String - , url : String - , existingGames : List GameCodeAndSecret - , seed : String - , browserNotificationsSupported : Bool - } - - -{-| A path to a part of the application. --} -type alias Path = - { gameCode : Maybe String - } - - -pathFromLocation : Navigation.Location -> Path -pathFromLocation location = - { gameCode = Maybe.map Tuple.second (String.uncons location.hash) - } diff --git a/client/src/MassiveDecks/Models/Card.elm b/client/src/MassiveDecks/Models/Card.elm deleted file mode 100644 index 6ac3623..0000000 --- a/client/src/MassiveDecks/Models/Card.elm +++ /dev/null @@ -1,71 +0,0 @@ -module MassiveDecks.Models.Card exposing (..) - -import String -import Dict exposing (Dict) -import MassiveDecks.Models.Player as Player -import MassiveDecks.Util as Util - - -{-| A call (black card) is composed of a list of strings making up the text of the card, with a blank space ('slot') for -a response implicitly existing inbetween each string. --} -type alias Call = - { id : String - , parts : List String - } - - -{-| A response (white card). --} -type alias Response = - { id : String - , text : String - } - - -{-| A hand of a player. --} -type alias Hand = - { hand : List Response - } - - -{-| Cards that have been played into a round for a call. --} -type alias PlayedCards = - List Response - - -{-| The number of slots on a given call. --} -slots : Call -> Int -slots call = - (List.length call.parts) - 1 - - -{-| Produce a string of the given call with the given played cards injected into it. --} -filled : Call -> PlayedCards -> String -filled call playedCards = - String.concat (Util.interleave (List.map .text playedCards) call.parts) - - -{-| Join the player ids to the cards played into a round. --} -playedCardsByPlayer : List Player.Id -> List PlayedCards -> Dict Player.Id PlayedCards -playedCardsByPlayer players cards = - List.map2 (,) players cards |> Dict.fromList - - -{-| The cards played by the winner of the game. --} -winningCards : List PlayedCards -> Player.PlayedByAndWinner -> Maybe PlayedCards -winningCards cards playedByAndWinner = - let - cardsByPlayer = - playedCardsByPlayer playedByAndWinner.playedBy cards - - winner = - playedByAndWinner.winner - in - Dict.get winner cardsByPlayer diff --git a/client/src/MassiveDecks/Models/Event.elm b/client/src/MassiveDecks/Models/Event.elm deleted file mode 100644 index f4a31a2..0000000 --- a/client/src/MassiveDecks/Models/Event.elm +++ /dev/null @@ -1,94 +0,0 @@ -module MassiveDecks.Models.Event exposing (Event(..), fromJson) - -import Json.Decode as Json -import MassiveDecks.Models.JSON.Decode exposing (..) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Game.Round as Round -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Models.Card as Card - - -{-| An event represents a change in the game state from the server. These are recieved by websocket. --} -type Event - = Sync Game.LobbyAndHand - | PlayerJoin Player - | PlayerStatus Player.Id Player.Status - | PlayerLeft Player.Id - | PlayerDisconnect Player.Id - | PlayerReconnect Player.Id - | PlayerScoreChange Player.Id Int - | HandChange Card.Hand - | RoundStart Player.Id Card.Call - | RoundPlayed Int - | RoundJudging (List Card.PlayedCards) - | RoundEnd Round.FinishedRound - | GameStart Player.Id Card.Call - | GameEnd - | ConfigChange Game.Config - | RoundTimeLimitHit - - -fromJson : String -> Result String Event -fromJson json = - Json.decodeString eventDecoder json - - -eventDecoder : Json.Decoder Event -eventDecoder = - (Json.field "event" Json.string) |> Json.andThen specificEventDecoder - - -specificEventDecoder : String -> Json.Decoder Event -specificEventDecoder name = - case name of - "Sync" -> - Json.map Sync (Json.field "lobbyAndHand" lobbyAndHandDecoder) - - "PlayerJoin" -> - Json.map PlayerJoin (Json.field "player" playerDecoder) - - "PlayerStatus" -> - Json.map2 PlayerStatus (Json.field "player" playerIdDecoder) (Json.field "status" playerStatusDecoder) - - "PlayerLeft" -> - Json.map PlayerLeft (Json.field "player" playerIdDecoder) - - "PlayerDisconnect" -> - Json.map PlayerDisconnect (Json.field "player" playerIdDecoder) - - "PlayerReconnect" -> - Json.map PlayerReconnect (Json.field "player" playerIdDecoder) - - "PlayerScoreChange" -> - Json.map2 PlayerScoreChange (Json.field "player" playerIdDecoder) (Json.field "score" Json.int) - - "HandChange" -> - Json.map HandChange (Json.field "hand" handDecoder) - - "RoundStart" -> - Json.map2 RoundStart (Json.field "czar" playerIdDecoder) (Json.field "call" callDecoder) - - "RoundPlayed" -> - Json.map RoundPlayed (Json.field "playedCards" Json.int) - - "RoundJudging" -> - Json.map RoundJudging (Json.field "playedCards" (Json.list (Json.list responseDecoder))) - - "RoundEnd" -> - Json.map RoundEnd (Json.field "finishedRound" finishedRoundDecoder) - - "GameStart" -> - Json.map2 GameStart (Json.field "czar" playerIdDecoder) (Json.field "call" callDecoder) - - "GameEnd" -> - Json.succeed GameEnd - - "ConfigChange" -> - Json.map ConfigChange (Json.field "config" configDecoder) - - "RoundTimeLimitHit" -> - Json.succeed RoundTimeLimitHit - - unknown -> - Json.fail (unknown ++ " is not a recognised event.") diff --git a/client/src/MassiveDecks/Models/Game.elm b/client/src/MassiveDecks/Models/Game.elm deleted file mode 100644 index 7d884b2..0000000 --- a/client/src/MassiveDecks/Models/Game.elm +++ /dev/null @@ -1,64 +0,0 @@ -module MassiveDecks.Models.Game exposing (..) - -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule - - -{-| The required information to rejoin a lobby - the ID and the secret. --} -type alias GameCodeAndSecret = - { gameCode : GameCode - , secret : Player.Secret - } - - -{-| A lobby ID is a string used to identify a given lobby. --} -type alias GameCode = - String - - -{-| Configuration for a game. --} -type alias Config = - { decks : List DeckInfo - , houseRules : List HouseRule.Id - , password : Maybe String - } - - -{-| Information about a deck of cards. --} -type alias DeckInfo = - { id : String - , name : String - , calls : Int - , responses : Int - } - - -type State - = Configuring - | Playing Round - | Finished - - -{-| A lobby. --} -type alias Lobby = - { gameCode : String - , owner : Player.Id - , config : Config - , players : List Player - , game : State - } - - -{-| A lobby and a player's hand. --} -type alias LobbyAndHand = - { lobby : Lobby - , hand : Card.Hand - } diff --git a/client/src/MassiveDecks/Models/Game/Round.elm b/client/src/MassiveDecks/Models/Game/Round.elm deleted file mode 100644 index f25080c..0000000 --- a/client/src/MassiveDecks/Models/Game/Round.elm +++ /dev/null @@ -1,93 +0,0 @@ -module MassiveDecks.Models.Game.Round exposing (..) - -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Player as Player - - -type alias Round = - { czar : Player.Id - , call : Card.Call - , state : State - } - - -{-| The possible states of the round. --} -type State - = P Playing - | J Judging - | F Finished - - -playing : Int -> Bool -> State -playing numberPlayed afterTimeLimit = - P (Playing numberPlayed afterTimeLimit) - - -{-| A round that is being played into. --} -type alias Playing = - { numberPlayed : Int - , afterTimeLimit : Bool - } - - -judging : List Card.PlayedCards -> Bool -> State -judging responses afterTimeLimit = - J (Judging responses afterTimeLimit) - - -{-| A round that is being judged. --} -type alias Judging = - { responses : List Card.PlayedCards - , afterTimeLimit : Bool - } - - -finished : List Card.PlayedCards -> Player.PlayedByAndWinner -> State -finished responses playedByAndWinner = - F (Finished responses playedByAndWinner) - - -{-| A round that has been completed. --} -type alias Finished = - { responses : List Card.PlayedCards - , playedByAndWinner : Player.PlayedByAndWinner - } - - -{-| A historical round that has been completed. --} -type alias FinishedRound = - { czar : Player.Id - , call : Card.Call - , state : Finished - } - - -afterTimeLimit : State -> Bool -afterTimeLimit state = - case state of - P playing -> - playing.afterTimeLimit - - J judging -> - judging.afterTimeLimit - - F finished -> - False - - -setAfterTimeLimit : Round -> Bool -> Round -setAfterTimeLimit round afterTimeLimit = - case round.state of - P playing -> - { round | state = P { playing | afterTimeLimit = afterTimeLimit } } - - J judging -> - { round | state = J { judging | afterTimeLimit = afterTimeLimit } } - - F finished -> - round diff --git a/client/src/MassiveDecks/Models/JSON/Decode.elm b/client/src/MassiveDecks/Models/JSON/Decode.elm deleted file mode 100644 index adcece5..0000000 --- a/client/src/MassiveDecks/Models/JSON/Decode.elm +++ /dev/null @@ -1,202 +0,0 @@ -module MassiveDecks.Models.JSON.Decode exposing (..) - -import Json.Decode exposing (..) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Models.Card as Card -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule - - -lobbyAndHandDecoder : Decoder Game.LobbyAndHand -lobbyAndHandDecoder = - map2 Game.LobbyAndHand - (field "lobby" lobbyDecoder) - (field "hand" handDecoder) - - -lobbyDecoder : Decoder Game.Lobby -lobbyDecoder = - map5 Game.Lobby - (field "gameCode" string) - (field "owner" playerIdDecoder) - (field "config" configDecoder) - (field "players" (list playerDecoder)) - (field "state" gameStateDecoder) - - -gameCodeAndSecretDecoder : Decoder Game.GameCodeAndSecret -gameCodeAndSecretDecoder = - map2 Game.GameCodeAndSecret - (field "gameCode" string) - (field "secret" playerSecretDecoder) - - -deckInfoDecoder : Decoder Game.DeckInfo -deckInfoDecoder = - map4 Game.DeckInfo - (field "id" string) - (field "name" string) - (field "calls" int) - (field "responses" int) - - -configDecoder : Decoder Game.Config -configDecoder = - map3 Game.Config - (field "decks" (list deckInfoDecoder)) - (field "houseRules" (list houseRuleDecoder)) - (maybe (field "password" string)) - - -handDecoder : Decoder Card.Hand -handDecoder = - map Card.Hand - (field "hand" (list responseDecoder)) - - -playerDecoder : Decoder Player -playerDecoder = - map6 Player - (field "id" playerIdDecoder) - (field "name" string) - (field "status" playerStatusDecoder) - (field "score" int) - (field "disconnected" bool) - (field "left" bool) - - -playerStatusDecoder : Decoder Player.Status -playerStatusDecoder = - customDecoder (string) (\name -> Player.nameToStatus name |> Result.fromMaybe ("Unknown player status '" ++ name ++ "'.")) - - -gameStateDecoder : Decoder Game.State -gameStateDecoder = - (field "gameState" string - |> andThen - (\gameState -> - case gameState of - "configuring" -> - succeed Game.Configuring - - "playing" -> - map Game.Playing (field "round" roundDecoder) - - "finished" -> - succeed Game.Finished - - _ -> - fail ("Unknown game state '" ++ gameState ++ "'.") - ) - ) - - -roundDecoder : Decoder Round -roundDecoder = - map3 Round - (field "czar" playerIdDecoder) - (field "call" callDecoder) - (field "state" roundStateDecoder) - - -roundStateDecoder : Decoder Round.State -roundStateDecoder = - (field "roundState" string - |> andThen - (\roundState -> - case roundState of - "playing" -> - map2 Round.playing - (field "numberPlayed" int) - (field "afterTimeLimit" bool) - - "judging" -> - map2 Round.judging - (field "cards" (list (list responseDecoder))) - (field "afterTimeLimit" bool) - - "finished" -> - map Round.F finishedStateDecoder - - _ -> - fail ("Unknown round state '" ++ roundState ++ "'.") - ) - ) - - -finishedStateDecoder : Decoder Round.Finished -finishedStateDecoder = - map2 Round.Finished - (field "cards" (list (list responseDecoder))) - (field "playedByAndWinner" playedByAndWinnerDecoder) - - -finishedRoundDecoder : Decoder Round.FinishedRound -finishedRoundDecoder = - map3 Round.FinishedRound - (field "czar" playerIdDecoder) - (field "call" callDecoder) - (field "state" finishedStateDecoder) - - -playedByAndWinnerDecoder : Decoder Player.PlayedByAndWinner -playedByAndWinnerDecoder = - map2 Player.PlayedByAndWinner - (field "playedBy" (list (playerIdDecoder))) - (field "winner" playerIdDecoder) - - -callDecoder : Decoder Card.Call -callDecoder = - map2 Card.Call - (field "id" string) - (field "parts" (list string)) - - -responseDecoder : Decoder Card.Response -responseDecoder = - map2 Card.Response - (field "id" string) - (field "text" string) - - -playerIdDecoder : Decoder Player.Id -playerIdDecoder = - int - - -playerSecretDecoder : Decoder Player.Secret -playerSecretDecoder = - map2 Player.Secret - (field "id" playerIdDecoder) - (field "secret" string) - - -houseRuleDecoder : Decoder HouseRule.Id -houseRuleDecoder = - customDecoder (string) (\name -> ruleNameToId name |> Result.fromMaybe ("Unknown house rule '" ++ name ++ "'.")) - - -ruleNameToId : String -> Maybe HouseRule.Id -ruleNameToId name = - case name of - "reboot" -> - Just HouseRule.Reboot - - _ -> - Nothing - - -customDecoder : Decoder b -> (b -> Result String a) -> Decoder a -customDecoder decoder toResult = - andThen - (\a -> - case toResult a of - Ok b -> - succeed b - - Err err -> - fail err - ) - decoder diff --git a/client/src/MassiveDecks/Models/JSON/Encode.elm b/client/src/MassiveDecks/Models/JSON/Encode.elm deleted file mode 100644 index c747b56..0000000 --- a/client/src/MassiveDecks/Models/JSON/Encode.elm +++ /dev/null @@ -1,46 +0,0 @@ -module MassiveDecks.Models.JSON.Encode exposing (..) - -import Json.Encode as Json -import MassiveDecks.Models.Player as Player - - -encodeCommand : String -> Player.Secret -> List ( String, Json.Value ) -> Json.Value -encodeCommand action playerSecret rest = - Json.object - (List.append - [ ( "command", Json.string action ) - , ( "secret", encodePlayerSecret playerSecret ) - ] - rest - ) - - -encodeDeckId : String -> ( String, Json.Value ) -encodeDeckId id = - ( "deckId", Json.string id ) - - -encodePlayerSecret : Player.Secret -> Json.Value -encodePlayerSecret playerSecret = - Json.object - [ ( "id", encodePlayerId playerSecret.id ) - , ( "secret", Json.string playerSecret.secret ) - ] - - -encodePlayerId : Player.Id -> Json.Value -encodePlayerId playerId = - Json.int playerId - - -encodeName : String -> Json.Value -encodeName name = - Json.object [ ( "name", Json.string name ) ] - - -encodeNameAndPassword : String -> String -> Json.Value -encodeNameAndPassword name password = - Json.object - [ ( "name", Json.string name ) - , ( "password", Json.string password ) - ] diff --git a/client/src/MassiveDecks/Models/Notification.elm b/client/src/MassiveDecks/Models/Notification.elm deleted file mode 100644 index 243e826..0000000 --- a/client/src/MassiveDecks/Models/Notification.elm +++ /dev/null @@ -1,55 +0,0 @@ -module MassiveDecks.Models.Notification exposing (..) - -import MassiveDecks.Models.Player as Player exposing (Player) - - -{-| A notification about a player. --} -type alias Notification = - { icon : String - , name : String - , description : String - , visible : Bool - } - - -{-| Hide the given notificaiton. --} -hide : Notification -> Notification -hide notification = - { notification | visible = False } - - -{-| Create a notification for a player joining the game. --} -playerJoin : Player.Id -> List Player -> Maybe Notification -playerJoin id players = - playerFromIdAndPlayers id players "sign-in" " has joined the game." - - -{-| Create a notification for a player reconnecting to the game. --} -playerReconnect : Player.Id -> List Player -> Maybe Notification -playerReconnect id players = - playerFromIdAndPlayers id players "sign-in" " has reconnected to the game." - - -{-| Create a notification for a player disconnecting from the game. --} -playerDisconnect : Player.Id -> List Player -> Maybe Notification -playerDisconnect id players = - playerFromIdAndPlayers id players "minus-circle" " has disconnected from the game." - - -{-| Create a notification for a player leaving the game. --} -playerLeft : Player.Id -> List Player -> Maybe Notification -playerLeft id players = - playerFromIdAndPlayers id players "sign-out" " has left the game." - - -playerFromIdAndPlayers : Player.Id -> List Player -> String -> String -> Maybe Notification -playerFromIdAndPlayers id players icon suffix = - Player.byId id players - |> Maybe.map .name - |> Maybe.map (\name -> Notification icon name (name ++ suffix) True) diff --git a/client/src/MassiveDecks/Models/Player.elm b/client/src/MassiveDecks/Models/Player.elm deleted file mode 100644 index c3150b0..0000000 --- a/client/src/MassiveDecks/Models/Player.elm +++ /dev/null @@ -1,106 +0,0 @@ -module MassiveDecks.Models.Player exposing (..) - -import MassiveDecks.Util as Util - - -{-| A game-unique identifier for a player. --} -type alias Id = - Int - - -{-| A player. --} -type alias Player = - { id : Id - , name : String - , status : Status - , score : Int - , disconnected : Bool - , left : Bool - } - - -{-| A list of ids to identify who played what responses into a round and the id of the winner of the round. --} -type alias PlayedByAndWinner = - { playedBy : List Id - , winner : Id - } - - -{-| A secret that a player uses to authenticate themselves to the server. --} -type alias Secret = - { id : Id - , secret : String - } - - -{-| The status of a player. --} -type Status - = NotPlayed - | Played - | Czar - | Ai - | Neutral - | Skipping - - -{-| The name of the status. --} -statusName : Status -> String -statusName status = - case status of - NotPlayed -> - "not-played" - - Played -> - "played" - - Czar -> - "czar" - - Ai -> - "ai" - - Neutral -> - "neutral" - - Skipping -> - "skipping" - - -{-| Get a status from a name. --} -nameToStatus : String -> Maybe Status -nameToStatus name = - case name of - "not-played" -> - Just NotPlayed - - "played" -> - Just Played - - "czar" -> - Just Czar - - "ai" -> - Just Ai - - "neutral" -> - Just Neutral - - "skipping" -> - Just Skipping - - _ -> - Nothing - - -{-| Get a player from a list of players by their id. --} -byId : Id -> List Player -> Maybe Player -byId id players = - Util.find (\player -> player.id == id) players diff --git a/client/src/MassiveDecks/Scenes/Config.elm b/client/src/MassiveDecks/Scenes/Config.elm deleted file mode 100644 index fe98e13..0000000 --- a/client/src/MassiveDecks/Scenes/Config.elm +++ /dev/null @@ -1,164 +0,0 @@ -module MassiveDecks.Scenes.Config exposing (update, view, init, subscriptions) - -import String -import Html exposing (Html) -import MassiveDecks.API as API -import MassiveDecks.API.Request as Request -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Scenes.Lobby.Models as Lobby -import MassiveDecks.Scenes.Config.Messages exposing (ConsumerMessage(..), Message(..), InputId(..), Deck(..)) -import MassiveDecks.Scenes.Config.Models exposing (Model) -import MassiveDecks.Scenes.Config.UI as UI -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Player as Player -import MassiveDecks.Util as Util exposing ((:>)) - - -{-| Create the initial model for the config screen. --} -init : Game.Lobby -> Player.Secret -> ( Model, Cmd ConsumerMessage ) -init lobby secret = - let - canChangeConfig = - lobby.owner == secret.id - - config = - lobby.config - - setPasswordButton = - if canChangeConfig then - UI.setPasswordButton - else - (\_ -> []) - in - { decks = [] - , deckIdInput = Input.initWithExtra DeckId "input-with-button" UI.deckIdInputLabel "" "Play Code" UI.addDeckButton (Util.cmd AddDeck) InputMessage - , passwordInput = Input.initWithExtra Password "input-with-button" UI.passwordInputLabel (config.password |> Maybe.withDefault "") "Password" setPasswordButton (Util.cmd SetPassword) InputMessage - , loadingDecks = [] - } - ! [ ( Password, Input.SetEnabled canChangeConfig ) |> InputMessage |> LocalMessage |> Util.cmd ] - - -{-| Subscriptions for the config screen. --} -subscriptions : Model -> Sub ConsumerMessage -subscriptions model = - Sub.none - - -{-| Render the config screen. --} -view : Lobby.Model -> Html ConsumerMessage -view lobbyModel = - UI.view lobbyModel |> Html.map LocalMessage - - -{-| Handles messages and alters the model as appropriate. --} -update : Message -> Lobby.Model -> ( Model, Cmd ConsumerMessage ) -update message lobbyModel = - let - lobby = - lobbyModel.lobby - - gameCode = - lobbyModel.lobby.gameCode - - secret = - lobbyModel.secret - - model = - lobbyModel.config - in - case message of - ConfigureDecks (Request rawDeckId) -> - let - deckId = - String.toUpper rawDeckId - in - { model | loadingDecks = model.loadingDecks ++ [ deckId ] } - ! [ Request.send (API.addDeck gameCode secret deckId) (addDeckErrorHandler deckId) ErrorMessage (\_ -> (Add deckId) |> ConfigureDecks |> LocalMessage) - , inputClearErrorCmd DeckId - ] - - AddDeck -> - ( model, Util.cmd (LocalMessage (ConfigureDecks (Request model.deckIdInput.value))) ) - - ConfigureDecks (Add deckId) -> - ( removeDeckLoadingSpinner deckId model, Cmd.none ) - - ConfigureDecks (Fail deckId errorMessage) -> - ( removeDeckLoadingSpinner deckId model, inputSetErrorCmd DeckId errorMessage ) - - InputMessage message -> - let - ( deckIdInput, deckIdMsg ) = - Input.update message lobbyModel.config.deckIdInput - - ( passwordInput, passwordMsg ) = - Input.update message lobbyModel.config.passwordInput - in - { model - | deckIdInput = deckIdInput - , passwordInput = passwordInput - } - ! [ Cmd.map LocalMessage deckIdMsg, Cmd.map LocalMessage passwordMsg ] - - AddAi -> - ( model, Request.send_ (API.newAi gameCode secret) ErrorMessage (\_ -> LocalMessage NoOp) ) - - StartGame -> - ( model, Request.send (API.newGame gameCode secret) newGameErrorHandler ErrorMessage HandUpdate ) - - EnableRule rule -> - ( model, Request.send_ (API.enableRule rule gameCode secret) ErrorMessage ignore ) - - DisableRule rule -> - ( model, Request.send_ (API.disableRule rule gameCode secret) ErrorMessage ignore ) - - SetPassword -> - ( model, Request.send_ (API.setPassword gameCode secret model.passwordInput.value) ErrorMessage ignore ) - - NoOp -> - ( model, Cmd.none ) - - -ignore : () -> ConsumerMessage -ignore = - (\_ -> LocalMessage NoOp) - - -inputClearErrorCmd : InputId -> Cmd ConsumerMessage -inputClearErrorCmd inputId = - ( inputId, Nothing |> Input.Error ) |> InputMessage |> LocalMessage |> Util.cmd - - -inputSetErrorCmd : InputId -> String -> Cmd ConsumerMessage -inputSetErrorCmd inputId error = - ( inputId, Just error |> Input.Error ) |> InputMessage |> LocalMessage |> Util.cmd - - -removeDeckLoadingSpinner : String -> Model -> Model -removeDeckLoadingSpinner deckId model = - { model | loadingDecks = List.filter ((/=) deckId) model.loadingDecks } - - -addDeckErrorHandler : String -> API.AddDeckError -> ConsumerMessage -addDeckErrorHandler deckId error = - case error of - API.CardcastTimeout -> - ConfigureDecks (Fail deckId "There was a problem accessing CardCast, please try again after a short wait.") |> LocalMessage - - API.DeckNotFound -> - ConfigureDecks (Fail deckId "The given play code doesn't exist, please check you have the correct code.") |> LocalMessage - - -newGameErrorHandler : API.NewGameError -> ConsumerMessage -newGameErrorHandler error = - case error of - API.GameInProgress -> - ErrorMessage <| Errors.New "Can't start the game - it is already in progress." False - - API.NotEnoughPlayers required -> - ErrorMessage <| Errors.New ("Can't start the game - you need at least " ++ (toString required) ++ " players to start the game.") False diff --git a/client/src/MassiveDecks/Scenes/Config/Messages.elm b/client/src/MassiveDecks/Scenes/Config/Messages.elm deleted file mode 100644 index e046ff4..0000000 --- a/client/src/MassiveDecks/Scenes/Config/Messages.elm +++ /dev/null @@ -1,49 +0,0 @@ -module MassiveDecks.Scenes.Config.Messages exposing (..) - -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Models.Card as Card -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule - - -{-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. --} -type ConsumerMessage - = HandUpdate Card.Hand - | ErrorMessage Errors.Message - | LocalMessage Message - - -{-| The messages used in the start screen. --} -type Message - = AddDeck - | ConfigureDecks Deck - | InputMessage (Input.Message InputId) - | AddAi - | StartGame - | EnableRule HouseRule.Id - | DisableRule HouseRule.Id - | SetPassword - | NoOp - - -type Deck - = Request DeckId - | Add DeckId - | Fail DeckId FailureMessage - - -type alias DeckId = - String - - -type alias FailureMessage = - String - - -{-| IDs for the inputs to differentiate between them in messages. --} -type InputId - = DeckId - | Password diff --git a/client/src/MassiveDecks/Scenes/Config/Models.elm b/client/src/MassiveDecks/Scenes/Config/Models.elm deleted file mode 100644 index 26bb9dd..0000000 --- a/client/src/MassiveDecks/Scenes/Config/Models.elm +++ /dev/null @@ -1,15 +0,0 @@ -module MassiveDecks.Scenes.Config.Models exposing (..) - -import MassiveDecks.Components.Input as Input -import MassiveDecks.Models.Game as Game -import MassiveDecks.Scenes.Config.Messages exposing (Message, InputId) - - -{-| The state of the config screen. --} -type alias Model = - { decks : List Game.DeckInfo - , deckIdInput : Input.Model InputId Message - , passwordInput : Input.Model InputId Message - , loadingDecks : List String - } diff --git a/client/src/MassiveDecks/Scenes/Config/UI.elm b/client/src/MassiveDecks/Scenes/Config/UI.elm deleted file mode 100644 index 14e9447..0000000 --- a/client/src/MassiveDecks/Scenes/Config/UI.elm +++ /dev/null @@ -1,293 +0,0 @@ -module MassiveDecks.Scenes.Config.UI exposing (view, deckIdInputLabel, passwordInputLabel, addDeckButton, setPasswordButton) - -import String -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Components.Input as Input -import MassiveDecks.Models.Game as Game -import MassiveDecks.Scenes.Lobby.Models as Lobby -import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule -import MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) -import MassiveDecks.Scenes.Config.Messages exposing (Message(..), InputId(..), Deck(..)) -import MassiveDecks.Util as Util - - -view : Lobby.Model -> Html Message -view lobbyModel = - let - model = - lobbyModel.config - - lobby = - lobbyModel.lobby - - decks = - lobby.config.decks - - enoughPlayers = - ((List.length lobby.players) > 1) - - enoughCards = - not (List.isEmpty decks) - - canNotChangeConfig = - lobbyModel.lobby.owner /= lobbyModel.secret.id - in - div [ id "config" ] - ((if canNotChangeConfig then - infoBar - else - [] - ) - ++ [ div [ id "config-content", class "mui-panel" ] - [ invite lobbyModel.init.url lobby.gameCode - , div [ class "mui-divider" ] [] - , h1 [] [ text "Game Setup" ] - , ul [ class "mui-tabs__bar" ] - [ li [ class "mui--is-active" ] - [ a - [ attribute "data-mui-toggle" "tab" - , attribute "data-mui-controls" "decks" - ] - [ text "Decks" ] - ] - , li [] - [ a - [ attribute "data-mui-toggle" "tab" - , attribute "data-mui-controls" "house-rules" - ] - [ text "House Rules" ] - ] - , li [] - [ a - [ attribute "data-mui-toggle" "tab" - , attribute "data-mui-controls" "lobby-settings" - ] - [ text "Lobby Settings" ] - ] - ] - , div [ id "decks", class "mui-tabs__pane mui--is-active" ] - [ deckList canNotChangeConfig decks model.loadingDecks model.deckIdInput ] - , div [ id "house-rules", class "mui-tabs__pane" ] - ([ rando canNotChangeConfig ] ++ (List.map (\rule -> houseRule canNotChangeConfig (List.member rule.id lobbyModel.lobby.config.houseRules) rule) houseRules)) - , div [ id "lobby-settings", class "mui-tabs__pane" ] - [ password model.passwordInput ] - , div [ class "mui-divider" ] [] - , startGameButton canNotChangeConfig enoughPlayers enoughCards - ] - ] - ) - - -infoBar : List (Html msg) -infoBar = - [ div [ id "info-bar", class "mui--z1" ] - [ Icon.icon "info-circle" - , text " " - , text "You can't change the configuration of the game, as you are not the owner." - ] - ] - - -passwordInputLabel : List (Html msg) -passwordInputLabel = - [ text "If blank, anyone with the game code can join." ] - - -password : Input.Model InputId Message -> Html Message -password passwordInputModel = - div [] - [ h3 [] [ Icon.icon "key", text " Privacy" ] - , p [] [ text "A password that players will need to enter to get in the game. People already in the game will not need to enter it, and anyone in the game will be able to see it." ] - , Input.view passwordInputModel - ] - - -setPasswordButton : String -> List (Html Message) -setPasswordButton password = - [ button - [ class "mui-btn mui-btn--small mui-btn--primary" - , onClick SetPassword - , title "Set the password." - ] - [ Icon.icon "lock" ] - ] - - -invite : String -> String -> Html msg -invite appUrl lobbyId = - let - url = - Util.lobbyUrl appUrl lobbyId - in - div [] - [ p [] - [ text "Invite others to the game with the code '" - , strong [ class "game-code" ] [ text lobbyId ] - , text "' to enter on the main page, or give them this link: " - ] - , p [] [ a [ href url ] [ text url ] ] - ] - - -deckIdInputLabel : List (Html msg) -deckIdInputLabel = - [ text " A " - , a [ href "https://www.cardcastgame.com/browse", target "_blank", rel "noopener" ] [ text "Cardcast" ] - , text " Play Code" - ] - - -addDeckButton : String -> List (Html Message) -addDeckButton deckId = - [ button - [ class "mui-btn mui-btn--small mui-btn--primary mui-btn--fab" - , disabled (String.isEmpty deckId) - , onClick (ConfigureDecks (Request deckId)) - , title "Add deck to game." - ] - [ Icon.icon "plus" ] - ] - - -deckList : Bool -> List Game.DeckInfo -> List String -> Input.Model InputId Message -> Html Message -deckList canNotChangeConfig decks loadingDecks deckId = - table [ class "decks mui-table" ] - [ thead [] - [ tr [] - [ th [] [ text "Id" ] - , th [] [ text "Name" ] - , th [ title "Calls" ] [ Icon.icon "square" ] - , th [ title "Responses" ] [ Icon.icon "square-o" ] - ] - ] - , Util.tbody [] - (List.concat - [ if (canNotChangeConfig && (List.isEmpty decks)) then - [ ( "!!emptyInfo", tr [] [ td [ colspan 4 ] [ text "No decks have been added yet." ] ] ) ] - else - [] - , emptyDeckListInfo ((not canNotChangeConfig) && (List.isEmpty decks) && List.isEmpty loadingDecks) - , List.map loadedDeckEntry decks - , List.map loadingDeckEntry loadingDecks - , if (canNotChangeConfig) then - [] - else - [ ( "!!input", tr [] [ td [ colspan 4 ] [ Input.view deckId ] ] ) ] - ] - ) - ] - - -loadedDeckEntry : Game.DeckInfo -> ( String, Html msg ) -loadedDeckEntry deck = - let - row = - tr [] - [ td [] [ deckLink deck.id ] - , td [ title deck.name ] [ text deck.name ] - , td [] [ text (toString deck.calls) ] - , td [] [ text (toString deck.responses) ] - ] - in - ( deck.id, row ) - - -loadingDeckEntry : String -> ( String, Html msg ) -loadingDeckEntry deckId = - let - row = - tr [] [ td [] [ deckLink deckId ], td [ colspan 3 ] [ Icon.spinner ] ] - in - ( deckId, row ) - - -deckLink : String -> Html msg -deckLink id = - a [ href ("https://www.cardcastgame.com/browse/deck/" ++ id), target "_blank", rel "noopener" ] [ text id ] - - -emptyDeckListInfo : Bool -> List ( String, Html Message ) -emptyDeckListInfo display = - if display then - [ ( "!!emptyInfo" - , tr [] - [ td [ colspan 4 ] - [ Icon.icon "info-circle" - , text " You will need to add at least one " - , a [ href "https://www.cardcastgame.com/browse", target "_blank", rel "noopener" ] [ text "Cardcast deck" ] - , text " to the game." - , text " Not sure? Try " - , a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (ConfigureDecks (Request "CAHBS")) - ] - [ text "clicking here to add the Cards Against Humanity base set" ] - , text "." - ] - ] - ) - ] - else - [] - - -houseRule : Bool -> Bool -> HouseRule -> Html Message -houseRule canNotChangeConfig enabled rule = - let - ( buttonText, command ) = - if enabled then - ( "Disable", DisableRule rule.id ) - else - ( "Enable", EnableRule rule.id ) - in - houseRuleTemplate canNotChangeConfig (HouseRule.toString rule.id) rule.name rule.icon rule.description buttonText command - - -houseRuleTemplate : Bool -> String -> String -> String -> String -> String -> msg -> Html msg -houseRuleTemplate canNotChangeConfig id_ title icon description buttonText message = - div [ id id_, class "house-rule" ] - [ div [] - [ h3 [] [ Icon.icon icon, text " ", text title ] - , button [ class "mui-btn mui-btn--small mui-btn--primary", onClick message, disabled canNotChangeConfig ] - [ text buttonText ] - ] - , p [] [ text description ] - ] - - -rando : Bool -> Html Message -rando canNotChangeConfig = - houseRuleTemplate canNotChangeConfig - "rando" - "Rando Cardrissian" - "cogs" - "Every round, one random card will be played for an imaginary player named Rando Cardrissian, if he wins, all players go home in a state of everlasting shame." - "Add an AI player" - AddAi - - -startGameWarning : Bool -> Html msg -startGameWarning canStart = - if canStart then - text "" - else - span [] [ Icon.icon "info-circle", text " You will need at least two players to start the game." ] - - -startGameButton : Bool -> Bool -> Bool -> Html Message -startGameButton notOwner enoughPlayers enoughCards = - div [ id "start-game" ] - [ startGameWarning enoughPlayers - , button - [ class "mui-btn mui-btn--primary mui-btn--raised" - , onClick StartGame - , disabled ((not (enoughPlayers && enoughCards)) || notOwner) - ] - [ text "Start Game" ] - ] diff --git a/client/src/MassiveDecks/Scenes/History.elm b/client/src/MassiveDecks/Scenes/History.elm deleted file mode 100644 index 671d018..0000000 --- a/client/src/MassiveDecks/Scenes/History.elm +++ /dev/null @@ -1,40 +0,0 @@ -module MassiveDecks.Scenes.History exposing (init, subscriptions, view, update) - -import Html exposing (Html) -import MassiveDecks.API as API -import MassiveDecks.API.Request as Request -import MassiveDecks.Scenes.History.Messages exposing (Message(..), ConsumerMessage(..)) -import MassiveDecks.Scenes.History.Models exposing (Model) -import MassiveDecks.Scenes.History.UI as UI -import MassiveDecks.Models.Player as Player exposing (Player) - - -init : String -> ( Model, Cmd ConsumerMessage ) -init gameCode = - ( { rounds = Nothing - } - , Request.send_ (API.getHistory gameCode) ErrorMessage (LocalMessage << Load) - ) - - -{-| Subscriptions for the playing scene. --} -subscriptions : Model -> Sub ConsumerMessage -subscriptions model = - Sub.none - - -{-| Render the history. --} -view : Model -> List Player -> Html ConsumerMessage -view = - UI.view - - -{-| Update the model. --} -update : Message -> Model -> ( Model, Cmd ConsumerMessage ) -update message model = - case message of - Load rounds -> - ( { model | rounds = Just rounds }, Cmd.none ) diff --git a/client/src/MassiveDecks/Scenes/History/Messages.elm b/client/src/MassiveDecks/Scenes/History/Messages.elm deleted file mode 100644 index a2f0c2d..0000000 --- a/client/src/MassiveDecks/Scenes/History/Messages.elm +++ /dev/null @@ -1,18 +0,0 @@ -module MassiveDecks.Scenes.History.Messages exposing (..) - -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Models.Game.Round as Round - - -{-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. --} -type ConsumerMessage - = ErrorMessage Errors.Message - | Close - | LocalMessage Message - - -{-| The messages used in the history scene. --} -type Message - = Load (List Round.FinishedRound) diff --git a/client/src/MassiveDecks/Scenes/History/Models.elm b/client/src/MassiveDecks/Scenes/History/Models.elm deleted file mode 100644 index 98cf17e..0000000 --- a/client/src/MassiveDecks/Scenes/History/Models.elm +++ /dev/null @@ -1,10 +0,0 @@ -module MassiveDecks.Scenes.History.Models exposing (Model) - -import MassiveDecks.Models.Game.Round as Round - - -{-| The state of the lobby. --} -type alias Model = - { rounds : Maybe (List Round.FinishedRound) - } diff --git a/client/src/MassiveDecks/Scenes/History/UI.elm b/client/src/MassiveDecks/Scenes/History/UI.elm deleted file mode 100644 index c2aec66..0000000 --- a/client/src/MassiveDecks/Scenes/History/UI.elm +++ /dev/null @@ -1,96 +0,0 @@ -module MassiveDecks.Scenes.History.UI exposing (view) - -import Dict -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import Html.Keyed as Keyed -import MassiveDecks.Scenes.History.Models exposing (Model) -import MassiveDecks.Scenes.History.Messages exposing (ConsumerMessage(..)) -import MassiveDecks.Scenes.Playing.UI.Cards as CardsUI -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Components.Icon as Icon - - -view : Model -> List Player -> Html ConsumerMessage -view model players = - let - content = - case model.rounds of - Just rounds -> - Keyed.ul [] (List.map (\round -> ( round.call.id, finishedRound players round )) rounds) - - Nothing -> - Icon.spinner - in - div - [ id "history" ] - [ h1 [ class "mui--divider-bottom" ] [ text "Previous Rounds" ] - , closeButton - , content - ] - - -finishedRound : List Player -> Round.FinishedRound -> Html msg -finishedRound players round = - let - state = - round.state - - playedBy = - state.playedByAndWinner.playedBy - - winner = - state.playedByAndWinner.winner - - czar = - Maybe.map - (\player -> [ Icon.icon "gavel", text " ", text player.name ]) - (Player.byId round.czar players) - |> Maybe.withDefault [] - - playedCardsByPlayer = - Card.playedCardsByPlayer playedBy state.responses |> Dict.toList - in - li - [ class "round" ] - [ div [] [ div [ class "who" ] czar, CardsUI.call round.call [] ] - , Keyed.ul [ class "plays" ] (List.map (\( id, cards ) -> ( toString id, responses players winner id cards )) playedCardsByPlayer) - ] - - -responses : List Player -> Player.Id -> Player.Id -> List Card.Response -> Html msg -responses players winnerId playerId responses = - let - winner = - playerId == winnerId - - winnerPrefix = - if (winner) then - [ Icon.icon "trophy", text " " ] - else - [] - - player = - Maybe.map - (\player -> winnerPrefix ++ [ text player.name ]) - (Player.byId playerId players) - |> Maybe.withDefault [] - in - li [] - [ div [ class "responses" ] - [ div [ classList [ ( "who", True ), ( "won", winner ) ] ] player - , Keyed.ul [] (List.map (\r -> ( r.id, li [] [ CardsUI.response False [] r ] )) responses) - ] - ] - - -closeButton : Html ConsumerMessage -closeButton = - button - [ class "mui-btn mui-btn--small mui-btn--fab" - , onClick Close - ] - [ Icon.icon "times" ] diff --git a/client/src/MassiveDecks/Scenes/Lobby.elm b/client/src/MassiveDecks/Scenes/Lobby.elm deleted file mode 100644 index 4325055..0000000 --- a/client/src/MassiveDecks/Scenes/Lobby.elm +++ /dev/null @@ -1,455 +0,0 @@ -module MassiveDecks.Scenes.Lobby exposing (update, view, init, subscriptions) - -import String -import Json.Encode exposing (encode) -import Task -import Time -import WebSocket -import AnimationFrame -import Html exposing (Html) -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.QR as QR -import MassiveDecks.Components.BrowserNotifications as BrowserNotifications -import MassiveDecks.Components.Overlay as Overlay exposing (Overlay) -import MassiveDecks.Components.TTS as TTS -import MassiveDecks.Models exposing (Init) -import MassiveDecks.Models.Event as Event exposing (Event) -import MassiveDecks.Models.JSON.Encode exposing (encodePlayerSecret) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Models.Notification as Notification exposing (Notification) -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Scenes.Playing as Playing -import MassiveDecks.Scenes.Playing.Messages as Playing -import MassiveDecks.Scenes.Config as Config -import MassiveDecks.Scenes.Config.Messages as Config -import MassiveDecks.Scenes.Lobby.UI as UI -import MassiveDecks.Scenes.Lobby.Messages exposing (ConsumerMessage(..), Message(..)) -import MassiveDecks.Scenes.Lobby.Models exposing (Model) -import MassiveDecks.Scenes.Lobby.Sidebar as Sidebar -import MassiveDecks.Util as Util exposing ((:>)) - - -{-| Create the initial model for the lobby. --} -init : Init -> Game.LobbyAndHand -> Player.Secret -> ( Model, Cmd ConsumerMessage ) -init init lobbyAndHand secret = - let - ( config, configCmd ) = - Config.init lobbyAndHand.lobby secret - in - { lobby = lobbyAndHand.lobby - , hand = lobbyAndHand.hand - , config = config - , playing = Playing.init init - , browserNotifications = BrowserNotifications.init init.browserNotificationsSupported False - , secret = secret - , init = init - , notification = Nothing - , qrNeedsRendering = False - , sidebar = Sidebar.init 768 {- Should match the enhance-width in less. -} - , tts = TTS.init - } - ! [ configCmd |> Cmd.map (ConfigMessage >> LocalMessage) ] - - -{-| Subscriptions for the lobby. --} -subscriptions : Model -> Sub ConsumerMessage -subscriptions model = - let - delegated = - case model.lobby.game of - Game.Configuring -> - Config.subscriptions model.config |> Sub.map ConfigMessage - - Game.Playing round -> - Playing.subscriptions model.playing |> Sub.map PlayingMessage - - Game.Finished -> - Sub.none - - websocket = - WebSocket.listen (webSocketUrl model.init.url model.lobby.gameCode) webSocketResponseDecoder - - browserNotifications = - BrowserNotifications.subscriptions model.browserNotifications |> Sub.map BrowserNotificationsMessage - - render = - if model.qrNeedsRendering then - [ AnimationFrame.diffs (\_ -> LocalMessage RenderQr) ] - else - [] - in - Sub.batch - ([ delegated |> Sub.map LocalMessage - , websocket - , browserNotifications |> Sub.map LocalMessage - ] - ++ render - ) - - -webSocketUrl : String -> String -> String -webSocketUrl url gameCode = - let - ( protocol, rest ) = - case String.split ":" url of - [] -> - ( "No protocol.", [] ) - - protocol :: rest -> - ( protocol, rest ) - - host = - String.join ":" rest - - baseUrl = - case protocol of - "http" -> - "ws:" ++ host - - "https" -> - "wss:" ++ host - - unknown -> - let - _ = - Debug.log "Assuming https due to unknown protocol for URL" unknown - in - "wss:" ++ host - in - baseUrl ++ "api/lobbies/" ++ gameCode ++ "/notifications" - - -webSocketResponseDecoder : String -> ConsumerMessage -webSocketResponseDecoder response = - if (response == "identify") then - Identify |> LocalMessage - else - case Event.fromJson response of - Ok event -> - LocalMessage <| Batch <| handleEvent event - - Err message -> - ErrorMessage <| Errors.New ("Error handling notification: " ++ message) True - - -{-| Render the lobby. --} -view : Model -> Html ConsumerMessage -view model = - UI.view model - - -{-| Handles messages and alters the model as appropriate. --} -update : Message -> Model -> ( Model, Cmd ConsumerMessage ) -update message model = - let - lobby = - model.lobby - in - case message of - ConfigMessage configMessage -> - case configMessage of - Config.ErrorMessage errorMessage -> - ( model, ErrorMessage errorMessage |> Util.cmd ) - - Config.HandUpdate hand -> - model |> updateLobbyAndHand (Game.LobbyAndHand model.lobby hand) - - Config.LocalMessage localMessage -> - let - ( config, cmd ) = - Config.update localMessage model - in - ( { model | config = config }, Cmd.map (LocalMessage << ConfigMessage) cmd ) - - PlayingMessage playingMessage -> - case playingMessage of - Playing.ErrorMessage errorMessage -> - ( model, ErrorMessage errorMessage |> Util.cmd ) - - Playing.HandUpdate hand -> - model |> updateLobbyAndHand (Game.LobbyAndHand model.lobby hand) - - Playing.TTSMessage ttsMessage -> - ( model, TTSMessage ttsMessage |> LocalMessage |> Util.cmd ) - - Playing.LocalMessage localMessage -> - case model.lobby.game of - Game.Playing round -> - let - ( playing, cmd ) = - Playing.update localMessage model round - in - ( { model | playing = playing }, Cmd.map (LocalMessage << PlayingMessage) cmd ) - - _ -> - ( model, Cmd.none ) - - SidebarMessage sidebarMessage -> - let - ( sidebarModel, cmd ) = - Sidebar.update sidebarMessage model.sidebar - in - ( { model | sidebar = sidebarModel }, Cmd.map (LocalMessage << SidebarMessage) cmd ) - - TTSMessage ttsMessage -> - let - ( ttsModel, cmd ) = - TTS.update ttsMessage model.tts - in - ( { model | tts = ttsModel }, Cmd.map (LocalMessage << TTSMessage) cmd ) - - BrowserNotificationsMessage notificationMessage -> - let - ( browserNotifications, localCmd, cmd ) = - BrowserNotifications.update notificationMessage model.browserNotifications - in - { model | browserNotifications = browserNotifications } - ! [ Cmd.map (LocalMessage << BrowserNotificationsMessage) localCmd - , Cmd.map overlayAlert cmd - ] - - BrowserNotificationForUser getUserId title iconName -> - let - cmd = - case (getUserId lobby) of - Just id -> - if (id == model.secret.id) then - Util.cmd (BrowserNotifications.notify { title = title, icon = icon model iconName } |> BrowserNotificationsMessage |> LocalMessage) - else - Cmd.none - - Nothing -> - Cmd.none - in - ( model, cmd ) - - UpdateLobbyAndHand lobbyAndHand -> - model |> updateLobbyAndHand lobbyAndHand - - UpdateLobby update -> - model |> updateLobbyAndHand (Game.LobbyAndHand (update lobby) model.hand) - - UpdateHand hand -> - model |> updateLobbyAndHand (Game.LobbyAndHand model.lobby hand) - - SetNotification notification -> - notificationChange model (notification lobby.players) - - Identify -> - ( model, Cmd.map LocalMessage (WebSocket.send (webSocketUrl model.init.url model.lobby.gameCode) (encodePlayerSecret model.secret |> encode 0)) ) - - DisplayInviteOverlay -> - { model | qrNeedsRendering = True } ! [ Util.cmd (UI.inviteOverlay model.init.url model.lobby.gameCode |> OverlayMessage) ] - - RenderQr -> - { model | qrNeedsRendering = False } ! [ QR.encodeAndRender "invite-qr-code" (Util.lobbyUrl model.init.url model.lobby.gameCode) ] - - DismissNotification notification -> - let - newModel = - if model.notification == Just notification then - { model | notification = Maybe.map Notification.hide model.notification } - else - model - in - ( newModel, Cmd.none ) - - Batch messages -> - model ! (List.map (Util.cmd << LocalMessage) messages) - - NoOp -> - ( model, Cmd.none ) - - -handleEvent : Event -> List Message -handleEvent event = - case event of - Event.Sync lobbyAndHand -> - [ UpdateLobbyAndHand lobbyAndHand ] - - Event.PlayerJoin player -> - [ SetNotification (Notification.playerJoin player.id) - , UpdateLobby (\lobby -> { lobby | players = lobby.players ++ [ player ] }) - ] - - Event.PlayerStatus player status -> - let - browserNotification = - case status of - Player.NotPlayed -> - [ BrowserNotificationForUser (\_ -> Just player) "You need to play a card for the round." "hourglass" ] - - Player.Skipping -> - [ BrowserNotificationForUser (\_ -> Just player) "You are being skipped due to inactivity." "fast-forward" ] - - _ -> - [] - in - [ updatePlayer player (\player -> { player | status = status }) - ] - ++ browserNotification - - Event.PlayerLeft player -> - [ SetNotification (Notification.playerLeft player) - , updatePlayer player (\player -> { player | left = True }) - ] - - Event.PlayerDisconnect player -> - [ SetNotification (Notification.playerDisconnect player) - , updatePlayer player (\player -> { player | disconnected = True }) - ] - - Event.PlayerReconnect player -> - [ SetNotification (Notification.playerReconnect player) - , updatePlayer player (\player -> { player | disconnected = False }) - ] - - Event.PlayerScoreChange player score -> - [ updatePlayer player (\player -> { player | score = score }) ] - - Event.HandChange hand -> - [ UpdateHand hand ] - - Event.RoundStart czar call -> - [ UpdateLobby (\lobby -> { lobby | game = Game.Playing (Round czar call (Round.playing 0 False)) }) ] - - Event.RoundPlayed playedCards -> - [ updateRoundState (\state -> Round.playing playedCards (Round.afterTimeLimit state)) ] - - Event.RoundJudging playedCards -> - [ updateRoundState (\state -> Round.judging playedCards False) - , BrowserNotificationForUser (\lobby -> lobbyRound lobby |> Maybe.map .czar) "You need to pick a winner for the round." "gavel" - ] - - Event.RoundEnd finishedRound -> - [ updateRoundState (\state -> Round.F finishedRound.state) - , PlayingMessage <| Playing.LocalMessage <| Playing.FinishRound finishedRound - ] - - Event.GameStart czar call -> - [ UpdateLobby (\lobby -> { lobby | game = Game.Playing (Round czar call (Round.playing 0 False)) }) ] - - Event.GameEnd -> - [ UpdateLobby (\lobby -> { lobby | game = Game.Finished }) - , UpdateHand { hand = [] } - ] - - Event.ConfigChange config -> - [ UpdateLobby (\lobby -> { lobby | config = config }) ] - - Event.RoundTimeLimitHit -> - [ updateRound (\round -> Round.setAfterTimeLimit round True) ] - - -lobbyRound : Game.Lobby -> Maybe Round -lobbyRound lobby = - case lobby.game of - Game.Playing round -> - Just round - - _ -> - Nothing - - -updatePlayer : Player.Id -> (Player -> Player) -> Message -updatePlayer playerId playerUpdate = - UpdateLobby - (\lobby -> - { lobby - | players = - List.map - (\player -> - if player.id == playerId then - playerUpdate player - else - player - ) - lobby.players - } - ) - - -updateRound : (Round -> Round) -> Message -updateRound roundUpdate = - UpdateLobby - (\lobby -> - let - game = - case lobby.game of - Game.Playing round -> - Game.Playing (roundUpdate round) - - other -> - other - in - { lobby | game = game } - ) - - -updateRoundState : (Round.State -> Round.State) -> Message -updateRoundState roundStateUpdate = - updateRound (\round -> { round | state = roundStateUpdate round.state }) - - -overlayAlert : BrowserNotifications.ConsumerMessage -> ConsumerMessage -overlayAlert message = - case message of - BrowserNotifications.PermissionChanged permission -> - case permission of - BrowserNotifications.Denied -> - (Overlay.Show - (Overlay "times-circle" - "Can't enable desktop notifications." - [ Html.text "You did not give Massive Decks permission to give you desktop notifications." ] - ) - ) - |> OverlayMessage - - _ -> - NoOp |> LocalMessage - - -icon : Model -> String -> Maybe String -icon model name = - Just (model.init.url ++ "assets/images/icons/" ++ name ++ ".png") - - -type alias Update = - Model -> ( Model, Cmd ConsumerMessage ) - - -updateLobbyAndHand : Game.LobbyAndHand -> Update -updateLobbyAndHand lobbyAndHand model = - { model - | lobby = lobbyAndHand.lobby - , hand = lobbyAndHand.hand - } - ! [ Util.cmd (Playing.LobbyAndHandUpdated |> Playing.LocalMessage |> PlayingMessage |> LocalMessage) ] - - -{-| Handles a change to the displayed notification. --} -notificationChange : Model -> Maybe Notification -> ( Model, Cmd ConsumerMessage ) -notificationChange model notification = - let - newNotification = - notification |> Util.or model.notification - - cmd = - case newNotification of - Just nn -> - let - dismiss = - Util.after (Time.second * 5) (Task.succeed nn) - in - Task.perform (LocalMessage << DismissNotification) dismiss - - Nothing -> - Cmd.none - in - ( { model | notification = newNotification }, cmd ) diff --git a/client/src/MassiveDecks/Scenes/Lobby/Messages.elm b/client/src/MassiveDecks/Scenes/Lobby/Messages.elm deleted file mode 100644 index 386e844..0000000 --- a/client/src/MassiveDecks/Scenes/Lobby/Messages.elm +++ /dev/null @@ -1,43 +0,0 @@ -module MassiveDecks.Scenes.Lobby.Messages exposing (..) - -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Models.Notification as Notification exposing (Notification) -import MassiveDecks.Scenes.Lobby.Sidebar as Sidebar -import MassiveDecks.Scenes.Config.Messages as Config -import MassiveDecks.Scenes.Playing.Messages as Playing -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.Overlay as Overlay -import MassiveDecks.Components.BrowserNotifications as BrowserNotifications -import MassiveDecks.Components.TTS as TTS - - -{-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. --} -type ConsumerMessage - = ErrorMessage Errors.Message - | OverlayMessage (Overlay.Message Message) - | Leave - | LocalMessage Message - - -{-| The messages used in the start screen. --} -type Message - = DismissNotification Notification - | SetNotification (List Player -> Maybe Notification) - | UpdateLobbyAndHand Game.LobbyAndHand - | UpdateLobby (Game.Lobby -> Game.Lobby) - | UpdateHand Card.Hand - | Identify - | DisplayInviteOverlay - | BrowserNotificationForUser (Game.Lobby -> Maybe Player.Id) String String - | RenderQr - | Batch (List Message) - | NoOp - | BrowserNotificationsMessage BrowserNotifications.Message - | ConfigMessage Config.ConsumerMessage - | PlayingMessage Playing.ConsumerMessage - | SidebarMessage Sidebar.Message - | TTSMessage TTS.Message diff --git a/client/src/MassiveDecks/Scenes/Lobby/Models.elm b/client/src/MassiveDecks/Scenes/Lobby/Models.elm deleted file mode 100644 index 5c4303a..0000000 --- a/client/src/MassiveDecks/Scenes/Lobby/Models.elm +++ /dev/null @@ -1,29 +0,0 @@ -module MassiveDecks.Scenes.Lobby.Models exposing (Model) - -import MassiveDecks.Models exposing (Init) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Player as Player -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Notification as Notification exposing (Notification) -import MassiveDecks.Scenes.Lobby.Sidebar as Sidebar -import MassiveDecks.Scenes.Config.Models as Config -import MassiveDecks.Scenes.Playing.Models as Playing -import MassiveDecks.Components.BrowserNotifications as BrowserNotifications -import MassiveDecks.Components.TTS as TTS - - -{-| The state of the lobby. --} -type alias Model = - { lobby : Game.Lobby - , hand : Card.Hand - , config : Config.Model - , playing : Playing.Model - , browserNotifications : BrowserNotifications.Model - , secret : Player.Secret - , init : Init - , notification : Maybe Notification - , qrNeedsRendering : Bool - , sidebar : Sidebar.Model - , tts : TTS.Model - } diff --git a/client/src/MassiveDecks/Scenes/Lobby/Sidebar.elm b/client/src/MassiveDecks/Scenes/Lobby/Sidebar.elm deleted file mode 100644 index c517f4e..0000000 --- a/client/src/MassiveDecks/Scenes/Lobby/Sidebar.elm +++ /dev/null @@ -1,50 +0,0 @@ -port module MassiveDecks.Scenes.Lobby.Sidebar exposing (Model, Message(..), init, update) - -import Task -import Window -import MassiveDecks.Util as Util - - -{-| This component handles the players sidebar for the lobby view. On a larger display, this bar is shown, but can be -hidden by the user. On small displays, it is hidden but can be popped out as a modal window over the top of the -lobby. - -No rendering is done within this module - instead the model should be inspected and the sidebar rendered as required. --} -type alias Model = - { enhanceWidth : Int - , hidden : Bool - , shownAsOverlay : Bool - } - - -type Message - = Toggle - | Show Int - | Hide - - -init : Int -> Model -init enhanceWidth = - { enhanceWidth = enhanceWidth - , hidden = False - , shownAsOverlay = False - } - - -update : Message -> Model -> ( Model, Cmd Message ) -update message model = - case message of - Toggle -> - ( model, Task.perform Show Window.width ) - - Show screenWidth -> - if model.shownAsOverlay then - ( { model | shownAsOverlay = False }, Cmd.none ) - else if screenWidth > model.enhanceWidth then - ( { model | hidden = not model.hidden }, Cmd.none ) - else - ( { model | shownAsOverlay = True }, Cmd.none ) - - Hide -> - ( { model | shownAsOverlay = False }, Cmd.none ) diff --git a/client/src/MassiveDecks/Scenes/Lobby/UI.elm b/client/src/MassiveDecks/Scenes/Lobby/UI.elm deleted file mode 100644 index 28fe1e1..0000000 --- a/client/src/MassiveDecks/Scenes/Lobby/UI.elm +++ /dev/null @@ -1,431 +0,0 @@ -module MassiveDecks.Scenes.Lobby.UI exposing (view, inviteOverlay) - -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import MassiveDecks.Components.About as About -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.QR as QR -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Components.Overlay as Overlay -import MassiveDecks.Components.BrowserNotifications as BrowserNotifications -import MassiveDecks.Components.TTS as TTS -import MassiveDecks.Scenes.Config as Config -import MassiveDecks.Scenes.Playing as Playing -import MassiveDecks.Scenes.Lobby.Models exposing (Model) -import MassiveDecks.Scenes.Lobby.Messages exposing (ConsumerMessage(..), Message(..)) -import MassiveDecks.Scenes.Lobby.Sidebar as Sidebar -import MassiveDecks.Models.Player as Player exposing (Player, Status(..)) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Notification as Notification exposing (Notification) -import MassiveDecks.Util as Util - - -view : Model -> Html ConsumerMessage -view model = - let - lobby = - model.lobby - - url = - model.init.url - - gameCode = - lobby.gameCode - - players = - lobby.players - - ( header, contents ) = - case lobby.game of - Game.Configuring -> - ( [], [ Config.view model |> Html.map (ConfigMessage >> LocalMessage) ] ) - - Game.Playing round -> - let - ( h, c ) = - Playing.view model round - in - ( h |> List.map (Html.map (PlayingMessage >> LocalMessage)), c |> List.map (Html.map (PlayingMessage >> LocalMessage)) ) - - Game.Finished -> - ( [], [] ) - in - root model.sidebar.hidden - [ appHeader header model - , spacer - , scores model.secret.id model.lobby.owner model.sidebar.shownAsOverlay players - , contentWrapper contents - ] - - -root : Bool -> List (Html msg) -> Html msg -root hideScores contents = - div [ classList [ ( "content", True ), ( "hide-scores", hideScores ) ] ] contents - - -contentWrapper : List (Html msg) -> Html msg -contentWrapper contents = - div [ id "content-wrapper" ] contents - - -spacer : Html msg -spacer = - div [ class "mui--appbar-height" ] [] - - -scores : Player.Id -> Player.Id -> Bool -> List Player -> Html ConsumerMessage -scores client owner shownAsOverlay players = - let - hideMessage = - LocalMessage (SidebarMessage Sidebar.Hide) - - closeLink = - if shownAsOverlay then - [ a - [ class "link close-link" - , title "Hide." - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick hideMessage - ] - [ Icon.icon "times" ] - ] - else - [] - - sidebar = - div [ id "scores", classList [ ( "shownAsOverlay", shownAsOverlay ), ( "mui--z1", True ) ] ] - [ div [ id "scores-title", class "mui--appbar-line-height mui--text-headline" ] ([ text "Players" ] ++ closeLink) - , div [ class "mui-divider" ] [] - , table [ class "mui-table" ] - [ thead [] - [ tr [] - [ th [ class "state", title "State" ] [ Icon.icon "tasks" ] - , th [ class "name" ] [ text "Player" ] - , th [ class "score", title "Score" ] [ Icon.icon "trophy" ] - ] - ] - , tbody [] (List.map (score client owner) players) - ] - ] - in - if shownAsOverlay then - div - [ id "mui-overlay" - , Util.onClickIfId "mui-overlay" hideMessage (LocalMessage NoOp) - , Util.onKeyDown "Escape" hideMessage (LocalMessage NoOp) - , tabindex 0 - ] - [ sidebar ] - else - sidebar - - -score : Player.Id -> Player.Id -> Player -> Html msg -score client owner player = - let - classes = - classList - [ ( Player.statusName player.status, True ) - , ( "disconnected", player.disconnected ) - , ( "left", player.left ) - , ( "you", player.id == client ) - ] - - prename = - List.concat - [ if player.id == owner then - [ Icon.icon "star", text " " ] - else - [] - , if player.id == client then - [ Icon.icon "user-circle", text " " ] - else - [] - , if player.disconnected then - [ Icon.icon "minus-circle", text " " ] - else - [] - ] - - afterNameTitle = - (if player.id == owner then - " (Owner)" - else - "" - ) - ++ (if player.id == client then - " (You)" - else - "" - ) - ++ (if player.disconnected then - " (Disconnected)" - else - "" - ) - in - tr [ classes ] - [ td [ class "state", title (statusDescription player) ] [ (playerIcon player) ] - , td [ class "name", title (player.name ++ afterNameTitle) ] (List.append prename [ text player.name ]) - , td [ class "score" ] [ text (toString player.score) ] - ] - - -appHeader : List (Html ConsumerMessage) -> Model -> Html ConsumerMessage -appHeader contents model = - (header [] - [ div [ class "mui-appbar mui--z1 mui--appbar-line-height" ] - [ div [ class "mui--appbar-line-height" ] - [ span [ class "score-buttons" ] ([ scoresButton ] ++ (notificationPopup model.notification)) - , span [ id "title", class "mui--text-title mui--visible-xs-inline-block" ] contents - , gameMenu model - ] - ] - ] - ) - - -scoresButton : Html ConsumerMessage -scoresButton = - button - [ class ("scores-toggle mui-btn mui-btn--small mui-btn--primary badged") - , title "Players." - , onClick (LocalMessage (SidebarMessage Sidebar.Toggle)) - ] - [ Icon.fwIcon "users" ] - - -notificationPopup : Maybe Notification -> List (Html ConsumerMessage) -notificationPopup notification = - case notification of - Just notification -> - let - hidden = - if notification.visible then - "" - else - "hide" - in - [ div - [ class ("badge mui--z2 " ++ hidden) - , title notification.description - , onClick (LocalMessage (DismissNotification notification)) - ] - [ Icon.icon notification.icon, text (" " ++ notification.name) ] - ] - - Nothing -> - [ div [ class ("badge mui--z2 hide") ] [] ] - - -statusDescription : Player -> String -statusDescription player = - case player.status of - NotPlayed -> - "Choosing" - - Played -> - "Played" - - Czar -> - "Round Czar" - - Ai -> - "A Computer" - - Neutral -> - "" - - Skipping -> - "Being Skipped" - - -statusIcon : Status -> Html msg -statusIcon status = - Maybe.map Icon.fwIcon (statusIconName status) |> Maybe.withDefault (text "") - - -statusIconName : Status -> Maybe String -statusIconName status = - case status of - NotPlayed -> - Just "hourglass" - - Played -> - Just "check" - - Czar -> - Just "gavel" - - Ai -> - Just "cogs" - - Neutral -> - Nothing - - Skipping -> - Just "fast-forward" - - -playerIcon : Player -> Html msg -playerIcon player = - if player.left then - Icon.icon "sign-out" - else - statusIcon player.status - - -{-| The overlay for inviting players to a lobby. --} -inviteOverlay : String -> String -> Overlay.Message msg -inviteOverlay appUrl gameCode = - let - url = - Util.lobbyUrl appUrl gameCode - - contents = - [ p [] [ text "To invite other players, simply send them this link: " ] - , p [] [ a [ href url ] [ text url ] ] - , p [] [ text "Have them scan this QR code: " ] - , QR.view "invite-qr-code" - , p [] [ text "Or give them this game code to enter on the start page: " ] - , p [] [ input [ readonly True, value gameCode ] [] ] - ] - in - Overlay.Show - { icon = "bullhorn" - , title = "Invite Players" - , contents = contents - } - - -notificationsMenuItem : BrowserNotifications.Model -> List (Html ConsumerMessage) -notificationsMenuItem model = - let - ( notClickable, enabled ) = - if not model.supported then - ( Just "Your browser does not support desktop notifications.", False ) - else if model.permission == Just BrowserNotifications.Denied then - ( Just "You have denied Massive Decks permission to display desktop notifications.", False ) - else - ( Nothing, model.enabled ) - - classes = - classList - [ ( "link", True ) - , ( "disabled", not (Util.isNothing notClickable) ) - ] - - extraAttrs = - case notClickable of - Nothing -> - [ onClick - (LocalMessage <| - BrowserNotificationsMessage <| - (if enabled then - BrowserNotifications.disable - else - BrowserNotifications.enable - ) - ) - ] - - Just msg -> - [ title msg ] - - attributes = - [ classes, attribute "tabindex" "0", attribute "role" "button" ] ++ extraAttrs - - description = - " " - ++ (if enabled then - "Disable" - else - "Enable" - ) - ++ " Notifications" - in - [ li [] - [ a attributes - [ Icon.fwIcon - (if enabled then - "bell-slash" - else - "bell" - ) - , text description - ] - ] - ] - - -{-| The menu for the game. --} -gameMenu : Model -> Html ConsumerMessage -gameMenu model = - let - url = - (Errors.reportUrl { url = model.init.url, version = model.init.version } - "I was [a short explanation of what you were doing] when [a short explanation of the bug]." - ) - in - div [ class "menu mui-dropdown" ] - [ button - [ class "mui-btn mui-btn--small mui-btn--primary" - , attribute "data-mui-toggle" "dropdown" - , title "Game menu." - ] - [ Icon.fwIcon "ellipsis-h" ] - , ul [ class "mui-dropdown__menu mui-dropdown__menu--right" ] - ([ li [] - [ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (DisplayInviteOverlay |> LocalMessage) - ] - [ Icon.fwIcon "bullhorn", text " Invite Players" ] - ] - ] - ++ (notificationsMenuItem model.browserNotifications) - ++ [ li [] - [ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (TTS.Enabled (not model.tts.enabled) |> TTSMessage |> LocalMessage) - ] - (if model.tts.enabled then - [ Icon.fwIcon "volume-off", text " Disable Speech" ] - else - [ Icon.fwIcon "volume-up", text " Enable Speech" ] - ) - ] - , li [] - [ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick Leave - ] - [ Icon.fwIcon "sign-out", text " Leave Game" ] - ] - , li [ class "mui-divider" ] [] - , li [] - [ a - [ class "link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (About.show model.init.version |> OverlayMessage) - ] - [ Icon.fwIcon "info-circle", text " About" ] - ] - , li [] - [ a [ href url, target "_blank", rel "noopener" ] - [ Icon.fwIcon "bug", text " Report a bug" ] - ] - ] - ) - ] diff --git a/client/src/MassiveDecks/Scenes/Playing.elm b/client/src/MassiveDecks/Scenes/Playing.elm deleted file mode 100644 index 112a046..0000000 --- a/client/src/MassiveDecks/Scenes/Playing.elm +++ /dev/null @@ -1,341 +0,0 @@ -module MassiveDecks.Scenes.Playing exposing (update, view, init, subscriptions) - -import Random -import String -import Html exposing (..) -import AnimationFrame -import MassiveDecks.API as API -import MassiveDecks.API.Request as Request -import MassiveDecks.Models exposing (Init) -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.TTS as TTS -import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Scenes.Lobby.Models as Lobby -import MassiveDecks.Scenes.History as History -import MassiveDecks.Scenes.History.Messages as History -import MassiveDecks.Scenes.Playing.UI as UI -import MassiveDecks.Scenes.Playing.UI.Cards as CardsUI -import MassiveDecks.Scenes.Playing.Models exposing (Model, ShownPlayedCards, ShownCard) -import MassiveDecks.Scenes.Playing.Messages exposing (ConsumerMessage(..), Message(..)) -import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule -import MassiveDecks.Scenes.Playing.HouseRule.Reboot as Reboot -import MassiveDecks.Util as Util - - -{-| Create the initial model for the playing scene. --} -init : Init -> Model -init init = - { picked = [] - , considering = Nothing - , finishedRound = Nothing - , shownPlayed = { animated = [], toAnimate = [] } - , seed = Random.initialSeed (hack init.seed) - , history = Nothing - } - - -houseRule : HouseRule.Id -> HouseRule -houseRule id = - case id of - HouseRule.Reboot -> - Reboot.rule - - -{-| We shouldn't need to do this! -int flags blow up at the moment. For now, we pass a string, but we should take an int from JS in the future. --} -hack : String -> Int -hack seed = - String.toInt seed |> Result.withDefault 0 - - -{-| Subscriptions for the playing scene. --} -subscriptions : Model -> Sub ConsumerMessage -subscriptions model = - if List.isEmpty model.shownPlayed.toAnimate then - Sub.none - else - AnimationFrame.diffs (\_ -> LocalMessage AnimatePlayedCards) - - -{-| Render the playing scene. --} -view : Lobby.Model -> Round -> ( List (Html ConsumerMessage), List (Html ConsumerMessage) ) -view model round = - let - ( header, content ) = - UI.view model round - in - ( header |> List.map (Html.map LocalMessage), content |> List.map (Html.map LocalMessage) ) - - -{-| Handles messages and alters the model as appropriate. --} -update : Message -> Lobby.Model -> Round -> ( Model, Cmd ConsumerMessage ) -update message lobbyModel round = - let - model = - lobbyModel.playing - - lobby = - lobbyModel.lobby - - secret = - lobbyModel.secret - - gameCode = - lobby.gameCode - in - case message of - Pick cardId -> - let - slots = - Card.slots round.call - - canPlay = - (List.length model.picked) < slots - - playing = - case round.state of - Round.P _ -> - True - - _ -> - False - in - if playing && canPlay then - ( { model | picked = model.picked ++ [ cardId ] }, Cmd.none ) - else - ( model, Cmd.none ) - - Withdraw cardId -> - ( { model | picked = List.filter ((/=) cardId) model.picked }, Cmd.none ) - - Play -> - ( { model | picked = [] } - , Request.send (API.play gameCode secret model.picked) playErrorHandler ErrorMessage HandUpdate - ) - - Consider potentialWinnerIndex -> - let - speech = - (case round.state of - Round.J judging -> - Util.get judging.responses potentialWinnerIndex |> Maybe.map (\fill -> ( round, fill )) - - _ -> - Nothing - ) - |> Maybe.map (\( round, callFill ) -> TTS.Say (CardsUI.callText round.call callFill) |> TTSMessage |> Util.cmd) - |> Maybe.withDefault Cmd.none - in - ( { model | considering = Just potentialWinnerIndex } - , speech - ) - - Choose winnerIndex -> - ( { model | considering = Nothing } - , Request.send (API.choose gameCode secret winnerIndex) chooseErrorHandler ErrorMessage ignore - ) - - NextRound -> - ( { model - | considering = Nothing - , finishedRound = Nothing - } - , Cmd.none - ) - - AnimatePlayedCards -> - let - ( shownPlayed, seed ) = - updatePositioning model.shownPlayed model.seed - in - ( { model - | seed = seed - , shownPlayed = shownPlayed - } - , Cmd.none - ) - - Skip playerIds -> - ( model, Request.send (API.skip gameCode secret playerIds) skipErrorHandler ErrorMessage ignore ) - - Back -> - ( model, Request.send_ (API.back gameCode secret) ErrorMessage ignore ) - - LobbyAndHandUpdated -> - lobbyAndHandUpdated lobbyModel round - - Redraw -> - ( model, Request.send (API.redraw lobbyModel.lobby.gameCode lobbyModel.secret) redrawErrorHandler ErrorMessage HandUpdate ) - - FinishRound finishedRound -> - let - cards = - finishedRound.state.responses - - winning = - Card.winningCards cards finishedRound.state.playedByAndWinner |> Maybe.withDefault [] - - speech = - Card.filled finishedRound.call winning - in - ( { model | finishedRound = Just finishedRound }, TTS.Say speech |> TTSMessage |> Util.cmd ) - - HistoryMessage historyMessage -> - case model.history of - Just history -> - case historyMessage of - History.ErrorMessage errorMessage -> - ( model, ErrorMessage errorMessage |> Util.cmd ) - - History.Close -> - ( { model | history = Nothing }, Cmd.none ) - - History.LocalMessage localMessage -> - let - ( newHistory, cmd ) = - History.update localMessage history - in - ( { model | history = Just newHistory }, Cmd.map (LocalMessage << HistoryMessage) cmd ) - - Nothing -> - ( model, Cmd.none ) - - ViewHistory -> - let - ( historyModel, command ) = - History.init lobbyModel.lobby.gameCode - in - ( { model | history = Just historyModel }, Cmd.map (LocalMessage << HistoryMessage) command ) - - NoOp -> - ( model, Cmd.none ) - - -ignore : () -> ConsumerMessage -ignore = - (\_ -> LocalMessage NoOp) - - -lobbyAndHandUpdated : Lobby.Model -> Round -> ( Model, Cmd ConsumerMessage ) -lobbyAndHandUpdated lobbyModel round = - let - lobby = - lobbyModel.lobby - - model = - lobbyModel.playing - - shownPlayed = - model.shownPlayed - - playedCards = - case round.state of - Round.P playing -> - Just playing.numberPlayed - - _ -> - Nothing - - ( newShownPlayed, seed ) = - case playedCards of - Just amount -> - let - toShow = - amount * (Card.slots round.call) - - existing = - (List.length shownPlayed.animated) + (List.length shownPlayed.toAnimate) - - ( new, seed ) = - addShownPlayed (toShow - existing) model.seed - in - ( ShownPlayedCards shownPlayed.animated (shownPlayed.toAnimate ++ new), seed ) - - Nothing -> - ( ShownPlayedCards [] [], model.seed ) - - newModel = - { model - | shownPlayed = newShownPlayed - , seed = seed - } - in - ( newModel, Cmd.none ) - - -redrawErrorHandler : API.RedrawError -> ConsumerMessage -redrawErrorHandler error = - case error of - API.NotEnoughPoints -> - ErrorMessage <| Errors.New "You do not have enough points to redraw your hand." False - - -chooseErrorHandler : API.ChooseError -> ConsumerMessage -chooseErrorHandler error = - case error of - API.NotCzar -> - ErrorMessage <| Errors.New "You can't pick a winner as you are not the card czar this round." False - - -playErrorHandler : API.PlayError -> ConsumerMessage -playErrorHandler error = - case error of - API.NotInRound -> - ErrorMessage <| Errors.New "You can't play as you are not in this round." False - - API.AlreadyPlayed -> - ErrorMessage <| Errors.New "You can't play as you have already played in this round." False - - API.AlreadyJudging -> - ErrorMessage <| Errors.New "You can't play as the round is already in it's judging phase." False - - API.WrongNumberOfCards got expected -> - ErrorMessage <| Errors.New ("You played the wrong number of cards - you played " ++ (toString got) ++ " cards, but the call needs " ++ (toString expected) ++ "cards.") False - - -skipErrorHandler : API.SkipError -> ConsumerMessage -skipErrorHandler error = - case error of - API.NotEnoughPlayersToSkip required -> - ErrorMessage <| Errors.New ("There are not enough players in the game to skip (must have at least " ++ (toString required) ++ ").") False - - API.PlayersNotSkippable -> - ErrorMessage <| Errors.New "The players can't be skipped as they are not inactive." False - - -addShownPlayed : Int -> Random.Seed -> ( List ShownCard, Random.Seed ) -addShownPlayed new seed = - Random.step (Random.list new initialRandomPositioning) seed - - -updatePositioning : ShownPlayedCards -> Random.Seed -> ( ShownPlayedCards, Random.Seed ) -updatePositioning shownPlayed seed = - let - ( newAnimated, newSeed ) = - Random.step (Random.list (List.length shownPlayed.toAnimate) randomPositioning) seed - in - ( ShownPlayedCards (shownPlayed.animated ++ newAnimated) [], newSeed ) - - -initialRandomPositioning : Random.Generator ShownCard -initialRandomPositioning = - Random.map3 (\r h l -> ShownCard r h l -100) - (Random.int -75 75) - (Random.int 0 50) - Random.bool - - -randomPositioning : Random.Generator ShownCard -randomPositioning = - Random.map4 ShownCard - (Random.int -90 90) - (Random.int 25 50) - Random.bool - (Random.int -5 1) diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm deleted file mode 100644 index 05f2d82..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm +++ /dev/null @@ -1,23 +0,0 @@ -module MassiveDecks.Scenes.Playing.HouseRule exposing (HouseRule, Action) - -import MassiveDecks.Scenes.Playing.HouseRule.Id exposing (Id) -import MassiveDecks.Scenes.Playing.Messages as Playing -import MassiveDecks.Scenes.Lobby.Models as Lobby - - -type alias HouseRule = - { id : Id - , icon : String - , name : String - , description : String - , actions : List Action - } - - -type alias Action = - { icon : String - , text : String - , description : String - , onClick : Playing.Message - , enabled : Lobby.Model -> Bool - } diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm deleted file mode 100644 index 11a1b66..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm +++ /dev/null @@ -1,9 +0,0 @@ -module MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) - -import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) -import MassiveDecks.Scenes.Playing.HouseRule.Reboot as Reboot - - -houseRules : List HouseRule -houseRules = - [ Reboot.rule ] diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm deleted file mode 100644 index 2bc4312..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm +++ /dev/null @@ -1,12 +0,0 @@ -module MassiveDecks.Scenes.Playing.HouseRule.Id exposing (Id(..), toString) - - -type Id - = Reboot - - -toString : Id -> String -toString id = - case id of - Reboot -> - "reboot" diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm deleted file mode 100644 index b431da4..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm +++ /dev/null @@ -1,34 +0,0 @@ -module MassiveDecks.Scenes.Playing.HouseRule.Reboot exposing (rule) - -import MassiveDecks.Models.Player as Player -import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) -import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule -import MassiveDecks.Scenes.Playing.Messages as Playing -import MassiveDecks.Scenes.Lobby.Models as Lobby - - -rule : HouseRule -rule = - { id = HouseRule.Reboot - , icon = "recycle" - , name = "Rebooting the Universe" - , description = "At any time, players may trade in a point to discard their hand and redraw." - , actions = [ rebootAction ] - } - - -rebootAction : HouseRule.Action -rebootAction = - { icon = "recycle" - , text = "Redraw" - , description = "Lose one point to discard your hand and draw a new one." - , onClick = Playing.Redraw - , enabled = checkEnabled - } - - -checkEnabled : Lobby.Model -> Bool -checkEnabled lobbyModel = - Player.byId lobbyModel.secret.id lobbyModel.lobby.players - |> Maybe.map (\player -> player.score > 0) - |> Maybe.withDefault False diff --git a/client/src/MassiveDecks/Scenes/Playing/Messages.elm b/client/src/MassiveDecks/Scenes/Playing/Messages.elm deleted file mode 100644 index 4d36f12..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/Messages.elm +++ /dev/null @@ -1,37 +0,0 @@ -module MassiveDecks.Scenes.Playing.Messages exposing (..) - -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.TTS as TTS -import MassiveDecks.Scenes.History.Messages as History -import MassiveDecks.Models.Player as Player -import MassiveDecks.Models.Game.Round as Round -import MassiveDecks.Models.Card as Card - - -{-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. --} -type ConsumerMessage - = HandUpdate Card.Hand - | TTSMessage TTS.Message - | ErrorMessage Errors.Message - | LocalMessage Message - - -{-| The messages used in the start screen. --} -type Message - = LobbyAndHandUpdated - | Pick String - | Withdraw String - | Play - | Consider Int - | Choose Int - | NextRound - | AnimatePlayedCards - | Skip (List Player.Id) - | Back - | Redraw - | FinishRound Round.FinishedRound - | ViewHistory - | HistoryMessage History.ConsumerMessage - | NoOp diff --git a/client/src/MassiveDecks/Scenes/Playing/Models.elm b/client/src/MassiveDecks/Scenes/Playing/Models.elm deleted file mode 100644 index 3e05f05..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/Models.elm +++ /dev/null @@ -1,31 +0,0 @@ -module MassiveDecks.Scenes.Playing.Models exposing (Model, ShownPlayedCards, ShownCard) - -import Random -import MassiveDecks.Models.Game.Round as Round -import MassiveDecks.Scenes.History.Models as History - - -{-| The state of the lobby. --} -type alias Model = - { picked : List String - , considering : Maybe Int - , finishedRound : Maybe Round.FinishedRound - , shownPlayed : ShownPlayedCards - , seed : Random.Seed - , history : Maybe History.Model - } - - -type alias ShownPlayedCards = - { animated : List ShownCard - , toAnimate : List ShownCard - } - - -type alias ShownCard = - { rotation : Int - , horizontalPos : Int - , isLeft : Bool - , verticalPos : Int - } diff --git a/client/src/MassiveDecks/Scenes/Playing/UI.elm b/client/src/MassiveDecks/Scenes/Playing/UI.elm deleted file mode 100644 index b49979d..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/UI.elm +++ /dev/null @@ -1,614 +0,0 @@ -module MassiveDecks.Scenes.Playing.UI exposing (view) - -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import Html.Keyed as Keyed -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Game.Round as Round exposing (Round) -import MassiveDecks.Models.Player as Player exposing (Player) -import MassiveDecks.Models.Card as Card -import MassiveDecks.Scenes.History as History -import MassiveDecks.Scenes.Lobby.Models as Lobby -import MassiveDecks.Scenes.Playing.UI.Cards as CardsUI -import MassiveDecks.Scenes.Playing.Models exposing (ShownCard) -import MassiveDecks.Scenes.Playing.Messages exposing (Message(..)) -import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) -import MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) -import MassiveDecks.Util as Util - - -view : Lobby.Model -> Round -> ( List (Html Message), List (Html Message) ) -view lobbyModel round = - let - model = - lobbyModel.playing - - lobby = - lobbyModel.lobby - - ( header, content ) = - case model.finishedRound of - Just round -> - winnerHeaderAndContents round lobby.players - - Nothing -> - ( [ Icon.icon "gavel", text (" " ++ (czarName lobby.players round.czar)) ], roundContents lobbyModel round ) - - timedOut = - Round.afterTimeLimit round.state - - judging = - case round.state of - Round.J _ -> - True - - _ -> - False - in - case model.history of - Nothing -> - ( header - , List.concat - [ infoBar lobby lobbyModel.secret - , content - , [ warningDrawer - (List.concat - [ skippingNotice lobby.players lobbyModel.secret.id - , timeoutNotice lobbyModel.secret.id lobby.players judging timedOut - , disconnectedNotice lobby.players - ] - ) - ] - ] - ) - - Just history -> - ( [], [ History.view history lobbyModel.lobby.players |> Html.map HistoryMessage ] ) - - -gameMenu : Lobby.Model -> Html Message -gameMenu lobbyModel = - let - enabled = - List.filter (\rule -> List.member rule.id lobbyModel.lobby.config.houseRules) houseRules - in - div [ class "action-menu mui-dropdown" ] - [ button - [ class "mui-btn mui-btn--small mui-btn--fab" - , title "Game actions." - , attribute "data-mui-toggle" "dropdown" - ] - [ Icon.icon "bars" ] - , ul [ class "mui-dropdown__menu mui-dropdown__menu--right" ] - ([ li [] - [ a - [ classList [ ( "link", True ) ] - , title "View previous rounds from the game." - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick ViewHistory - ] - [ Icon.fwIcon "history", text " ", text "Game History" ] - ] - ] - ++ (List.concatMap (gameMenuItems lobbyModel) enabled) - ) - ] - - -gameMenuItems : Lobby.Model -> HouseRule -> List (Html Message) -gameMenuItems lobbyModel rule = - List.map (gameMenuItem lobbyModel rule) rule.actions - - -gameMenuItem : Lobby.Model -> HouseRule -> HouseRule.Action -> Html Message -gameMenuItem lobbyModel rule action = - let - enabled = - action.enabled lobbyModel - - message = - if enabled then - action.onClick - else - NoOp - in - li [] - [ a - [ classList [ ( "link", True ), ( "disabled", not enabled ) ] - , title action.description - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick message - ] - [ Icon.fwIcon action.icon, text " ", text action.text ] - ] - - -roundContents : Lobby.Model -> Round -> List (Html Message) -roundContents lobbyModel round = - let - lobby = - lobbyModel.lobby - - hand = - lobbyModel.hand.hand - - model = - lobbyModel.playing - - picked = - getAllById model.picked hand - - id = - lobbyModel.secret.id - - isCzar = - round.czar == id - - canPlay = - List.filter (\player -> player.id == id) lobby.players - |> List.all (\player -> player.status == Player.NotPlayed) - - callFill = - case round.state of - Round.P _ -> - picked - - Round.J judging -> - Maybe.withDefault [] (model.considering |> Maybe.andThen (Util.get judging.responses)) - - Round.F _ -> - [] - - pickedOrChosen = - case round.state of - Round.P _ -> - pickedView picked (Card.slots round.call) (model.shownPlayed.animated ++ model.shownPlayed.toAnimate) - - Round.J judging -> - case model.considering of - Just considering -> - case Util.get judging.responses considering of - Just consideringCards -> - [ consideringView considering consideringCards isCzar ] - - Nothing -> - [] - - Nothing -> - [] - - Round.F _ -> - [] - - playedOrHand = - case round.state of - Round.P _ -> - handView model.picked (not canPlay) hand - - Round.J judging -> - playedView isCzar judging.responses - - Round.F _ -> - div [] [] - in - [ playArea - ([ div [ class "round-area" ] (List.concat [ [ CardsUI.call round.call callFill ], pickedOrChosen ]) - , playedOrHand - , gameMenu lobbyModel - ] - ) - ] - - -getAllById : List String -> List Card.Response -> List Card.Response -getAllById ids cards = - List.filterMap (getById cards) ids - - -getById : List Card.Response -> String -> Maybe Card.Response -getById cards id = - List.filter (\card -> card.id == id) cards |> List.head - - -consideringView : Int -> List Card.Response -> Bool -> Html Message -consideringView considering consideringCards isCzar = - let - extra = - if isCzar then - [ ( "!!button", chooseButton considering ) ] - else - [] - in - div [] ([ Keyed.ol [ class "considering" ] ((List.map (\card -> ( card.id, li [] [ (playedResponse card) ] )) consideringCards) ++ extra) ]) - - -winnerHeaderAndContents : Round.FinishedRound -> List Player -> ( List (Html Message), List (Html Message) ) -winnerHeaderAndContents round players = - let - cards = - round.state.responses - - winning = - Card.winningCards cards round.state.playedByAndWinner |> Maybe.withDefault [] - - winner = - Maybe.map .name (Util.get players round.state.playedByAndWinner.winner) |> Maybe.withDefault "" - in - ( [ Icon.icon "trophy", text (" " ++ winner) ] - , [ div [ class "winner mui-panel" ] - [ h1 [] [ Icon.icon "trophy" ] - , h2 [] [ text (" " ++ Card.filled round.call winning) ] - , h3 [] [ text ("- " ++ winner) ] - ] - , button [ id "next-round-button", class "mui-btn mui-btn--primary mui-btn--raised", onClick NextRound ] - [ text "Next Round" ] - ] - ) - - -czarName : List Player -> Player.Id -> String -czarName players czarId = - (List.filter (\player -> player.id == czarId) players) |> List.head |> Maybe.map .name |> Maybe.withDefault "" - - -playArea : List (Html Message) -> Html Message -playArea contents = - div [ class "play-area" ] contents - - -response : List String -> Bool -> Card.Response -> ( String, Html Message ) -response picked disabled response = - let - isPicked = - List.member response.id picked - - clickHandler = - if isPicked || disabled then - [] - else - [ onClick (Pick response.id) ] - in - ( response.id, CardsUI.response isPicked clickHandler response ) - - -blankResponse : ShownCard -> Html Message -blankResponse shownCard = - div [ class "card mui-panel", positioning shownCard ] [] - - -positioning : ShownCard -> Html.Attribute msg -positioning shownCard = - let - horizontalDirection = - if shownCard.isLeft then - "left" - else - "right" - in - style - [ ( "transform", "rotate(" ++ (toString shownCard.rotation) ++ "deg)" ) - , ( horizontalDirection, (toString shownCard.horizontalPos) ++ "%" ) - , ( "top", (toString shownCard.verticalPos) ++ "%" ) - ] - - -handRender : Bool -> List ( String, Html Message ) -> Html Message -handRender disabled contents = - let - classes = - "hand mui--divider-top" - ++ if disabled then - " disabled" - else - "" - in - Keyed.ul [ class classes ] (List.map (\( key, item ) -> ( key, li [] [ item ] )) contents) - - -handView : List String -> Bool -> List Card.Response -> Html Message -handView picked disabled responses = - handRender disabled (List.map (response picked disabled) responses) - - -pickedResponse : Card.Response -> ( String, Html Message ) -pickedResponse response = - let - item = - li [] - [ div [ class "card response mui-panel" ] - [ div [ class "response-text" ] - [ text (Util.firstLetterToUpper response.text) - , text "." - ] - , withdrawButton response.id - ] - ] - in - ( response.id, item ) - - -pickedView : List Card.Response -> Int -> List ShownCard -> List (Html Message) -pickedView picked slots shownPlayed = - let - numberPicked = - List.length picked - - pb = - if (numberPicked < slots) then - [] - else - [ playButton ] - in - [ div [ class "picked" ] - ([ Keyed.ol [] (List.map pickedResponse picked) ] ++ pb) - , div [ class "others-picked" ] (List.map blankResponse shownPlayed) - ] - - -withdrawButton : String -> Html Message -withdrawButton id = - button - [ class "withdraw-button mui-btn mui-btn--small mui-btn--danger mui-btn--fab" - , title "Take back this response." - , onClick (Withdraw id) - ] - [ Icon.icon "times" ] - - -playButton : Html Message -playButton = - button - [ class "play-button mui-btn mui-btn--small mui-btn--accent mui-btn--fab", title "Play these responses.", onClick Play ] - [ Icon.icon "check" ] - - -playedView : Bool -> List Card.PlayedCards -> Html Message -playedView isCzar responses = - ol [ class "played mui--divider-top" ] - (List.indexedMap (\index pc -> li [] [ (playedCards isCzar index pc) ]) responses) - - -playedCards : Bool -> Int -> Card.PlayedCards -> Html Message -playedCards isCzar playedId cards = - ol - [ onClick (Consider playedId) ] - (List.map (\card -> li [] [ (playedResponse card) ]) cards) - - -playedResponse : Card.Response -> Html Message -playedResponse response = - div [ class "card response mui-panel" ] - [ div [ class "response-text" ] - [ text (Util.firstLetterToUpper response.text), text "." ] - ] - - -chooseButton : Int -> Html Message -chooseButton playedId = - button [ class "choose-button mui-btn mui-btn--small mui-btn--accent mui-btn--fab", onClick (Choose playedId) ] - [ Icon.icon "trophy" ] - - -infoBar : Game.Lobby -> Player.Secret -> List (Html Message) -infoBar lobby secret = - let - content = - statusInfo lobby.players secret.id |> Util.or (stateInfo lobby.game) - in - case content of - Just message -> - [ div [ id "info-bar", class "mui--z1" ] - [ Icon.icon "info-circle" - , text " " - , text message - ] - ] - - Nothing -> - [] - - -statusInfo : List Player -> Player.Id -> Maybe String -statusInfo players id = - case players |> Util.find (\player -> player.id == id) |> Maybe.map .status of - Just status -> - case status of - Player.Skipping -> - Nothing - - {- There is a warning for this instead. -} - Player.Neutral -> - Just "You joined while this round was already in play, you will be able to play next round." - - Player.Czar -> - Just "As card czar for this round - you don't play into the round, you pick the winner." - - _ -> - Nothing - - Nothing -> - Nothing - - -stateInfo : Game.State -> Maybe String -stateInfo state = - case state of - Game.Playing round -> - case round.state of - Round.J _ -> - Just "The card czar is now picking a winner." - - _ -> - Nothing - - _ -> - Nothing - - -warningDrawer : List (Html Message) -> Html Message -warningDrawer contents = - let - hidden = - List.isEmpty contents - - classes = - [ ( "hidden", hidden ) - ] - in - div [ id "warning-drawer", classList classes ] - [ button - [ attribute "onClick" "toggleWarningDrawer()" - , class "toggle mui-btn mui-btn--small mui-btn--fab" - , title "Warning notices." - ] - [ Icon.icon "exclamation-triangle" ] - , div [ class "top" ] [] - , div [ class "contents" ] contents - ] - - -skippingNotice : List Player -> Player.Id -> List (Html Message) -skippingNotice players id = - let - status = - players |> Util.find (\player -> player.id == id) |> Maybe.map .status - - renderSkippingNoticeIfSkipping = - (\status -> - case status of - Player.Skipping -> - renderSkippingNotice - - _ -> - [] - ) - in - Maybe.map renderSkippingNoticeIfSkipping status |> Maybe.withDefault [] - - -renderSkippingNotice : List (Html Message) -renderSkippingNotice = - [ div [ class "notice" ] - [ h3 [] [ Icon.icon "fast-forward" ] - , span [] [ text "You are currently being skipped because you took too long to play." ] - , div [ class "actions" ] - [ button - [ class "mui-btn mui-btn--small" - , onClick Back - , title "Rejoin the game." - ] - [ Icon.icon "sign-in", text " Rejoin" ] - ] - ] - ] - - -timeoutNotice : Player.Id -> List Player -> Bool -> Bool -> List (Html Message) -timeoutNotice playerId players judging timeout = - let - description = - if judging then - "picked a winnner for the round" - else - "played into the round" - - requiredStatus = - if judging then - Player.Czar - else - Player.NotPlayed - - timedOutPlayers = - List.filter (\player -> player.status == requiredStatus) players - - timedOutNames = - Util.joinWithAnd (List.map .name timedOutPlayers) - - timedOutIds = - List.map .id timedOutPlayers - - includesPlayer = - List.member playerId timedOutIds - in - if timeout then - Maybe.map (renderTimeoutNotice includesPlayer description timedOutIds (Util.pluralHas timedOutPlayers)) timedOutNames - |> Maybe.map (\item -> [ item ]) - |> Maybe.withDefault [] - else - [] - - -renderTimeoutNotice : Bool -> String -> List Player.Id -> String -> String -> Html Message -renderTimeoutNotice includesPlayer description ids has names = - if includesPlayer then - div [ class "notice" ] - [ h3 [] [ Icon.icon "exclamation-circle" ] - , span [] - [ text "The time has run out for you to have " - , text description - , text " and you can now be skipped." - ] - , div [ class "actions" ] [] - ] - else - div [ class "notice" ] - [ h3 [] [ Icon.icon "minus-circle" ] - , span [] - [ text names - , text " " - , text has - , text " not " - , text description - , text " before the round timer ran out." - ] - , div [ class "actions" ] - [ button - [ class "mui-btn mui-btn--small" - , onClick (Skip ids) - , title "They will be removed from this round, and won't be in future rounds until they come back." - ] - [ Icon.icon "fast-forward", text " Skip" ] - ] - ] - - -disconnectedNotice : List Player -> List (Html Message) -disconnectedNotice players = - let - disconnected = - List.filter (\player -> player.disconnected && (not (player.status == Player.Skipping))) players - - disconnectedNames = - Util.joinWithAnd (List.map .name disconnected) - - disconnectedIds = - List.map .id disconnected - in - Maybe.map (renderDisconnectedNotice disconnectedIds (Util.pluralHas disconnected)) disconnectedNames - |> Maybe.map (\item -> [ item ]) - |> Maybe.withDefault [] - - -renderDisconnectedNotice : List Player.Id -> String -> String -> Html Message -renderDisconnectedNotice ids has disconnectedNames = - div [ class "notice" ] - [ h3 [] [ Icon.icon "minus-circle" ] - , span [] - [ text disconnectedNames - , text " " - , text has - , text " disconnected from the game." - ] - , div [ class "actions" ] - [ button - [ class "mui-btn mui-btn--small" - , onClick (Skip ids) - , title "They will be removed from this round, and won't be in future rounds until they reconnect." - ] - [ Icon.icon "fast-forward", text " Skip" ] - ] - ] diff --git a/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm b/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm deleted file mode 100644 index 51dfc3d..0000000 --- a/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm +++ /dev/null @@ -1,85 +0,0 @@ -module MassiveDecks.Scenes.Playing.UI.Cards exposing (call, callText, response) - -import String -import Html exposing (..) -import Html.Attributes exposing (..) -import MassiveDecks.Models.Card as Card -import MassiveDecks.Util as Util - - -call : Card.Call -> List Card.Response -> Html msg -call call picked = - let - responseFirst = - call.parts |> List.head |> Maybe.map ((==) "") |> Maybe.withDefault False - - pickedText = - List.map .text picked - - ( parts, responses ) = - if responseFirst then - ( call.parts, Util.mapFirst Util.firstLetterToUpper pickedText ) - else - ( Util.mapFirst Util.firstLetterToUpper call.parts, pickedText ) - - spanned = - List.map (\part -> span [] [ text part ]) call.parts - - withSlots = - Util.interleave (slots (Card.slots call) "" responses) spanned - - callContents = - if responseFirst then - List.tail withSlots |> Maybe.withDefault withSlots - else - withSlots - in - div [ class "card call mui-panel" ] [ div [ class "call-text" ] callContents ] - - -callText : Card.Call -> List Card.Response -> String -callText call picked = - let - responseFirst = - call.parts |> List.head |> Maybe.map ((==) "") |> Maybe.withDefault False - - pickedText = - List.map .text picked - - ( parts, responses ) = - if responseFirst then - ( call.parts, Util.mapFirst Util.firstLetterToUpper pickedText ) - else - ( Util.mapFirst Util.firstLetterToUpper call.parts, pickedText ) - - extra = - (Card.slots call) - List.length picked - - withSlots = - Util.interleave (List.concat [ pickedText, List.repeat extra "blank" ]) call.parts - in - String.join " " withSlots - - -slot : String -> Html msg -slot value = - (span [ class "slot" ] [ text value ]) - - -slots : Int -> String -> List String -> List (Html msg) -slots count placeholder picked = - let - extra = - count - List.length picked - in - List.concat [ picked, List.repeat extra placeholder ] |> List.map slot - - -response : Bool -> List (Attribute msg) -> Card.Response -> Html msg -response picked attributes response = - let - classes = - [ classList [ ( "card", True ), ( "response", True ), ( "mui-panel", True ), ( "picked", picked ) ] ] - in - div (List.concat [ classes, attributes ]) - [ div [ class "response-text" ] [ text (Util.firstLetterToUpper response.text), text "." ] ] diff --git a/client/src/MassiveDecks/Scenes/Start.elm b/client/src/MassiveDecks/Scenes/Start.elm deleted file mode 100644 index b9fc628..0000000 --- a/client/src/MassiveDecks/Scenes/Start.elm +++ /dev/null @@ -1,354 +0,0 @@ -module MassiveDecks.Scenes.Start exposing (update, urlUpdate, view, init, subscriptions) - -import String -import Html exposing (..) -import Navigation -import MassiveDecks.Models exposing (Init, Path, pathFromLocation) -import MassiveDecks.Models.Game as Game -import MassiveDecks.Components.Tabs as Tabs -import MassiveDecks.Components.Storage as Storage -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.Overlay as Overlay exposing (Overlay) -import MassiveDecks.Components.Title as Title -import MassiveDecks.Scenes.Start.Messages exposing (InputId(..), Message(..), Tab(..)) -import MassiveDecks.Scenes.Start.Models exposing (Model) -import MassiveDecks.Scenes.Start.UI as UI -import MassiveDecks.Scenes.Lobby as Lobby -import MassiveDecks.Scenes.Lobby.Messages as Lobby -import MassiveDecks.API as API -import MassiveDecks.API.Request as Request -import MassiveDecks.Util as Util - - -{-| Create the initial model for the start screen. --} -init : Init -> Navigation.Location -> ( Model, Cmd Message ) -init init location = - let - path = - pathFromLocation location - - tab = - if path.gameCode |> Maybe.withDefault "" |> String.isEmpty then - Create - else - Join - in - ( { lobby = Nothing - , init = init - , path = path - , nameInput = Input.init Name "name-input" [ text "Your name in the game." ] "" "Nickname" (Util.cmd SubmitCurrentTab) InputMessage - , gameCodeInput = Input.init GameCode "game-code-input" [ text "The code for the game to join." ] (path.gameCode |> Maybe.withDefault "") "" (Util.cmd JoinLobbyAsNewPlayer) InputMessage - , passwordInput = Input.init Password "password-input" [ text "The password for the game to join." ] "" "Password" (Util.cmd JoinLobbyAsNewPlayer) InputMessage - , passwordRequired = Nothing - , errors = Errors.init - , overlay = Overlay.init OverlayMessage - , buttonsEnabled = True - , tabs = Tabs.init [ Tabs.Tab Create [ text "Create" ], Tabs.Tab Join [ text "Join" ] ] tab TabsMessage - , storage = init.existingGames - } - , Maybe.map (TryExistingGame >> Util.cmd) path.gameCode |> Maybe.withDefault Cmd.none - ) - - -{-| Subscriptions for the start screen. --} -subscriptions : Model -> Sub Message -subscriptions model = - case model.lobby of - Nothing -> - Sub.none - - Just lobby -> - Lobby.subscriptions lobby |> Sub.map LobbyMessage - - -{-| Render the start scene. --} -view : Model -> Html Message -view model = - let - contents = - case model.lobby of - Nothing -> - UI.view model - - Just lobby -> - Html.map LobbyMessage (Lobby.view lobby) - in - div [] - ([ contents - , Errors.view { url = model.init.url, version = model.init.version } model.errors |> Html.map ErrorMessage - ] - ++ Overlay.view model.overlay - ) - - -{-| Handles changes to the url. --} -urlUpdate : Path -> Model -> ( Model, Cmd Message ) -urlUpdate path model = - let - noGameCode = - case path.gameCode of - Just _ -> - False - - Nothing -> - True - - setInput = - path.gameCode |> Maybe.map (\gameCode -> ( GameCode, Input.SetDefaultValue gameCode ) |> InputMessage |> Util.cmd) - in - { model - | path = path - , lobby = - if noGameCode then - Nothing - else - model.lobby - , buttonsEnabled = True - } - ! [ (if noGameCode then - Cmd.none - else - Tabs.SetTab Join |> TabsMessage |> Util.cmd - ) - , setInput - |> Maybe.withDefault Cmd.none - , path.gameCode - |> Maybe.map (\gc -> Title.set ("Game " ++ gc ++ " - " ++ title)) - |> Maybe.withDefault (Title.set title) - , path.gameCode - |> Maybe.map (TryExistingGame >> Util.cmd) - |> Maybe.withDefault Cmd.none - ] - - -{-| Handles messages and alters the model as appropriate. --} -update : Message -> Model -> ( Model, Cmd Message ) -update message model = - case message of - ErrorMessage message -> - let - ( newErrors, cmd ) = - Errors.update message model.errors - in - ( { model | errors = newErrors }, Cmd.map ErrorMessage cmd ) - - PathChange path -> - urlUpdate path model - - TabsMessage tabsMessage -> - ( { model | tabs = (Tabs.update tabsMessage model.tabs) }, Cmd.none ) - - ClearExistingGame existingGame -> - { model | storage = Storage.leave existingGame model.storage } - ! [ Overlay "info-circle" "Game over." [ text ("The game " ++ existingGame.gameCode ++ " has ended.") ] - |> Overlay.Show - |> OverlayMessage - |> Util.cmd - , Storage.Store |> StorageMessage |> Util.cmd - , Navigation.newUrl model.init.url - ] - - TryExistingGame gameCode -> - let - existing = - List.filter (.gameCode >> ((==) gameCode)) model.storage |> List.head - - cmd = - Maybe.map (\existing -> JoinLobbyAsExistingPlayer existing.secret existing.gameCode |> Util.cmd) existing - |> Maybe.withDefault (Navigation.modifyUrl model.init.url) - in - ( model, cmd ) - - CreateLobby -> - ( { model | buttonsEnabled = False } - , Request.send_ - (API.createLobby model.nameInput.value) - ErrorMessage - (\gameCodeAndSecret -> StoreCredentialsAndMoveToLobby gameCodeAndSecret.gameCode gameCodeAndSecret.secret) - ) - - SubmitCurrentTab -> - case model.tabs.current of - Create -> - ( model, Util.cmd CreateLobby ) - - Join -> - ( model, Util.cmd JoinLobbyAsNewPlayer ) - - SetButtonsEnabled enabled -> - ( { model | buttonsEnabled = enabled }, Cmd.none ) - - SetPasswordRequired -> - ( { model | passwordRequired = Just model.gameCodeInput.value }, Cmd.none ) - - JoinLobbyAsNewPlayer -> - ( { model | buttonsEnabled = False }, Util.cmd (JoinGivenLobbyAsNewPlayer model.gameCodeInput.value) ) - - JoinGivenLobbyAsNewPlayer gameCode -> - case List.filter (.gameCode >> ((==) gameCode)) model.storage |> List.head of - Nothing -> - model - ! [ Request.send (API.newPlayer gameCode model.nameInput.value model.passwordInput.value) - newPlayerErrorHandler - ErrorMessage - (StoreCredentialsAndMoveToLobby gameCode) - ] - - Just _ -> - model - ! [ MoveToLobby gameCode |> Util.cmd - , UI.alreadyInGameOverlay - |> Overlay.Show - |> OverlayMessage - |> Util.cmd - ] - - StoreCredentialsAndMoveToLobby gameCode secret -> - { model | storage = Storage.join (Game.GameCodeAndSecret gameCode secret) model.storage } - ! [ Storage.Store |> StorageMessage |> Util.cmd - , MoveToLobby gameCode |> Util.cmd - ] - - MoveToLobby gameCode -> - model ! [ Navigation.newUrl (model.init.url ++ "#" ++ gameCode) ] - - JoinLobbyAsExistingPlayer secret gameCode -> - model - ! [ Request.send (API.getLobbyAndHand gameCode secret) - (getLobbyAndHandErrorHandler (Game.GameCodeAndSecret gameCode secret)) - ErrorMessage - (JoinLobby secret) - ] - - JoinLobby secret lobbyAndHand -> - let - ( lobby, cmd ) = - Lobby.init model.init lobbyAndHand secret - in - { model | lobby = Just lobby } - ! [ cmd |> Cmd.map LobbyMessage - ] - - InputMessage message -> - let - ( nameInput, nameCmd ) = - Input.update message model.nameInput - - ( gameCodeInput, gameCodeCmd ) = - Input.update message model.gameCodeInput - - ( passwordInput, passwordCmd ) = - Input.update message model.passwordInput - in - ( { model - | nameInput = nameInput - , gameCodeInput = gameCodeInput - , passwordInput = passwordInput - } - , Cmd.batch [ nameCmd, gameCodeCmd, passwordCmd ] - ) - - OverlayMessage overlayMessage -> - ( { model | overlay = Overlay.update overlayMessage model.overlay }, Cmd.none ) - - StorageMessage storageMessage -> - let - ( storageModel, cmd ) = - Storage.update storageMessage model.storage - in - ( { model | storage = storageModel }, cmd |> Cmd.map StorageMessage ) - - LobbyMessage message -> - case message of - Lobby.ErrorMessage message -> - ( model, Util.cmd (ErrorMessage message) ) - - Lobby.OverlayMessage message -> - ( model, Util.cmd (OverlayMessage (Overlay.map (Lobby.LocalMessage >> LobbyMessage) message)) ) - - Lobby.Leave -> - let - ( leave, storage ) = - case model.lobby of - Nothing -> - ( [], model.storage ) - - Just lobby -> - ( [ Request.send_ (API.leave lobby.lobby.gameCode lobby.secret) ErrorMessage (\_ -> NoOp) - , Storage.Store |> StorageMessage |> Util.cmd - ] - , Storage.leave (Game.GameCodeAndSecret lobby.lobby.gameCode lobby.secret) model.storage - ) - in - { model - | lobby = Nothing - , buttonsEnabled = True - , storage = storage - } - ! ([ Navigation.newUrl model.init.url - ] - ++ leave - ) - - Lobby.LocalMessage message -> - case model.lobby of - Nothing -> - ( model, Cmd.none ) - - Just lobby -> - let - ( newLobby, cmd ) = - Lobby.update message lobby - in - ( { model | lobby = Just newLobby }, Cmd.map LobbyMessage cmd ) - - Batch messages -> - ( model, messages |> List.map Util.cmd |> Cmd.batch ) - - NoOp -> - ( model, Cmd.none ) - - -title : String -title = - "Massive Decks" - - -newPlayerErrorHandler : API.NewPlayerError -> Message -newPlayerErrorHandler error = - let - errorMessage = - case error of - API.NameInUse -> - ( Name, Just "This name is already in use in this game, try something else." |> Input.Error ) |> InputMessage - - API.PasswordWrong -> - Batch - [ SetPasswordRequired - , ( Password, Just "This game requires a password, please check you have the right one." |> Input.Error ) |> InputMessage - ] - - API.NewPlayerLobbyNotFound -> - ( GameCode, Just "This game doesn't exist - check you have the right code." |> Input.Error ) |> InputMessage - in - Batch [ SetButtonsEnabled True, errorMessage ] - - -getLobbyAndHandErrorHandler : Game.GameCodeAndSecret -> API.GetLobbyAndHandError -> Message -getLobbyAndHandErrorHandler gameCodeAndSecret error = - let - errorMessage = - case error of - API.LobbyNotFound -> - ClearExistingGame gameCodeAndSecret - - API.SecretWrongOrNotAPlayer -> - ClearExistingGame gameCodeAndSecret - in - Batch [ SetButtonsEnabled True, errorMessage ] diff --git a/client/src/MassiveDecks/Scenes/Start/Messages.elm b/client/src/MassiveDecks/Scenes/Start/Messages.elm deleted file mode 100644 index 8342bab..0000000 --- a/client/src/MassiveDecks/Scenes/Start/Messages.elm +++ /dev/null @@ -1,52 +0,0 @@ -module MassiveDecks.Scenes.Start.Messages exposing (..) - -import MassiveDecks.Models exposing (Path) -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.Tabs as Tabs -import MassiveDecks.Components.Overlay as Overlay -import MassiveDecks.Components.Storage as Storage -import MassiveDecks.Scenes.Lobby.Messages as Lobby -import MassiveDecks.Models.Game as Game -import MassiveDecks.Models.Player as Player - - -{-| The messages used in the start screen. --} -type Message - = SubmitCurrentTab - | PathChange Path - | CreateLobby - | SetButtonsEnabled Bool - | JoinLobbyAsNewPlayer - | JoinGivenLobbyAsNewPlayer String - | JoinLobbyAsExistingPlayer Player.Secret String - | StoreCredentialsAndMoveToLobby String Player.Secret - | MoveToLobby String - | JoinLobby Player.Secret Game.LobbyAndHand - | TryExistingGame String - | ClearExistingGame Game.GameCodeAndSecret - | InputMessage (Input.Message InputId) - | LobbyMessage Lobby.ConsumerMessage - | ErrorMessage Errors.Message - | OverlayMessage (Overlay.Message Message) - | TabsMessage (Tabs.Message Tab) - | StorageMessage Storage.Message - | SetPasswordRequired - | Batch (List Message) - | NoOp - - -{-| IDs for the inputs to differentiate between them in messages. --} -type InputId - = Name - | GameCode - | Password - - -{-| Tabs for the start page. --} -type Tab - = Create - | Join diff --git a/client/src/MassiveDecks/Scenes/Start/Models.elm b/client/src/MassiveDecks/Scenes/Start/Models.elm deleted file mode 100644 index db5f764..0000000 --- a/client/src/MassiveDecks/Scenes/Start/Models.elm +++ /dev/null @@ -1,28 +0,0 @@ -module MassiveDecks.Scenes.Start.Models exposing (..) - -import MassiveDecks.Models exposing (Init, Path) -import MassiveDecks.Scenes.Lobby.Models as Lobby -import MassiveDecks.Components.Tabs as Tabs -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Errors as Errors -import MassiveDecks.Components.Overlay as Overlay -import MassiveDecks.Components.Storage as Storage -import MassiveDecks.Scenes.Start.Messages exposing (Message, InputId, Tab) - - -{-| The state of the start screen. --} -type alias Model = - { lobby : Maybe Lobby.Model - , init : Init - , path : Path - , nameInput : Input.Model InputId Message - , gameCodeInput : Input.Model InputId Message - , passwordInput : Input.Model InputId Message - , passwordRequired : Maybe String - , errors : Errors.Model - , overlay : Overlay.Model Message - , buttonsEnabled : Bool - , tabs : Tabs.Model Tab Message - , storage : Storage.Model - } diff --git a/client/src/MassiveDecks/Scenes/Start/UI.elm b/client/src/MassiveDecks/Scenes/Start/UI.elm deleted file mode 100644 index fd437ba..0000000 --- a/client/src/MassiveDecks/Scenes/Start/UI.elm +++ /dev/null @@ -1,151 +0,0 @@ -module MassiveDecks.Scenes.Start.UI exposing (view, alreadyInGameOverlay) - -import String -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import Html.Keyed as Keyed -import MassiveDecks.Models.Game as Game -import MassiveDecks.Components.Tabs as Tabs -import MassiveDecks.Components.Icon as Icon -import MassiveDecks.Components.About as About -import MassiveDecks.Components.Input as Input -import MassiveDecks.Components.Overlay as Overlay exposing (Overlay) -import MassiveDecks.Scenes.Start.Models exposing (Model) -import MassiveDecks.Scenes.Start.Messages exposing (Message(..), Tab(..)) -import MassiveDecks.Scenes.Lobby.Messages as Lobby - - -alreadyInGameOverlay : Overlay Message -alreadyInGameOverlay = - Overlay - "info-circle" - "Already in game." - [ p [] [ text "You are already in this game, so you have joined as an existing player." ] - , p [] - [ text "If you want to join as a new player, please " - , a - [ class "link" - , title "Leave the game." - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick - (Batch - [ Lobby.Leave |> LobbyMessage - , Overlay.Hide |> OverlayMessage - ] - ) - ] - [ text "leave the game" ] - , text " first." - ] - ] - - -{-| Render the start screen. --} -view : Model -> Html Message -view model = - let - nameEntered = - not (String.isEmpty model.nameInput.value) - - gameCodeEntered = - not (String.isEmpty model.gameCodeInput.value) - - versionInfo = - if String.isEmpty model.init.version then - [] - else - [ text " Version ", text model.init.version ] - in - div [ id "start-screen" ] - [ div [ id "start-screen-content", class "mui-panel" ] - ([ h1 [ class "mui--divider-bottom" ] [ text "Massive Decks" ] - ] - ++ (existingGames model.storage) - ++ [ Input.view model.nameInput - ] - ++ (Tabs.view (renderTab nameEntered gameCodeEntered model) model.tabs) - ++ [ a - [ class "about-link mui--divider-top link" - , attribute "tabindex" "0" - , attribute "role" "button" - , onClick (About.show model.init.version |> OverlayMessage) - ] - [ Icon.icon "question-circle", text " About" ] - , div [ id "forkongithub" ] - [ div [] - [ a [ href "https://github.com/lattyware/massivedecks", target "_blank", rel "noopener" ] - [ Icon.icon "github", text " Fork me on GitHub" ] - ] - ] - ] - ) - , footer [] - [ a [ href "https://github.com/Lattyware/massivedecks" ] - [ img [ src "images/icon.svg", alt "The Massive Decks logo.", title "Massive Decks" ] [] ] - , p [] versionInfo - ] - ] - - -renderTab : Bool -> Bool -> Model -> Tab -> List (Html Message) -renderTab nameEntered gameCodeEntered model tab = - case tab of - Create -> - [ createLobbyButton (nameEntered && model.buttonsEnabled) ] - - Join -> - List.concat - [ [ Input.view model.gameCodeInput - ] - , (if model.passwordRequired == (Just model.gameCodeInput.value) then - [ Input.view model.passwordInput ] - else - [] - ) - , [ joinLobbyButton (nameEntered && gameCodeEntered && model.buttonsEnabled) - ] - ] - - -existingGames : List Game.GameCodeAndSecret -> List (Html msg) -existingGames games = - if List.isEmpty games then - [] - else - [ div [ class "rejoin mui--divider-bottom" ] - [ span [] [ text "You can rejoin " ] - , Keyed.ul [] (List.map existingGame games) - ] - ] - - -existingGame : Game.GameCodeAndSecret -> ( String, Html msg ) -existingGame game = - ( game.gameCode, li [] [ a [ href ("#" ++ game.gameCode) ] [ text ("Game \"" ++ game.gameCode ++ "\"") ] ] ) - - -{-| A button to join an existing lobby. --} -joinLobbyButton : Bool -> Html Message -joinLobbyButton enabled = - button - [ class "mui-btn mui-btn--large mui-btn--primary" - , onClick JoinLobbyAsNewPlayer - , disabled (not enabled) - ] - [ text "Join Game" ] - - -{-| A button to create a new lobby. --} -createLobbyButton : Bool -> Html Message -createLobbyButton enabled = - button - [ class "mui-btn mui-btn--large mui-btn--primary" - , onClick CreateLobby - , disabled (not enabled) - ] - [ text "Create Game" ] diff --git a/client/src/MassiveDecks/Util.elm b/client/src/MassiveDecks/Util.elm deleted file mode 100644 index b1db7e0..0000000 --- a/client/src/MassiveDecks/Util.elm +++ /dev/null @@ -1,237 +0,0 @@ -module MassiveDecks.Util exposing (..) - -import Json.Decode as Json -import Task exposing (Task) -import String -import Process -import Time exposing (Time) -import Html exposing (..) -import Html.Keyed as Keyed -import Html.Events exposing (..) - - -{-| Since a value of `Never` can never exist, this can't ever actually happen. Used to fill gaps where the type system -requires a method, but it'll never get called. --} -impossible : Never -> a -impossible n = - impossible n - - -{-| Add an item to a list if the value is not nothing. --} -andMaybe : Maybe a -> List a -> List a -andMaybe maybeExtra values = - List.append values (Maybe.map (\value -> [ value ]) maybeExtra |> Maybe.withDefault []) - - -or : Maybe a -> Maybe a -> Maybe a -or a b = - case a of - Just _ -> - a - - Nothing -> - b - - -{-| Give the first value from the list that matches the given condition. If none do, return `Nothing`. --} -find : (a -> Bool) -> List a -> Maybe a -find check items = - List.filter check items - |> List.head - - -{-| Take two lists and inteleave them into each other. The first element will come from the second list. --} -interleave : List a -> List a -> List a -interleave list1 list2 = - case list1 of - [] -> - list2 - - x :: xs -> - case list2 of - [] -> - list1 - - y :: ys -> - y :: x :: interleave xs ys - - -{-| An url to a lobby from the base url and the lobby id. --} -lobbyUrl : String -> String -> String -lobbyUrl url lobbyId = - url ++ "#" ++ lobbyId - - -{-| Create a command that just sends the given message instantly. --} -cmd : msg -> Cmd msg -cmd message = - Task.perform identity (Task.succeed message) - - -{-| Chains two updates together to produce a single update. --} -(:>) : (model -> ( model, Cmd msg )) -> (model -> ( model, Cmd msg )) -> model -> ( model, Cmd msg ) -(:>) a b model = - let - ( aModel, aMsg ) = - a model - - ( bModel, bMsg ) = - b aModel - in - bModel ! [ aMsg, bMsg ] - - -{-| Reduce the list to the given indices, and give those indicies with the elements. --} -getAllWithIndex : List a -> List Int -> List ( Int, a ) -getAllWithIndex list indices = - getAll (List.indexedMap (,) list) indices - - -{-| Reduce the list to the given indices. --} -getAll : List a -> List Int -> List a -getAll list indices = - List.filterMap (get list) indices - - -{-| Get the element at the given index, or nothing. --} -get : List a -> Int -> Maybe a -get list index = - case List.drop index list of - [] -> - Nothing - - item :: _ -> - Just item - - -{-| Make the first character of the string uppercase. --} -firstLetterToUpper : String -> String -firstLetterToUpper str = - (String.toUpper (String.left 1 str)) ++ (String.dropLeft 1 str) - - -{-| map over the first element of a list only, returning the rest as-is. --} -mapFirst : (a -> a) -> List a -> List a -mapFirst f xs = - List.indexedMap - (\index x -> - if index == 0 then - f x - else - x - ) - xs - - -{-| Get "has" or "have" as appropriate for the given number of items. --} -pluralHas : List a -> String -pluralHas items = - case List.length items of - 1 -> - "has" - - _ -> - "have" - - -{-| Join the given strings together with commas and spacing, as in a list, adding an 'and' between the last two -elements. --} -joinWithAnd : List String -> Maybe String -joinWithAnd items = - case items of - [] -> - Nothing - - head :: [] -> - Just head - - first :: second :: [] -> - Just (first ++ " and " ++ second) - - head :: rest -> - Just (head ++ ", " ++ (joinWithAnd rest |> Maybe.withDefault "")) - - -{-| Apply every function in the list to the given element. --} -apply : List (a -> b) -> a -> List b -apply fs value = - List.map (\f -> f value) fs - - -{-| Perform a task after a given period of time. --} -after : Time -> Task x a -> Task x a -after waitFor task = - Process.sleep waitFor |> Task.andThen (\_ -> task) - - -{-| Check if a Maybe is Nothing. --} -isNothing : Maybe a -> Bool -isNothing m = - case m of - Just _ -> - False - - Nothing -> - True - - -{-| Add an event handler for keyboard key presses. --} -onKeyDown : String -> msg -> msg -> Attribute msg -onKeyDown key message noOp = - on "keydown" - (Json.at [ "key" ] Json.string - |> Json.map - (\pressed -> - if pressed == key then - message - else - noOp - ) - ) - - -{-| Perform an action on click, only if the id of the clicked element matches (i.e: only on click for a given element -in the tree). --} -onClickIfId : String -> msg -> msg -> Attribute msg -onClickIfId targetId message noOp = - on "click" - (ifIdDecoder - |> Json.map - (\clickedId -> - if clickedId == targetId then - message - else - noOp - ) - ) - - -ifIdDecoder : Json.Decoder String -ifIdDecoder = - Json.at [ "target", "id" ] Json.string - - -{-| Create a tbody where each row is keyed. --} -tbody : List (Attribute msg) -> List ( String, Html msg ) -> Html msg -tbody = - Keyed.node "tbody" diff --git a/client/src/elm/MassiveDecks.elm b/client/src/elm/MassiveDecks.elm new file mode 100644 index 0000000..197cedd --- /dev/null +++ b/client/src/elm/MassiveDecks.elm @@ -0,0 +1,284 @@ +module MassiveDecks exposing (main) + +import Browser +import Browser.Navigation as Navigation +import Html exposing (Html) +import Html.Attributes as HtmlA +import Json.Decode as Json +import MassiveDecks.Cast.Client as Cast +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Error.Model as Error +import MassiveDecks.Error.Overlay as Overlay +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Pages as Pages +import MassiveDecks.Pages.Lobby as Lobby +import MassiveDecks.Pages.Lobby.GameCode as GameCode +import MassiveDecks.Pages.Model as Pages exposing (Page) +import MassiveDecks.Pages.Route as Route exposing (Route) +import MassiveDecks.Pages.Spectate as Spectate +import MassiveDecks.Pages.Start as Start +import MassiveDecks.Pages.Unknown as Unknown +import MassiveDecks.Ports as Ports +import MassiveDecks.ServerConnection as ServerConnection +import MassiveDecks.Settings as Settings +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util as Util +import MassiveDecks.Util.Url as Url +import Url exposing (Url) + + +type alias Model = + { page : Page + , shared : Shared + , errorOverlay : Error.Overlay + } + + +main : Program Json.Value Model Msg +main = + Browser.application + { init = init + , view = view + , update = update + , subscriptions = subscriptions + , onUrlRequest = onUrlRequest + , onUrlChange = onUrlChange + } + + +init : Json.Value -> Url -> Navigation.Key -> ( Model, Cmd Msg ) +init flags url key = + let + route = + Route.fromUrl url + + { settings, browserLanguages } = + Json.decodeValue Decoders.flags flags |> Result.toMaybe |> Maybe.withDefault (Flags Settings.defaults []) + + ( initialisedSettings, settingsCmd ) = + Settings.init settings + + shared = + { language = Lang.defaultLanguage + , key = key + , origin = Url.origin url + , settings = initialisedSettings + , browserLanguage = Lang.findBestMatch browserLanguages + , castStatus = Cast.NoDevicesAvailable + } + + ( page, pageCmd ) = + Pages.fromRoute shared Nothing route |> changePage shared + in + ( { page = page, shared = shared, errorOverlay = Overlay.init }, Cmd.batch [ pageCmd, settingsCmd ] ) + + +subscriptions : Model -> Sub Msg +subscriptions model = + Sub.batch + [ Cast.subscriptions + , model.page |> Pages.subscriptions + ] + + +onUrlRequest : Browser.UrlRequest -> Msg +onUrlRequest urlRequest = + case urlRequest of + Browser.Internal url -> + url |> Route.fromUrl |> ChangePage + + Browser.External _ -> + -- We intentionally block external URLs. Use `Util.blankA` to make a link that opens in a new window. + BlockedExternalUrl + + +onUrlChange : Url -> Msg +onUrlChange url = + PageChanged url + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + let + oldRoute = + Pages.toRoute model.page + in + case msg of + ChangePage route -> + if oldRoute /= route then + changePageFromRoute model.shared route model.page |> Util.modelLift (\p -> { model | page = p }) + + else + ( model, Cmd.none ) + + PageChanged url -> + let + route = + Route.fromUrl url + + ( page, pageCmd ) = + if oldRoute /= route then + Pages.fromRoute model.shared (Just model.page) route + + else + ( model.page, Cmd.none ) + + cutConnection = + case model.page of + Pages.Lobby _ -> + case page of + Pages.Lobby _ -> + Cmd.none + + _ -> + ServerConnection.disconnect + + _ -> + Cmd.none + in + ( { model | page = page }, Cmd.batch [ pageCmd, cutConnection ] ) + + JoinLobby name auth -> + let + shared = + model.shared + + ( ( lobby, lobbyCmd ), ( settings, settingsCmd ) ) = + case Lobby.init shared { gameCode = auth.claims.gc } (Just auth) of + Route.Continue continue -> + ( continue |> Util.modelLift Pages.Lobby |> changePage shared + , Settings.onJoinLobby auth name shared.settings + ) + + Route.Redirect redirect -> + ( changePageFromRoute model.shared redirect model.page, ( shared.settings, Cmd.none ) ) + in + ( { model + | shared = { shared | settings = settings } + , page = lobby + } + , Cmd.batch [ lobbyCmd, settingsCmd ] + ) + + StartMsg startMsg -> + case model.page of + Pages.Start startModel -> + Util.modelLift (\newStartModel -> { model | page = Pages.Start newStartModel }) + (Start.update startMsg startModel) + + _ -> + ( model, Cmd.none ) + + SpectateMsg spectateMsg -> + case model.page of + Pages.Spectate spectateModel -> + Util.modelLift (\newSpectateModel -> { model | page = Pages.Spectate newSpectateModel }) + (Spectate.update spectateMsg spectateModel) + + _ -> + ( model, Cmd.none ) + + SettingsMsg settingsMsg -> + let + shared = + model.shared + in + Settings.update settingsMsg shared.settings + |> Util.lift (\s -> { model | shared = { shared | settings = s } }) SettingsMsg + + LobbyMsg lobbyMsg -> + case model.page of + Pages.Lobby lobbyModel -> + Lobby.update lobbyMsg lobbyModel + |> Util.modelLift (\newLobbyModel -> { model | page = Pages.Lobby newLobbyModel }) + + _ -> + ( model, Cmd.none ) + + CastStatusUpdate status -> + let + shared = + model.shared + in + ( { model | shared = { shared | castStatus = status } }, Cmd.none ) + + TryCast auth -> + ( model, Cast.tryCast model.shared auth.token ) + + ErrorMsg errorMsg -> + Overlay.update errorMsg model.errorOverlay + |> Util.modelLift (\o -> { model | errorOverlay = o }) + + Refresh -> + ( model, Navigation.reload ) + + -- TODO: Maybe popup something here? + BlockedExternalUrl -> + ( model, Cmd.none ) + + Copy id -> + ( model, Ports.copyText id ) + + NoOp -> + ( model, Cmd.none ) + + +view : Model -> Browser.Document Msg +view model = + let + body = + case model.page of + Pages.Start m -> + Start.view model.shared m + + Pages.Lobby m -> + Lobby.view model.shared m + + Pages.Spectate m -> + Spectate.view model.shared m + + Pages.Unknown m -> + Unknown.view model.shared m + + settingsPanel = + case model.page of + Pages.Start _ -> + [ Html.div [ HtmlA.class "start-settings" ] [ Settings.view model.shared ] ] + + _ -> + [] + + errorOverlay = + Overlay.view model.shared (Pages.toRoute model.page) model.errorOverlay + + defaultTitle = + Lang.string model.shared Strings.MassiveDecks + + title = + case model.page of + Pages.Lobby m -> + m.lobby |> Maybe.map (\l -> l.name ++ " (" ++ (m.auth.claims.gc |> GameCode.toString) ++ ")") + + _ -> + Nothing + in + { title = title |> Maybe.withDefault defaultTitle + , body = List.concat [ settingsPanel, errorOverlay, body ] + } + + + +{- Private -} + + +changePageFromRoute : Shared -> Route -> Page -> ( Page, Cmd Msg ) +changePageFromRoute shared route oldPage = + Pages.fromRoute shared (Just oldPage) route |> changePage shared + + +changePage : Shared -> ( Page, Cmd Msg ) -> ( Page, Cmd Msg ) +changePage shared ( page, cmd ) = + ( page, Cmd.batch [ page |> Pages.toRoute |> Route.url |> Navigation.pushUrl shared.key, cmd ] ) diff --git a/client/src/elm/MassiveDecks/Animated.elm b/client/src/elm/MassiveDecks/Animated.elm new file mode 100644 index 0000000..9ae175f --- /dev/null +++ b/client/src/elm/MassiveDecks/Animated.elm @@ -0,0 +1,127 @@ +module MassiveDecks.Animated exposing + ( Animated + , Msg(..) + , State + , animate + , enterAfter + , exitAfter + , subscriptions + , update + , view + ) + +import Browser.Events as BrowserEvents +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Util.Cmd as Cmd + + +{-| If given, removeDone will return a model with items that are done removed. The value given is the duration of the +exiting animation. +-} +type alias Settings = + { removeDone : Maybe Int + } + + +{-| The state of the animated item. +-} +type State + = New + | Entering + | Exiting + + +{-| An animated item. +-} +type alias Animated item = + { item : item + , state : State + } + + +type Msg item + = Enter item + | Exit item + | Remove item + + +type alias Model item = + List (Animated item) + + +subscriptions : Model item -> Sub (Msg item) +subscriptions model = + model |> List.filterMap subscription |> Sub.batch + + +view : (Html.Attribute msg -> item -> Html msg) -> Animated item -> Html msg +view render item = + render (class item.state) item.item + + +update : Settings -> Msg item -> Model item -> ( Model item, Cmd (Msg item) ) +update settings msg model = + case msg of + Enter item -> + ( model |> List.map (setState Entering item), Cmd.none ) + + Exit item -> + ( model |> List.map (setState Exiting item) + , settings.removeDone |> Maybe.map (\after -> Remove item |> Cmd.after after) |> Maybe.withDefault Cmd.none + ) + + Remove item -> + ( model |> List.filter (\i -> item /= i.item), Cmd.none ) + + +animate : item -> Animated item +animate item = + { state = New + , item = item + } + + +enterAfter : Int -> item -> Cmd (Msg item) +enterAfter milliseconds item = + Enter item |> Cmd.after milliseconds + + +exitAfter : Int -> item -> Cmd (Msg item) +exitAfter milliseconds item = + Exit item |> Cmd.after milliseconds + + + +{- Private -} + + +subscription : Animated item -> Maybe (Sub (Msg item)) +subscription item = + if item.state == New then + BrowserEvents.onAnimationFrame (always (Enter item.item)) |> Just + + else + Nothing + + +setState : State -> item -> Animated item -> Animated item +setState state target item = + if item.item == target then + { item | state = state } + + else + item + + +class : State -> Html.Attribute msg +class state = + case state of + New -> + HtmlA.classList [ ( "animation", True ) ] + + Entering -> + HtmlA.classList [ ( "animation", True ), ( "entering", True ) ] + + Exiting -> + HtmlA.classList [ ( "animation", True ), ( "exiting", True ) ] diff --git a/client/src/elm/MassiveDecks/Card.elm b/client/src/elm/MassiveDecks/Card.elm new file mode 100644 index 0000000..3ee4e5e --- /dev/null +++ b/client/src/elm/MassiveDecks/Card.elm @@ -0,0 +1,233 @@ +module MassiveDecks.Card exposing + ( id + , slotCount + , source + , view + , viewFilled + , viewUnknownCall + , viewUnknownResponse + ) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Card.Model exposing (..) +import MassiveDecks.Card.Parts as Parts exposing (Parts) +import MassiveDecks.Card.Source as Source +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Strings exposing (MdString(..)) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.String as String + + +{-| Render a card to HTML, with the given side face-up. +-} +view : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> Card -> Html msg +view shared decks visibleSide extraAttributes card = + viewInternal shared decks visibleSide extraAttributes [] card + + +{-| View a call rendered with the given play filling the slots. +-} +viewFilled : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> List String -> Call -> Html msg +viewFilled shared decks visibleSide extraAttributes play card = + viewInternal shared decks visibleSide extraAttributes play (C card) + + +{-| Render an unknown response to HTML, face-down. +-} +viewUnknownResponse : List (Html.Attribute msg) -> Html msg +viewUnknownResponse extraAttributes = + viewUnknown (HtmlA.class "response" :: extraAttributes) + + +{-| Render an unknown response to HTML, face-down. +-} +viewUnknownCall : List (Html.Attribute msg) -> Html msg +viewUnknownCall extraAttributes = + viewUnknown (HtmlA.class "call" :: extraAttributes) + + +{-| Get the source of a card. +-} +source : Card -> Source +source card = + card |> details |> .source + + +{-| Get the id of a card. +-} +id : Card -> Id +id card = + card |> details |> .id + + +{-| How many slots there are on a call. +-} +slotCount : Call -> Int +slotCount call = + call.body |> Parts.slotCount + + + +{- Private -} + + +viewInternal : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> List String -> Card -> Html msg +viewInternal shared decks visibleSide extraAttributes play card = + Html.div + ([ HtmlA.classList + [ ( "game-card", True ) + , ( cardTypeClass card, True ) + , ( "face-down", visibleSide == Back ) + ] + ] + ++ extraAttributes + ) + [ Html.div [ HtmlA.class "aspect" ] + [ front shared decks play card + , back + ] + ] + + +viewUnknown : List (Html.Attribute msg) -> Html msg +viewUnknown extraAttributes = + Html.div + ([ HtmlA.class "game-card face-down" ] ++ extraAttributes) + [ Html.div [ HtmlA.class "aspect" ] + [ back ] + ] + + +instructions : Shared -> Parts -> List (Html msg) +instructions shared parts = + let + slots = + Parts.slotCount parts + + instructionViews = + List.concat [ extraCardsInstruction shared slots, pickInstruction shared slots ] + in + if List.length instructionViews > 0 then + [ Html.ol + [ HtmlA.class "instructions" ] + instructionViews + ] + + else + [] + + +extraCardsInstruction : Shared -> Int -> List (Html msg) +extraCardsInstruction shared slots = + let + extraCards = + slots - 1 + in + if extraCards > 0 then + [ Html.li [] [ Draw { numberOfCards = extraCards } |> Lang.html shared ] + ] + + else + [] + + +pickInstruction : Shared -> Int -> List (Html msg) +pickInstruction shared slots = + if slots > 1 then + [ Html.li [] [ Pick { numberOfCards = slots } |> Lang.html shared ] + ] + + else + [] + + +cardSide : Side -> List (Html msg) -> Html msg +cardSide side content = + Html.div [ HtmlA.classList [ ( "side", True ), ( "front", side == Front ), ( "back", side == Back ) ] ] content + + +back : Html msg +back = + cardSide Back + [ Html.div [ HtmlA.class "content" ] + [ Html.p [] [ Html.span [] [ Html.text "Massive" ] ], Html.p [] [ Html.span [] [ Html.text "Decks" ] ] ] + ] + + +front : Shared -> List Configure.Deck -> List String -> Card -> Html msg +front shared decks play card = + cardSide Front + [ Html.div [ HtmlA.class "content" ] + (case card of + C call -> + Parts.viewFilled play call.body + + R response -> + [ Html.p [] [ Html.span [] [ response.body |> String.capitalise |> Html.text ] ] ] + ) + , info shared decks card + ] + + +info : Shared -> List Configure.Deck -> Card -> Html msg +info shared decks card = + let + instructionsInfo = + case card of + C call -> + instructions shared call.body + + R _ -> + [] + in + Html.div [ HtmlA.class "info" ] + ([ card |> source |> viewSource decks + ] + ++ instructionsInfo + ) + + +viewSource : List Configure.Deck -> Source -> Html msg +viewSource decks s = + let + sourceDetails = + decks + |> List.filter (\d -> Source.equals (Source.Ex d.source) s) + |> List.head + |> Maybe.andThen .summary + |> Maybe.map .details + |> Maybe.withDefault (Source.details s) + in + Html.div + [ HtmlA.class "source" ] + [ Maybe.transformWith (Html.text sourceDetails.name) makeLink sourceDetails.url ] + + +makeLink : Html msg -> String -> Html msg +makeLink text url = + Html.blankA [ HtmlA.href url ] [ text ] + + +cardTypeClass : Card -> String +cardTypeClass card = + case card of + C _ -> + "call" + + R _ -> + "response" + + +details : Card -> Details +details card = + case card of + C call -> + call.details + + R response -> + response.details diff --git a/client/src/elm/MassiveDecks/Card/Model.elm b/client/src/elm/MassiveDecks/Card/Model.elm new file mode 100644 index 0000000..b80df3d --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Model.elm @@ -0,0 +1,99 @@ +module MassiveDecks.Card.Model exposing + ( Call + , Card(..) + , Details + , Id + , Response + , Side(..) + , Type + , UnknownResponse + , call + , frontSide + , response + ) + +import MassiveDecks.Card.Parts exposing (..) +import MassiveDecks.Card.Source.Model exposing (..) + + +{-| A unique ID for a card. +-} +type alias Id = + String + + +{-| The side of a card. +-} +type Side + = Front + | Back + + +{-| Get a side from a boolean test. +-} +frontSide : Bool -> Side +frontSide isFront = + if isFront then + Front + + else + Back + + +{-| A general card. +-} +type Card + = C Call + | R Response + + +{-| A call card. +-} +type alias Call = + Type Parts + + +{-| A simple constructor for a call card. +-} +call : Parts -> Id -> Source -> Call +call parts id source = + { details = { source = source, id = id } + , body = parts + } + + +{-| A response card. +-} +type alias Response = + Type String + + +{-| A simple constructor for a response card. +-} +response : String -> Id -> Source -> Response +response text id source = + { details = { source = source, id = id } + , body = text + } + + +{-| A response that isn't known to the user. Only the back can be displayed. +-} +type alias UnknownResponse = + Id + + +{-| The data for a type of card. +-} +type alias Type body = + { details : Details + , body : body + } + + +{-| The general details for a card. +-} +type alias Details = + { source : Source + , id : Id + } diff --git a/client/src/elm/MassiveDecks/Card/Parts.elm b/client/src/elm/MassiveDecks/Card/Parts.elm new file mode 100644 index 0000000..ef2b935 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Parts.elm @@ -0,0 +1,198 @@ +module MassiveDecks.Card.Parts exposing + ( Part(..) + , Parts + , Transform(..) + , fromList + , map + , slotCount + , unsafeFromList + , view + , viewFilled + , viewSingleLine + ) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Attributes.Aria as Aria +import MassiveDecks.Util.String as String + + +{-| A transform to apply to the value in a slot. +-} +type Transform + = UpperCase + | Capitalize + | None + + +{-| A part of a call's text. This is either just text or a position for a call to be inserted in-game. +-} +type Part + = Text String + | Slot Transform + + +{-| Represents a line as a part of a part. Between each one the text will be forced to line break. +-} +type alias Line = + List Part + + +{-| A collection of `Line`s. It is guaranteed to have at least one `Slot`. +-} +type Parts + = Parts (List Line) + + +{-| A predicate checking if a part is a slot. +-} +isSlot : Part -> Bool +isSlot part = + case part of + Text _ -> + False + + Slot _ -> + True + + +{-| Construct a `Parts` from a `List` of `Line`s. This will fail if there is not at least one `Slot`. +-} +fromList : List Line -> Maybe Parts +fromList lines = + if List.any (List.any isSlot) lines then + Just (Parts lines) + + else + Nothing + + +{-| TODO: Remove +-} +unsafeFromList : List Line -> Parts +unsafeFromList lines = + Parts lines + + +{-| Apply the given function to each `Part`, returning the resulting `List`. +-} +map : (Line -> a) -> Parts -> List a +map f lines = + lines |> extract |> List.map f + + +{-| The number of `Slot`s in the `Parts`. This will be one or more. +-} +slotCount : Parts -> Int +slotCount lines = + lines |> extract |> List.concat |> List.filter isSlot |> List.length + + +{-| Render the `Parts` to HTML. +-} +view : Parts -> List (Html msg) +view parts = + viewFilled [] parts + + +viewSingleLine : Parts -> List (Html msg) +viewSingleLine (Parts lines) = + lines |> List.concat |> viewParts [] + + +{-| Render the `Parts` with slots filled with the given values. +-} +viewFilled : List String -> Parts -> List (Html msg) +viewFilled play (Parts lines) = + viewLines play lines + + + +{- Private -} + + +viewLines : List String -> List Line -> List (Html msg) +viewLines play lines = + case lines of + firstLine :: restLines -> + let + slots = + firstLine |> List.filter isSlot |> List.length + in + (viewParts (List.take slots play) firstLine |> Html.p []) :: viewLines (List.drop slots play) restLines + + [] -> + [] + + +viewParts : List String -> List Part -> List (Html msg) +viewParts play parts = + case parts of + firstPart :: restParts -> + case firstPart of + Text string -> + viewText False string ++ viewParts play restParts + + Slot transform -> + case play of + firstPlay :: restPlay -> + viewText True (applyTransform transform firstPlay) ++ viewParts restPlay restParts + + [] -> + Html.span [ HtmlA.class "slot" ] [] :: viewParts [] restParts + + [] -> + [] + + +applyTransform : Transform -> String -> String +applyTransform transform value = + case transform of + UpperCase -> + String.toUpper value + + Capitalize -> + String.capitalise value + + None -> + value + + +viewText : Bool -> String -> List (Html msg) +viewText slot string = + let + words = + string |> splitWords |> List.map (\word -> Html.span [] [ Html.text word ]) + in + if slot then + [ Html.span [ HtmlA.class "slot" ] words ] + + else + words + + +splitWords : String -> List String +splitWords string = + case String.uncons string of + Nothing -> + [] + + Just ( first, rest ) -> + case first of + ' ' -> + String.fromChar first :: splitWords rest + + other -> + case splitWords rest of + [] -> + [ String.fromChar other ] + + head :: tail -> + String.cons other head :: tail + + +extract : Parts -> List Line +extract lines = + case lines of + Parts list -> + list diff --git a/client/src/elm/MassiveDecks/Card/Play.elm b/client/src/elm/MassiveDecks/Card/Play.elm new file mode 100644 index 0000000..b57e1f7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Play.elm @@ -0,0 +1,37 @@ +module MassiveDecks.Card.Play exposing + ( Id + , Known + , Play + , asKnown + ) + +import MassiveDecks.Card.Model as Card + + +{-| The id for a play. +-} +type alias Id = + String + + +{-| A known or unknown play. +-} +type alias Play = + { id : Id + , responses : Maybe (List Card.Response) + } + + +{-| A known play. +-} +type alias Known = + { id : Id + , responses : List Card.Response + } + + +{-| Get a play as a known play, if possible. +-} +asKnown : Play -> Maybe Known +asKnown play = + play.responses |> Maybe.map (Known play.id) diff --git a/client/src/elm/MassiveDecks/Card/Source.elm b/client/src/elm/MassiveDecks/Card/Source.elm new file mode 100644 index 0000000..b054b9c --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source.elm @@ -0,0 +1,112 @@ +module MassiveDecks.Card.Source exposing + ( default + , details + , editor + , empty + , emptyMatching + , equals + , logo + , name + , tooltip + , validate + ) + +import Html exposing (Html) +import MassiveDecks.Card.Source.Cardcast as Cardcast +import MassiveDecks.Card.Source.Fake as Fake +import MassiveDecks.Card.Source.Methods exposing (..) +import MassiveDecks.Card.Source.Model exposing (..) +import MassiveDecks.Components as Components +import MassiveDecks.Model exposing (..) +import Weightless as Wl +import Weightless.Attributes as WlA + + +default : External +default = + Cardcast.empty + + +equals : Source -> Source -> Bool +equals a b = + (methods a |> .equals) b + + +empty : String -> Maybe External +empty n = + case n of + "Cardcast" -> + Just Cardcast.empty + + _ -> + Nothing + + +emptyMatching : External -> External +emptyMatching source = + source |> Ex |> name |> empty |> Maybe.withDefault default + + +name : Source -> String +name source = + () |> (methods source |> .name) + + +validate : Source -> Maybe (Components.Message msg) +validate source = + () |> (methods source |> .problem) + + +details : Source -> Details +details source = + () |> (methods source |> .details) + + +tooltip : Source -> Maybe ( String, Html msg ) +tooltip source = + case () |> (methods source |> .tooltip) of + Just ( id, rendered ) -> + Just + ( id + , Wl.tooltip + (List.concat + [ [ WlA.anchor id + , WlA.fixed + , WlA.anchorOpenEvents [ "mouseover" ] + , WlA.anchorCloseEvents [ "mouseout" ] + ] + , WlA.anchorOrigin WlA.XRight WlA.YCenter + , WlA.transformOrigin WlA.XLeft WlA.YCenter + ] + ) + [ rendered ] + ) + + Nothing -> + Nothing + + +logo : Source -> Maybe (Html msg) +logo source = + () |> (methods source |> .logo) + + +editor : Shared -> Source -> (External -> msg) -> Html msg +editor shared source = + shared |> (methods source |> .editor) + + + +{- Private -} + + +methods : Source -> Methods msg +methods source = + case source of + Ex external -> + case external of + Cardcast playCode -> + Cardcast.methods playCode + + Fake -> + Fake.methods diff --git a/client/src/elm/MassiveDecks/Card/Source/Cardcast.elm b/client/src/elm/MassiveDecks/Card/Source/Cardcast.elm new file mode 100644 index 0000000..a2a558d --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source/Cardcast.elm @@ -0,0 +1,84 @@ +module MassiveDecks.Card.Source.Cardcast exposing (empty, methods) + +import Html as Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Card.Source.Cardcast.Model exposing (..) +import MassiveDecks.Card.Source.Methods as Source +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Components as Components +import MassiveDecks.Model exposing (..) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import Url.Builder as Url +import Weightless as Wl +import Weightless.Attributes as WlA + + +methods : PlayCode -> Source.Methods msg +methods playCode = + { problem = \() -> problem playCode + , details = \() -> details playCode + , tooltip = \() -> tooltip playCode |> Just + , logo = \() -> Just logo + , name = \() -> "Cardcast" + , editor = editor playCode + , equals = equals playCode + } + + +empty : Source.External +empty = + "" |> playCode |> Source.Cardcast + + + +{- Private -} + + +equals : PlayCode -> Source -> Bool +equals (PlayCode pc) source = + case source of + Source.Ex (Source.Cardcast (PlayCode other)) -> + pc == other + + _ -> + False + + +problem : PlayCode -> Maybe (Components.Message msg) +problem (PlayCode pc) = + if String.isEmpty pc then + Strings.CardcastEmptyPlayCode |> Components.info |> Just + + else + Nothing + + +editor : PlayCode -> Shared -> (Source.External -> msg) -> Html msg +editor (PlayCode pc) shared update = + Wl.textField + [ HtmlA.value pc + , HtmlA.class "primary" + , WlA.outlined + , HtmlE.onInput (playCode >> Source.Cardcast >> update) + , Strings.CardcastPlayCode |> Lang.label shared + ] + [ Html.span [ WlA.textFieldSlot WlA.BeforeText ] [ logo ] ] + + +details : PlayCode -> Source.Details +details (PlayCode pc) = + { name = "Cardcast " ++ pc + , url = Just (Url.crossOrigin "https://www.cardcastgame.com" [ "browse", "deck", pc ] []) + } + + +tooltip : PlayCode -> ( String, Html msg ) +tooltip (PlayCode pc) = + ( "cardcast-" ++ pc, Html.span [] [ logo, Html.text pc ] ) + + +logo : Html msg +logo = + Html.span [ HtmlA.class "cardcast-logo" ] [] diff --git a/client/src/elm/MassiveDecks/Card/Source/Cardcast/Model.elm b/client/src/elm/MassiveDecks/Card/Source/Cardcast/Model.elm new file mode 100644 index 0000000..eb45c43 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source/Cardcast/Model.elm @@ -0,0 +1,16 @@ +module MassiveDecks.Card.Source.Cardcast.Model exposing (PlayCode(..), playCode) + +{-| -} + + +{-| A code for a Cardcast deck. +-} +type PlayCode + = PlayCode String + + +{-| Create a cardcast PlayCode from a string. +-} +playCode : String -> PlayCode +playCode string = + string |> String.toUpper |> PlayCode diff --git a/client/src/elm/MassiveDecks/Card/Source/Fake.elm b/client/src/elm/MassiveDecks/Card/Source/Fake.elm new file mode 100644 index 0000000..4dacd1e --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source/Fake.elm @@ -0,0 +1,20 @@ +module MassiveDecks.Card.Source.Fake exposing (methods) + +import MassiveDecks.Card.Source.Methods as Source +import MassiveDecks.Util.Html as Html + + +methods : Source.Methods msg +methods = + { problem = \() -> Nothing + , details = + \() -> + { name = "" + , url = Nothing + } + , tooltip = \() -> Nothing + , logo = \() -> Nothing + , name = \() -> "" + , editor = \_ -> \_ -> Html.nothing + , equals = \_ -> False + } diff --git a/client/src/elm/MassiveDecks/Card/Source/Methods.elm b/client/src/elm/MassiveDecks/Card/Source/Methods.elm new file mode 100644 index 0000000..7ca3b6c --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source/Methods.elm @@ -0,0 +1,19 @@ +module MassiveDecks.Card.Source.Methods exposing (Methods) + +import Html exposing (Html) +import MassiveDecks.Card.Source.Model exposing (..) +import MassiveDecks.Components as Components +import MassiveDecks.Model exposing (..) + + +{-| A collection of methods applied to the source data. +-} +type alias Methods msg = + { problem : () -> Maybe (Components.Message msg) + , details : () -> Details + , tooltip : () -> Maybe ( String, Html msg ) + , logo : () -> Maybe (Html msg) + , name : () -> String + , editor : Shared -> (External -> msg) -> Html msg + , equals : Source -> Bool + } diff --git a/client/src/elm/MassiveDecks/Card/Source/Model.elm b/client/src/elm/MassiveDecks/Card/Source/Model.elm new file mode 100644 index 0000000..6943eb9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Source/Model.elm @@ -0,0 +1,46 @@ +module MassiveDecks.Card.Source.Model exposing + ( Details + , External(..) + , LoadFailureReason(..) + , Source(..) + , Summary + ) + +import MassiveDecks.Card.Source.Cardcast.Model as Cardcast + + +{-| Details on where game data came from. +-} +type Source + = Ex External + | Fake + + +{-| An external source. +-} +type External + = Cardcast Cardcast.PlayCode + + +{-| A summary of the contents of the source deck. +-} +type alias Summary = + { details : Details + , calls : Int + , responses : Int + } + + +{-| Details about the source deck. +-} +type alias Details = + { name : String + , url : Maybe String + } + + +{-| The reason a source failed to load. +-} +type LoadFailureReason + = SourceFailure + | NotFound diff --git a/client/src/elm/MassiveDecks/Cast.elm b/client/src/elm/MassiveDecks/Cast.elm new file mode 100644 index 0000000..d8afff9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Cast.elm @@ -0,0 +1,114 @@ +module MassiveDecks.Cast exposing (main) + +{-| An alternative main module used when acting as a cast server. +-} + +import Browser +import Browser.Navigation as Navigation +import Html exposing (Html) +import Html.Attributes as HtmlA +import Json.Decode as Json +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Pages.Lobby.Token as Token +import MassiveDecks.Pages.Spectate as Spectate +import MassiveDecks.Pages.Spectate.Model as Spectate +import MassiveDecks.Settings as Settings +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util as Util +import MassiveDecks.Util.Url as Url +import Url exposing (Url) + + +type alias Model = + { shared : Shared + , spectate : Maybe Spectate.Model + } + + +main : Program Json.Value Model Msg +main = + Browser.application + { init = init + , view = view + , update = update + , subscriptions = subscriptions + , onUrlRequest = onUrlRequest + , onUrlChange = onUrlChange + } + + + +{- Private -} + + +init : Json.Value -> Url -> Navigation.Key -> ( Model, Cmd Msg ) +init flags url key = + let + castFlags = + flags + |> Json.decodeValue Decoders.castFlags + |> Result.toMaybe + + auth = + castFlags + |> Maybe.map (.token >> Token.decode) + |> Maybe.andThen Result.toMaybe + + ( spectate, cmd ) = + case auth of + Just a -> + Spectate.init { lobby = { gameCode = a.claims.gc } } |> Util.modelLift Just + + Nothing -> + ( Nothing, Cmd.none ) + + ( settings, _ ) = + Settings.init Settings.defaults + + shared = + { language = castFlags |> Maybe.map .language |> Maybe.withDefault Lang.defaultLanguage + , key = key + , origin = Url.origin url + , settings = settings + , browserLanguage = Nothing + , castStatus = Cast.NoDevicesAvailable + } + in + ( { shared = shared, spectate = spectate }, cmd ) + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none + + +onUrlRequest : Browser.UrlRequest -> Msg +onUrlRequest _ = + NoOp + + +onUrlChange : Url -> Msg +onUrlChange _ = + NoOp + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update _ model = + ( model, Cmd.none ) + + +view : Model -> Browser.Document Msg +view model = + { title = Strings.MassiveDecks |> Lang.string model.shared + , body = model.spectate |> Maybe.map (Spectate.view model.shared) |> Maybe.withDefault (errorView model.shared) + } + + +errorView : Shared -> List (Html msg) +errorView shared = + -- TODO: Real impl + [ Html.div [ HtmlA.class "spectate" ] [ Strings.CastError |> Lang.html shared ] ] diff --git a/client/src/elm/MassiveDecks/Cast/Client.elm b/client/src/elm/MassiveDecks/Cast/Client.elm new file mode 100644 index 0000000..eed2417 --- /dev/null +++ b/client/src/elm/MassiveDecks/Cast/Client.elm @@ -0,0 +1,32 @@ +module MassiveDecks.Cast.Client exposing + ( subscriptions + , tryCast + ) + +import Json.Decode as Json +import MassiveDecks.Cast.Model exposing (..) +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Models.Encoders as Encoders +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Ports as Ports +import MassiveDecks.Strings.Languages as Lang + + +{-| Try to start a cast session. +-} +tryCast : Shared -> Lobby.Token -> Cmd msg +tryCast shared token = + { token = token, language = Lang.currentLanguage shared } |> Encoders.castFlags |> Ports.tryCast + + +{-| The subscription to get messages about cast status changes. +-} +subscriptions : Sub Msg +subscriptions = + Json.decodeValue Decoders.castStatus + >> Result.toMaybe + >> Maybe.withDefault NoDevicesAvailable + >> CastStatusUpdate + |> Ports.castStatus diff --git a/client/src/elm/MassiveDecks/Cast/Model.elm b/client/src/elm/MassiveDecks/Cast/Model.elm new file mode 100644 index 0000000..b8cab0b --- /dev/null +++ b/client/src/elm/MassiveDecks/Cast/Model.elm @@ -0,0 +1,24 @@ +module MassiveDecks.Cast.Model exposing + ( Flags + , Status(..) + ) + +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Strings.Languages.Model exposing (Language) + + +{-| The cast state. +-} +type Status + = NoDevicesAvailable + | NotConnected + | Connecting + | Connected String + + +{-| The initial date required for the cast. +-} +type alias Flags = + { token : Lobby.Token + , language : Language + } diff --git a/client/src/elm/MassiveDecks/Components.elm b/client/src/elm/MassiveDecks/Components.elm new file mode 100644 index 0000000..a1d8980 --- /dev/null +++ b/client/src/elm/MassiveDecks/Components.elm @@ -0,0 +1,160 @@ +module MassiveDecks.Components exposing + ( Fix + , Message + , Severity(..) + , error + , errorWithFix + , floatingActionButton + , formSection + , iconButton + , iconButtonStyled + , info + , linkButton + , message + , warning + ) + +{-| Reusable interface elements. +-} + +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon exposing (Icon) +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Model exposing (..) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| A section containing inputs and messages.. +-} +formSection : Shared -> String -> Html msg -> List (Message msg) -> Html msg +formSection shared id component messages = + Html.div [ HtmlA.id id, HtmlA.class "form-section" ] (component :: (messages |> List.filterMap (message shared))) + + +type alias Fix msg = + { text : MdString + , msg : msg + } + + +type Severity + = Info + | Warning + | Error + + +type alias Message msg = + Maybe (InternalMessage msg) + + +message : Shared -> Message msg -> Maybe (Html msg) +message shared msg = + msg |> Maybe.map (internalMessage shared) + + +info : MdString -> Message msg +info mdString = + Just + { severity = Info + , description = mdString + , fix = Nothing + } + + +warning : MdString -> Message msg +warning mdString = + Just + { severity = Warning + , description = mdString + , fix = Nothing + } + + +error : MdString -> Message msg +error mdString = + Just + { severity = Error + , description = mdString + , fix = Nothing + } + + +errorWithFix : MdString -> MdString -> msg -> Message msg +errorWithFix errorString fixString fix = + Just + { severity = Error + , description = errorString + , fix = Just { text = fixString, msg = fix } + } + + +linkButton : List (Html.Attribute msg) -> List (Html msg) -> Html msg +linkButton attrs contents = + Html.button (HtmlA.class "link-button" :: attrs) contents + + +{-| A button that is just an icon. +-} +iconButton : List (Html.Attribute msg) -> Icon -> Html msg +iconButton attrs icon = + iconButtonStyled attrs ( [], icon ) + + +{-| A button that is just an icon with styles on the icon. +-} +iconButtonStyled : List (Html.Attribute msg) -> ( List (Html.Attribute msg), Icon ) -> Html msg +iconButtonStyled attrs ( styles, icon ) = + Wl.button (List.concat [ [ WlA.fab, WlA.inverted, WlA.flat ], attrs ]) [ Icon.viewStyled styles icon ] + + +{-| A circular button designed to be the primary action on a page. +Only one of these should exist on screen at any time. +-} +floatingActionButton : List (Html.Attribute msg) -> Icon -> Html msg +floatingActionButton attrs icon = + Wl.button (List.concat [ [ WlA.fab ], attrs ]) [ Icon.view icon ] + + + +{- Private -} + + +type alias InternalMessage msg = + { severity : Severity + , description : MdString + , fix : Maybe (Fix msg) + } + + +internalMessage : Shared -> InternalMessage msg -> Html msg +internalMessage shared { severity, description, fix } = + let + ( class, icon ) = + case severity of + Info -> + ( "info", Icon.infoCircle ) + + Warning -> + ( "warning", Icon.exclamationTriangle ) + + Error -> + ( "inline-error", Icon.exclamationCircle ) + + fixLink = + case fix of + Just { text, msg } -> + [ Html.text " ", linkButton [ msg |> HtmlE.onClick ] [ text |> Lang.html shared ] ] + + Nothing -> + [] + in + Html.span [ HtmlA.class class ] + [ Icon.viewStyled [ Icon.fw ] icon + , Html.span [] ((description |> Lang.html shared) :: fixLink) + ] diff --git a/client/src/elm/MassiveDecks/Components/Menu.elm b/client/src/elm/MassiveDecks/Components/Menu.elm new file mode 100644 index 0000000..814b37a --- /dev/null +++ b/client/src/elm/MassiveDecks/Components/Menu.elm @@ -0,0 +1,102 @@ +module MassiveDecks.Components.Menu exposing + ( Item + , Menu + , Part(..) + , item + , view + ) + +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon exposing (Icon) +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Model exposing (..) +import MassiveDecks.Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Html as Html +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| A menu. +-} +type alias Menu msg = + List (Part msg) + + +{-| A part of a menu. +-} +type Part msg + = Part (Item msg) + | Separator + | Nothing + + +{-| A menu item +-} +type alias Item msg = + { icon : Icon + , text : MdString + , description : MdString + , action : Maybe msg + } + + +{-| Convenience function for an item part. +-} +item : Icon -> MdString -> MdString -> Maybe msg -> Part msg +item icon text description action = + Item icon text description action |> Part + + +{-| Render a menu to Html. +-} +view : Shared -> String -> WlA.XOrigin -> WlA.YOrigin -> Menu msg -> Html msg +view shared anchorId xOrigin yOrigin menu = + Wl.popover + (List.concat + [ [ WlA.anchor anchorId + , WlA.fixed + , WlA.anchorOpenEvents [ "click" ] + , HtmlA.id "game-menu" + , HtmlA.class "menu" + , WlA.disableFocusTrap + ] + , WlA.anchorOrigin xOrigin yOrigin + ] + ) + [ Wl.popoverCard [] [ Html.ul [] (menu |> List.map (menuItem shared)) ] ] + + + +{- Private -} + + +menuItem : Shared -> Part msg -> Html msg +menuItem shared mi = + case mi of + Part { icon, text, description, action } -> + let + actionAttrs = + action + |> Maybe.map (\m -> [ m |> HtmlE.onClick, WlA.clickable ]) + |> Maybe.withDefault [ WlA.disabled ] + + attrs = + (description |> Lang.title shared) :: actionAttrs + in + Html.li [] + [ Wl.listItem attrs + [ Icon.viewStyled [ WlA.listItemSlot WlA.BeforeItem, Icon.fw ] icon + + -- We have an icon already, so we don't want to enhance this. + , text |> Lang.string shared |> Html.text + ] + ] + + Separator -> + Html.li [] [ Html.hr [] [] ] + + Nothing -> + Html.nothing diff --git a/client/src/elm/MassiveDecks/Error.elm b/client/src/elm/MassiveDecks/Error.elm new file mode 100644 index 0000000..12f8801 --- /dev/null +++ b/client/src/elm/MassiveDecks/Error.elm @@ -0,0 +1,146 @@ +module MassiveDecks.Error exposing (view) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import Http +import Json.Decode as Json +import MassiveDecks.Error.Model exposing (..) +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Route as Route exposing (Route) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Version exposing (version) +import Url.Builder +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| A view of an error. +-} +view : Shared -> Route -> Error -> Html msg +view shared route error = + let + model = + render error + + report = + model.details |> Maybe.map (body shared route model.description) + + reportView = + report |> Maybe.map (viewReport shared) |> Maybe.withDefault [] + in + Wl.expansion + (List.concat + [ [ WlA.name "errors", HtmlA.class "error" ] + , [ WlA.disabled ] |> Maybe.justIf (report == Nothing) |> Maybe.withDefault [] + ] + ) + (Html.span [ WlA.expansionSlot WlA.ETitle ] [ Strings.Error |> Lang.html shared ] + :: Html.span [ WlA.expansionSlot WlA.EDescription ] [ model.description |> Lang.html shared ] + :: reportView + ) + + + +{- Private -} + + +viewReport : Shared -> String -> List (Html msg) +viewReport shared report = + let + github = + "https://github.com" + + path = + [ "Lattyware", "massivedecks", "issues", "new" ] + + reportBody = + (Strings.ReportErrorBody |> Lang.string shared) + ++ "\n\n" + ++ report + + url = + Url.Builder.crossOrigin github path [ Url.Builder.string "body" reportBody ] + in + [ Html.div [ HtmlA.class "report" ] + [ Wl.textArea [ HtmlA.class "body", WlA.readonly, WlA.value report ] [] + , Html.span [ HtmlA.class "link" ] + [ Html.blankA [ HtmlA.href url ] [ Strings.ReportError |> Lang.html shared ] + ] + ] + ] + + +body : Shared -> Route -> MdString -> String -> String +body shared route description details = + let + developerInfo = + -- Intentionally untranslated - this is for developer use, not users. + "\n\nContext for developers:\n\tVersion: " + ++ version + ++ "\n\tPage: " + ++ Route.externalUrl shared.origin route + ++ "\n\tEnglish Error: " + ++ Lang.givenLanguageString Lang.En description + ++ "\n\tDetails: " + ++ details + in + (description |> Lang.string shared) + ++ developerInfo + + +type alias Model = + { description : MdString + , details : Maybe String + } + + +render : Error -> Model +render error = + case error of + Http httpError -> + case httpError of + Http.NetworkError -> + Model Strings.NetworkError Nothing + + Http.BadBody message -> + Model Strings.BadPayloadError ("Decoding error: " ++ message |> Just) + + Http.BadStatus code -> + case code of + 504 -> + Model Strings.ServerDownError Nothing + + _ -> + Model Strings.BadStatusError (code |> String.fromInt |> Just) + + Http.Timeout -> + Model Strings.TimeoutError Nothing + + Http.BadUrl url -> + Model Strings.BadUrlError (Just ("Url: " ++ url)) + + Token tokenError -> + case tokenError of + Lobby.InvalidTokenStructure token -> + Model Strings.BadPayloadError ("Token didn't have expected structure: " ++ token |> Just) + + Lobby.TokenBase64Error msg -> + Model Strings.BadPayloadError ("Base 64 decode error on token: " ++ msg |> Just) + + Lobby.TokenJsonError jsonError -> + let + err = + jsonError |> Json |> render + in + { err | details = Just ("JSON decode error on token: " ++ (err.details |> Maybe.withDefault "")) } + + Json jsonError -> + Model Strings.BadPayloadError (jsonError |> Json.errorToString |> Just) + + Generic string -> + Model string Nothing diff --git a/client/src/elm/MassiveDecks/Error/Messages.elm b/client/src/elm/MassiveDecks/Error/Messages.elm new file mode 100644 index 0000000..ba9f9e5 --- /dev/null +++ b/client/src/elm/MassiveDecks/Error/Messages.elm @@ -0,0 +1,8 @@ +module MassiveDecks.Error.Messages exposing (Msg(..)) + +import MassiveDecks.Error.Model exposing (Error) + + +type Msg + = Add Error + | Clear diff --git a/client/src/elm/MassiveDecks/Error/Model.elm b/client/src/elm/MassiveDecks/Error/Model.elm new file mode 100644 index 0000000..9d81b66 --- /dev/null +++ b/client/src/elm/MassiveDecks/Error/Model.elm @@ -0,0 +1,24 @@ +module MassiveDecks.Error.Model exposing + ( Error(..) + , Overlay + ) + +import Http +import Json.Decode as Json +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Strings exposing (MdString) + + +{-| A generic error for the application as a whole. +-} +type Error + = Http Http.Error + | Json Json.Error + | Token Lobby.TokenDecodingError + | Generic MdString + + +{-| An overlay displaying a number of errors. +-} +type alias Overlay = + { errors : List Error } diff --git a/client/src/elm/MassiveDecks/Error/Overlay.elm b/client/src/elm/MassiveDecks/Error/Overlay.elm new file mode 100644 index 0000000..6e24c90 --- /dev/null +++ b/client/src/elm/MassiveDecks/Error/Overlay.elm @@ -0,0 +1,63 @@ +module MassiveDecks.Error.Overlay exposing (init, update, view) + +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Components as Components +import MassiveDecks.Error as Error +import MassiveDecks.Error.Messages exposing (..) +import MassiveDecks.Error.Model as Error exposing (Error) +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Route as Route exposing (Route) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import Weightless as Wl +import Weightless.Attributes as WlA + + +init : Error.Overlay +init = + { errors = [] } + + +view : Shared -> Route -> Error.Overlay -> List (Html Global.Msg) +view shared route model = + if List.isEmpty model.errors then + [] + + else + [ Html.div [ HtmlA.class "error-overlay" ] + [ Html.div [ HtmlA.class "actions" ] + [ Components.iconButton + [ HtmlA.class "close" + , Clear |> Global.ErrorMsg |> HtmlE.onClick + , Strings.Close |> Lang.title shared + ] + Icon.times + , Components.iconButton + [ HtmlA.class "refresh" + , Global.Refresh |> HtmlE.onClick + , Strings.Refresh |> Lang.title shared + ] + Icon.redo + ] + , Wl.card [ HtmlA.class "help" ] + [ Html.h3 [] [ Lang.html shared Strings.ErrorHelpTitle ] + , Html.p [] [ Lang.html shared Strings.ErrorHelp ] + ] + , Html.div [ HtmlA.class "errors" ] (List.map (Error.view shared route) model.errors) + ] + ] + + +update : Msg -> Error.Overlay -> ( Error.Overlay, Cmd Global.Msg ) +update msg model = + case msg of + Add error -> + ( { model | errors = error :: model.errors }, Cmd.none ) + + Clear -> + ( { model | errors = [] }, Cmd.none ) diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm new file mode 100644 index 0000000..2956949 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game.elm @@ -0,0 +1,454 @@ +module MassiveDecks.Game exposing + ( applyGameEvent + , applyGameStarted + , init + , subscriptions + , update + , view + ) + +import Browser.Dom as Dom +import Dict exposing (Dict) +import FontAwesome.Icon as Icon exposing (Icon) +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Game.Action as Action +import MassiveDecks.Game.Action.Model as Action exposing (Action) +import MassiveDecks.Game.Messages exposing (..) +import MassiveDecks.Game.Model exposing (..) +import MassiveDecks.Game.Player as Player exposing (Player) +import MassiveDecks.Game.Round as Round exposing (Round) +import MassiveDecks.Game.Round.Complete as Complete +import MassiveDecks.Game.Round.Judging as Judging +import MassiveDecks.Game.Round.Playing as Playing +import MassiveDecks.Game.Round.Revealing as Revealing +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Actions as Actions +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Events as Events +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util as Util +import MassiveDecks.Util.Html as Html +import Set +import Task +import Weightless as Wl +import Weightless.Attributes as WlA + + +init : Game -> List Card.Response -> Round.Pick -> ( Model, Cmd Global.Msg ) +init game hand pick = + let + model = + emptyModel game + + ( round, cmd ) = + case game.round of + Round.P r -> + Util.modelLift Round.P (Playing.init r pick) + + _ -> + ( game.round, Cmd.none ) + in + ( { model | hand = hand, game = { game | round = round } }, cmd ) + + +update : Msg -> Model -> ( Model, Cmd Global.Msg ) +update msg model = + let + game = + model.game + in + case msg of + Pick id -> + case game.round of + Round.P playingRound -> + let + picks = + playingRound.pick + + picked = + if List.member id picks.cards then + List.filter ((/=) id) picks.cards + + else + picks.cards ++ [ id ] + + extra = + max 0 (List.length picked - (playingRound.call |> Card.slotCount)) + + newRound = + Round.P { playingRound | pick = { picks | cards = picked |> List.drop extra } } + in + ( { model | game = { game | round = newRound } }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + PickPlay id -> + let + newRound = + case game.round of + Round.J judging -> + { judging | pick = Just id } |> Round.J + + _ -> + game.round + in + ( { model | game = { game | round = newRound } }, Cmd.none ) + + Submit -> + case game.round of + Round.P playingRound -> + let + picks = + playingRound.pick + + newRound = + Round.P { playingRound | pick = { picks | state = Round.Submitted } } + in + ( { model | game = { game | round = newRound } }, Actions.submit picks.cards ) + + _ -> + ( model, Cmd.none ) + + TakeBack -> + case game.round of + Round.P playingRound -> + let + picks = + playingRound.pick + + newRound = + Round.P { playingRound | pick = { picks | state = Round.Selected } } + in + ( { model | game = { game | round = newRound } }, Actions.takeBack ) + + _ -> + ( model, Cmd.none ) + + ScrollToTop -> + ( model, Dom.setViewportOf "scroll-frame" 0 0 |> Task.attempt (\_ -> Global.NoOp) ) + + Reveal play -> + ( model, Actions.reveal play ) + + Judge -> + let + cmd = + case game.round of + Round.J judging -> + judging.pick |> Maybe.map Actions.judge |> Maybe.withDefault Cmd.none + + _ -> + Cmd.none + in + ( model, cmd ) + + Like -> + -- TODO: Actually sending this somewhere. + let + newRound = + case game.round of + Round.J judging -> + { judging + | liked = + judging.pick + |> Maybe.map (\p -> Set.insert p judging.liked) + |> Maybe.withDefault judging.liked + } + |> Round.J + + _ -> + game.round + in + ( { model | game = { game | round = newRound } }, Cmd.none ) + + SetPlayStyles playStyles -> + ( { model | playStyles = playStyles }, Cmd.none ) + + +subscriptions : Model -> Sub Global.Msg +subscriptions model = + Sub.none + + +view : Shared -> Lobby.Auth -> List Configure.Deck -> Dict User.Id User -> Model -> Html Global.Msg +view shared auth decks users model = + let + { instruction, action, content, fillCallWith } = + case game.round of + Round.P round -> + Playing.view shared auth decks model round + + Round.R round -> + Revealing.view shared auth decks round + + Round.J round -> + Judging.view shared auth decks round + + Round.C round -> + Complete.view shared decks users round + + game = + model.game + + { call } = + Round.data game.round + + parts = + fillCallWith |> List.map .body + + renderedCall = + call |> Card.viewFilled shared decks Card.Front [] parts + + help = + case instruction of + Just i -> + Html.div [ HtmlA.class "context-help" ] [ Icon.view Icon.questionCircle, i |> Lang.html shared ] + + Nothing -> + Html.nothing + in + Html.div [ HtmlA.id "game" ] + [ help + , Html.div [ HtmlA.class "card-area" ] + [ Html.div [ HtmlA.class "play-area" ] + [ Html.div [ HtmlA.class "cards-in-play" ] [ renderedCall ] + , viewAction shared action + ] + , content + , Html.div [ HtmlA.class "scroll-top-spacer" ] [] + + -- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping. + , Html.div [ HtmlA.class "scroll-top" ] + [ Wl.button + [ WlA.flat + , WlA.fab + , WlA.inverted + , ScrollToTop |> lift |> HtmlE.onClick + ] + [ Icon.view Icon.arrowUp ] + ] + ] + ] + + +viewAction : Shared -> Maybe Action -> Html Global.Msg +viewAction shared visible = + Html.div [] (Action.actions |> List.map (Action.view shared visible)) + + +applyGameEvent : Events.GameEvent -> Model -> ( Model, Cmd Global.Msg ) +applyGameEvent gameEvent model = + case gameEvent of + Events.HandRedrawn redraw -> + case redraw of + Events.Player { hand } -> + -- TODO: Impl + ( model, Cmd.none ) + + Events.Other { player } -> + --let + -- playerRecord = + -- Dict.get player model.game.players + -- + -- newPlayerRecord = + -- Dict.insert player { playerRecord | score = playerRecord.score - lobby.config.rules.houseRules } + --in + -- TODO: Impl + ( model, Cmd.none ) + + Events.PlayRevealed { id, play } -> + let + game = + model.game + + newRound = + case model.game.round of + Round.R r -> + let + plays = + r.plays |> List.map (reveal id play) + + known = + plays |> List.filterMap Play.asKnown + in + if List.length known == List.length plays then + Round.judging r.id r.czar r.players r.call known |> Round.J + + else + { r | plays = plays } |> Round.R + + _ -> + -- TODO: Error + model.game.round + in + ( { model | game = { game | round = newRound } }, Cmd.none ) + + Events.PlaySubmitted { by } -> + case model.game.round of + Round.P round -> + let + game = + model.game + in + ( { model | game = { game | round = Round.P { round | played = Set.insert by round.played } } } + , Cmd.none + ) + + _ -> + -- TODO: Error + ( model, Cmd.none ) + + Events.PlayTakenBack { by } -> + case model.game.round of + Round.P round -> + let + game = + model.game + in + ( { model | game = { game | round = Round.P { round | played = Set.remove by round.played } } } + , Cmd.none + ) + + _ -> + -- TODO: Error + ( model, Cmd.none ) + + Events.RoundFinished { winner, playedBy } -> + let + game = + model.game + + newPlayers = + game.players |> Dict.update winner (Maybe.map (\p -> { p | score = p.score + 1 })) + + newRound = + case model.game.round of + Round.J r -> + let + plays = + r.plays |> List.filterMap (resolvePlayedBy playedBy) |> Dict.fromList + in + Round.complete r.id r.czar r.players r.call plays winner |> Round.C + + _ -> + -- TODO: Error + model.game.round + in + ( { model | game = { game | round = newRound, players = newPlayers } }, Cmd.none ) + + Events.RoundStarted { id, czar, players, call, drawn } -> + let + oldGame = + model.game + + drawnAsList = + drawn |> Maybe.withDefault [] + + ( newRound, cmd ) = + Playing.init (Round.playing id czar players call Set.empty) Round.noPick + + game = + { oldGame | round = Round.P newRound } + in + ( { model | game = game, hand = model.hand ++ drawnAsList }, cmd ) + + Events.StartRevealing { plays } -> + case model.game.round of + Round.P r -> + let + oldGame = + model.game + + newRound = + Round.revealing + r.id + r.czar + r.players + r.call + (plays |> List.map (\id -> Play id Nothing)) + |> Round.R + in + ( { model | game = { oldGame | round = newRound } }, Cmd.none ) + + _ -> + -- TODO: Error + ( model, Cmd.none ) + + +applyGameStarted : Lobby -> Round.Playing -> List Card.Response -> ( Model, Cmd Global.Msg ) +applyGameStarted lobby round hand = + let + users = + lobby.users |> Dict.toList |> List.map (\( id, _ ) -> id) + + defaultPlayer = + { control = Player.Human, score = 0 } + + game = + { round = Round.P round + , history = [] + , playerOrder = users + , players = + users + |> List.map (\id -> ( id, defaultPlayer )) + |> Dict.fromList + , rules = lobby.config.rules + , winner = Nothing + } + in + init game hand Round.noPick + + + +{- Private -} + + +reveal : Play.Id -> List Card.Response -> Play -> Play +reveal target responses play = + case play.responses of + Nothing -> + if play.id == target then + Play play.id (Just responses) + + else + play + + _ -> + play + + +resolvePlayedBy : Dict Play.Id User.Id -> Play.Known -> Maybe ( User.Id, List Card.Response ) +resolvePlayedBy playedBy k = + Dict.get k.id playedBy |> Maybe.map (\u -> ( u, k.responses )) + + +type alias ActionDetails msg = + { content : List (Html msg) + , title : MdString + } + + +type alias RoundDetails msg = + { instruction : MdString + , callback : List (Html.Attribute msg) + , picks : List Card.Id + , callFlipped : Bool + , playing : Bool + } + + +lift : Msg -> Global.Msg +lift msg = + msg |> Lobby.GameMsg |> Global.LobbyMsg diff --git a/client/src/elm/MassiveDecks/Game/Action.elm b/client/src/elm/MassiveDecks/Game/Action.elm new file mode 100644 index 0000000..d521ec6 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Action.elm @@ -0,0 +1,59 @@ +module MassiveDecks.Game.Action exposing (actions, view) + +import FontAwesome.Icon exposing (Icon) +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Components as Components +import MassiveDecks.Game.Action.Model exposing (..) +import MassiveDecks.Game.Messages as Game +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Html.Attributes as HtmlA +import Weightless.Attributes as WlA + + +actions : List Action +actions = + [ Submit, TakeBack, Judge, Like ] + + +view : Shared -> Maybe Action -> Action -> Html Global.Msg +view shared visible action = + let + { icon, attrs, title, onClick } = + case action of + Submit -> + IconView Icon.check [ HtmlA.class "important" ] Strings.SubmitPlay Game.Submit + + TakeBack -> + IconView Icon.undo [ WlA.inverted, WlA.outlined ] Strings.TakeBackPlay Game.TakeBack + + Judge -> + IconView Icon.trophy [ HtmlA.class "important" ] Strings.JudgePlay Game.Judge + + Like -> + IconView Icon.thumbsUp [ WlA.inverted, WlA.outlined ] Strings.LikePlay Game.Like + in + Components.floatingActionButton + (List.concat + [ [ title |> Lang.title shared + , onClick |> Lobby.GameMsg |> Global.LobbyMsg |> HtmlE.onClick + , HtmlA.classList [ ( "action", True ), ( "exited", visible /= Just action ) ] + ] + , attrs + ] + ) + icon + + +type alias IconView = + { icon : Icon + , attrs : List (Html.Attribute Global.Msg) + , title : MdString + , onClick : Game.Msg + } diff --git a/client/src/elm/MassiveDecks/Game/Action/Model.elm b/client/src/elm/MassiveDecks/Game/Action/Model.elm new file mode 100644 index 0000000..d4f54f9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Action/Model.elm @@ -0,0 +1,8 @@ +module MassiveDecks.Game.Action.Model exposing (Action(..)) + + +type Action + = Submit + | TakeBack + | Judge + | Like diff --git a/client/src/elm/MassiveDecks/Game/Messages.elm b/client/src/elm/MassiveDecks/Game/Messages.elm new file mode 100644 index 0000000..5ad19a4 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Messages.elm @@ -0,0 +1,17 @@ +module MassiveDecks.Game.Messages exposing (Msg(..)) + +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play +import MassiveDecks.Game.Model exposing (..) + + +type Msg + = Pick Card.Id + | Submit + | TakeBack + | PickPlay Play.Id + | Reveal Play.Id + | Judge + | Like + | ScrollToTop + | SetPlayStyles PlayStyles diff --git a/client/src/elm/MassiveDecks/Game/Model.elm b/client/src/elm/MassiveDecks/Game/Model.elm new file mode 100644 index 0000000..454e421 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Model.elm @@ -0,0 +1,79 @@ +module MassiveDecks.Game.Model exposing + ( CardStyle + , Game + , Model + , PlayStyle + , PlayStyles + , RoundView + , emptyModel + ) + +import Dict exposing (Dict) +import Html exposing (Html) +import MassiveDecks.Card.Model as Card +import MassiveDecks.Game.Action.Model exposing (Action) +import MassiveDecks.Game.Player exposing (Player) +import MassiveDecks.Game.Round as Round exposing (Round) +import MassiveDecks.Game.Rules exposing (Rules) +import MassiveDecks.Strings exposing (MdString) +import MassiveDecks.User as User + + +{-| A model for the game scene. +-} +type alias Model = + { game : Game + , hand : List Card.Response + , playStyles : PlayStyles + } + + +{-| A model not yet configured with player-specific data. +-} +emptyModel : Game -> Model +emptyModel game = + { game = game + , hand = [] + , playStyles = Dict.empty + } + + +{-| A game. +-} +type alias Game = + { round : Round + , history : List Round.Complete + , playerOrder : List User.Id + , players : Dict User.Id Player + , rules : Rules + , winner : Maybe User.Id + } + + +{-| A view of a round in the game. +-} +type alias RoundView msg = + { instruction : Maybe MdString + , action : Maybe Action + , content : Html msg + , fillCallWith : List Card.Response + } + + +{-| Data to render all the plays in a round. +-} +type alias PlayStyles = + Dict User.Id PlayStyle + + +{-| Data to render a play. +-} +type alias PlayStyle = + List CardStyle + + +{-| Data to render a card in a play. +-} +type alias CardStyle = + { rotation : Float + } diff --git a/client/src/elm/MassiveDecks/Game/Player.elm b/client/src/elm/MassiveDecks/Game/Player.elm new file mode 100644 index 0000000..6989717 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Player.elm @@ -0,0 +1,77 @@ +module MassiveDecks.Game.Player exposing + ( Control(..) + , PlayState(..) + , Player + , Role(..) + , Score + , isCzar + , playState + , role + ) + +import MassiveDecks.Game.Round as Round exposing (Round) +import MassiveDecks.User as User +import Set + + +{-| A player in the game. +-} +type alias Player = + { control : Control + , score : Score + } + + +{-| A score in the game. +-} +type alias Score = + Int + + +{-| How the player is being controlled. +-} +type Control + = Human + | Computer + + +{-| The role the player currently has in the game. +-} +type Role + = RCzar + | RPlayer + + +{-| The state of a player in regards to playing into a round. +-} +type PlayState + = NotInRound + | Playing + | Played + + +role : Round -> User.Id -> Role +role round id = + if isCzar round id then + RCzar + + else + RPlayer + + +isCzar : Round -> User.Id -> Bool +isCzar round id = + (round |> Round.data |> .czar) == id + + +playState : Round.Playing -> User.Id -> PlayState +playState round id = + if Set.member id round.players then + if Set.member id round.played then + Played + + else + Playing + + else + NotInRound diff --git a/client/src/elm/MassiveDecks/Game/Round.elm b/client/src/elm/MassiveDecks/Game/Round.elm new file mode 100644 index 0000000..9ea5421 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round.elm @@ -0,0 +1,160 @@ +module MassiveDecks.Game.Round exposing + ( Complete + , Data + , Id + , Judging + , Pick + , PickState(..) + , Playing + , Revealing + , Round(..) + , complete + , data + , idDecoder + , judging + , noPick + , playing + , revealing + ) + +import Dict exposing (Dict) +import Json.Decode as Json +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.User as User +import Set exposing (Set) + + +{-| A unique id for a round. +-} +type Id + = Id String + + +idDecoder : Json.Decoder Id +idDecoder = + Json.string |> Json.map Id + + +{-| A round during a game. +-} +type Round + = P Playing + | R Revealing + | J Judging + | C Complete + + +{-| A round while users are playing a round. +-} +type alias Playing = + Data { played : Set User.Id, pick : Pick } + + +playing : Id -> User.Id -> Set User.Id -> Card.Call -> Set User.Id -> Playing +playing id czar players call played = + { id = id + , czar = czar + , players = players + , call = call + , played = played + , pick = { state = Selected, cards = [] } + } + + +type alias Revealing = + Data { plays : List Play } + + +revealing : Id -> User.Id -> Set User.Id -> Card.Call -> List Play -> Revealing +revealing id czar players call plays = + { id = id + , czar = czar + , players = players + , call = call + , plays = plays + } + + +{-| A round while the czar is judging a round. +-} +type alias Judging = + Data { plays : List Play.Known, pick : Maybe Play.Id, liked : Set Play.Id } + + +judging : Id -> User.Id -> Set User.Id -> Card.Call -> List Play.Known -> Judging +judging id czar players call plays = + { id = id + , czar = czar + , players = players + , call = call + , plays = plays + , pick = Nothing + , liked = Set.empty + } + + +{-| A round that has been finished. +-} +type alias Complete = + Data { plays : Dict User.Id (List Card.Response), winner : User.Id } + + +complete : Id -> User.Id -> Set User.Id -> Card.Call -> Dict User.Id (List Card.Response) -> User.Id -> Complete +complete id czar players call plays winner = + { id = id + , czar = czar + , players = players + , call = call + , plays = plays + , winner = winner + } + + +{-| Data common to all rounds. +-} +type alias Data specific = + { specific + | id : Id + , czar : User.Id + , players : Set User.Id + , call : Card.Call + } + + +{-| The user's pick for the round. +-} +type alias Pick = + { state : PickState, cards : List Card.Id } + + +noPick = + { state = Selected, cards = [] } + + +{-| Whether the pick has been committed to the server yet. +-} +type PickState + = Selected + | Submitted + + +data : Round -> Data {} +data round = + case round of + P rd -> + extract rd + + J rd -> + extract rd + + C rd -> + extract rd + + R rd -> + extract rd + + +extract : Data a -> Data {} +extract rd = + { id = rd.id, call = rd.call, czar = rd.czar, players = rd.players } diff --git a/client/src/elm/MassiveDecks/Game/Round/Complete.elm b/client/src/elm/MassiveDecks/Game/Round/Complete.elm new file mode 100644 index 0000000..d4a30f8 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round/Complete.elm @@ -0,0 +1,63 @@ +module MassiveDecks.Game.Round.Complete exposing (view) + +import Dict exposing (Dict) +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Keyed as HtmlK +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card +import MassiveDecks.Game.Model exposing (..) +import MassiveDecks.Game.Round as Round +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe + + +view : Shared -> List Configure.Deck -> Dict User.Id User -> Round.Complete -> RoundView Global.Msg +view shared decks users round = + let + winning = + round.plays |> Dict.get round.winner + in + { instruction = Nothing + , action = Nothing + , content = + HtmlK.ul [ HtmlA.class "judging plays" ] + (round.plays |> Dict.toList |> List.map (viewPlay shared decks users round.winner)) + , fillCallWith = winning |> Maybe.withDefault [] + } + + +viewPlay : + Shared + -> List Configure.Deck + -> Dict User.Id User + -> User.Id + -> ( User.Id, List Card.Response ) + -> ( String, Html Global.Msg ) +viewPlay shared decks users winner ( id, responses ) = + let + cards = + responses |> List.map (\r -> Card.view shared decks Card.Front [] (Card.R r)) + + playedBy = + users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) + + byline = + [ Icon.view Icon.trophy |> Maybe.justIf (winner == id) |> Maybe.withDefault Html.nothing + , Html.text playedBy + ] + in + ( id + , Html.li [ HtmlA.class "play-with-byline" ] + [ Html.span [ HtmlA.class "byline" ] byline + , Html.div [ HtmlA.class "play" ] cards + ] + ) diff --git a/client/src/elm/MassiveDecks/Game/Round/Judging.elm b/client/src/elm/MassiveDecks/Game/Round/Judging.elm new file mode 100644 index 0000000..42afdcc --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round/Judging.elm @@ -0,0 +1,68 @@ +module MassiveDecks.Game.Round.Judging exposing (view) + +import Html.Attributes as HtmlA +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play +import MassiveDecks.Game.Action.Model as Action +import MassiveDecks.Game.Messages exposing (..) +import MassiveDecks.Game.Model exposing (..) +import MassiveDecks.Game.Player as Player +import MassiveDecks.Game.Round as Round +import MassiveDecks.Game.Round.Plays as Plays +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model exposing (Auth) +import MassiveDecks.Strings as Strings +import MassiveDecks.Util.Maybe as Maybe +import Set exposing (Set) + + +view : Shared -> Auth -> List Configure.Deck -> Round.Judging -> RoundView Global.Msg +view shared auth decks round = + let + role = + Player.role (Round.J round) auth.claims.uid + + ( action, instruction ) = + case role of + Player.RCzar -> + ( Maybe.map (always Action.Judge) round.pick, Strings.RevealPlaysInstruction ) + + Player.RPlayer -> + ( Maybe.andThen (\p -> Maybe.justIf (not (Set.member p round.liked)) Action.Like) round.pick, Strings.WaitingForCzarInstruction ) + + picked = + round.plays + |> List.filter (\play -> Just play.id == round.pick) + |> List.head + |> Maybe.map .responses + |> Maybe.withDefault [] + + details = + round.plays |> List.map (playDetails shared decks round.liked) + in + { instruction = Just instruction + , action = action + , content = details |> Plays.view "judging" round.pick + , fillCallWith = picked + } + + + +{- Private -} + + +playDetails : Shared -> List Configure.Deck -> Set Play.Id -> Play.Known -> Plays.Details Global.Msg +playDetails shared decks liked { id, responses } = + let + cards = + responses + |> List.map (\r -> Card.view shared decks Card.Front [] (Card.R r)) + + attrs = + [ HtmlA.class "liked" ] |> Maybe.justIf (Set.member id liked) |> Maybe.withDefault [] + in + Plays.Details id cards (id |> PickPlay |> Lobby.GameMsg |> Global.LobbyMsg |> Just) attrs diff --git a/client/src/elm/MassiveDecks/Game/Round/Playing.elm b/client/src/elm/MassiveDecks/Game/Round/Playing.elm new file mode 100644 index 0000000..ae6217f --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round/Playing.elm @@ -0,0 +1,146 @@ +module MassiveDecks.Game.Round.Playing exposing + ( init + , view + ) + +import Dict exposing (Dict) +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card +import MassiveDecks.Game.Action.Model as Action +import MassiveDecks.Game.Messages as Game +import MassiveDecks.Game.Model exposing (..) +import MassiveDecks.Game.Player as Player +import MassiveDecks.Game.Round as Round +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Strings as Strings +import MassiveDecks.User as User +import MassiveDecks.Util.List as List +import MassiveDecks.Util.Random as Random +import Random +import Set exposing (Set) + + +init : Round.Playing -> Round.Pick -> ( Round.Playing, Cmd Global.Msg ) +init round pick = + let + cmd = + round.players + |> playStylesGenerator (Card.slotCount round.call) + |> Random.generate (Game.SetPlayStyles >> lift) + in + ( { round | pick = pick } + , cmd + ) + + +view : Shared -> Lobby.Auth -> List Configure.Deck -> Model -> Round.Playing -> RoundView Global.Msg +view shared auth decks model round = + let + slots = + Card.slotCount round.call + + missingFromPick = + slots - (round.pick.cards |> List.length) + + ( action, instruction, notPlaying ) = + if Player.isCzar (Round.P round) auth.claims.uid then + ( Nothing, Strings.CzarsDontPlayInstruction, True ) + + else + case round.pick.state of + Round.Selected -> + if missingFromPick > 0 then + ( Nothing, Strings.PlayInstruction { numberOfCards = missingFromPick }, False ) + + else + ( Just Action.Submit, Strings.SubmitInstruction, False ) + + Round.Submitted -> + ( Just Action.TakeBack, Strings.WaitingForPlaysInstruction, True ) + + hand = + Html.div [ HtmlA.classList [ ( "hand", True ), ( "not-playing", notPlaying ) ] ] + (model.hand |> List.map (round.pick.cards |> viewHandCard shared decks)) + + backgroundPlays = + Html.div [ HtmlA.class "background-plays" ] + (round.players |> Set.toList |> List.map (viewBackgroundPlay model.playStyles slots round.played)) + + picked = + round.pick.cards + |> List.map (\id -> List.find (\c -> c.details.id == id) model.hand) + |> List.filterMap identity + in + { instruction = Just instruction + , action = action + , content = + Html.div [] + [ hand + , backgroundPlays + ] + , fillCallWith = picked + } + + + +{- Private -} + + +viewHandCard : Shared -> List Configure.Deck -> List Card.Id -> Card.Response -> Html Global.Msg +viewHandCard shared decks picked response = + Card.view + shared + decks + Card.Front + [ HtmlA.classList [ ( "picked", List.member response.details.id picked ) ] + , response.details.id |> Game.Pick |> lift |> HtmlE.onClick + ] + (Card.R response) + + +viewBackgroundPlay : PlayStyles -> Int -> Set User.Id -> User.Id -> Html msg +viewBackgroundPlay playStyles slots played for = + let + -- TODO: Move to css variable --rotation when possible. + cards = + playStyles + |> Dict.get for + |> Maybe.withDefault (List.repeat slots { rotation = 0 }) + |> List.map viewBackgroundPlayCard + in + Html.div [ HtmlA.classList [ ( "play", True ), ( "played", Set.member for played ) ] ] cards + + +viewBackgroundPlayCard : CardStyle -> Html msg +viewBackgroundPlayCard playStyle = + Card.viewUnknownResponse [ "rotate(" ++ String.fromFloat playStyle.rotation ++ "turn)" |> HtmlA.style "transform" ] + + +lift : Game.Msg -> Global.Msg +lift msg = + msg |> Lobby.GameMsg |> Global.LobbyMsg + + +playStylesGenerator : Int -> Set User.Id -> Random.Generator PlayStyles +playStylesGenerator cards players = + Random.map Dict.fromList + (players |> Set.toList |> List.map (playStylesEntryGenerator cards) |> Random.disparateList) + + +playStylesEntryGenerator : Int -> User.Id -> Random.Generator ( User.Id, List CardStyle ) +playStylesEntryGenerator cards userId = + Random.map (\playStyles -> ( userId, playStyles )) + (Random.list cards playStyleGenerator) + + +playStyleGenerator : Random.Generator CardStyle +playStyleGenerator = + Random.map CardStyle + (Random.float 0 1) diff --git a/client/src/elm/MassiveDecks/Game/Round/Plays.elm b/client/src/elm/MassiveDecks/Game/Round/Plays.elm new file mode 100644 index 0000000..74cae4f --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round/Plays.elm @@ -0,0 +1,44 @@ +module MassiveDecks.Game.Round.Plays exposing (Details, view) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Html.Keyed as HtmlK +import MassiveDecks.Card.Play as Play +import MassiveDecks.Util.Html.Attributes as HtmlA + + +{-| The details needed to render a specific play. +-} +type alias Details msg = + { id : Play.Id + , responses : List (Html msg) + , action : Maybe msg + , attrs : List (Html.Attribute msg) + } + + +{-| View a collection of plays. +-} +view : String -> Maybe Play.Id -> List (Details msg) -> Html msg +view class picked details = + HtmlK.ul [ HtmlA.classList [ ( class, True ), ( "plays", True ) ] ] (details |> List.map (viewPlay picked)) + + + +{- Private -} + + +viewPlay : Maybe Play.Id -> Details msg -> ( String, Html msg ) +viewPlay picked { id, responses, action, attrs } = + ( id + , Html.li + (List.concat + [ [ HtmlA.classList [ ( "play", True ), ( "picked", picked == Just id ) ] + , action |> Maybe.map HtmlE.onClick |> Maybe.withDefault HtmlA.nothing + ] + , attrs + ] + ) + (responses |> List.map (\card -> Html.div [ HtmlA.class "overlap" ] [ card ])) + ) diff --git a/client/src/elm/MassiveDecks/Game/Round/Revealing.elm b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm new file mode 100644 index 0000000..1eeb216 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm @@ -0,0 +1,63 @@ +module MassiveDecks.Game.Round.Revealing exposing (view) + +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Game.Messages exposing (..) +import MassiveDecks.Game.Model exposing (RoundView) +import MassiveDecks.Game.Player as Player +import MassiveDecks.Game.Round as Round +import MassiveDecks.Game.Round.Plays as Plays +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model exposing (Auth) +import MassiveDecks.Strings as Strings +import MassiveDecks.Util.Maybe as Maybe + + +view : Shared -> Auth -> List Configure.Deck -> Round.Revealing -> RoundView Global.Msg +view shared auth decks round = + let + role = + Player.role (Round.R round) auth.claims.uid + + instruction = + case role of + Player.RCzar -> + Strings.RevealPlaysInstruction + + Player.RPlayer -> + Strings.WaitingForCzarInstruction + + slots = + Card.slotCount round.call + + plays = + round.plays |> List.map (playDetails shared decks slots (role == Player.RCzar)) + + -- TODO: Last revealed. + lastRevealed = + [] + in + { instruction = Just instruction + , action = Nothing + , content = plays |> Plays.view "revealing" Nothing + , fillCallWith = lastRevealed + } + + + +{- Private -} + + +playDetails : Shared -> List Configure.Deck -> Int -> Bool -> Play -> Plays.Details Global.Msg +playDetails shared decks slots isCzar { id, responses } = + let + cards = + responses + |> Maybe.map (List.map (\r -> Card.view shared decks Card.Front [] (Card.R r))) + |> Maybe.withDefault (List.repeat slots (Card.viewUnknownResponse [])) + in + Plays.Details id cards (Reveal id |> Lobby.GameMsg |> Global.LobbyMsg |> Maybe.justIf isCzar) [] diff --git a/client/src/elm/MassiveDecks/Game/Rules.elm b/client/src/elm/MassiveDecks/Game/Rules.elm new file mode 100644 index 0000000..1eae60e --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/Rules.elm @@ -0,0 +1,115 @@ +module MassiveDecks.Game.Rules exposing + ( HouseRule + , HouseRuleChange(..) + , HouseRules + , PackingHeat + , Rando + , Reboot + , Rules + , apply + , packingHeat + , rando + , reboot + ) + +{-| Game rules. +-} + +import MassiveDecks.Strings as Strings exposing (MdString) + + +{-| The base rules for a game. +-} +type alias Rules = + { handSize : Int + , scoreLimit : Maybe Int + , houseRules : HouseRules + } + + +type alias HouseRules = + { rando : Maybe Rando + , packingHeat : Maybe PackingHeat + , reboot : Maybe Reboot + } + + +type alias PackingHeat = + {} + + +type alias Reboot = + { cost : Int } + + +type alias Rando = + { number : Int } + + +type HouseRuleChange + = RandoChange (Maybe Rando) + | PackingHeatChange (Maybe PackingHeat) + | RebootChange (Maybe Reboot) + + +apply : HouseRuleChange -> HouseRules -> HouseRules +apply change houseRules = + case change of + RandoChange c -> + { houseRules | rando = c } + + PackingHeatChange c -> + { houseRules | packingHeat = c } + + RebootChange c -> + { houseRules | reboot = c } + + +type alias HouseRule a = + { default : a + , change : Maybe a -> HouseRuleChange + , title : MdString + , description : Maybe a -> MdString + , extract : HouseRules -> Maybe a + , insert : Maybe a -> HouseRules -> HouseRules + } + + +rando : HouseRule Rando +rando = + { default = { number = 1 } + , change = RandoChange + , title = Strings.HouseRuleRandoCardrissian + , description = always Strings.HouseRuleRandoCardrissianDescription + , extract = .rando + , insert = \r -> \hr -> { hr | rando = r } + } + + +reboot : HouseRule Reboot +reboot = + let + description = + Maybe.map .cost + >> Maybe.map (\c -> { cost = Just c }) + >> Maybe.withDefault { cost = Nothing } + >> Strings.HouseRuleRebootDescription + in + { default = { cost = 1 } + , change = RebootChange + , title = Strings.HouseRuleReboot + , description = description + , extract = .reboot + , insert = \r -> \hr -> { hr | reboot = r } + } + + +packingHeat : HouseRule PackingHeat +packingHeat = + { default = {} + , change = PackingHeatChange + , title = Strings.HouseRulePackingHeat + , description = always Strings.HouseRulePackingHeatDescription + , extract = .packingHeat + , insert = \ph -> \hr -> { hr | packingHeat = ph } + } diff --git a/client/src/elm/MassiveDecks/LocalStorage.elm b/client/src/elm/MassiveDecks/LocalStorage.elm new file mode 100644 index 0000000..1c75565 --- /dev/null +++ b/client/src/elm/MassiveDecks/LocalStorage.elm @@ -0,0 +1,20 @@ +module MassiveDecks.LocalStorage exposing + ( Store + , store + ) + +import Json.Decode as Json +import MassiveDecks.Models.Encoders as Encoders +import MassiveDecks.Ports as Ports +import MassiveDecks.Settings.Model as Settings exposing (Settings) + + +{-| The type for an outbound port for storing user settings in local storage. +-} +type alias Store msg = + Json.Value -> Cmd msg + + +store : Settings -> Cmd msg +store settings = + settings |> Encoders.settings |> Ports.storeSettings diff --git a/client/src/elm/MassiveDecks/Messages.elm b/client/src/elm/MassiveDecks/Messages.elm new file mode 100644 index 0000000..fe15bf8 --- /dev/null +++ b/client/src/elm/MassiveDecks/Messages.elm @@ -0,0 +1,30 @@ +module MassiveDecks.Messages exposing (Msg(..)) + +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Error.Messages as Error +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Route exposing (Route) +import MassiveDecks.Pages.Spectate.Messages as Spectate +import MassiveDecks.Pages.Start.Messages as Start +import MassiveDecks.Settings.Messages as Settings +import Url exposing (Url) + + +{-| The main message type for the application. +-} +type Msg + = ChangePage Route + | PageChanged Url + | JoinLobby String Lobby.Auth + | SpectateMsg Spectate.Msg + | StartMsg Start.Msg + | LobbyMsg Lobby.Msg + | SettingsMsg Settings.Msg + | ErrorMsg Error.Msg + | CastStatusUpdate Cast.Status + | TryCast Lobby.Auth + | Refresh + | BlockedExternalUrl + | Copy String + | NoOp diff --git a/client/src/elm/MassiveDecks/Model.elm b/client/src/elm/MassiveDecks/Model.elm new file mode 100644 index 0000000..9cf4ec6 --- /dev/null +++ b/client/src/elm/MassiveDecks/Model.elm @@ -0,0 +1,29 @@ +port module MassiveDecks.Model exposing + ( Flags + , Shared + ) + +import Browser.Navigation as Navigation +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Settings.Model as Settings exposing (Settings) +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) + + +{-| Model shared by all pages. +-} +type alias Shared = + { language : Language + , origin : String + , key : Navigation.Key + , settings : Settings.Model + , browserLanguage : Maybe Language + , castStatus : Cast.Status + } + + +{-| Flags passed to the application from init code. +-} +type alias Flags = + { settings : Settings + , browserLanguages : List String + } diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm new file mode 100644 index 0000000..5043e32 --- /dev/null +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -0,0 +1,937 @@ +module MassiveDecks.Models.Decoders exposing + ( castFlags + , castStatus + , event + , eventOrMdError + , externalSource + , flags + , gameCode + , language + , lobbyState + , lobbySummary + , lobbyToken + , privilege + , revealingRound + , settings + , tokenValidity + , userId + , userSummary + ) + +import Dict exposing (Dict) +import Json.Decode as Json +import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Parts as Parts exposing (Part, Parts) +import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Card.Source.Cardcast.Model as Cardcast +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Game.Model as Game exposing (Game) +import MassiveDecks.Game.Player as Player exposing (Player) +import MassiveDecks.Game.Round as Round exposing (Round) +import MassiveDecks.Game.Rules as Rules exposing (Rules) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Models.MdError as MdError exposing (MdError) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Events as Events exposing (Event) +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby) +import MassiveDecks.Pages.Start.LobbyBrowser.Model as LobbyBrowser +import MassiveDecks.Settings.Model as Settings exposing (Settings) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) +import MassiveDecks.User as User exposing (User) +import Set exposing (Set) + + +castStatus : Json.Decoder Cast.Status +castStatus = + Json.field "status" Json.string + |> Json.andThen + (\status -> + case status of + "NoDevicesAvailable" -> + Json.succeed Cast.NoDevicesAvailable + + "NotConnected" -> + Json.succeed Cast.NotConnected + + "Connecting" -> + Json.succeed Cast.Connecting + + "Connected" -> + Json.map Cast.Connected + (Json.field "name" Json.string) + + _ -> + Json.fail ("Unknown cast status: " ++ status) + ) + + +castFlags : Json.Decoder Cast.Flags +castFlags = + Json.map2 Cast.Flags + (Json.field "token" lobbyToken) + (Json.field "language" language) + + +flags : Json.Decoder Flags +flags = + Json.map2 Flags + (Json.field "settings" settings) + (Json.field "browserLanguages" (Json.list Json.string)) + + +settings : Json.Decoder Settings +settings = + Json.map6 Settings + (Json.field "tokens" (Json.dict lobbyToken)) + (Json.field "openUserList" Json.bool) + (Json.maybe (Json.field "lastUsedName" Json.string)) + (Json.field "recentDecks" (Json.list externalSource)) + (Json.maybe (Json.field "chosenLanguage" language)) + (Json.field "compactCards" Json.bool) + + +gameCode : Json.Decoder GameCode +gameCode = + Json.string |> Json.map GameCode.trusted + + +externalSource : Json.Decoder Source.External +externalSource = + Json.field "source" Json.string |> Json.andThen sourceByName + + +source : Json.Decoder Source +source = + externalSource |> Json.map Source.Ex + + +sourceByName : String -> Json.Decoder Source.External +sourceByName name = + case name of + "Cardcast" -> + Json.field "playCode" Json.string |> Json.map (Cardcast.playCode >> Source.Cardcast) + + _ -> + "Unknown source \"" ++ name ++ "\"" |> Json.fail + + +tokenValidity : Json.Decoder (Dict Lobby.Token Bool) +tokenValidity = + Json.dict Json.bool + + +lobbyToken : Json.Decoder Lobby.Token +lobbyToken = + Json.string + + +language : Json.Decoder Language +language = + Json.string + |> Json.andThen + (\code -> + case Lang.fromCode code of + Just lang -> + Json.succeed lang + + Nothing -> + Json.fail ("Unknown language code: '" ++ code ++ "'.") + ) + + +lobby : Json.Decoder Lobby +lobby = + Json.map6 Lobby + (Json.field "name" Json.string) + (Json.field "public" Json.bool) + (Json.field "users" users) + (Json.field "owner" userId) + (Json.field "config" config) + --(Json.field "game" game |> Json.map (Game.emptyModel >> Just)) + (Json.maybe (Json.field "game" game |> Json.map Game.emptyModel)) + + +game : Json.Decoder Game +game = + Json.map6 Game + (Json.field "round" round) + (Json.field "history" (Json.list completeRound)) + (Json.field "playerOrder" (Json.list userId)) + (Json.field "players" (Json.dict player)) + (Json.field "rules" rules) + (Json.maybe (Json.field "winner" userId)) + + +config : Json.Decoder Configure.Config +config = + Json.map4 Configure.Config + (Json.field "rules" rules) + (Json.field "decks" (Json.list deck)) + (Json.maybe (Json.field "password" Json.string)) + (Json.field "version" Json.string) + + +deck : Json.Decoder Configure.Deck +deck = + Json.map2 Configure.Deck + (Json.field "source" externalSource) + (Json.maybe (Json.field "summary" summary)) + + +summary : Json.Decoder Source.Summary +summary = + Json.map3 Source.Summary + (Json.field "details" details) + (Json.field "calls" Json.int) + (Json.field "responses" Json.int) + + +details : Json.Decoder Source.Details +details = + Json.map2 Source.Details + (Json.field "name" Json.string) + (Json.maybe (Json.field "url" Json.string)) + + +rules : Json.Decoder Rules +rules = + Json.map3 Rules + (Json.field "handSize" Json.int) + (Json.maybe (Json.field "scoreLimit" score)) + (Json.field "houseRules" houseRules) + + +houseRules : Json.Decoder Rules.HouseRules +houseRules = + Json.map3 Rules.HouseRules + (Json.maybe (Json.field "rando" rando)) + (Json.maybe (Json.field "packingHeat" packingHeat)) + (Json.maybe (Json.field "reboot" reboot)) + + +packingHeat : Json.Decoder Rules.PackingHeat +packingHeat = + {} |> Json.succeed + + +reboot : Json.Decoder Rules.Reboot +reboot = + Json.map Rules.Reboot + (Json.field "cost" score) + + +rando : Json.Decoder Rules.Rando +rando = + Json.map Rules.Rando + (Json.field "number" score) + + +player : Json.Decoder Player +player = + Json.map2 Player + (Json.field "control" control) + (Json.field "score" score) + + +control : Json.Decoder Player.Control +control = + Json.string + |> Json.andThen + (\name -> + case name of + "Human" -> + Json.succeed Player.Human + + "Computer" -> + Json.succeed Player.Computer + + _ -> + Json.fail ("Unknown controller: " ++ name) + ) + + +score : Json.Decoder Player.Score +score = + Json.int + + +users : Json.Decoder (Dict User.Id User) +users = + Json.dict user + + +user : Json.Decoder User +user = + Json.map5 User + (Json.field "name" Json.string) + (Json.field "presence" userPresence) + (Json.field "connection" userConnection) + (Json.field "privilege" privilege) + (Json.field "role" userRole) + + +userConnection : Json.Decoder User.Connection +userConnection = + Json.string + |> Json.andThen + (\name -> + case name of + "Connected" -> + Json.succeed User.Connected + + "Disconnected" -> + Json.succeed User.Disconnected + + _ -> + Json.fail ("Unknown connection state: " ++ name) + ) + + +userPresence : Json.Decoder User.Presence +userPresence = + Json.string + |> Json.andThen + (\name -> + case name of + "Joined" -> + Json.succeed User.Joined + + "Left" -> + Json.succeed User.Left + + _ -> + Json.fail ("Unknown presence state: " ++ name) + ) + + +role : Json.Decoder User.Role +role = + Json.string + |> Json.andThen + (\name -> + case name of + "Spectator" -> + Json.succeed User.Spectator + + "Player" -> + Json.succeed User.Player + + _ -> + Json.fail ("Unknown role: " ++ name) + ) + + +lobbyState : Json.Decoder Lobby.State +lobbyState = + Json.string + |> Json.andThen + (\name -> + case name of + "Playing" -> + Json.succeed Lobby.Playing + + "SettingUp" -> + Json.succeed Lobby.SettingUp + + _ -> + Json.fail ("Unknown lobby state: " ++ name) + ) + + +lobbySummary : Json.Decoder LobbyBrowser.Summary +lobbySummary = + Json.map4 LobbyBrowser.Summary + (Json.field "name" Json.string) + (Json.field "gameCode" gameCode) + (Json.field "state" lobbyState) + (Json.field "users" userSummary) + + +userId : Json.Decoder User.Id +userId = + Json.string + + +privilege : Json.Decoder User.Privilege +privilege = + Json.string + |> Json.andThen + (\name -> + case name of + "Privileged" -> + Json.succeed User.Privileged + + "Unprivileged" -> + Json.succeed User.Unprivileged + + _ -> + Json.fail ("Unknown privilege level '" ++ name ++ "'.") + ) + + +userSummary : Json.Decoder LobbyBrowser.UserSummary +userSummary = + Json.map2 LobbyBrowser.UserSummary + (Json.field "players" Json.int) + (Json.field "spectators" Json.int) + + +eventOrMdError : Json.Decoder (Result MdError Event) +eventOrMdError = + Json.oneOf + [ event |> Json.map Result.Ok + , mdError |> Json.map Result.Err + ] + + +event : Json.Decoder Event +event = + Json.field "event" Json.string |> Json.andThen eventByName + + +eventByName : String -> Json.Decoder Event +eventByName name = + case name of + "Sync" -> + sync + + "Connected" -> + connection User.Connected + + "Disconnected" -> + connection User.Disconnected + + "Joined" -> + Json.field "name" Json.string + |> Json.andThen (\n -> Events.UserJoined { name = n } |> presence) + + "Left" -> + presence Events.UserLeft + + "DecksChanged" -> + configured decksChanged + + "ScoreLimitSet" -> + configured scoreLimitSet + + "HandSizeSet" -> + configured handSizeSet + + "PasswordSet" -> + configured passwordSet + + "HouseRuleChanged" -> + configured houseRuleChanged + + "GameStarted" -> + gameStarted + + "RoundStarted" -> + gameEvent roundStarted + + "PlaySubmitted" -> + gameEvent playSubmitted + + "PlayTakenBack" -> + gameEvent playTakenBack + + "StartRevealing" -> + gameEvent startRevealing + + "PlayRevealed" -> + gameEvent playRevealed + + "RoundFinished" -> + gameEvent roundFinished + + _ -> + Json.fail ("Unknown event '" ++ name ++ "'.") + + +roundFinished : Json.Decoder Events.GameEvent +roundFinished = + Json.map2 (\wi -> \pb -> Events.RoundFinished { winner = wi, playedBy = pb }) + (Json.field "winner" userId) + (Json.field "playedBy" (Json.dict userId)) + + +playRevealed : Json.Decoder Events.GameEvent +playRevealed = + Json.map2 (\id -> \pl -> Events.PlayRevealed { id = id, play = pl }) + (Json.field "id" playId) + (Json.field "play" (Json.list response)) + + +startRevealing : Json.Decoder Events.GameEvent +startRevealing = + Json.map (\ps -> Events.StartRevealing { plays = ps }) + (Json.field "plays" (Json.list playId)) + + +gameEvent : Json.Decoder Events.GameEvent -> Json.Decoder Event +gameEvent = + Json.map Events.Game + + +playSubmitted : Json.Decoder Events.GameEvent +playSubmitted = + Json.map (\by -> Events.PlaySubmitted { by = by }) + (Json.field "by" userId) + + +playTakenBack : Json.Decoder Events.GameEvent +playTakenBack = + Json.map (\by -> Events.PlayTakenBack { by = by }) + (Json.field "by" userId) + + +houseRuleChanged : Json.Decoder Events.ConfigChanged +houseRuleChanged = + Json.map (\c -> Events.HouseRuleChanged { change = c }) + (Json.field "change" houseRuleChange) + + +houseRuleChange : Json.Decoder Rules.HouseRuleChange +houseRuleChange = + Json.andThen houseRuleChangeFromName + (Json.field "houseRule" Json.string) + + +houseRuleChangeFromName : String -> Json.Decoder Rules.HouseRuleChange +houseRuleChangeFromName name = + case name of + "Rando" -> + maybeHouseRuleChange (Json.field "number" Json.int) Rules.Rando Rules.RandoChange + + "PackingHeat" -> + maybeHouseRuleChange (Json.succeed ()) (always Rules.PackingHeat) Rules.PackingHeatChange + + "Reboot" -> + maybeHouseRuleChange (Json.field "cost" Json.int) Rules.Reboot Rules.RebootChange + + _ -> + Json.fail ("Unknown house rule (for change) \"" ++ name ++ "\"") + + +maybeHouseRuleChange : + Json.Decoder a + -> (a -> b) + -> (Maybe b -> Rules.HouseRuleChange) + -> Json.Decoder Rules.HouseRuleChange +maybeHouseRuleChange parseSettings wrapSettings wrapChange = + Json.map (Maybe.map wrapSettings >> wrapChange) + (Json.maybe (Json.field "settings" parseSettings)) + + +roundStarted : Json.Decoder Events.GameEvent +roundStarted = + Json.map5 (\id -> \ca -> \cz -> \pl -> \d -> { id = id, call = ca, czar = cz, players = pl, drawn = d } |> Events.RoundStarted) + (Json.field "id" Round.idDecoder) + (Json.field "call" call) + (Json.field "czar" userId) + (Json.field "players" playerSet) + (Json.maybe (Json.field "drawn" (Json.list response))) + + +gameStarted : Json.Decoder Events.Event +gameStarted = + Json.map2 (\r -> \h -> { round = r, hand = h } |> Events.GameStarted) + (Json.field "round" playingRound) + (Json.field "hand" (Json.list response)) + + +sync : Json.Decoder Event +sync = + Json.map3 (\ls -> \h -> \p -> Events.Sync { state = ls, hand = h, play = p }) + (Json.field "state" lobby) + (Json.maybe (Json.field "hand" (Json.list response))) + (Json.maybe (Json.field "play" (Json.list cardId))) + + +connection : User.Connection -> Json.Decoder Event +connection state = + Json.map (\u -> Events.Connection { user = u, state = state }) + (Json.field "user" userId) + + +presence : Events.PresenceState -> Json.Decoder Event +presence state = + Json.map (\u -> Events.Presence { user = u, state = state }) + (Json.field "user" userId) + + +passwordSet : Json.Decoder Events.ConfigChanged +passwordSet = + Json.map (\password -> Events.PasswordSet { password = password }) + (Json.maybe (Json.field "password" Json.string)) + + +configured : Json.Decoder Events.ConfigChanged -> Json.Decoder Event +configured configChangedDecoder = + Json.map2 (\c -> \v -> Events.Configured { change = c, version = v }) + configChangedDecoder + (Json.field "version" Json.string) + + +decksChanged : Json.Decoder Events.ConfigChanged +decksChanged = + Json.map2 (\c -> \d -> Events.DecksChanged { change = c, deck = d }) + (Json.field "change" deckChange) + (Json.field "deck" externalSource) + + +scoreLimitSet : Json.Decoder Events.ConfigChanged +scoreLimitSet = + Json.map (\limit -> Events.ScoreLimitSet { limit = limit }) + (Json.maybe (Json.field "scoreLimit" Json.int)) + + +handSizeSet : Json.Decoder Events.ConfigChanged +handSizeSet = + Json.map (\size -> Events.HandSizeSet { size = size }) + (Json.field "handSize" Json.int) + + +deckChange : Json.Decoder Events.DeckChange +deckChange = + Json.oneOf + [ Json.field "change" Json.string |> Json.andThen deckChangeByName + , Json.string |> Json.andThen deckChangeByName + ] + + +deckChangeByName : String -> Json.Decoder Events.DeckChange +deckChangeByName name = + case name of + "Add" -> + Json.succeed Events.Add + + "Remove" -> + Json.succeed Events.Remove + + "Load" -> + Json.field "summary" summary |> Json.map (\s -> Events.Load { summary = s }) + + "Fail" -> + Json.field "reason" failReason |> Json.map (\r -> Events.Fail { reason = r }) + + _ -> + "Unknown deck change \"" ++ name ++ "\"" |> Json.fail + + +failReason : Json.Decoder Source.LoadFailureReason +failReason = + Json.string |> Json.andThen failReasonByName + + +failReasonByName : String -> Json.Decoder Source.LoadFailureReason +failReasonByName name = + case name of + "SourceFailure" -> + Json.succeed Source.SourceFailure + + "NotFound" -> + Json.succeed Source.NotFound + + _ -> + "Unknown failure reason \"" ++ name ++ "\"" |> Json.fail + + +play : Json.Decoder Play +play = + Json.map2 Play + (Json.field "id" playId) + (Json.maybe (Json.field "play" (Json.list response))) + + +playId : Json.Decoder Play.Id +playId = + Json.string + + +knownPlay : Json.Decoder Play.Known +knownPlay = + Json.map2 Play.Known + (Json.field "id" playId) + (Json.field "play" (Json.list response)) + + +cardId : Json.Decoder Card.Id +cardId = + Json.string + + +card : Json.Decoder Card +card = + Json.oneOf [ call |> Json.map Card.C, response |> Json.map Card.R ] + + +response : Json.Decoder Card.Response +response = + Json.map3 Card.response + (Json.field "text" Json.string) + (Json.field "id" cardId) + (Json.field "source" source) + + +call : Json.Decoder Card.Call +call = + Json.map3 Card.call + (Json.field "parts" parts) + (Json.field "id" cardId) + (Json.field "source" source) + + +parts : Json.Decoder Parts +parts = + Json.list (Json.list part) + |> Json.map Parts.fromList + |> Json.andThen (Maybe.map Json.succeed >> Maybe.withDefault (Json.fail "Given no slot in call.")) + + +part : Json.Decoder Part +part = + Json.oneOf + [ Json.string |> Json.map Parts.Text + , transform |> Json.map Parts.Slot + ] + + +transform : Json.Decoder Parts.Transform +transform = + Json.maybe (Json.field "transform" Json.string) |> Json.andThen transformByName + + +transformByName : Maybe String -> Json.Decoder Parts.Transform +transformByName maybeName = + case maybeName of + Nothing -> + Json.succeed Parts.None + + Just name -> + case name of + "UpperCase" -> + Json.succeed Parts.UpperCase + + "Capitalize" -> + Json.succeed Parts.Capitalize + + _ -> + Json.fail ("Unknown transform '" ++ name ++ "'.") + + +round : Json.Decoder Round +round = + Json.field "stage" Json.string |> Json.andThen roundByName + + +roundByName : String -> Json.Decoder Round +roundByName name = + case name of + "Playing" -> + playingRound |> Json.map Round.P + + "Revealing" -> + revealingRound |> Json.map Round.R + + "Judging" -> + judgingRound |> Json.map Round.J + + "Complete" -> + completeRound |> Json.map Round.C + + _ -> + Json.fail ("Unknown round stage '" ++ name ++ "'.") + + +playerSet : Json.Decoder (Set User.Id) +playerSet = + Json.list userId |> Json.map Set.fromList + + +playingRound : Json.Decoder Round.Playing +playingRound = + Json.map5 Round.playing + (Json.field "id" Round.idDecoder) + (Json.field "czar" userId) + (Json.field "players" playerSet) + (Json.field "call" call) + (Json.field "played" playerSet) + + +revealingRound : Json.Decoder Round.Revealing +revealingRound = + Json.map5 Round.revealing + (Json.field "id" Round.idDecoder) + (Json.field "czar" userId) + (Json.field "players" playerSet) + (Json.field "call" call) + (Json.field "plays" (Json.list play)) + + +judgingRound : Json.Decoder Round.Judging +judgingRound = + Json.map5 Round.judging + (Json.field "id" Round.idDecoder) + (Json.field "czar" userId) + (Json.field "players" playerSet) + (Json.field "call" call) + (Json.field "plays" (Json.list knownPlay)) + + +completeRound : Json.Decoder Round.Complete +completeRound = + Json.map6 Round.complete + (Json.field "id" Round.idDecoder) + (Json.field "czar" userId) + (Json.field "players" playerSet) + (Json.field "call" call) + (Json.field "plays" (Json.dict (Json.list response))) + (Json.field "winner" userId) + + +playerRole : Json.Decoder Player.Role +playerRole = + Json.string |> Json.andThen playerRoleByName + + +playerRoleByName : String -> Json.Decoder Player.Role +playerRoleByName name = + case name of + "Czar" -> + Json.succeed Player.RCzar + + "Player" -> + Json.succeed Player.RPlayer + + _ -> + Json.fail ("Unknown player role: '" ++ name ++ "'.") + + +userRole : Json.Decoder User.Role +userRole = + Json.string |> Json.andThen userRoleByName + + +userRoleByName : String -> Json.Decoder User.Role +userRoleByName name = + case name of + "Spectator" -> + Json.succeed User.Spectator + + "Player" -> + Json.succeed User.Player + + _ -> + Json.fail ("Unknown user role: '" ++ name ++ "'.") + + +mdError : Json.Decoder MdError +mdError = + Json.field "error" Json.string |> Json.andThen mdErrorByName + + +mdErrorByName : String -> Json.Decoder MdError +mdErrorByName name = + case name of + "IncorrectPlayerRole" -> + incorrectPlayerRole |> Json.map MdError.ActionExecution + + "IncorrectUserRole" -> + incorrectUserRole |> Json.map MdError.ActionExecution + + "IncorrectRoundStageError" -> + incorrectRoundStageError |> Json.map MdError.ActionExecution + + "ConfigEditConflictError" -> + configEditConflictError |> Json.map MdError.ActionExecution + + "Unprivileged" -> + Json.succeed (MdError.ActionExecution MdError.Unprivileged) + + "GameNotStarted" -> + Json.succeed (MdError.ActionExecution MdError.GameNotStarted) + + "AuthenticationFailure" -> + authenticationError |> Json.map MdError.Authentication + + "LobbyNotFound" -> + Json.map2 (\r -> \g -> MdError.LobbyNotFound { reason = r, gameCode = g }) + (Json.field "reason" lobbyError) + (Json.field "gameCode" gameCode) + + "OutOfCardsError" -> + Json.succeed MdError.OutOfCardsError |> Json.map MdError.Game + + _ -> + Json.fail ("Unknown error: '" ++ name ++ "'.") + + +incorrectRoundStageError : Json.Decoder MdError.ActionExecutionError +incorrectRoundStageError = + Json.map2 (\s -> \e -> MdError.IncorrectRoundStageError { stage = s, expected = e }) + (Json.field "stage" Json.string) + (Json.field "expected" Json.string) + + +configEditConflictError : Json.Decoder MdError.ActionExecutionError +configEditConflictError = + Json.map2 (\v -> \e -> MdError.ConfigEditConflictError { version = v, expected = e }) + (Json.field "version" Json.string) + (Json.field "expected" Json.string) + + +incorrectPlayerRole : Json.Decoder MdError.ActionExecutionError +incorrectPlayerRole = + Json.map2 (\r -> \e -> MdError.IncorrectPlayerRole { role = r, expected = e }) + (Json.field "role" playerRole) + (Json.field "expected" playerRole) + + +incorrectUserRole : Json.Decoder MdError.ActionExecutionError +incorrectUserRole = + Json.map2 (\r -> \e -> MdError.IncorrectUserRole { role = r, expected = e }) + (Json.field "role" userRole) + (Json.field "expected" userRole) + + +authenticationError : Json.Decoder MdError.AuthenticationError +authenticationError = + Json.field "reason" Json.string |> Json.andThen authenticationErrorByName + + +authenticationErrorByName : String -> Json.Decoder MdError.AuthenticationError +authenticationErrorByName name = + case name of + "IncorrectIssuer" -> + Json.succeed MdError.IncorrectIssuer + + "InvalidAuthentication" -> + Json.succeed MdError.InvalidAuthentication + + "InvalidLobbyPassword" -> + Json.succeed MdError.InvalidLobbyPassword + + _ -> + Json.fail ("Unknown authentication failure reason: '" ++ name ++ "'.") + + +lobbyError : Json.Decoder MdError.LobbyNotFoundError +lobbyError = + Json.field "reason" Json.string |> Json.andThen lobbyErrorByName + + +lobbyErrorByName : String -> Json.Decoder MdError.LobbyNotFoundError +lobbyErrorByName name = + case name of + "Closed" -> + Json.succeed MdError.Closed + + "DoesNotExist" -> + Json.succeed MdError.DoesNotExist + + _ -> + Json.fail ("Unknown lobby error reason: '" ++ name ++ "'.") diff --git a/client/src/elm/MassiveDecks/Models/Encoders.elm b/client/src/elm/MassiveDecks/Models/Encoders.elm new file mode 100644 index 0000000..fc1422f --- /dev/null +++ b/client/src/elm/MassiveDecks/Models/Encoders.elm @@ -0,0 +1,110 @@ +module MassiveDecks.Models.Encoders exposing + ( castFlags + , checkAlive + , houseRuleChange + , language + , lobbyCreation + , lobbyToken + , settings + , source + , userRegistration + ) + +import Dict +import Json.Encode as Json +import MassiveDecks.Card.Source.Cardcast.Model as Cardcast +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Game.Rules as Rules +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Start.Model as Start +import MassiveDecks.Settings.Model as Settings exposing (Settings) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) +import MassiveDecks.User as User + + +checkAlive : List Lobby.Token -> Json.Value +checkAlive tokens = + [ ( "tokens", Json.list lobbyToken tokens ) ] |> Json.object + + +settings : Settings -> Json.Value +settings s = + let + lun = + s.lastUsedName |> Maybe.map (\n -> [ ( "lastUsedName", Json.string n ) ]) |> Maybe.withDefault [] + + cl = + s.chosenLanguage |> Maybe.map (\l -> [ ( "chosenLanguage", language l ) ]) |> Maybe.withDefault [] + + fields = + List.concat + [ [ ( "tokens", s.tokens |> Dict.toList |> List.map (\( gc, t ) -> ( gc, Json.string t )) |> Json.object ) + , ( "openUserList", Json.bool s.openUserList ) + , ( "recentDecks", Json.list source s.recentDecks ) + , ( "compactCards", Json.bool s.compactCards ) + ] + , lun + , cl + ] + in + Json.object fields + + +lobbyCreation : Start.LobbyCreation -> Json.Value +lobbyCreation c = + Json.object + [ ( "owner", c.owner |> userRegistration ) ] + + +castFlags : Cast.Flags -> Json.Value +castFlags cf = + Json.object + [ ( "token", cf.token |> Json.string ) + , ( "language", cf.language |> language ) + ] + + +lobbyToken : Lobby.Token -> Json.Value +lobbyToken = + Json.string + + +source : Source.External -> Json.Value +source s = + case s of + Source.Cardcast (Cardcast.PlayCode playCode) -> + Json.object [ ( "source", "Cardcast" |> Json.string ), ( "playCode", playCode |> Json.string ) ] + + +language : Language -> Json.Value +language l = + Lang.code l |> Json.string + + +userRegistration : User.Registration -> Json.Value +userRegistration r = + Json.object + [ ( "name", r.name |> Json.string ) ] + + +houseRuleChange : Rules.HouseRuleChange -> Json.Value +houseRuleChange change = + let + ( name, maybeSettings ) = + case change of + Rules.RandoChange maybe -> + ( "Rando", maybe |> Maybe.map (\rando -> Json.object [ ( "number", Json.int rando.number ) ]) ) + + Rules.PackingHeatChange maybe -> + ( "PackingHeat", maybe |> Maybe.map (\_ -> Json.object []) ) + + Rules.RebootChange maybe -> + ( "Reboot", maybe |> Maybe.map (\reboot -> Json.object [ ( "cost", Json.int reboot.cost ) ]) ) + + ruleSettings = + maybeSettings |> Maybe.map (\s -> [ ( "settings", s ) ]) |> Maybe.withDefault [] + in + Json.object + (( "houseRule", name |> Json.string ) :: ruleSettings) diff --git a/client/src/elm/MassiveDecks/Models/MdError.elm b/client/src/elm/MassiveDecks/Models/MdError.elm new file mode 100644 index 0000000..ca0ba3c --- /dev/null +++ b/client/src/elm/MassiveDecks/Models/MdError.elm @@ -0,0 +1,42 @@ +module MassiveDecks.Models.MdError exposing + ( ActionExecutionError(..) + , AuthenticationError(..) + , GameStateError(..) + , LobbyNotFoundError(..) + , MdError(..) + ) + +import MassiveDecks.Game.Player as Player +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.User as User + + +type MdError + = ActionExecution ActionExecutionError + | Authentication AuthenticationError + | LobbyNotFound { reason : LobbyNotFoundError, gameCode : GameCode } + | Game GameStateError + + +type ActionExecutionError + = IncorrectPlayerRole { role : Player.Role, expected : Player.Role } + | IncorrectUserRole { role : User.Role, expected : User.Role } + | IncorrectRoundStageError { stage : String, expected : String } + | ConfigEditConflictError { version : String, expected : String } + | Unprivileged + | GameNotStarted + + +type AuthenticationError + = IncorrectIssuer + | InvalidAuthentication + | InvalidLobbyPassword + + +type LobbyNotFoundError + = Closed + | DoesNotExist + + +type GameStateError + = OutOfCardsError diff --git a/client/src/elm/MassiveDecks/Pages.elm b/client/src/elm/MassiveDecks/Pages.elm new file mode 100644 index 0000000..62e543f --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages.elm @@ -0,0 +1,93 @@ +module MassiveDecks.Pages exposing + ( fromRoute + , subscriptions + , toRoute + ) + +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Lobby as Lobby +import MassiveDecks.Pages.Model as Model exposing (..) +import MassiveDecks.Pages.Route as Route exposing (Route) +import MassiveDecks.Pages.Spectate as Spectate +import MassiveDecks.Pages.Start as Start +import MassiveDecks.Pages.Unknown as Unknown +import MassiveDecks.Util as Util + + +subscriptions : Page -> Sub Msg +subscriptions model = + case model of + Lobby lobby -> + Lobby.subscriptions lobby + + _ -> + Sub.none + + +{-| Construct a page model from a route and an existing model. +If the existing model is of the same type, only the route will be changed. +-} +fromRoute : Shared -> Maybe Page -> Route -> ( Page, Cmd Msg ) +fromRoute shared oldModel route = + case route of + Route.Start r -> + (case oldModel of + Just (Start old) -> + Start.changeRoute r old + + _ -> + Start.init shared r + ) + |> Util.modelLift Start + + Route.Lobby r -> + case oldModel of + Just (Lobby old) -> + Lobby.changeRoute r old |> Util.modelLift Lobby + + _ -> + case Lobby.init shared r Nothing of + Route.Continue ( lobby, cmd ) -> + ( Lobby lobby, cmd ) + + Route.Redirect redirect -> + fromRoute shared oldModel redirect + + Route.Spectate r -> + (case oldModel of + Just (Spectate old) -> + Spectate.changeRoute r old + + _ -> + Spectate.init r + ) + |> Util.modelLift Spectate + + Route.Unknown r -> + (case oldModel of + Just (Unknown old) -> + Unknown.changeRoute r old + + _ -> + Unknown.init r + ) + |> Util.modelLift Unknown + + +{-| Extract a route from a page model. +-} +toRoute : Page -> Route +toRoute model = + case model of + Model.Start start -> + Start.route start |> Route.Start + + Model.Lobby lobby -> + Lobby.route lobby |> Route.Lobby + + Model.Spectate spectate -> + Spectate.route spectate |> Route.Spectate + + Model.Unknown unknown -> + Unknown.route unknown |> Route.Unknown diff --git a/client/src/elm/MassiveDecks/Pages/Lobby.elm b/client/src/elm/MassiveDecks/Pages/Lobby.elm new file mode 100644 index 0000000..c354c8e --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby.elm @@ -0,0 +1,617 @@ +module MassiveDecks.Pages.Lobby exposing + ( changeRoute + , init + , route + , subscriptions + , update + , view + ) + +import Dict exposing (Dict) +import FontAwesome.Attributes as Icon +import FontAwesome.Brands as Icon +import FontAwesome.Icon as Icon exposing (Icon) +import FontAwesome.Layering as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Html.Keyed as HtmlK +import MassiveDecks.Animated as Animated exposing (Animated) +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Cast.Model as Cast +import MassiveDecks.Components as Components +import MassiveDecks.Components.Menu as Menu +import MassiveDecks.Error.Messages as Error +import MassiveDecks.Error.Model as Error +import MassiveDecks.Game as Game +import MassiveDecks.Game.Model as Game exposing (Game) +import MassiveDecks.Game.Player as Player exposing (Player) +import MassiveDecks.Game.Round as Round exposing (Round) +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Models.MdError as MdError +import MassiveDecks.Pages.Lobby.Configure as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) +import MassiveDecks.Pages.Lobby.Events as Events +import MassiveDecks.Pages.Lobby.GameCode as GameCode +import MassiveDecks.Pages.Lobby.Invite as Invite +import MassiveDecks.Pages.Lobby.Messages exposing (..) +import MassiveDecks.Pages.Lobby.Model exposing (..) +import MassiveDecks.Pages.Lobby.Route exposing (..) +import MassiveDecks.Pages.Route as Route +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.ServerConnection as ServerConnection +import MassiveDecks.Settings as Settings +import MassiveDecks.Settings.Messages as Settings +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util as Util +import MassiveDecks.Util.Maybe as Maybe +import Weightless as Wl +import Weightless.Attributes as WlA + + +changeRoute : Route -> Model -> ( Model, Cmd Global.Msg ) +changeRoute r model = + ( { model | route = r, lobby = Nothing, selectedUser = Nothing }, Cmd.none ) + + +init : Shared -> Route -> Maybe Auth -> Route.Fork ( Model, Cmd Global.Msg ) +init shared r auth = + let + fallbackAuth = + Maybe.first + [ auth |> Maybe.andThen (Maybe.validate (\a -> a.claims.gc == r.gameCode)) + , Settings.auths shared.settings.settings |> Dict.get (r.gameCode |> GameCode.toString) + ] + in + fallbackAuth + |> Maybe.map + (\a -> + Route.Continue + ( { route = r + , auth = a + , lobby = Nothing + , selectedUser = Nothing + , configure = Configure.init + , notificationId = 0 + , notifications = [] + , inviteDialogOpen = False + } + , ServerConnection.connect a.claims.gc a.token + ) + ) + |> Maybe.withDefault (Route.Redirect (Route.Start { section = Start.Join (Just r.gameCode) })) + + +route : Model -> Route +route model = + model.route + + +subscriptions : Model -> Sub Global.Msg +subscriptions model = + Sub.batch + [ model.lobby |> Maybe.andThen .game |> Maybe.map Game.subscriptions |> Maybe.withDefault Sub.none + , model.notifications |> Animated.subscriptions |> Sub.map (NotificationMsg >> Global.LobbyMsg) + , ServerConnection.notifications + (EventReceived >> Global.LobbyMsg) + (ErrorReceived >> Global.LobbyMsg) + (Error.Json >> Error.Add >> Global.ErrorMsg) + ] + + +update : Msg -> Model -> ( Model, Cmd Global.Msg ) +update msg model = + case msg of + SelectUser user -> + ( { model | selectedUser = user }, Cmd.none ) + + GameMsg gameMsg -> + case model.lobby of + Just l -> + l.game + |> Maybe.map + (Game.update gameMsg + >> Util.modelLift (\newGame -> { model | lobby = Just { l | game = Just newGame } }) + ) + |> Maybe.withDefault ( model, Cmd.none ) + + Nothing -> + ( model, Cmd.none ) + + EventReceived event -> + case model.lobby of + Just lobby -> + case event of + Events.Sync { state, hand, play } -> + applySync model state hand play + + Events.Configured configured -> + ( model.lobby + |> Maybe.map (\l -> applyConfigured configured l model) + |> Maybe.withDefault model + , Cmd.none + ) + + Events.GameStarted { round, hand } -> + Util.modelLift + (\g -> { model | lobby = Just { lobby | game = Just g } }) + (Game.applyGameStarted lobby round hand) + + Events.Game gameEvent -> + lobby.game + |> Maybe.map + (\game -> + Util.modelLift + (\g -> { model | lobby = Just { lobby | game = Just g } }) + (Game.applyGameEvent gameEvent game) + ) + |> Maybe.withDefault ( model, Cmd.none ) + + Events.Connection { user, state } -> + let + message = + case state of + User.Connected -> + UserConnected user + + User.Disconnected -> + UserDisconnected user + in + addNotification message model + + Events.Presence { user, state } -> + let + newUsers = + case state of + Events.UserJoined { name } -> + Dict.insert + user + { name = name + , presence = User.Joined + , connection = User.Connected + , privilege = User.Unprivileged + , role = User.Player + } + lobby.users + + Events.UserLeft -> + Dict.remove user lobby.users + + message = + case state of + Events.UserJoined _ -> + UserJoined user + + Events.UserLeft -> + UserLeft user + in + addNotification message { model | lobby = Just { lobby | users = newUsers } } + + Nothing -> + case event of + Events.Sync { state, hand, play } -> + applySync model state hand play + + _ -> + ( model, Cmd.none ) + + ErrorReceived error -> + case error of + MdError.Authentication reason -> + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + ConfigureMsg configureMsg -> + case model.lobby of + Just l -> + Util.modelLift (\c -> { model | configure = c }) + (Configure.update configureMsg model.configure l.config) + + Nothing -> + ( model, Cmd.none ) + + NotificationMsg notificationMsg -> + Util.lift + (\notifications -> { model | notifications = notifications }) + (NotificationMsg >> Global.LobbyMsg) + (Animated.update { removeDone = Just notificationAnimationDuration } notificationMsg model.notifications) + + ToggleInviteDialog -> + ( { model | inviteDialogOpen = not model.inviteDialogOpen }, Cmd.none ) + + +view : Shared -> Model -> List (Html Global.Msg) +view shared model = + let + usersShown = + shared.settings.settings.openUserList + + castAttrs = + case shared.castStatus of + Cast.NoDevicesAvailable -> + Nothing + + Cast.NotConnected -> + Just [ Strings.Cast |> Lang.title shared ] + + Cast.Connecting -> + Just [ Strings.CastConnecting |> Lang.title shared, HtmlA.class "connecting" ] + + Cast.Connected name -> + Just [ Strings.CastConnected { deviceName = name } |> Lang.title shared, HtmlA.class "connected" ] + + castButton = + castAttrs + |> Maybe.map (viewCastButton model.auth) + |> Maybe.withDefault [] + + usersIcon = + if usersShown then + Icon.eyeSlash + + else + Icon.users + + title = + model.lobby + |> Maybe.andThen .game + |> Maybe.map (.game >> .round >> Round.data >> .call >> .body >> Parts.viewSingleLine) + |> Maybe.withDefault [] + + notifications = + model.notifications |> List.map (keyedViewNotification shared model.lobby) + in + [ Html.div + [ HtmlA.classList + [ ( "lobby", True ) + , ( "compact-cards", shared.settings.settings.compactCards ) + ] + ] + [ Html.div [ HtmlA.id "top-bar" ] + [ Html.div [ HtmlA.class "left" ] + (List.concat + [ [ Components.iconButtonStyled + [ HtmlE.onClick (usersShown |> not |> Settings.ChangeOpenUserList |> Global.SettingsMsg) + , Strings.ToggleUserList |> Lang.title shared + ] + ( [ Icon.lg ], usersIcon ) + , gameMenu shared + ] + , castButton + ] + ) + , Html.div [ HtmlA.class "title-call" ] title + , Html.div [] [ Settings.view shared ] + ] + , model.lobby + |> Maybe.map (viewLobby shared model.configure usersShown model.auth model.selectedUser) + |> Maybe.withDefault + (Html.div [ HtmlA.class "loading" ] + [ Icon.viewStyled [ Icon.spin, Icon.fa3x ] Icon.circleNotch ] + ) + , HtmlK.ol [ HtmlA.class "notifications" ] notifications + ] + , Invite.dialog shared + model.auth.claims.gc + (model.lobby |> Maybe.andThen (.config >> .password)) + model.inviteDialogOpen + ] + + + +{- Private -} + + +gameMenu : Shared -> Html msg +gameMenu shared = + let + id = + "game-menu-button" + + menuItems = + [ Menu.item Icon.bullhorn Strings.InvitePlayers Strings.InvitePlayers Nothing + , Menu.Separator + , Menu.item Icon.userClock Strings.SetAway Strings.SetAway Nothing + , Menu.item Icon.signOutAlt Strings.LeaveGame Strings.LeaveGame Nothing + , Menu.Separator + , Menu.item Icon.info Strings.AboutTheGame Strings.AboutTheGame Nothing + , Menu.item Icon.bug Strings.ReportError Strings.ReportError Nothing + ] + in + Html.div [] + [ Components.iconButtonStyled [ HtmlA.id id, Strings.GameMenu |> Lang.title shared ] + ( [ Icon.lg ], Icon.bars ) + , Menu.view shared id WlA.XLeft WlA.YBottom menuItems + ] + + +applyConfigured : { change : Events.ConfigChanged, version : String } -> Lobby -> Model -> Model +applyConfigured { change, version } oldLobby oldModel = + let + ( config, configure ) = + Configure.applyChange change oldLobby.config oldModel.configure + + lobby = + Just { oldLobby | config = { config | version = version } } + in + { oldModel | lobby = lobby, configure = configure } + + +notificationDuration : Int +notificationDuration = + 3500 + + +notificationAnimationDuration : Int +notificationAnimationDuration = + 200 + + +applySync : Model -> Lobby -> Maybe (List Card.Response) -> Maybe (List Card.Id) -> ( Model, Cmd Global.Msg ) +applySync model state hand pick = + let + play = + pick |> Maybe.map (\cards -> { state = Round.Submitted, cards = cards }) |> Maybe.withDefault Round.noPick + + ( game, cmd ) = + case state.game of + Just g -> + Util.modelLift Just (Game.init g.game (hand |> Maybe.withDefault []) play) + + Nothing -> + ( Nothing, Cmd.none ) + in + ( { model + | lobby = Just { state | game = game } + , configure = Configure.updateFromConfig state.config model.configure + } + , cmd + ) + + +addNotification : NotificationMessage -> Model -> ( Model, Cmd Global.Msg ) +addNotification message model = + let + notification = + { id = model.notificationId, message = message } + + notifications = + model.notifications ++ [ Animated.animate notification ] + in + ( { model | notifications = notifications, notificationId = model.notificationId + 1 } + , Animated.exitAfter notificationDuration notification + |> Cmd.map (NotificationMsg >> Global.LobbyMsg) + ) + + +keyedViewNotification : Shared -> Maybe Lobby -> Animated Notification -> ( String, Html Global.Msg ) +keyedViewNotification shared lobby notification = + ( String.fromInt notification.item.id + , Html.li [] [ Animated.view (viewNotification shared (lobby |> Maybe.map .users)) notification ] + ) + + +viewNotification : Shared -> Maybe (Dict User.Id User) -> Html.Attribute Global.Msg -> Notification -> Html Global.Msg +viewNotification shared users animationState notification = + let + ( icon, message ) = + case notification.message of + UserConnected id -> + ( Icon.view Icon.plug + , Strings.UserConnected { username = username shared users id } |> Lang.html shared + ) + + UserDisconnected id -> + ( Icon.layers [] [ Icon.view Icon.plug, Icon.view Icon.slash ] + , Strings.UserDisconnected { username = username shared users id } |> Lang.html shared + ) + + UserJoined id -> + ( Icon.view Icon.signInAlt + , Strings.UserJoined { username = username shared users id } |> Lang.html shared + ) + + UserLeft id -> + ( Icon.view Icon.signOutAlt + , Strings.UserLeft { username = username shared users id } |> Lang.html shared + ) + in + Wl.card + [ HtmlA.class "notification", animationState ] + [ Html.div [ HtmlA.class "content" ] + [ Html.span [ HtmlA.class "icon" ] [ icon ] + , Html.span [ HtmlA.class "message" ] [ message ] + , Wl.button + [ WlA.flat + , WlA.inverted + , notification |> Animated.Exit |> NotificationMsg |> Global.LobbyMsg |> HtmlE.onClick + , HtmlA.class "action" + ] + [ Strings.Dismiss |> Lang.html shared ] + ] + ] + + +username : Shared -> Maybe (Dict User.Id User) -> User.Id -> String +username shared users id = + users + |> Maybe.andThen (\u -> Dict.get id u) + |> Maybe.map .name + |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) + + +viewLobby : Shared -> Configure.Model -> Bool -> Auth -> Maybe User.Id -> Lobby -> Html Global.Msg +viewLobby shared configure usersShown auth selectedUser lobby = + let + game = + lobby.game |> Maybe.map .game + + privileged = + auth.claims.pvg == User.Privileged + + content = + [ viewUsers shared auth.claims.uid selectedUser lobby.users game + , Html.div [ HtmlA.id "scroll-frame" ] + [ lobby.game + |> Maybe.map (Game.view shared auth lobby.config.decks lobby.users) + |> Maybe.withDefault (Configure.view shared privileged configure auth.claims.gc lobby lobby.config) + ] + ] + in + Html.div [ HtmlA.classList [ ( "content-wrapper", True ), ( "collapsed-users", not usersShown ) ] ] content + + +viewUsers : Shared -> User.Id -> Maybe User.Id -> Dict User.Id User -> Maybe Game -> Html Global.Msg +viewUsers shared player selectedUser users game = + Wl.card [ HtmlA.class "users" ] + [ Html.div [ HtmlA.class "collapsible" ] + [ users + |> Dict.toList + |> byRole + |> List.map (viewRoleGroup shared player selectedUser game) + |> HtmlK.ol [] + ] + ] + + +viewRoleGroup : Shared -> User.Id -> Maybe User.Id -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html Global.Msg ) +viewRoleGroup shared player selectedUser game ( role, users ) = + ( role |> roleId + , Html.li [] + [ Html.span [] [ role |> roleDescription |> Lang.html shared ] + , HtmlK.ol [] (users |> List.map (viewUser shared player selectedUser game)) + ] + ) + + +roles : List User.Role +roles = + [ User.Player, User.Spectator ] + + +byRole : List ( User.Id, User ) -> List ( User.Role, List ( User.Id, User ) ) +byRole users = + roles + |> List.map (\role -> ( role, users |> List.filter (\( _, user ) -> user.role == role) )) + |> List.filter (\( _, us ) -> not (List.isEmpty us)) + + +viewUser : Shared -> User.Id -> Maybe User.Id -> Maybe Game -> ( User.Id, User ) -> ( String, Html Global.Msg ) +viewUser shared player selectedUser game ( userId, user ) = + let + ( secondary, score ) = + userDetails shared game userId user + + id = + "user-" ++ userId + + menuItems = + [ Menu.item Icon.userCog Strings.Promote Strings.Promote Nothing + , Menu.Separator + , Menu.item Icon.userClock Strings.SetAway Strings.SetAway Nothing + , Menu.item Icon.ban Strings.KickUser Strings.KickUser Nothing + ] + in + ( userId + , Html.li [] + [ Wl.listItem + [ Just userId |> SelectUser |> Global.LobbyMsg |> HtmlE.onClick + , HtmlA.classList [ ( "mdc-menu-surface--anchor", True ), ( "you", player == userId ) ] + , WlA.clickable + , HtmlA.id id + ] + [ Html.div [ HtmlA.class "user compressed-terms" ] + (Html.span [ HtmlA.title user.name ] [ Html.text user.name ] + :: secondary + ) + , Html.span [ WlA.listItemSlot WlA.AfterItem ] score + ] + , Menu.view shared id WlA.XRight WlA.YTop menuItems + ] + ) + + +userDetails : Shared -> Maybe Game -> User.Id -> User -> ( List (Html Global.Msg), List (Html Global.Msg) ) +userDetails shared game userId user = + let + player = + game |> Maybe.map .players |> Maybe.andThen (Dict.get userId) + + round = + game |> Maybe.map .round + + details = + [ Strings.Privileged |> Maybe.justIf (user.privilege == User.Privileged) + , player |> Maybe.andThen (\p -> Strings.Ai |> Maybe.justIf (p.control == Player.Computer)) + , round |> Maybe.andThen (\r -> Strings.Czar |> Maybe.justIf (Player.isCzar r userId)) + , playStateDetail round userId + ] + + score = + player |> Maybe.map (\p -> Strings.Score { total = p.score }) + in + ( viewDetails shared details, viewDetails shared [ score ] ) + + +playStateDetail : Maybe Round -> User.Id -> Maybe MdString +playStateDetail round userId = + case round of + Just (Round.P p) -> + case Player.playState p userId of + Player.Playing -> + Just Strings.Playing + + Player.Played -> + Just Strings.Played + + Player.NotInRound -> + Nothing + + _ -> + Nothing + + +viewDetails : Shared -> List (Maybe MdString) -> List (Html Global.Msg) +viewDetails shared details = + details |> List.filterMap identity |> List.map (Lang.html shared) |> List.intersperse (Html.text " ") + + +roleDescription : User.Role -> Strings.MdString +roleDescription role = + case role of + User.Player -> + Strings.Players + + User.Spectator -> + Strings.Spectators + + +roleId : User.Role -> String +roleId role = + case role of + User.Player -> + "players" + + User.Spectator -> + "spectators" + + +viewCastButton : Auth -> List (Html.Attribute Global.Msg) -> List (Html Global.Msg) +viewCastButton auth attrs = + [ Components.iconButtonStyled + (List.concat + [ [ HtmlA.class "cast-button" + , auth |> Global.TryCast |> HtmlE.onClick + ] + , attrs + ] + ) + ( [ Icon.lg ], Icon.chromecast ) + ] diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm new file mode 100644 index 0000000..989f29d --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm @@ -0,0 +1,95 @@ +module MassiveDecks.Pages.Lobby.Actions exposing + ( addDeck + , changeHouseRule + , judge + , removeDeck + , reveal + , setHandSize + , setPassword + , setScoreLimit + , startGame + , submit + , takeBack + ) + +import Json.Encode as Json +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Game.Rules as Rules +import MassiveDecks.Models.Encoders as Encoders +import MassiveDecks.ServerConnection as ServerConnection + + +addDeck : String -> Source.External -> Cmd msg +addDeck = + changeDecks "Add" + + +removeDeck : String -> Source.External -> Cmd msg +removeDeck = + changeDecks "Remove" + + +startGame : Cmd msg +startGame = + action "StartGame" [] + + +setScoreLimit : Maybe Int -> String -> Cmd msg +setScoreLimit value = + configAction "SetScoreLimit" (value |> Maybe.map (\v -> [ ( "scoreLimit", v |> Json.int ) ]) |> Maybe.withDefault []) + + +setHandSize : Int -> String -> Cmd msg +setHandSize value = + configAction "SetHandSize" [ ( "handSize", value |> Json.int ) ] + + +setPassword : Maybe String -> String -> Cmd msg +setPassword value = + configAction "SetPassword" (value |> Maybe.map (\v -> [ ( "password", v |> Json.string ) ]) |> Maybe.withDefault []) + + +changeHouseRule : Rules.HouseRuleChange -> String -> Cmd msg +changeHouseRule value = + configAction "ChangeHouseRule" [ ( "change", value |> Encoders.houseRuleChange ) ] + + +submit : List Card.Id -> Cmd msg +submit play = + action "Submit" [ ( "play", play |> Json.list Json.string ) ] + + +takeBack : Cmd msg +takeBack = + action "TakeBack" [] + + +reveal : Play.Id -> Cmd msg +reveal play = + action "Reveal" [ ( "play", play |> Json.string ) ] + + +judge : Play.Id -> Cmd msg +judge play = + action "Judge" [ ( "winner", play |> Json.string ) ] + + + +{- Private -} + + +changeDecks : String -> String -> Source.External -> Cmd msg +changeDecks change version source = + configAction "ChangeDecks" [ ( "deck", source |> Encoders.source ), ( "change", change |> Json.string ) ] version + + +configAction : String -> List ( String, Json.Value ) -> String -> Cmd msg +configAction name data version = + action name (( "if", version |> Json.string ) :: data) + + +action : String -> List ( String, Json.Value ) -> Cmd msg +action name data = + ( "action", name |> Json.string ) :: data |> Json.object |> ServerConnection.message diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm new file mode 100644 index 0000000..918dd5a --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm @@ -0,0 +1,797 @@ +module MassiveDecks.Pages.Lobby.Configure exposing + ( applyChange + , init + , update + , updateFromConfig + , view + ) + +import Dict exposing (Dict) +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Card.Source as Source +import MassiveDecks.Card.Source.Cardcast.Model as Cardcast +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Components as Components +import MassiveDecks.Game.Rules as Rules +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.Actions as Actions +import MassiveDecks.Pages.Lobby.Configure.Messages exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model exposing (..) +import MassiveDecks.Pages.Lobby.Events as Events +import MassiveDecks.Pages.Lobby.GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Invite as Invite +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Html.Attributes as HtmlA +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.Result as Result +import Weightless as Wl +import Weightless.Attributes as WlA + + +init : Model +init = + { deckToAdd = Source.default + , deckErrors = [] + , handSize = 10 + , scoreLimit = Just 25 + , tab = Decks + , password = Nothing + , houseRules = + { rando = Nothing + , packingHeat = Nothing + , reboot = Nothing + } + } + + +updateFromConfig : Config -> Model -> Model +updateFromConfig config model = + { deckToAdd = model.deckToAdd + , deckErrors = model.deckErrors + , tab = model.tab + , handSize = config.rules.handSize + , scoreLimit = config.rules.scoreLimit + , password = config.password + , houseRules = config.rules.houseRules + } + + +update : Msg -> Model -> Config -> ( Model, Cmd msg ) +update msg model config = + case msg of + AddDeck source -> + ( { model | deckToAdd = Source.emptyMatching source }, Actions.addDeck config.version source ) + + RemoveDeck source -> + ( model, Actions.removeDeck config.version source ) + + UpdateSource source -> + ( { model | deckToAdd = source }, Cmd.none ) + + ChangeTab t -> + ( { model | tab = t }, Cmd.none ) + + StartGame -> + ( model, Actions.startGame ) + + HandSizeChange target value -> + ( { model | handSize = value }, ifRemote (Actions.setHandSize value config.version) target ) + + ScoreLimitChange target value -> + ( { model | scoreLimit = value }, ifRemote (Actions.setScoreLimit value config.version) target ) + + PasswordChange target value -> + let + send = + ifRemote (Actions.setPassword value config.version) target + + cmd = + case value of + Just pw -> + Maybe.justIf (String.length pw >= 1) send |> Maybe.withDefault Cmd.none + + Nothing -> + send + in + ( { model | password = value }, cmd ) + + HouseRuleChange target value -> + let + send = + ifRemote (Actions.changeHouseRule value config.version) target + in + ( { model | houseRules = model.houseRules |> Rules.apply value }, send ) + + +view : Shared -> Bool -> Model -> GameCode -> Lobby -> Config -> Html Global.Msg +view shared canEdit model gameCode lobby config = + Html.div [ HtmlA.class "configure" ] + [ Wl.card [] + [ Html.div [ HtmlA.class "title" ] + [ Html.h2 [] [ lobby.name |> Html.text ] + , Html.div [] + [ Invite.button shared + , Strings.GameCode { code = gameCode } |> Lang.html shared + ] + ] + , Wl.tabGroup [ WlA.align WlA.Center ] (tabs |> List.map (tab shared model.tab)) + , tabContent shared canEdit model lobby config + ] + , Wl.card [] + [ startGameSegment shared canEdit lobby config + ] + ] + + +applyChange : Events.ConfigChanged -> Config -> Model -> ( Config, Model ) +applyChange configChange oldConfig oldConfigure = + let + oldRules = + oldConfig.rules + in + case configChange of + Events.DecksChanged decksChanged -> + applyDeckChange decksChanged oldConfig oldConfigure + + Events.HandSizeSet { size } -> + ( { oldConfig | rules = { oldRules | handSize = size } }, { oldConfigure | handSize = size } ) + + Events.ScoreLimitSet { limit } -> + ( { oldConfig | rules = { oldRules | scoreLimit = limit } }, { oldConfigure | scoreLimit = limit } ) + + Events.PasswordSet { password } -> + ( { oldConfig | password = password } + , { oldConfigure | password = password } + ) + + Events.HouseRuleChanged { change } -> + ( { oldConfig | rules = { oldRules | houseRules = oldRules.houseRules |> Rules.apply change } } + , { oldConfigure | houseRules = oldConfigure.houseRules |> Rules.apply change } + ) + + + +{- Private -} + + +startGameSegment : Shared -> Bool -> Lobby -> Config -> Html Global.Msg +startGameSegment shared canEdit lobby config = + let + startErrors = + startGameProblems lobby.users config + + startGameAttrs = + if List.isEmpty startErrors && canEdit then + [ StartGame |> lift |> HtmlE.onClick ] + + else + [ WlA.disabled ] + in + Components.formSection shared + "start-game" + (Wl.button startGameAttrs [ Strings.StartGame |> Lang.html shared ]) + (startErrors |> Maybe.justIf canEdit |> Maybe.withDefault []) + + +addSummary : Source.External -> Source.Summary -> Deck -> Deck +addSummary target summary deckSource = + if deckSource.source == target then + Deck target (Just summary) + + else + deckSource + + +applyDeckChange : { change : Events.DeckChange, deck : Source.External } -> Config -> Model -> ( Config, Model ) +applyDeckChange event config configure = + let + change = + event.change + + deckSource = + event.deck + + newConfig = + case change of + Events.Add -> + { config | decks = config.decks ++ [ Deck deckSource Nothing ] } + + Events.Remove -> + { config | decks = config.decks |> List.filter (.source >> (/=) deckSource) } + + Events.Load { summary } -> + { config | decks = config.decks |> List.map (addSummary deckSource summary) } + + Events.Fail _ -> + { config | decks = config.decks |> List.filter (.source >> (/=) deckSource) } + + newConfigure = + case change of + Events.Fail { reason } -> + { configure | deckErrors = { deck = deckSource, reason = reason } :: configure.deckErrors } + + _ -> + configure + in + ( newConfig, newConfigure ) + + +startGameProblems : Dict User.Id User -> Config -> List (Components.Message Global.Msg) +startGameProblems users config = + let + -- We assume decks will have calls/responses. + summaries = + \getTypeAmount -> config.decks |> List.map (.summary >> Maybe.map getTypeAmount >> Maybe.withDefault 1) + + noDecks = + List.length config.decks == 0 + + loadingDecks = + config.decks |> List.any (.summary >> Maybe.isNothing) + + deckIssues = + if noDecks then + [ Components.errorWithFix + Strings.NeedAtLeastOneDeck + Strings.NoDecksHint + ("CAHBS" |> Cardcast.playCode |> Source.Cardcast |> AddDeck |> lift) + |> Just + ] + + else if loadingDecks then + [ Strings.WaitForDecks |> Components.info |> Just ] + + else + [ Strings.MissingCardType { cardType = Strings.Call } + |> Components.error + |> Maybe.justIf ((summaries .calls |> List.sum) < 1) + , Strings.MissingCardType { cardType = Strings.Response } + |> Components.error + |> Maybe.justIf ((summaries .responses |> List.sum) < 1) + ] + + playerCount = + users |> Dict.values |> List.filter (\user -> user.role == User.Player) |> List.length + + playerIssues = + [ Components.errorWithFix + Strings.NeedAtLeastThreePlayers + Strings.Invite + (Lobby.ToggleInviteDialog |> Global.LobbyMsg) + -- TODO: 3! 2 for testing. + |> Maybe.justIf (playerCount < 2) + ] + in + [ deckIssues, playerIssues ] |> List.concat |> List.filterMap identity + + +ifRemote : Cmd msg -> Target -> Cmd msg +ifRemote cmd target = + case target of + Local -> + Cmd.none + + Remote -> + cmd + + +tabs : List Tab +tabs = + [ Decks, Rules, Game ] + + +tab : Shared -> Tab -> Tab -> Html Global.Msg +tab shared currently target = + Wl.tab + ((target |> ChangeTab |> lift |> always |> HtmlE.onCheck) + :: ([ WlA.checked ] |> Maybe.justIf (currently == target) |> Maybe.withDefault []) + ) + [ target |> tabName |> Lang.html shared ] + + +tabName : Tab -> MdString +tabName target = + case target of + Decks -> + Strings.ConfigureDecks + + Rules -> + Strings.ConfigureRules + + Game -> + Strings.ConfigureGame + + +tabContent : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg +tabContent shared canEdit model lobby config = + case model.tab of + Decks -> + configureDecks shared canEdit model lobby config + + Rules -> + configureRules shared canEdit model lobby config + + Game -> + configureGameSettings shared canEdit model lobby config + + +configureRules : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg +configureRules shared canEdit model lobby config = + Html.div [ HtmlA.class "rules" ] + [ Html.div [ HtmlA.class "core-rules" ] + [ Html.h3 [] [ Strings.GameRulesTitle |> Lang.html shared ] + , handSize shared canEdit model config + , scoreLimit shared canEdit model config + ] + , houseRules shared canEdit model lobby config + ] + + +handSize : Shared -> Bool -> Model -> Config -> Html Global.Msg +handSize shared canEdit model config = + let + change = + \t -> \s -> s |> String.toInt |> Maybe.withDefault 10 |> HandSizeChange t |> lift + + value = + model.handSize + in + Components.formSection shared + "hand-size" + (Html.div + [ HtmlA.class "multipart" ] + [ Wl.textField + [ WlA.type_ WlA.Number + , value |> String.fromInt |> WlA.value + , Strings.HandSize |> Lang.string shared |> WlA.label + , 3 |> WlA.min + , 50 |> WlA.max + , change Local |> HtmlE.onInput + , HandSizeChange Remote value |> lift |> HtmlE.onBlur + , HtmlA.class "primary" + , WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault HtmlA.nothing + , WlA.outlined + ] + [] + , Components.iconButton + [ WlA.disabled + |> Maybe.justIf (not canEdit || config.rules.handSize == value) + |> Maybe.withDefault (value |> HandSizeChange Remote |> lift |> HtmlE.onClick) + ] + (Icon.save |> Maybe.justIf (config.rules.handSize /= value) |> Maybe.withDefault Icon.check) + ] + ) + [ Components.info Strings.HandSizeDescription ] + + +scoreLimit : Shared -> Bool -> Model -> Config -> Html Global.Msg +scoreLimit shared canEdit model config = + let + change = + \t -> \s -> s |> String.toInt |> ScoreLimitChange t |> lift + + value = + model.scoreLimit + in + Components.formSection shared + "score-limit" + (Html.div + [ HtmlA.class "multipart" ] + [ Wl.switch + [ (\on -> 25 |> Maybe.justIf on |> ScoreLimitChange Remote |> lift) |> HtmlE.onCheck + , WlA.checked |> Maybe.justIf (value /= Nothing) |> Maybe.withDefault HtmlA.nothing + , WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault HtmlA.nothing + ] + , Wl.textField + [ HtmlA.class "primary" + , WlA.type_ WlA.Number + , value |> Maybe.map String.fromInt |> Maybe.withDefault "" |> WlA.value + , Strings.ScoreLimit |> Lang.string shared |> WlA.label + , 1 |> WlA.min + , 10000 |> WlA.max + , change Local |> HtmlE.onInput + , ScoreLimitChange Remote value |> lift |> HtmlE.onBlur + , WlA.disabled |> Maybe.justIf (not canEdit || value == Nothing) |> Maybe.withDefault HtmlA.nothing + , WlA.outlined + ] + [] + , Components.iconButton + [ WlA.disabled + |> Maybe.justIf (not canEdit || config.rules.scoreLimit == value) + |> Maybe.withDefault (value |> ScoreLimitChange Remote |> lift |> HtmlE.onClick) + ] + (Icon.save |> Maybe.justIf (config.rules.scoreLimit /= value) |> Maybe.withDefault Icon.check) + ] + ) + [ Components.info Strings.ScoreLimitDescription ] + + +houseRules : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg +houseRules shared canEdit model lobby config = + Html.div [ HtmlA.class "house-rules" ] + [ Html.h3 [] [ Strings.HouseRulesTitle |> Lang.html shared ] + , rando shared canEdit model config + , packingHeat shared canEdit model config + , reboot shared canEdit model config + ] + + +type alias ViewHouseRuleSettings houseRule = + Shared -> Bool -> houseRule -> (houseRule -> Global.Msg) -> List (Html Global.Msg) + + +houseRule : Shared -> String -> Rules.HouseRule a -> Bool -> Model -> Config -> ViewHouseRuleSettings a -> Html Global.Msg +houseRule shared id { default, change, title, description, extract, insert } canEdit model config viewSettings = + let + localValue = + model.houseRules |> extract + + enabled = + localValue |> Maybe.isJust + + toggle = + \checked -> + default + |> Maybe.justIf checked + |> change + |> HouseRuleChange Remote + |> lift + + save = + localValue |> change |> HouseRuleChange Remote |> lift |> HtmlE.onClick + + saved = + localValue == (config.rules.houseRules |> extract) + + settings = + localValue + |> Maybe.map (\v -> viewSettings shared canEdit v (Just >> change >> HouseRuleChange Local >> lift)) + |> Maybe.withDefault [] + in + Html.div [ HtmlA.classList [ ( "house-rule", True ), ( "enabled", enabled ) ] ] + [ Components.formSection + shared + id + (Html.div [ HtmlA.class "multipart" ] + [ Wl.switch + [ WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault (toggle |> HtmlE.onCheck) + , WlA.checked |> Maybe.justIf enabled |> Maybe.withDefault HtmlA.nothing + ] + , Html.h4 [ HtmlA.class "primary" ] [ Lang.html shared title ] + , Components.iconButton [ save, WlA.disabled |> Maybe.justIf saved |> Maybe.withDefault HtmlA.nothing ] + (Icon.check |> Maybe.justIf saved |> Maybe.withDefault Icon.save) + ] + ) + [ Components.info (localValue |> description) ] + , Html.div [ HtmlA.class "house-rule-settings" ] settings + ] + + +rando : Shared -> Bool -> Model -> Config -> Html Global.Msg +rando shared canEdit model config = + houseRule shared "rando" Rules.rando canEdit model config randoSettings + + +randoSettings : Shared -> Bool -> Rules.Rando -> (Rules.Rando -> Global.Msg) -> List (Html Global.Msg) +randoSettings shared canEdit value localChange = + [ Components.formSection + shared + "rando-number" + (Wl.textField + [ Strings.HouseRuleRandoCardrissianNumber |> Lang.label shared + , HtmlA.class "primary" + , WlA.type_ WlA.Number + , WlA.min 1 + , WlA.max 10 + , Maybe.justIf (not canEdit) WlA.disabled |> Maybe.withDefault HtmlA.nothing + , value.number |> String.fromInt |> WlA.value + , String.toInt + >> Maybe.map (\n -> { value | number = n } |> localChange) + >> Maybe.withDefault Global.NoOp + |> HtmlE.onInput + ] + [] + ) + [ Strings.HouseRuleRandoCardrissianNumberDescription |> Components.info ] + ] + + +packingHeat : Shared -> Bool -> Model -> Config -> Html Global.Msg +packingHeat shared canEdit model config = + houseRule shared "packing-heat" Rules.packingHeat canEdit model config packingHeatSettings + + +packingHeatSettings : Shared -> Bool -> Rules.PackingHeat -> (Rules.PackingHeat -> Global.Msg) -> List (Html Global.Msg) +packingHeatSettings shared canEdit value localChange = + [] + + +reboot : Shared -> Bool -> Model -> Config -> Html Global.Msg +reboot shared canEdit model config = + houseRule shared "reboot" Rules.reboot canEdit model config rebootSettings + + +rebootSettings : Shared -> Bool -> Rules.Reboot -> (Rules.Reboot -> Global.Msg) -> List (Html Global.Msg) +rebootSettings shared canEdit value localChange = + [ Components.formSection + shared + "reboot-cost" + (Wl.textField + [ Strings.HouseRuleRebootCost |> Lang.label shared + , HtmlA.class "primary" + , WlA.type_ WlA.Number + , WlA.min 1 + , WlA.max 50 + , Maybe.justIf (not canEdit) WlA.disabled |> Maybe.withDefault HtmlA.nothing + , value.cost |> String.fromInt |> WlA.value + , String.toInt + >> Maybe.map (\c -> { value | cost = c } |> localChange) + >> Maybe.withDefault Global.NoOp + |> HtmlE.onInput + ] + [] + ) + [ Strings.HouseRuleRebootCostDescription |> Components.info ] + ] + + +configureDecks : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg +configureDecks shared canEdit model lobby config = + let + hint = + if canEdit then + Components.linkButton + [ "CAHBS" |> Cardcast.playCode |> Source.Cardcast |> AddDeck |> lift |> HtmlE.onClick + ] + [ Strings.NoDecksHint |> Lang.html shared ] + + else + Html.nothing + + tableContent = + if List.isEmpty config.decks then + [ Html.tr [ HtmlA.class "empty-info" ] + [ Html.td [ HtmlA.colspan 3 ] + [ Html.p [] + [ Icon.view Icon.ghost + , Html.text " " + , Strings.NoDecks |> Lang.html shared + ] + , hint + ] + ] + ] + + else + config.decks |> List.map (deck shared canEdit) + + editor = + if canEdit then + [ addDeckWidget shared config.decks model.deckToAdd + ] + + else + [] + in + Html.div [ HtmlA.class "decks", HtmlA.class "compressed-terms" ] + (List.concat + [ [ Html.table [] + [ Html.colgroup [] + [ Html.col [ HtmlA.class "deck-name" ] [] + , Html.col [ HtmlA.class "count" ] [] + , Html.col [ HtmlA.class "count" ] [] + ] + , Html.thead [] + [ Html.tr [] + [ Html.th [ HtmlA.class "deck-name", HtmlA.scope "col" ] [ Strings.Deck |> Lang.html shared ] + , Html.th [ HtmlA.scope "col" ] [ Strings.Call |> Lang.html shared ] + , Html.th [ HtmlA.scope "col" ] [ Strings.Response |> Lang.html shared ] + ] + ] + , Html.tbody [] tableContent + ] + ] + , editor + ] + ) + + +configureGameSettings : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg +configureGameSettings shared canEdit model lobby config = + let + passwordAttrs = + List.concat + [ case model.password of + Just value -> + [ value |> WlA.value ] + + Nothing -> + [ "" |> WlA.value, WlA.disabled ] + , [ Strings.GamePassword |> Lang.string shared |> WlA.label + , WlA.minLength 1 + , WlA.outlined + , HtmlA.class "primary" + ] + , [ WlA.readonly ] + |> Maybe.justIf (not canEdit) + |> Maybe.withDefault + [ HtmlE.onInput (Just >> PasswordChange Local >> lift) + , HtmlE.onBlur (model.password |> PasswordChange Remote |> lift) + ] + ] + + passwordSwitchAttrs = + List.concat + [ [ WlA.disabled ] + |> Maybe.justIf (not canEdit) + |> Maybe.withDefault [ HtmlE.onCheck (defaultPassword >> PasswordChange Remote >> lift) ] + , case model.password of + Just _ -> + [ WlA.checked ] + + Nothing -> + [] + ] + + password = + Components.formSection + shared + "add-deck" + (Html.div [ HtmlA.class "multipart" ] + [ Wl.switch passwordSwitchAttrs + , Wl.textField passwordAttrs [] + , Components.iconButton + [ WlA.disabled + |> Maybe.justIf (not canEdit || config.password == model.password) + |> Maybe.withDefault (model.password |> PasswordChange Remote |> lift |> HtmlE.onClick) + ] + (Icon.save |> Maybe.justIf (config.password /= model.password) |> Maybe.withDefault Icon.check) + ] + ) + [ Components.info Strings.GamePasswordDescription + , Components.warning Strings.PasswordNotSecured + ] + in + Html.div [ HtmlA.class "game-settings" ] + [ password + ] + + +defaultPassword : Bool -> Maybe String +defaultPassword enabled = + "" |> Maybe.justIf enabled + + +addDeckWidget : Shared -> List Deck -> Source.External -> Html Global.Msg +addDeckWidget shared existing deckToAdd = + let + submit = + deckToAdd |> submitDeckAction existing + in + Html.form + [ submit |> Result.map (lift >> HtmlE.onSubmit) |> Result.withDefault HtmlA.nothing ] + [ Components.formSection + shared + "add-deck" + (Html.div [ HtmlA.class "multipart" ] + [ Wl.select + [ HtmlA.id "source-selector" + , WlA.outlined + , HtmlE.onInput (Source.empty >> Maybe.withDefault Source.default >> UpdateSource >> lift) + ] + [ Html.option [ HtmlA.value "Cardcast" ] + [ Html.text "Cardcast" + ] + ] + , Source.editor shared (deckToAdd |> Source.Ex) (UpdateSource >> lift) + , Components.floatingActionButton + [ HtmlA.type_ "submit" + , Result.isError submit |> HtmlA.disabled + , Strings.AddDeck |> Lang.title shared + ] + Icon.plus + ] + ) + [ submit |> Result.error |> Maybe.withDefault Nothing ] + ] + + +submitDeckAction : List Deck -> Source.External -> Result (Components.Message Global.Msg) Msg +submitDeckAction existing deckToAdd = + let + potentialProblem = + if List.any (.source >> Source.Ex >> Source.equals (Source.Ex deckToAdd)) existing then + Strings.DeckAlreadyAdded |> Components.error |> Just + + else + Source.validate (Source.Ex deckToAdd) + in + case potentialProblem of + Just problem -> + problem |> Result.Err + + Nothing -> + deckToAdd |> AddDeck |> Result.Ok + + +lift : Msg -> Global.Msg +lift = + Lobby.ConfigureMsg >> Global.LobbyMsg + + +deck : Shared -> Bool -> Deck -> Html Global.Msg +deck shared canEdit givenDeck = + let + source = + givenDeck.source + + row = + case givenDeck.summary of + Just summary -> + [ Html.td [] [ name shared canEdit source False summary.details ] + , Html.td [] [ summary.calls |> String.fromInt |> Html.text ] + , Html.td [] [ summary.responses |> String.fromInt |> Html.text ] + ] + + Nothing -> + [ Html.td [ HtmlA.colspan 3 ] [ source |> Source.Ex |> Source.details |> name shared canEdit source True ] + ] + in + Html.tr [ HtmlA.class "deck-row" ] row + + +name : Shared -> Bool -> Source.External -> Bool -> Source.Details -> Html Global.Msg +name shared canEdit source loading details = + let + removeButton = + if canEdit then + [ Components.iconButton + [ source |> RemoveDeck |> lift |> HtmlE.onClick + , Strings.RemoveDeck |> Lang.title shared + , HtmlA.class "remove-button" + ] + Icon.minus + ] + + else + [] + + ( maybeId, maybeTooltip ) = + source |> Source.Ex |> Source.tooltip |> Maybe.decompose + + attrs = + maybeId |> Maybe.map (\id -> [ HtmlA.id id ]) |> Maybe.withDefault [] + + nameText = + Html.text details.name + + tooltip = + maybeTooltip |> Maybe.map (\t -> [ t ]) |> Maybe.withDefault [] + + linkOrText = + [ Html.span attrs [ Maybe.transformWith nameText makeLink details.url ] ] + + spinner = + if loading then + [ Icon.viewStyled [ Icon.spin ] Icon.circleNotch ] + + else + [] + in + Html.td [ HtmlA.class "name" ] (List.concat [ linkOrText, removeButton, spinner, tooltip ]) + + +makeLink : Html msg -> String -> Html msg +makeLink text url = + Html.blankA [ HtmlA.href url ] [ text ] diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Messages.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Messages.elm new file mode 100644 index 0000000..71e5bb3 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Messages.elm @@ -0,0 +1,24 @@ +module MassiveDecks.Pages.Lobby.Configure.Messages exposing (Msg(..), Target(..)) + +import MassiveDecks.Card.Source.Model as Source +import MassiveDecks.Game.Rules as Rules +import MassiveDecks.Pages.Lobby.Configure.Model exposing (..) + + +type Msg + = AddDeck Source.External + | RemoveDeck Source.External + | UpdateSource Source.External + | ChangeTab Tab + | StartGame + | HandSizeChange Target Int + | ScoreLimitChange Target (Maybe Int) + | PasswordChange Target (Maybe String) + | HouseRuleChange Target Rules.HouseRuleChange + + +{-| We don't want to push every tiny change to the server. Instead we only push some changes. +-} +type Target + = Remote + | Local diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm new file mode 100644 index 0000000..56cded4 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm @@ -0,0 +1,49 @@ +module MassiveDecks.Pages.Lobby.Configure.Model exposing + ( Config + , Deck + , DeckError + , Model + , Tab(..) + ) + +import MassiveDecks.Card.Source.Model as Source +import MassiveDecks.Game.Rules as Rules exposing (Rules) + + +type Tab + = Decks + | Rules + | Game + + +type alias DeckError = + { reason : Source.LoadFailureReason + , deck : Source.External + } + + +type alias Model = + { deckToAdd : Source.External + , deckErrors : List DeckError + , handSize : Int + , scoreLimit : Maybe Int + , password : Maybe String + , tab : Tab + , houseRules : Rules.HouseRules + } + + +{-| A deck in the configuration, either loaded or not. +-} +type alias Deck = + { source : Source.External, summary : Maybe Source.Summary } + + +{-| Configuration for a lobby. +-} +type alias Config = + { rules : Rules + , decks : List Deck + , password : Maybe String + , version : String + } diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm new file mode 100644 index 0000000..fcb7211 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm @@ -0,0 +1,73 @@ +module MassiveDecks.Pages.Lobby.Events exposing + ( ConfigChanged(..) + , DeckChange(..) + , Event(..) + , GameEvent(..) + , PresenceState(..) + , Redraw(..) + ) + +import Dict exposing (Dict) +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Play as Play +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Game.Round as Round +import MassiveDecks.Game.Rules as Rules +import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby) +import MassiveDecks.User as User +import Set exposing (Set) + + +{-| An event from the server. +-} +type Event + = Sync { state : Lobby, hand : Maybe (List Card.Response), play : Maybe (List Card.Id) } + | Connection { user : User.Id, state : User.Connection } + | Presence { user : User.Id, state : PresenceState } + | Configured { change : ConfigChanged, version : String } + -- Not a game event because we don't need to be in a game + | GameStarted { round : Round.Playing, hand : List Card.Response } + | Game GameEvent + + +{-| The user's intentional presence in the lobby. +-} +type PresenceState + = UserJoined { name : String } + | UserLeft + + +type ConfigChanged + = DecksChanged { change : DeckChange, deck : Source.External } + | HandSizeSet { size : Int } + | ScoreLimitSet { limit : Maybe Int } + | PasswordSet { password : Maybe String } + | HouseRuleChanged { change : Rules.HouseRuleChange } + + +type DeckChange + = Add + | Remove + | Load { summary : Source.Summary } + | Fail { reason : Source.LoadFailureReason } + + +type Redraw + = Player { hand : List Card.Response } + | Other { player : User.Id } + + +type GameEvent + = HandRedrawn Redraw + | PlayRevealed { id : Play.Id, play : List Card.Response } + | PlaySubmitted { by : User.Id } + | PlayTakenBack { by : User.Id } + | RoundFinished { winner : User.Id, playedBy : Dict Play.Id User.Id } + | RoundStarted + { id : Round.Id + , czar : User.Id + , players : Set User.Id + , call : Card.Call + , drawn : Maybe (List Card.Response) + } + | StartRevealing { plays : List Play.Id } diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/GameCode.elm b/client/src/elm/MassiveDecks/Pages/Lobby/GameCode.elm new file mode 100644 index 0000000..23db8b0 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/GameCode.elm @@ -0,0 +1,49 @@ +module MassiveDecks.Pages.Lobby.GameCode exposing + ( GameCode + , fromString + , toString + , trusted + ) + +import Set exposing (Set) + + +{-| A code for the lobby to identify it for users. +-} +type GameCode + = GameCode String + + +{-| If we get the game code from a trusted source, then we don't need to sanity check it. +-} +trusted : String -> GameCode +trusted = + GameCode + + +{-| Get a game code from a string by sanitising it. +-} +fromString : String -> Maybe GameCode +fromString string = + case string of + "" -> + Nothing + + _ -> + string |> String.toUpper |> String.filter (\c -> Set.member c alphabet) |> GameCode |> Just + + +{-| Get a string from a game code. +-} +toString : GameCode -> String +toString (GameCode gameCode) = + gameCode + + + +{- Private -} + + +alphabet : Set Char +alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" |> String.toList |> Set.fromList diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Invite.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Invite.elm new file mode 100644 index 0000000..2e198c2 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Invite.elm @@ -0,0 +1,144 @@ +module MassiveDecks.Pages.Lobby.Invite exposing (button, dialog, overlay) + +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Json.Decode +import MassiveDecks.Components as Components +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Pages.Route as Route +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Html as Html +import QRCode +import Url exposing (Url) +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| A button to show the dialog. +-} +button : Shared -> Html Global.Msg +button shared = + Components.iconButton + [ Lang.title shared Strings.Invite + , Lobby.ToggleInviteDialog |> Global.LobbyMsg |> HtmlE.onClick + ] + Icon.bullhorn + + +{-| A dialog overlay that displays information on how to invite people to the game. +-} +dialog : Shared -> GameCode -> Maybe String -> Bool -> Html Global.Msg +dialog shared gameCode password open = + let + lobbyUrl = + url shared gameCode + in + Html.div + [ HtmlA.id "invite-dialog" + , HtmlA.classList [ ( "open", open ) ] + , Lobby.ToggleInviteDialog |> Global.LobbyMsg |> HtmlE.onClick + ] + [ Components.iconButton + [ HtmlA.class "close" + , Lobby.ToggleInviteDialog + |> Global.LobbyMsg + |> onClickNoPropegation + , Strings.Close |> Lang.title shared + ] + Icon.times + , Wl.card [ onClickNoPropegation Global.NoOp ] + [ Strings.InviteExplanation { gameCode = gameCode, password = password } |> Lang.html shared + , Components.formSection shared + "invite-link" + (Html.div [ HtmlA.class "multipart" ] + [ Html.input + [ HtmlA.readonly True + , HtmlA.value lobbyUrl + , HtmlA.class "primary" + , HtmlA.id "invite-link-field" + ] + [] + , Components.iconButton [ "invite-link-field" |> Global.Copy |> HtmlE.onClick ] Icon.copy + ] + ) + [ Components.info Strings.InviteLinkHelp ] + , lobbyUrl |> qr + ] + ] + + +{-| A widget that displays information on how to join the game. +-} +overlay : Shared -> GameCode -> Html msg +overlay shared gameCode = + Html.div [ HtmlA.class "invite" ] + [ Html.div [ HtmlA.class "join-info" ] + [ Html.p [] [ Strings.JoinTheGame |> Lang.html shared ] + , Html.p [] [ Strings.GameCode { code = gameCode } |> Lang.html shared ] + , Html.p [] [ Html.text (stripProtocol shared.origin) ] + ] + , Html.div [ HtmlA.class "qr-code" ] [ url shared gameCode |> qr ] + ] + + + +{- Private -} + + +onClickNoPropegation : msg -> Html.Attribute msg +onClickNoPropegation msg = + HtmlE.stopPropagationOn "click" (Json.Decode.succeed ( msg, True )) + + +qr : String -> Html msg +qr lobbyUrl = + lobbyUrl + |> QRCode.encodeWith QRCode.Low + |> Result.map QRCode.toSvg + |> Result.withDefault Html.nothing + + +{-| Get the url for a lobby. +-} +url : Shared -> GameCode -> String +url shared gameCode = + Route.externalUrl shared.origin (Route.Start { section = Start.Join (Just gameCode) }) + + +{-| We assume that the protocol and root path don't matter, to simplify the shown URL. +This should be fine as long as http redirects to https, which is good practice. +If the origin doesn't parse we probably have bigger problems, but we just return it unaltered. +-} +stripProtocol : String -> String +stripProtocol stringUrl = + Url.fromString stringUrl + |> Maybe.map fromUrl + |> Maybe.withDefault stringUrl + + +fromUrl : Url -> String +fromUrl nonStringUrl = + let + portPart = + case nonStringUrl.port_ of + Nothing -> + "" + + Just port_ -> + ":" ++ String.fromInt port_ + + pathPart = + if nonStringUrl.path == "/" then + "" + + else + nonStringUrl.path + in + nonStringUrl.host ++ portPart ++ pathPart diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm new file mode 100644 index 0000000..1f6e0d9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm @@ -0,0 +1,19 @@ +module MassiveDecks.Pages.Lobby.Messages exposing (Msg(..)) + +import MassiveDecks.Animated as Animated +import MassiveDecks.Game.Messages as Game +import MassiveDecks.Models.MdError exposing (MdError) +import MassiveDecks.Pages.Lobby.Configure.Messages as Configure +import MassiveDecks.Pages.Lobby.Events exposing (Event) +import MassiveDecks.Pages.Lobby.Model exposing (..) +import MassiveDecks.User as User exposing (User) + + +type Msg + = SelectUser (Maybe User.Id) + | GameMsg Game.Msg + | EventReceived Event + | ErrorReceived MdError + | ConfigureMsg Configure.Msg + | NotificationMsg (Animated.Msg Notification) + | ToggleInviteDialog diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm new file mode 100644 index 0000000..fd3a6a9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm @@ -0,0 +1,108 @@ +module MassiveDecks.Pages.Lobby.Model exposing + ( Auth + , Claims + , Lobby + , Model + , Notification + , NotificationId + , NotificationMessage(..) + , State(..) + , Token + , TokenDecodingError(..) + ) + +import Dict exposing (Dict) +import Json.Decode as Json +import MassiveDecks.Animated exposing (Animated) +import MassiveDecks.Game.Model as Game exposing (Game) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Route exposing (..) +import MassiveDecks.User as User exposing (User) + + +{-| Data for the lobby page. +-} +type alias Model = + { route : Route + , auth : Auth + , lobby : Maybe Lobby + , configure : Configure.Model + , selectedUser : Maybe User.Id + , notificationId : NotificationId + , notifications : List (Animated Notification) + , inviteDialogOpen : Bool + } + + +{-| A lobby. +-} +type alias Lobby = + { name : String + , public : Bool + , users : Dict User.Id User + , owner : User.Id + , config : Configure.Config + , game : Maybe Game.Model + } + + +{-| The state of a lobby. +-} +type State + = Playing + | SettingUp + + +{-| A JSON Web Token for authentication with the server. +-} +type alias Token = + String + + +{-| Some claims and the token they came from. +-} +type alias Auth = + { token : Token + , claims : Claims + } + + +{-| The decoded (but not verified) claims of a JWT. +-} +type alias Claims = + { gc : GameCode + , uid : User.Id + , pvg : User.Privilege + } + + +{-| An error while trying to decode a token. +-} +type TokenDecodingError + = InvalidTokenStructure Token + | TokenJsonError Json.Error + | TokenBase64Error String + + +{-| A transient notification for the user. +-} +type alias Notification = + { id : NotificationId + , message : NotificationMessage + } + + +{-| A unique id for a notification. +-} +type alias NotificationId = + Int + + +{-| Data defining a message for a notification for the user. +-} +type NotificationMessage + = UserConnected User.Id + | UserDisconnected User.Id + | UserJoined User.Id + | UserLeft User.Id diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Route.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Route.elm new file mode 100644 index 0000000..9b58576 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Route.elm @@ -0,0 +1,43 @@ +module MassiveDecks.Pages.Lobby.Route exposing + ( Route + , partsAndFragment + , route + ) + +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Util.Maybe as Maybe + + +{-| A route for a lobby page. +-} +type alias Route = + { gameCode : GameCode + } + + +route : List String -> Maybe String -> Maybe Route +route parts _ = + case parts of + [ name, gameCode ] -> + gameCode + |> Maybe.justIf (name == lobbiesName) + |> Maybe.andThen GameCode.fromString + |> Maybe.map (\gc -> { gameCode = gc }) + + _ -> + Nothing + + +{-| Get the parts and fragment for a URL based on the given route. +-} +partsAndFragment : Route -> ( List String, Maybe String ) +partsAndFragment r = + ( [ lobbiesName, r.gameCode |> GameCode.toString ], Nothing ) + + + +{- Private -} + + +lobbiesName = + "games" diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Token.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Token.elm new file mode 100644 index 0000000..f71efa7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Token.elm @@ -0,0 +1,39 @@ +module MassiveDecks.Pages.Lobby.Token exposing (decode) + +import Base64 +import Json.Decode as Json +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Pages.Lobby.Model exposing (..) + + +{-| Decode a token to some claims. +-} +decode : Token -> Result TokenDecodingError Auth +decode token = + case token |> String.split "." of + _ :: body :: _ :: [] -> + body + |> Base64.decode + |> Result.mapError TokenBase64Error + |> Result.andThen + (\json -> + json + |> Json.decodeString decodeClaims + |> Result.mapError TokenJsonError + |> Result.map (\claims -> Auth token claims) + ) + + _ -> + Err (InvalidTokenStructure token) + + + +{- Private -} + + +decodeClaims : Json.Decoder Claims +decodeClaims = + Json.map3 Claims + (Json.field "gc" Decoders.gameCode) + (Json.field "uid" Decoders.userId) + (Json.field "pvg" Decoders.privilege) diff --git a/client/src/elm/MassiveDecks/Pages/Model.elm b/client/src/elm/MassiveDecks/Pages/Model.elm new file mode 100644 index 0000000..4bca4a7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Model.elm @@ -0,0 +1,15 @@ +module MassiveDecks.Pages.Model exposing (Page(..)) + +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Spectate.Model as Spectate +import MassiveDecks.Pages.Start.Model as Start +import MassiveDecks.Pages.Unknown.Model as Unknown + + +{-| A distinct page within the app. +-} +type Page + = Start Start.Model + | Lobby Lobby.Model + | Spectate Spectate.Model + | Unknown Unknown.Model diff --git a/client/src/elm/MassiveDecks/Pages/Route.elm b/client/src/elm/MassiveDecks/Pages/Route.elm new file mode 100644 index 0000000..097f71b --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Route.elm @@ -0,0 +1,104 @@ +module MassiveDecks.Pages.Route exposing + ( Fork(..) + , Route(..) + , externalUrl + , fromUrl + , href + , url + ) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Pages.Lobby.Route as Lobby +import MassiveDecks.Pages.Spectate.Route as Spectate +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.Pages.Unknown.Route as Unknown +import MassiveDecks.Util.Maybe as Maybe +import Url exposing (Url) +import Url.Builder + + +{-| A route in the application represents a navigable subsection of it. +These can be transformed to and from URLs. +-} +type Route + = Start Start.Route + | Lobby Lobby.Route + | Spectate Spectate.Route + | Unknown Unknown.Route + + +{-| Either some data, or a redirect to a new route. +-} +type Fork a + = Continue a + | Redirect Route + + +{-| Get the URL for the given page. +-} +url : Route -> String +url route = + urlRoot Url.Builder.Absolute route + + +{-| Get the URL for the given page, with the origin - useful for giving the user a link to copy/paste, etc... +-} +externalUrl : String -> Route -> String +externalUrl origin route = + urlRoot (Url.Builder.CrossOrigin origin) route + + +{-| Get the page described by the given URL. +-} +fromUrl : Url -> Route +fromUrl given = + let + parts = + given.path |> String.split "/" |> List.filter ((/=) "") + + fragment = + given.fragment + in + [ Start.route parts fragment |> Maybe.map Start + , Lobby.route parts fragment |> Maybe.map Lobby + , Spectate.route parts fragment |> Maybe.map Spectate + ] + |> Maybe.first + |> Maybe.withDefault (Unknown.route parts fragment |> Unknown) + + +{-| Convenience to construct an `href` attribute for a link to the given page. +-} +href : Route -> Html.Attribute msg +href = + url >> HtmlA.href + + + +{- Private -} + + +urlRoot : Url.Builder.Root -> Route -> String +urlRoot root route = + let + ( parts, fragment ) = + partsAndFragment route + in + Url.Builder.custom root parts [] fragment + + +partsAndFragment : Route -> ( List String, Maybe String ) +partsAndFragment route = + case route of + Start startRoute -> + Start.partsAndFragment startRoute + + Lobby lobbyRoute -> + Lobby.partsAndFragment lobbyRoute + + Spectate spectateRoute -> + Spectate.partsAndFragment spectateRoute + + Unknown unknownRoute -> + Unknown.partsAndFragment unknownRoute diff --git a/client/src/elm/MassiveDecks/Pages/Spectate.elm b/client/src/elm/MassiveDecks/Pages/Spectate.elm new file mode 100644 index 0000000..f6b359b --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Spectate.elm @@ -0,0 +1,304 @@ +module MassiveDecks.Pages.Spectate exposing + ( changeRoute + , init + , route + , update + , view + ) + +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Card.Source.Cardcast.Model as Cardcast +import MassiveDecks.Card.Source.Model as Source +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Route as Route +import MassiveDecks.Pages.Spectate.Messages as Spectate +import MassiveDecks.Pages.Spectate.Model exposing (..) +import MassiveDecks.Pages.Spectate.Route exposing (..) +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Random as Random +import QRCode +import Random +import Random.Float as Random +import Round +import Url exposing (Url) + + +changeRoute : Route -> Model -> ( Model, Cmd Msg ) +changeRoute r model = + ( { model | route = r }, Cmd.none ) + + +init : Route -> ( Model, Cmd Msg ) +init r = + let + call = + Card.call + ([ [ Parts.Slot Parts.None ] ] |> Parts.unsafeFromList) + "test" + ("B80VA" |> Cardcast.playCode |> Source.Cardcast |> Source.Ex) + + slots = + Parts.slotCount call.body + + testPlays = + [ Just "1" + , Just "2" + , Just "3" + , Just "4" + , Just "5" + , Just "6" + ] + |> List.map playingPlay + in + ( { route = r + , call = call + , plays = Playing testPlays + } + , askForRotations slots testPlays + ) + + +route : Model -> Route +route model = + model.route + + +update : Spectate.Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + Spectate.Rotations animations -> + case model.plays of + Playing playing -> + let + newPlays = + animations + |> List.foldl updateMatchingPlays playing + in + ( { model | plays = Playing newPlays }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + +view : Shared -> Model -> List (Html Msg) +view shared model = + let + playCount = + model.plays |> getPlays |> List.length + + angle = + turns 1 / toFloat playCount + + slots = + Card.slotCount model.call + + qr = + Route.externalUrl shared.origin (Route.Start { section = Start.Join (Just model.route.lobby.gameCode) }) + |> QRCode.encodeWith QRCode.Low + |> Result.map (\encoded -> [ QRCode.toSvg encoded ]) + |> Result.withDefault [] + in + [ Html.div + [ HtmlA.class "spectate" ] + [ Html.div [ HtmlA.class "middle" ] + [ Card.view shared [] Card.Front [] (Card.C model.call) + + --, HtmlA.style "--play-count" (model.plays |> List.length |> String.fromInt) + , Html.ul [ HtmlA.class "plays" ] (viewPlays shared slots angle model.plays) + ] + ] + , Html.div [ HtmlA.class "join-info" ] + [ Html.p [] [ Strings.JoinTheGame |> Lang.html shared ] + , Html.p [] [ Strings.GameCode { code = model.route.lobby.gameCode } |> Lang.html shared ] + , Html.p [] [ Html.text (stripProtocol shared.origin) ] + ] + , Html.div [ HtmlA.class "qr-code" ] qr + ] + + + +{- Private -} + + +{-| We assume that the protocol and root path don't matter, to simplify the shown URL. +This should be fine as long as http redirects to https, which is good practice. +If the origin doesn't parse we probably have bigger problems, but we just return it unaltered. +-} +stripProtocol : String -> String +stripProtocol stringUrl = + Url.fromString stringUrl + |> Maybe.map fromUrl + |> Maybe.withDefault stringUrl + + +fromUrl : Url -> String +fromUrl url = + let + portPart = + case url.port_ of + Nothing -> + "" + + Just port_ -> + ":" ++ String.fromInt port_ + + pathPart = + if url.path == "/" then + "" + + else + url.path + in + url.host ++ portPart ++ pathPart + + +updateMatchingPlays : { play : Play.Id, rotation : Rotations } -> List PlayingPlay -> List PlayingPlay +updateMatchingPlays animation plays = + plays + |> List.map + (Maybe.map + (\p -> + if p.play == animation.play then + { p | animation = Just animation.rotation } + + else + p + ) + ) + + +getPlays : Plays -> List (Maybe Play) +getPlays round = + case round of + Playing plays -> + plays |> List.map (Maybe.map (\pp -> Play pp.play Nothing)) + + Judging plays -> + plays |> List.map (\pp -> Just (Play pp.play.id (Just pp.play.responses))) + + Finished { plays } -> + plays |> List.map (\pp -> Just (Play pp.play.id (Just pp.play.responses))) + + +askForRotations : Int -> List PlayingPlay -> Cmd Msg +askForRotations slots plays = + plays + |> List.filterMap (rotationForPlay slots) + |> Random.disparateList + |> Random.generate (SpectateMsg << Spectate.Rotations) + + +rotationForPlay : Int -> PlayingPlay -> Maybe (Random.Generator { play : Play.Id, rotation : Rotations }) +rotationForPlay slots play = + play + |> Maybe.andThen + (\p -> + case p.animation of + Nothing -> + Random.map (\r -> { play = p.play, rotation = r }) + (Random.list slots (Random.normal 0 0.25)) + |> Just + + _ -> + Nothing + ) + + +viewPlays : Shared -> Int -> Float -> Plays -> List (Html Msg) +viewPlays shared slots anglePerPlay plays = + let + renderedCards = + case plays of + Playing playingPlays -> + playingPlays + |> List.map + (\p -> + ( viewPlayingPlay slots p + , p + |> Maybe.map (.animation >> (==) Nothing) + |> Maybe.withDefault False + ) + ) + + Judging judgingPlays -> + judgingPlays |> List.map (\p -> ( viewKnownPlay shared p.play p.rotation, False )) + + Finished finishedPlays -> + finishedPlays.plays |> List.map (\p -> ( viewKnownPlay shared p.play p.rotation, False )) + in + List.indexedMap + (\index -> + \( play, offScreen ) -> + let + angle = + toFloat index * anglePerPlay + + distance = + if offScreen then + 100 + + else + 20 + in + Html.li + ([ HtmlA.class "player", rotated (angle - turns 0.25) ] + ++ positionFromAngle angle distance + ) + -- TODO: CSS custom variables can't be set right now. + [ Html.div [ HtmlA.class "play set", HtmlA.style "--cards-in-play" (String.fromInt slots) ] play + ] + ) + renderedCards + + +positionFromAngle : Float -> Float -> List (Html.Attribute msg) +positionFromAngle angle distance = + let + left = + cos angle * distance + + top = + sin angle * distance + in + [ HtmlA.style "left" (Round.round 4 left ++ "em") + , HtmlA.style "top" (Round.round 4 top ++ "em") + ] + + +viewPlayingPlay : Int -> PlayingPlay -> List (Html msg) +viewPlayingPlay slots play = + case play of + Just p -> + List.map + (\a -> + Card.viewUnknownResponse [ rotated a ] + ) + (p.animation |> Maybe.withDefault (defaultedRotations slots p.animation)) + + Nothing -> + [ Icon.view Icon.clock ] + + +viewKnownPlay : Shared -> Play.Known -> Rotations -> List (Html msg) +viewKnownPlay shared play rotations = + List.map2 (\c -> \a -> Card.view shared [] Card.Front [ rotated a ] (Card.R c)) play.responses rotations + + +defaultedRotations : Int -> Maybe Rotations -> Rotations +defaultedRotations slots rotations = + rotations |> Maybe.withDefault (List.repeat slots 0) + + +rotated : Float -> Html.Attribute msg +rotated angle = + HtmlA.style "transform" ("rotateZ(" ++ Round.round 6 angle ++ "rad)") diff --git a/client/src/elm/MassiveDecks/Pages/Spectate/Messages.elm b/client/src/elm/MassiveDecks/Pages/Spectate/Messages.elm new file mode 100644 index 0000000..07f70f7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Spectate/Messages.elm @@ -0,0 +1,8 @@ +module MassiveDecks.Pages.Spectate.Messages exposing (Msg(..)) + +import MassiveDecks.Card.Play as Play +import MassiveDecks.Pages.Spectate.Model exposing (..) + + +type Msg + = Rotations (List { play : Play.Id, rotation : Rotations }) diff --git a/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm b/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm new file mode 100644 index 0000000..ef4ddf7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm @@ -0,0 +1,67 @@ +module MassiveDecks.Pages.Spectate.Model exposing + ( FinishedPlays + , JudgingPlay + , Model + , OwnedPlay + , PlayingPlay + , Plays(..) + , Rotations + , playingPlay + ) + +import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Play as Play +import MassiveDecks.Pages.Spectate.Route exposing (..) +import MassiveDecks.User as User + + +{-| Data for the lobby page. +-} +type alias Model = + { route : Route + , call : Card.Call + , plays : Plays + } + + +{-| The various states of plays depending on round state. +-} +type Plays + = Playing (List PlayingPlay) + | Judging (List JudgingPlay) + | Finished FinishedPlays + + +type alias Rotations = + List Float + + +{-| A convenience constructor for `PlayingPlay`. +-} +playingPlay : Maybe Play.Id -> PlayingPlay +playingPlay play = + Maybe.map (\p -> { play = p, animation = Nothing }) play + + +{-| A play while people are playing cards. If `Nothing`, then the user hasn't played yet. +-} +type alias PlayingPlay = + Maybe { play : Play.Id, animation : Maybe Rotations } + + +{-| A play that is actively being judged. +-} +type alias JudgingPlay = + { play : Play.Known, rotation : Rotations, revealed : Bool } + + +{-| Plays for a finished round. +-} +type alias FinishedPlays = + { plays : List OwnedPlay, winner : User.Id } + + +{-| A play with a revealed user it was played by. +-} +type alias OwnedPlay = + { playedBy : User.Id, play : Play.Known, rotation : Rotations } diff --git a/client/src/elm/MassiveDecks/Pages/Spectate/Route.elm b/client/src/elm/MassiveDecks/Pages/Spectate/Route.elm new file mode 100644 index 0000000..1bd94d9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Spectate/Route.elm @@ -0,0 +1,52 @@ +module MassiveDecks.Pages.Spectate.Route exposing + ( Route + , partsAndFragment + , route + ) + +import MassiveDecks.Pages.Lobby.Route as Lobby + + +{-| A route for a lobby page. +-} +type alias Route = + { lobby : Lobby.Route + } + + +route : List String -> Maybe String -> Maybe Route +route parts fragment = + let + oneLess = + List.length parts - 1 + + allButLast = + parts |> List.take oneLess + + last = + parts |> List.drop oneLess |> List.head + in + if last == Just spectateName then + Lobby.route allButLast fragment |> Maybe.map (\lobby -> { lobby = lobby }) + + else + Nothing + + +{-| Get the parts and fragment for a URL based on the given route. +-} +partsAndFragment : Route -> ( List String, Maybe String ) +partsAndFragment r = + let + ( parts, fragment ) = + Lobby.partsAndFragment r.lobby + in + ( parts ++ [ spectateName ], fragment ) + + + +{- Private -} + + +spectateName = + "spectate" diff --git a/client/src/elm/MassiveDecks/Pages/Start.elm b/client/src/elm/MassiveDecks/Pages/Start.elm new file mode 100644 index 0000000..d1f47bc --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start.elm @@ -0,0 +1,456 @@ +module MassiveDecks.Pages.Start exposing + ( changeRoute + , init + , route + , update + , view + ) + +import Dict +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon exposing (Icon) +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Card.Source.Model as Source +import MassiveDecks.Error as Error +import MassiveDecks.Error.Model as Error exposing (Error) +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Lobby.Token as Token +import MassiveDecks.Pages.Route as Route +import MassiveDecks.Pages.Start.LobbyBrowser as LobbyBrowser +import MassiveDecks.Pages.Start.Messages exposing (..) +import MassiveDecks.Pages.Start.Model exposing (..) +import MassiveDecks.Pages.Start.Route exposing (..) +import MassiveDecks.Requests.Api as Api +import MassiveDecks.Requests.HttpData as HttpData +import MassiveDecks.Requests.HttpData.Messages as HttpData +import MassiveDecks.Requests.HttpData.Model as HttpData exposing (HttpData) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util as Util +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Version as Version +import Reread +import Svg.Attributes as SvgA +import Weightless as Wl +import Weightless.Attributes as WlA + + +changeRoute : Route -> Model -> ( Model, Cmd Global.Msg ) +changeRoute r model = + let + ( lobbies, cmd ) = + case r.section of + Find -> + LobbyBrowser.refresh model.lobbies + + _ -> + ( model.lobbies, Cmd.none ) + in + ( { model + | route = r + , gameCode = Maybe.first [ gameCodeForRoute r, model.gameCode ] + , lobbies = lobbies + } + , cmd + ) + + +init : Shared -> Route -> ( Model, Cmd Global.Msg ) +init shared r = + let + ( lobbies, lobbiesCmd ) = + LobbyBrowser.init + in + ( { route = r + , name = shared.settings.settings.lastUsedName |> Maybe.withDefault "" + , gameCode = gameCodeForRoute r + , lobbies = lobbies + , newLobbyRequest = HttpData.initLazy + , joinLobbyRequest = HttpData.initLazy + } + , lobbiesCmd + ) + + +route : Model -> Route +route model = + model.route + + +update : Msg -> Model -> ( Model, Cmd Global.Msg ) +update msg model = + case msg of + GameCodeChanged gameCode -> + ( { model | gameCode = gameCode |> GameCode.fromString }, Cmd.none ) + + NameChanged name -> + ( { model | name = name }, Cmd.none ) + + StartGame httpDataMsg -> + Util.modelLift (\nlr -> { model | newLobbyRequest = nlr }) + (HttpData.update (startGameRequest model.name) httpDataMsg model.newLobbyRequest) + + JoinGame httpDataMsg -> + case model.gameCode of + Just gc -> + Util.modelLift (\jlr -> { model | joinLobbyRequest = jlr }) + (HttpData.update + (joinGameRequest gc model.name) + httpDataMsg + model.joinLobbyRequest + ) + + _ -> + ( model, Cmd.none ) + + LobbyBrowserMsg lbm -> + Util.modelLift (\lobbies -> { model | lobbies = lobbies }) (LobbyBrowser.update lbm model.lobbies) + + +view : Shared -> Model -> List (Html Global.Msg) +view shared model = + [ Html.div [ HtmlA.class "page start" ] + [ Html.header [ HtmlA.class "title-card" ] + [ Html.h1 [] [ Html.div [ HtmlA.class "card-slicer" ] [ Card.viewUnknownCall [] ] ] + , Html.div [ HtmlA.class "subtitle" ] + [ Html.div [ HtmlA.class "card-slicer" ] + [ Card.view shared [] Card.Front [] (subtitleCard shared) + ] + ] + ] + , Wl.card [] + [ Wl.tabGroup [ WlA.align WlA.Center ] + [ tab shared New model Icon.plus Strings.NewGame + , tab shared (Join model.gameCode) model Icon.signInAlt Strings.JoinPrivateGame + , tab shared Find model Icon.search Strings.FindPublicGame + , tab shared About model Icon.questionCircle Strings.AboutTheGame + ] + , sectionContent shared model + ] + , Html.footer [ HtmlA.class "version-info" ] + [ Html.div [ HtmlA.class "logos" ] + [ Html.blankA + [ HtmlA.class "logo" + , Strings.MDProject |> Lang.title shared + , HtmlA.href "https://github.com/Lattyware/massivedecks" + ] + [ Icon.viewStyled [ Strings.MDLogoDescription |> Lang.alt shared ] Reread.mdIcon ] + , Html.blankA + [ HtmlA.class "logo" + , Strings.DevelopedByReread |> Lang.title shared + , HtmlA.href "https://www.rereadgames.com/" + ] + [ Icon.viewStyled [ Strings.RereadLogoDescription |> Lang.alt shared ] Reread.icon ] + ] + , Html.p [ HtmlA.class "version" ] + [ Html.text "\"" + , Strings.MassiveDecks |> Lang.html shared + , Html.text "\" " + , Strings.Version { versionNumber = Version.version } |> Lang.html shared + ] + ] + ] + ] + + + +{- Private -} + + +startGameRequest : String -> HttpData.Pull Global.Msg +startGameRequest name = + HttpData.interceptedRequest + (Api.newLobby { owner = { name = name } }) + (Token.decode >> Result.mapError Error.Token) + (StartGame >> Global.StartMsg) + (Global.JoinLobby name) + + +joinGameRequest : GameCode -> String -> HttpData.Pull Global.Msg +joinGameRequest gameCode name = + HttpData.interceptedRequest + (Api.joinLobby gameCode { name = name }) + (Token.decode >> Result.mapError Error.Token) + (StartGame >> Global.StartMsg) + (Global.JoinLobby name) + + +loadingOrLoaded : Model -> Bool +loadingOrLoaded model = + [ model.newLobbyRequest ] |> List.any HttpData.loadingOrLoaded + + +subtitleCard : Shared -> Card +subtitleCard shared = + Card.response (Strings.ShortGameDescription |> Lang.string shared) "" Source.Fake |> Card.R + + +gameCodeForRoute : Route -> Maybe GameCode +gameCodeForRoute r = + case r.section of + Join (Just gc) -> + Just gc + + _ -> + Nothing + + +tab : Shared -> Section -> Model -> Icon -> MdString -> Html Global.Msg +tab shared targetSection model icon title = + let + r = + model.route + in + Wl.tab + (List.concat + [ [ Route.Start { r | section = targetSection } |> Global.ChangePage |> HtmlE.onClick + ] + , [ WlA.checked ] |> Maybe.justIf (sectionsMatch r.section targetSection) |> Maybe.withDefault [] + ] + ) + [ Icon.view icon + , title |> Lang.html shared + ] + + +sectionsMatch : Section -> Section -> Bool +sectionsMatch first second = + case first of + Join _ -> + case second of + Join _ -> + True + + _ -> + False + + other -> + other == second + + +sectionContent : Shared -> Model -> Html Global.Msg +sectionContent shared model = + case model.route.section of + New -> + newContent shared model + + Join _ -> + joinContent shared model + + Find -> + LobbyBrowser.view shared (Route.Start model.route) model.lobbies + + About -> + aboutContent shared + + +newContent : Shared -> Model -> Html Global.Msg +newContent shared model = + let + loading = + loadingOrLoaded model + + buttonAttr = + if model.name == "" || loading then + HtmlA.disabled True + + else + StartGame HttpData.Pull |> Global.StartMsg |> HtmlE.onClick + + buttonIcon = + if loading then + Icon.viewStyled [ Icon.spin ] Icon.circleNotch + + else + Icon.view Icon.play + + error = + model.newLobbyRequest.error + |> Maybe.map + (\e -> + [ Error.view shared (Route.Start model.route) e ] + ) + |> Maybe.withDefault [] + in + Html.div [ HtmlA.class "new-game start-tab" ] + (List.concat + [ error + , nameField shared model + , [ Wl.button + [ buttonAttr + ] + [ buttonIcon, Strings.PlayGame |> Lang.html shared ] + ] + ] + ) + + +joinContent : Shared -> Model -> Html Global.Msg +joinContent shared model = + let + loading = + loadingOrLoaded model + + buttonAttr = + if String.isEmpty model.name || model.gameCode == Nothing || loading then + HtmlA.disabled True + + else + JoinGame HttpData.Pull |> Global.StartMsg |> HtmlE.onClick + + buttonIcon = + if loading then + Icon.viewStyled [ Icon.spin ] Icon.circleNotch + + else + Icon.view Icon.play + in + Html.div [ HtmlA.class "join-game start-tab" ] + (List.concat + [ rejoinSection shared model + , nameField shared model + , [ Html.div [ HtmlA.class "form-chunk" ] + [ Wl.textField + [ HtmlA.class "game-code-input" + , GameCodeChanged >> Global.StartMsg |> HtmlE.onInput + , WlA.value (model.gameCode |> Maybe.map GameCode.toString |> Maybe.withDefault "") + , WlA.outlined + , Strings.GameCodeTerm |> Lang.label shared + ] + -- "game-code" + -- (Weightless.Help [ Strings.GameCodeHowToAcquire |> Lang.html shared ]) + [] + ] + , Wl.button + [ buttonAttr + ] + [ buttonIcon, Strings.PlayGame |> Lang.html shared ] + ] + ] + ) + + +rejoinSection : Shared -> Model -> List (Html Global.Msg) +rejoinSection shared model = + let + lobbies = + shared.settings.settings.tokens |> Dict.values |> List.map Token.decode + in + if List.isEmpty lobbies then + [] + + else + [ Html.div [ HtmlA.class "rejoin" ] + [ Html.h3 [] [ Strings.RejoinTitle |> Lang.html shared ] + , Html.ul [] (lobbies |> List.filterMap (rejoinLobby shared)) + ] + ] + + +rejoinLobby : Shared -> Result Lobby.TokenDecodingError Lobby.Auth -> Maybe (Html Global.Msg) +rejoinLobby shared result = + case result of + Ok auth -> + Html.li [] + [ Html.a [ Route.Lobby { gameCode = auth.claims.gc } |> Route.href ] + [ Strings.RejoinGame { code = auth.claims.gc } |> Lang.html shared + ] + ] + |> Just + + Err _ -> + Nothing + + +nameField : Shared -> Model -> List (Html Global.Msg) +nameField shared model = + let + -- TODO: Wire to real error. + -- help = + -- if False then + -- Wl.Error [ Strings.NameInUse |> Lang.html shared ] + -- + -- else + -- Wl.None + _ = + "" + in + [ Html.div [ HtmlA.class "form-chunk" ] + [ Wl.textField + [ NameChanged + >> Global.StartMsg + |> HtmlE.onInput + , WlA.value model.name + , Strings.NameLabel |> Lang.label shared + , WlA.outlined + ] + [] + ] + ] + + +aboutContent : Shared -> Html Global.Msg +aboutContent shared = + let + html = + Lang.html shared + in + Html.div [ HtmlA.class "about" ] + [ Html.h2 [] [ Strings.WhatIsThis |> html ] + , Html.p [] [ Strings.GameDescription |> html ] + , Html.h2 [] [ Strings.Rules |> html ] + , Html.p [] [ Strings.RulesHand |> html ] + , Html.p [] [ Strings.RulesCzar |> html ] + , Html.p [] [ Strings.RulesPlaying |> html ] + , Html.p [] [ Strings.RulesJudging |> html ] + , Html.figure [ HtmlA.class "example-card" ] + [ Card.view shared [] Card.Front [] examplePick2 + , Html.figcaption [] + [ Strings.ExamplePickDescription |> html ] + ] + , Html.h3 [] [ Strings.RulesPickTitle |> html ] + , Html.p [] [ Strings.RulesPick |> html ] + , Html.p [] [ Strings.RulesDraw |> html ] + , Html.h2 [] [ Strings.HouseRulesTitle |> html ] + , Html.p [] [ Strings.HouseRules |> html ] + , Html.ul [ Icon.ul, SvgA.class "rule-list" ] (houseRules |> List.map (houseRule shared)) + ] + + +houseRules : List ( MdString, MdString ) +houseRules = + [ ( Strings.HouseRuleReboot, Strings.HouseRuleRebootDescription { cost = Nothing } ) + , ( Strings.HouseRulePackingHeat, Strings.HouseRulePackingHeatDescription ) + , ( Strings.HouseRuleRandoCardrissian, Strings.HouseRuleRandoCardrissianDescription ) + ] + + +houseRule : Shared -> ( MdString, MdString ) -> Html msg +houseRule shared ( name, description ) = + Html.li [] + [ Html.h3 [] [ name |> Lang.html shared ] + , Html.p [] [ description |> Lang.html shared ] + ] + + +examplePick2 : Card +examplePick2 = + Card.call + (Parts.unsafeFromList + [ [ Parts.Slot Parts.None, Parts.Text " + ", Parts.Slot Parts.None ] + , [ Parts.Text " = ", Parts.Slot Parts.None ] + ] + ) + "" + Source.Fake + |> Card.C diff --git a/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser.elm b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser.elm new file mode 100644 index 0000000..6c467d5 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser.elm @@ -0,0 +1,170 @@ +module MassiveDecks.Pages.Start.LobbyBrowser exposing + ( init + , refresh + , update + , view + ) + +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Html.Keyed as HtmlK +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Lobby.GameCode as GameCode +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Route as Route exposing (Route) +import MassiveDecks.Pages.Start.LobbyBrowser.Messages exposing (..) +import MassiveDecks.Pages.Start.LobbyBrowser.Model exposing (..) +import MassiveDecks.Pages.Start.Messages as Start +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.Requests.Api as Api +import MassiveDecks.Requests.HttpData as HttpData +import MassiveDecks.Requests.HttpData.Messages as HttpData +import MassiveDecks.Requests.HttpData.Model as HttpData exposing (HttpData) +import MassiveDecks.Strings as Strings exposing (MdString(..)) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.List as List +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| Set up the lobby browser. +-} +init : ( Model, Cmd Global.Msg ) +init = + HttpData.init requestLobbySummaries + + +{-| An html view of the lobby browser. +-} +view : Shared -> Route -> Model -> Html Global.Msg +view shared route summaries = + Html.div [ HtmlA.class "lobby-browser" ] + [ Html.div [ HtmlA.class "top" ] + [ Html.h2 [] [ LobbyBrowserTitle |> Lang.html shared ] + , HttpData.refreshButton shared summaries |> Html.map (SummaryUpdate >> lift) + ] + , HttpData.view shared route (SummaryUpdate >> lift) (lobbyList shared) summaries + ] + + +{-| Update the state of the lobby browser. +-} +update : Msg -> Model -> ( Model, Cmd Global.Msg ) +update msg model = + case msg of + SummaryUpdate summaries -> + HttpData.update requestLobbySummaries summaries model + + +{-| Try to refresh the lobbies. +-} +refresh : Model -> ( Model, Cmd Global.Msg ) +refresh model = + update (HttpData.Pull |> SummaryUpdate) model + + + +{- Private -} + + +requestLobbySummaries : HttpData.Pull Global.Msg +requestLobbySummaries = + HttpData.request Api.lobbySummaries |> Cmd.map (SummaryUpdate >> lift) + + +lift : Msg -> Global.Msg +lift = + Start.LobbyBrowserMsg >> Global.StartMsg + + +lobbyList : Shared -> List Summary -> Html Global.Msg +lobbyList shared summaries = + if List.isEmpty summaries then + Html.div [ HtmlA.class "empty-info" ] + [ Icon.view Icon.ghost + , NoPublicGames |> Lang.html shared + , Html.text " " + , Html.a [ Route.Start { section = Start.New } |> Route.href ] [ StartYourOwn |> Lang.html shared ] + ] + + else + HtmlK.ul [] + (summaries + |> byState + |> List.filter (\( _, lobbies ) -> not (List.isEmpty lobbies)) + |> List.map (stateGroup shared) + |> List.mappedIntersperse sep + ) + + +states : List Lobby.State +states = + [ Lobby.SettingUp + , Lobby.Playing + ] + + +stateId : Lobby.State -> String +stateId state = + case state of + Lobby.Playing -> + "playing" + + Lobby.SettingUp -> + "setting-up" + + +stateDescription : Lobby.State -> MdString +stateDescription state = + case state of + Lobby.Playing -> + PlayingGame + + Lobby.SettingUp -> + SettingUpGame + + +sep : ( String, Html msg ) -> ( String, Html msg ) -> ( String, Html msg ) +sep ( before, _ ) ( after, _ ) = + ( "Sep" ++ before ++ after + , Html.hr [] [] + ) + + +byState : List Summary -> List ( Lobby.State, List Summary ) +byState lobbies = + states |> List.map (\state -> ( state, lobbies |> List.filter (.state >> (==) state) )) + + +stateGroup : Shared -> ( Lobby.State, List Summary ) -> ( String, Html Global.Msg ) +stateGroup shared ( state, lobbies ) = + ( state |> stateId + , Html.li [] + [ Html.div [] + [ Html.h2 [] [ state |> stateDescription |> Lang.html shared ] + , HtmlK.ul [] (lobbies |> List.map (lobby shared)) + ] + ] + ) + + +lobby : Shared -> Summary -> ( String, Html Global.Msg ) +lobby shared data = + ( data.gameCode |> GameCode.toString + , Wl.listItem + [ HtmlE.onClick (Route.Start { section = Start.Join (Just data.gameCode) } |> Global.ChangePage) + , WlA.clickable + ] + [ Html.span [] [ Html.text data.name ] + , Html.span [] [ Strings.GameCode { code = data.gameCode } |> Lang.html shared ] + , Icon.viewStyled + [ HtmlA.title "Join Game" + , WlA.listItemSlot WlA.AfterItem + ] + Icon.signInAlt + ] + ) diff --git a/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Messages.elm b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Messages.elm new file mode 100644 index 0000000..42cbf15 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Messages.elm @@ -0,0 +1,8 @@ +module MassiveDecks.Pages.Start.LobbyBrowser.Messages exposing (Msg(..)) + +import MassiveDecks.Pages.Start.LobbyBrowser.Model exposing (..) +import MassiveDecks.Requests.HttpData.Messages as HttpData + + +type Msg + = SummaryUpdate (HttpData.Msg (List Summary)) diff --git a/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm new file mode 100644 index 0000000..c7722a9 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm @@ -0,0 +1,33 @@ +module MassiveDecks.Pages.Start.LobbyBrowser.Model exposing + ( Model + , Summary + , UserSummary + ) + +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Requests.HttpData.Model exposing (HttpData) + + +{-| The model for the lobby browser. +-} +type alias Model = + HttpData (List Summary) + + +{-| An external summary of a lobby. +-} +type alias Summary = + { name : String + , gameCode : GameCode + , state : Lobby.State + , users : UserSummary + } + + +{-| A summary of the users in a lobby. +-} +type alias UserSummary = + { players : Int + , spectators : Int + } diff --git a/client/src/elm/MassiveDecks/Pages/Start/Messages.elm b/client/src/elm/MassiveDecks/Pages/Start/Messages.elm new file mode 100644 index 0000000..0d863d5 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/Messages.elm @@ -0,0 +1,13 @@ +module MassiveDecks.Pages.Start.Messages exposing (Msg(..)) + +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Start.LobbyBrowser.Messages as LobbyBrowser +import MassiveDecks.Requests.HttpData.Messages as HttpData + + +type Msg + = GameCodeChanged String + | NameChanged String + | StartGame (HttpData.Msg Lobby.Auth) + | JoinGame (HttpData.Msg Lobby.Auth) + | LobbyBrowserMsg LobbyBrowser.Msg diff --git a/client/src/elm/MassiveDecks/Pages/Start/Model.elm b/client/src/elm/MassiveDecks/Pages/Start/Model.elm new file mode 100644 index 0000000..fcc95e1 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/Model.elm @@ -0,0 +1,30 @@ +module MassiveDecks.Pages.Start.Model exposing + ( LobbyCreation + , Model + ) + +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Start.LobbyBrowser.Model as LobbyBrowser +import MassiveDecks.Pages.Start.Route exposing (Route) +import MassiveDecks.Requests.HttpData.Model as HttpData exposing (HttpData) +import MassiveDecks.User as User + + +{-| Data for the start page. +-} +type alias Model = + { route : Route + , lobbies : LobbyBrowser.Model + , name : String + , gameCode : Maybe GameCode + , newLobbyRequest : HttpData Lobby.Auth + , joinLobbyRequest : HttpData Lobby.Auth + } + + +{-| A request to create a new lobby. +-} +type alias LobbyCreation = + { owner : User.Registration + } diff --git a/client/src/elm/MassiveDecks/Pages/Start/Route.elm b/client/src/elm/MassiveDecks/Pages/Start/Route.elm new file mode 100644 index 0000000..4c37f94 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Start/Route.elm @@ -0,0 +1,108 @@ +module MassiveDecks.Pages.Start.Route exposing + ( Route + , Section(..) + , partsAndFragment + , route + ) + +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Util.Maybe as Maybe + + +{-| A route for the start page. +-} +type alias Route = + { section : Section + } + + +{-| A section of the start page. +-} +type Section + = New + | Join (Maybe GameCode) + | Find + | About + + +route : List String -> Maybe String -> Maybe Route +route givenParts fragment = + let + section = + if givenParts == parts then + Just + (case fragment of + Just frag -> + if frag == newFragment then + New + + else if frag == findFragment then + Find + + else if frag == aboutFragment then + About + + else if frag == joinFragment then + Join Nothing + + else + Join + (frag |> GameCode.fromString) + + Nothing -> + New + ) + + else + Nothing + in + section |> Maybe.map (\s -> { section = s }) + + +{-| Get the parts and fragment for a URL based on the given route. +-} +partsAndFragment : Route -> ( List String, Maybe String ) +partsAndFragment r = + ( parts + , case r.section of + New -> + Just newFragment + + Join gameCode -> + Just (gameCode |> Maybe.map GameCode.toString |> Maybe.withDefault joinFragment) + + Find -> + Just findFragment + + About -> + Just aboutFragment + ) + + + +{- Private -} + + +parts : List String +parts = + [] + + +newFragment : String +newFragment = + "new" + + +joinFragment : String +joinFragment = + "join" + + +findFragment : String +findFragment = + "find" + + +aboutFragment : String +aboutFragment = + "about" diff --git a/client/src/elm/MassiveDecks/Pages/Unknown.elm b/client/src/elm/MassiveDecks/Pages/Unknown.elm new file mode 100644 index 0000000..aa40a94 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Unknown.elm @@ -0,0 +1,50 @@ +module MassiveDecks.Pages.Unknown exposing + ( changeRoute + , init + , route + , view + ) + +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Messages exposing (..) +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Route as Route +import MassiveDecks.Pages.Start.Route as Start +import MassiveDecks.Pages.Unknown.Model exposing (..) +import MassiveDecks.Pages.Unknown.Route exposing (..) +import MassiveDecks.Strings exposing (MdString(..)) +import MassiveDecks.Strings.Languages as Lang +import Weightless + + +changeRoute : Route -> Model -> ( Model, Cmd Msg ) +changeRoute r model = + ( { model | route = r }, Cmd.none ) + + +init : Route -> ( Model, Cmd Msg ) +init r = + ( { route = r }, Cmd.none ) + + +route : Model -> Route +route model = + model.route + + +view : Shared -> Model -> List (Html Msg) +view shared _ = + [ Html.div [ HtmlA.class "page unknown-page" ] + [ Weightless.card [] + [ Html.h1 [] [ Icon.view Icon.exclamationCircle, Lang.html shared UnknownPageTitle ] + , Html.p [] + [ Html.a [ Route.url (Route.Start { section = Start.New }) |> HtmlA.href ] + [ Lang.html shared GoBackHome + ] + ] + ] + ] + ] diff --git a/client/src/elm/MassiveDecks/Pages/Unknown/Model.elm b/client/src/elm/MassiveDecks/Pages/Unknown/Model.elm new file mode 100644 index 0000000..4ab3220 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Unknown/Model.elm @@ -0,0 +1,9 @@ +module MassiveDecks.Pages.Unknown.Model exposing (Model) + +import MassiveDecks.Pages.Unknown.Route exposing (..) + + +{-| Data for the unknown page page. +-} +type alias Model = + { route : Route } diff --git a/client/src/elm/MassiveDecks/Pages/Unknown/Route.elm b/client/src/elm/MassiveDecks/Pages/Unknown/Route.elm new file mode 100644 index 0000000..45f60ee --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Unknown/Route.elm @@ -0,0 +1,25 @@ +module MassiveDecks.Pages.Unknown.Route exposing + ( Route + , partsAndFragment + , route + ) + +{-| A route for the unknown page page. +-} + + +type alias Route = + { requestedPage : List String + } + + +route : List String -> Maybe String -> Route +route parts _ = + { requestedPage = parts } + + +{-| Get the parts and fragment for a URL based on the given route. +-} +partsAndFragment : Route -> ( List String, Maybe String ) +partsAndFragment r = + ( r.requestedPage, Nothing ) diff --git a/client/src/elm/MassiveDecks/Ports.elm b/client/src/elm/MassiveDecks/Ports.elm new file mode 100644 index 0000000..3cb5ec1 --- /dev/null +++ b/client/src/elm/MassiveDecks/Ports.elm @@ -0,0 +1,28 @@ +port module MassiveDecks.Ports exposing + ( castStatus + , copyText + , serverRecv + , serverSend + , storeSettings + , tryCast + ) + +import Json.Decode as Json + + +port storeSettings : Json.Value -> Cmd msg + + +port tryCast : Json.Value -> Cmd msg + + +port castStatus : (Json.Value -> msg) -> Sub msg + + +port serverSend : Json.Value -> Cmd msg + + +port serverRecv : (String -> msg) -> Sub msg + + +port copyText : String -> Cmd msg diff --git a/client/src/elm/MassiveDecks/Requests/Api.elm b/client/src/elm/MassiveDecks/Requests/Api.elm new file mode 100644 index 0000000..ce26776 --- /dev/null +++ b/client/src/elm/MassiveDecks/Requests/Api.elm @@ -0,0 +1,80 @@ +module MassiveDecks.Requests.Api exposing + ( checkAlive + , joinLobby + , lobbySummaries + , newLobby + ) + +import Dict exposing (Dict) +import Http +import Json.Decode +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Models.Encoders as Encoders +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Start.LobbyBrowser.Model as LobbyBrowser +import MassiveDecks.Pages.Start.Model as Start +import MassiveDecks.Requests.Request exposing (Request) +import MassiveDecks.User as User +import Url.Builder + + +{-| List the public lobbies. +-} +lobbySummaries : Request (List LobbyBrowser.Summary) +lobbySummaries = + { method = "GET" + , headers = [] + , url = url [ "games" ] + , body = Http.emptyBody + , expect = Http.expectJson identity (Json.Decode.list Decoders.lobbySummary) + , timeout = Nothing + , tracker = Nothing + } + + +{-| Create a new lobby. +-} +newLobby : Start.LobbyCreation -> Request Lobby.Token +newLobby creation = + { method = "POST" + , headers = [] + , url = url [ "games" ] + , body = creation |> Encoders.lobbyCreation |> Http.jsonBody + , expect = Http.expectJson identity Decoders.lobbyToken + , timeout = Nothing + , tracker = Nothing + } + + +joinLobby : GameCode -> User.Registration -> Request Lobby.Token +joinLobby gameCode registration = + { method = "POST" + , headers = [] + , url = url [ "games", gameCode |> GameCode.toString ] + , body = registration |> Encoders.userRegistration |> Http.jsonBody + , expect = Http.expectJson identity Decoders.lobbyToken + , timeout = Nothing + , tracker = Nothing + } + + +checkAlive : List Lobby.Token -> Request (Dict Lobby.Token Bool) +checkAlive tokens = + { method = "POST" + , headers = [] + , url = url [ "games", "alive" ] + , body = tokens |> Encoders.checkAlive |> Http.jsonBody + , expect = Http.expectJson identity Decoders.tokenValidity + , timeout = Nothing + , tracker = Nothing + } + + + +{- Private -} + + +url : List String -> String +url path = + Url.Builder.absolute ([ "api" ] ++ path) [] diff --git a/client/src/elm/MassiveDecks/Requests/HttpData.elm b/client/src/elm/MassiveDecks/Requests/HttpData.elm new file mode 100644 index 0000000..258f753 --- /dev/null +++ b/client/src/elm/MassiveDecks/Requests/HttpData.elm @@ -0,0 +1,195 @@ +module MassiveDecks.Requests.HttpData exposing + ( autoRefresh + , init + , initLazy + , interceptedRequest + , loadingOrLoaded + , mappedRequest + , refreshButton + , request + , update + , view + ) + +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Http +import MassiveDecks.Error as Error +import MassiveDecks.Error.Model as Error exposing (Error) +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Route exposing (Route) +import MassiveDecks.Requests.HttpData.Messages exposing (..) +import MassiveDecks.Requests.HttpData.Model exposing (..) +import MassiveDecks.Requests.Request exposing (Request) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Result as Result +import Time +import Weightless as Wl +import Weightless.Attributes as WlA + + +{-| Set up the empty HttpData and send a request to load the data. +-} +init : Pull msg -> ( HttpData result, Cmd msg ) +init req = + ( initLazy, req ) + + +{-| Set up the empty HttpData with no initial request, the request can be made later. +-} +initLazy : HttpData result +initLazy = + { loading = False, data = Nothing, error = Nothing } + + +{-| Tries to refresh the data every X milliseconds. +-} +autoRefresh : Float -> Sub (Msg result) +autoRefresh every = + Time.every every (\_ -> Pull) + + +{-| If the data has been loaded, or is currently loading. +-} +loadingOrLoaded : HttpData result -> Bool +loadingOrLoaded model = + model.loading || model.data /= Nothing + + +{-| Update the data with the response from the request. +-} +update : Pull msg -> Msg result -> HttpData result -> ( HttpData result, Cmd msg ) +update req msg model = + case msg of + Pull -> + if not model.loading then + ( { model | loading = True }, req ) + + else + ( model, Cmd.none ) + + Response result -> + let + loadingStoppedModel = + { model | loading = False } + in + case result of + Ok response -> + ( { loadingStoppedModel + | data = Just response + , error = Nothing + } + , Cmd.none + ) + + Err error -> + ( { loadingStoppedModel | error = Just error }, Cmd.none ) + + +{-| A view over the data with any error received trying to load (or refresh) if it isn't there (or prefixed if during a +refresh). +-} +view : Shared -> Route -> (Msg result -> msg) -> (result -> Html msg) -> HttpData result -> Html msg +view shared route wrap viewResult model = + let + error = + model.error |> Maybe.map (Error.view shared route) + + result = + model.data |> Maybe.map viewResult + + content = + List.filterMap identity [ error, result ] + + contentOrSpinner = + if List.isEmpty content then + [ Html.div [ HtmlA.class "info" ] [ refreshButton shared model |> Html.map wrap ] ] + + else + content + in + Html.div [ HtmlA.class "loaded-data" ] contentOrSpinner + + +{-| Show a refresh button for the data. +-} +refreshButton : Shared -> HttpData result -> Html (Msg result) +refreshButton shared model = + let + title = + [ Strings.Refresh |> Lang.title shared ] + + onClick = + if not model.loading then + [ HtmlE.onClick Pull ] + + else + [] + + style = + [ WlA.flat + , WlA.fab + , WlA.inverted + ] + + spin = + if model.loading then + [ Icon.spin ] + + else + [] + in + Wl.button + (List.concat [ style, title, onClick ]) + [ Icon.viewStyled spin Icon.sync ] + + +{-| A request that just stores the result in the `HttpData`. +-} +request : Request result -> Cmd (Msg result) +request req = + mappedRequest req Ok + + +{-| A request that just maps the result before storage in the `HttpData`. +-} +mappedRequest : Request response -> (response -> Result Error result) -> Pull (Msg result) +mappedRequest req f = + interceptedRequest req f identity (Ok >> Response) + + +{-| A request that just maps the result before storage in the `HttpData`, as well as intercepting successes to perform +an action (note that this means the value will never get filled! +-} +interceptedRequest : + Request response + -> (response -> Result Error result) + -> (Msg result -> msg) + -> (result -> msg) + -> Pull msg +interceptedRequest req f wrap intercept = + req |> Http.request |> Cmd.map (mapResponse f wrap intercept) + + + +{- Private -} + + +mapResponse : + (response -> Result Error result) + -> (Msg result -> msg) + -> (result -> msg) + -> Result Http.Error response + -> msg +mapResponse f wrap intercept result = + result + |> Result.mapError Error.Http + |> Result.andThen f + |> Result.map intercept + |> Result.mapError (Err >> Response >> wrap) + |> Result.unify diff --git a/client/src/elm/MassiveDecks/Requests/HttpData/Messages.elm b/client/src/elm/MassiveDecks/Requests/HttpData/Messages.elm new file mode 100644 index 0000000..7da35c3 --- /dev/null +++ b/client/src/elm/MassiveDecks/Requests/HttpData/Messages.elm @@ -0,0 +1,10 @@ +module MassiveDecks.Requests.HttpData.Messages exposing (Msg(..)) + +import MassiveDecks.Error.Model as Error exposing (Error) + + +{-| A message for HttpData. +-} +type Msg result + = Pull + | Response (Result Error result) diff --git a/client/src/elm/MassiveDecks/Requests/HttpData/Model.elm b/client/src/elm/MassiveDecks/Requests/HttpData/Model.elm new file mode 100644 index 0000000..e3e8ae6 --- /dev/null +++ b/client/src/elm/MassiveDecks/Requests/HttpData/Model.elm @@ -0,0 +1,21 @@ +module MassiveDecks.Requests.HttpData.Model exposing + ( HttpData + , Pull + ) + +import MassiveDecks.Error.Model as Error exposing (Error) + + +{-| Some data that is requested and received via an HTTP request. +-} +type alias HttpData result = + { loading : Bool + , data : Maybe result + , error : Maybe Error + } + + +{-| The command used to request data. +-} +type alias Pull msg = + Cmd msg diff --git a/client/src/elm/MassiveDecks/Requests/Request.elm b/client/src/elm/MassiveDecks/Requests/Request.elm new file mode 100644 index 0000000..19ad588 --- /dev/null +++ b/client/src/elm/MassiveDecks/Requests/Request.elm @@ -0,0 +1,14 @@ +module MassiveDecks.Requests.Request exposing (Request) + +import Http + + +type alias Request msg = + { method : String + , headers : List Http.Header + , url : String + , body : Http.Body + , expect : Http.Expect (Result Http.Error msg) + , timeout : Maybe Float + , tracker : Maybe String + } diff --git a/client/src/elm/MassiveDecks/ServerConnection.elm b/client/src/elm/MassiveDecks/ServerConnection.elm new file mode 100644 index 0000000..929156a --- /dev/null +++ b/client/src/elm/MassiveDecks/ServerConnection.elm @@ -0,0 +1,58 @@ +module MassiveDecks.ServerConnection exposing + ( connect + , disconnect + , message + , notifications + ) + +import Json.Decode as JsonDecode +import Json.Encode as Json +import MassiveDecks.Models.Decoders as Decoders +import MassiveDecks.Models.MdError exposing (MdError) +import MassiveDecks.Pages.Lobby.Events exposing (Event) +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Ports as Ports + + +connect : GameCode -> Lobby.Token -> Cmd msg +connect gameCode token = + Json.object + [ ( "gameCode", gameCode |> GameCode.toString |> Json.string ) + , ( "token", Json.string token ) + ] + |> Ports.serverSend + + +message : Json.Value -> Cmd msg +message value = + Json.object [ ( "message", value |> Json.encode 0 |> Json.string ) ] |> Ports.serverSend + + +disconnect : Cmd msg +disconnect = + Json.object [] |> Ports.serverSend + + +notifications : (Event -> msg) -> (MdError -> msg) -> (JsonDecode.Error -> msg) -> Sub msg +notifications handle handleError handleJsonError = + Ports.serverRecv + (JsonDecode.decodeString Decoders.eventOrMdError >> eventOrError handle handleError handleJsonError) + + +eventOrError : + (Event -> msg) + -> (MdError -> msg) + -> (JsonDecode.Error -> msg) + -> Result JsonDecode.Error (Result MdError Event) + -> msg +eventOrError handle handleError handleJsonError result = + case result of + Ok (Ok event) -> + handle event + + Ok (Err error) -> + handleError error + + Err error -> + handleJsonError error diff --git a/client/src/elm/MassiveDecks/Settings.elm b/client/src/elm/MassiveDecks/Settings.elm new file mode 100644 index 0000000..be8fdaa --- /dev/null +++ b/client/src/elm/MassiveDecks/Settings.elm @@ -0,0 +1,289 @@ +module MassiveDecks.Settings exposing + ( auths + , defaults + , init + , onJoinLobby + , update + , view + ) + +import Dict exposing (Dict) +import Dict.Extra as Dict +import FontAwesome.Attributes as Icon +import FontAwesome.Icon as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Http +import MassiveDecks.Components as Components +import MassiveDecks.LocalStorage as LocalStorage +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (..) +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Pages.Lobby.Token as Token +import MassiveDecks.Requests.Api as Api +import MassiveDecks.Settings.Messages exposing (..) +import MassiveDecks.Settings.Model exposing (..) +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) +import MassiveDecks.Util.Result as Result +import Weightless as Wl +import Weightless.Attributes as WlA + + +init : Settings -> ( Model, Cmd Global.Msg ) +init settings = + let + cmd = + if Dict.isEmpty settings.tokens then + Cmd.none + + else + settings.tokens + |> Dict.values + |> Api.checkAlive + |> Http.request + |> Cmd.map + (Result.map (RemoveInvalid >> Global.SettingsMsg) + >> Result.mapError (always Global.NoOp) + >> Result.unify + ) + in + ( { settings = settings + , open = False + } + , cmd + ) + + +defaults : Settings +defaults = + { tokens = Dict.empty + , lastUsedName = Nothing + , openUserList = False + , recentDecks = [] + , chosenLanguage = Nothing + , compactCards = False + } + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + ToggleOpen -> + ( { model | open = not model.open }, Cmd.none ) + + ChangeLang language -> + changeSettings (\s -> { s | chosenLanguage = language }) model + + ChangeCompactCards enabled -> + changeSettings (\s -> { s | compactCards = enabled }) model + + ChangeOpenUserList open -> + changeSettings (\s -> { s | openUserList = open }) model + + RemoveInvalid tokenValidity -> + let + newTokens = + model.settings.tokens + |> Dict.filter (\_ -> \t -> Dict.get t tokenValidity |> Maybe.withDefault True) + in + changeSettings (\s -> { s | tokens = newTokens }) model + + +view : Shared -> Html Global.Msg +view shared = + let + model = + shared.settings + + icon = + if model.open then + Icon.check + + else + Icon.cog + + button = + Components.iconButtonStyled + [ HtmlA.id "settings-button" + , ToggleOpen |> Global.SettingsMsg |> HtmlE.onClick + , Strings.SettingsTitle |> Lang.title shared + ] + ( [ Icon.lg ], icon ) + + panel = + Wl.card [ HtmlA.classList [ ( "settings-panel", True ), ( "open", model.open ) ] ] + [ Html.h3 [] [ Strings.SettingsTitle |> Lang.html shared ] + , Html.div [ HtmlA.class "body" ] + [ languageSelector shared + , compactSwitch shared + , speechSwitch shared + , notificationsSwitch shared + ] + ] + in + Html.div [] [ button, panel ] + + +{-| Add a token to the token list and set the last used name, because the user has joined a lobby. +We take an `Auth` here even though we don't really use it to ensure the token has been successfully parsed. +-} +onJoinLobby : Lobby.Auth -> String -> Model -> ( Model, Cmd msg ) +onJoinLobby auth name model = + let + settings = + model.settings + + updatedSettings = + { settings + | tokens = Dict.insert (auth.claims.gc |> GameCode.toString) auth.token settings.tokens + , lastUsedName = Just name + } + in + ( { model | settings = updatedSettings }, LocalStorage.store updatedSettings ) + + +{-| Get all the tokens in settings as `Auth`s by game code. +-} +auths : Settings -> Dict String Lobby.Auth +auths settings = + -- Only legit tokens should be in settings, so we ignore any that aren't. + settings.tokens + |> Dict.filterMap (\_ -> \v -> v |> Token.decode |> Result.toMaybe) + + + +{- Private -} + + +changeSettings : (Settings -> Settings) -> Model -> ( Model, Cmd msg ) +changeSettings f model = + let + settings = + f model.settings + in + ( { model | settings = settings }, LocalStorage.store settings ) + + +compactSwitch : Shared -> Html Global.Msg +compactSwitch shared = + let + model = + shared.settings + + settings = + model.settings + in + Components.formSection shared + "compact-cards" + (Html.div + [ HtmlA.class "multipart" ] + [ Wl.switch + [ HtmlA.checked settings.compactCards + , HtmlE.onCheck (ChangeCompactCards >> Global.SettingsMsg) + ] + , Html.label [] + [ Icon.view Icon.searchMinus + , Html.text " " + , Strings.CompactCardsSetting |> Lang.html shared + ] + ] + ) + [ Components.info Strings.CompactCardsExplanation ] + + + +-- TODO: Impl + + +speechSwitch : Shared -> Html Global.Msg +speechSwitch shared = + Components.formSection shared + "speech" + (Html.div + [ HtmlA.class "multipart" ] + [ Wl.switch [] + , Html.label [] + [ Icon.view Icon.volumeUp + , Html.text " " + , Strings.SpeechSetting |> Lang.html shared + ] + ] + ) + [ Components.info Strings.SpeechExplanation ] + + + +-- TODO: Impl + + +notificationsSwitch : Shared -> Html Global.Msg +notificationsSwitch shared = + Components.formSection shared + "notifications" + (Html.div + [ HtmlA.class "multipart" ] + [ Wl.switch [] + , Html.label [] + [ Icon.view Icon.bell + , Html.text " " + , Strings.NotificationsSetting |> Lang.html shared + ] + ] + ) + [ Components.info Strings.NotificationsExplanation + , Components.info Strings.NotificationsBrowserPermissions + ] + + +languageSelector : Shared -> Html Global.Msg +languageSelector shared = + let + selected = + Lang.currentLanguage shared + in + Components.formSection + shared + "language" + (Wl.select + [ HtmlE.onInput onChangeLang + , Strings.LanguageSetting |> Lang.string shared |> WlA.label + , WlA.outlined + ] + (Lang.languages |> List.map (languageOption selected)) + ) + [ Components.info Strings.MissingLanguage ] + + +onChangeLang : String -> Global.Msg +onChangeLang code = + Lang.fromCode code |> ChangeLang |> Global.SettingsMsg + + +languageOption : Language -> Language -> Html msg +languageOption currentLanguage language = + let + autonym = + language |> Lang.autonym + + nameInCurrentLanguage = + language |> Lang.languageName |> Lang.givenLanguageString currentLanguage + + name = + if autonym == nameInCurrentLanguage then + [ autonym |> Html.text ] + + else + [ autonym |> Html.text + , Html.text "(" + , nameInCurrentLanguage |> Html.text + , Html.text ")" + ] + in + Html.option + [ language |> Lang.code |> HtmlA.value, HtmlA.selected (currentLanguage == language) ] + name diff --git a/client/src/elm/MassiveDecks/Settings/Messages.elm b/client/src/elm/MassiveDecks/Settings/Messages.elm new file mode 100644 index 0000000..184db73 --- /dev/null +++ b/client/src/elm/MassiveDecks/Settings/Messages.elm @@ -0,0 +1,13 @@ +module MassiveDecks.Settings.Messages exposing (Msg(..)) + +import Dict exposing (Dict) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Strings.Languages.Model exposing (Language) + + +type Msg + = ChangeLang (Maybe Language) + | ChangeCompactCards Bool + | ChangeOpenUserList Bool + | ToggleOpen + | RemoveInvalid (Dict Lobby.Token Bool) diff --git a/client/src/elm/MassiveDecks/Settings/Model.elm b/client/src/elm/MassiveDecks/Settings/Model.elm new file mode 100644 index 0000000..5159590 --- /dev/null +++ b/client/src/elm/MassiveDecks/Settings/Model.elm @@ -0,0 +1,27 @@ +module MassiveDecks.Settings.Model exposing (Model, Settings) + +import Dict exposing (Dict) +import MassiveDecks.Card.Source.Model as Source exposing (Source) +import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) + + +{-| The model for the settings panel. +-} +type alias Model = + { settings : Settings + , open : Bool + } + + +{-| Persisted data. +This is really more than just user settings, it's any persistent data we store in the user's local storage. +-} +type alias Settings = + { tokens : Dict String Lobby.Token + , openUserList : Bool + , lastUsedName : Maybe String + , recentDecks : List Source.External + , chosenLanguage : Maybe Language + , compactCards : Bool + } diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm new file mode 100644 index 0000000..0f72dc1 --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -0,0 +1,321 @@ +module MassiveDecks.Strings exposing (MdString(..)) + +{-| This module deals with text that is shown to the user in the application - strings. +-} + +import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) + + + +{- Translator notes + + This file contains a list of every string in the game, along with a description to help translators. + Please reference it while translating. + + Adding to the game: + + The first note is that adding the translation to the game itself is somewhat technical, if you struggle with it, + please feel free to make a bug and get a developer to help you - you can do the translation itself more simply + and a developer can do the work of adding the translation, and skip this section. + + The best way to get started is to copy the "Languages/En.elm" translation. Add the language to the lists in both + "Languages/Model.elm" and "Languages.elm" (at the top and at the bottom in "pick"). + + e.g: Adding English (Great Britain). The IETF language tag for this is en-GB, so we'll use EnGB as the name. + + In "Languages/Model.elm": + type Language + = En + ... + | EnGB -- Add this line. + + In "Languages.elm": + languages : List Language + languages = + [ En + ... + , EnGB -- Add this line. + ] + + ... + + pack : Language -> Translation.Pack + pack language = + case language of + En -> + EnLang.pack + + ... + + EnGB -> -- Add this line. + EnGBLang.pack -- Add this line. + + This should result in a new language that works exactly as English does. You can now start changing the metadata. + + In "Languages/EnGB.elm" (replacing EnGB with your language): + pack : Translation.Pack + pack = + { code = "en-GB" -- Change this to the IETF language tag for the language. + , name = English + , translate = translate + } + + You will notice that we also need a string describing the language. You can do this in this file. + + In this file: + -- Language Names + | English -- The name of the English language (no specific dialect). + ... + | BritishEnglish -- Add this line. + + Note that once you do this, you'll need to add a translation for this to every other language as well. + + In "Languages/EnGB.elm" (replacing EnGB with your language): + pack : Translation.Pack + pack = + { code = "en-GB" + , name = BritishEnglish -- Change this to your new string. + , translate = translate + } + + You can now start changing the actual strings. These are created either from raw text or references to other + strings: + MassiveDecks -> + [ Text "Massive Decks" ] + + WhatIsThis -> + [ Text "What is ", Ref MassiveDecks, Text "?" ] + + This will render was "What is Massive Decks?", but if you changed "MassiveDecks" to "Test", it would result in + "What is Test?". This allows you to avoid translating the same thing many times and allows the text to be + enhanced with things like links automatically. + + In general, you should be able to just translate the raw text in quotes and shuffle things around. The more + complex parts should pretty much remain as-is. + + Note that some strings take arguments: + UserConnected { username } -> + [ Text username, Text " has reconnected to the game." ] + + Note that some arguments are numbers, and these have to be converted to strings: + Score { total } -> + [ Text (String.fromInt total) ] + + Note this is code, rather than configuration, so complex operations can be performed. "Plural" is an example + of this, as is "asWord". If you are not a programmer, these may be awkward - please feel free to explain the + behaviour you need in a bug to get a developer to help out for awkward cases like different grammar rules for + plurals, etc... + + Terminology: + + Note the difference between "Players" and "Users". Players play the game, users includes both players and + spectators, and is the more general term. + + A play code is a Cardcast-specific thing that specifies a deck on their platform, and is not the same thing + as a game code, which allows a user to join a game in Massive Decks. + + Other Notes: + + Please use unicode characters as appropriate. E.g: the em-dash (—) (which is particularly awkward to notice + in monospaced fonts, real quotes (“”), apostrophes (’) and ellipsis (…). + + Likewise, unicode offers us valuable hinting tools to control hyphenation and line breaking: + A unicode soft hyphen (\u{00AD}) indicated a good point in a word to break it if needed. + A unicode zero-width space (\u{200B}) can do the same thing without a hyphen (hyphens should be preferred, + however, ). + For the reverse (no line breaking between words), use a unicode non-breaking space (\u{00A0}). + +-} + + +{-| Each type represents a message that may be shown to the user. Some have arguments that are variable but should be +included in some form in the message. +-} +type MdString + = MassiveDecks -- The name of the game. + | Close -- Close a dialog window. + -- Special + {- Plural only makes sense for nouns, other results will be nonsense. If the amount is not given, it's an unknown + quantity. If the value is `1`, then it should be singular. + -} + | Plural { singular : MdString, amount : Maybe Int } -- The plural version of the given string. + -- Start screen. + | Version { versionNumber : String } -- The version of the game being played. + | ShortGameDescription -- A one-line description of the game. + | WhatIsThis -- A title for a section describing the game. + | GameDescription -- A long description of the game. + | NewGame -- The action of creating a new game. (Short, ideally one word). + | FindPublicGame -- The action of finding a public game to join. (Short, ideally one word). + | JoinPrivateGame -- The action of joining a private game the user was invited to. (Short, ideally one word). + | PlayGame -- The action of joining a game to play it. (Short, ideally one word). + | AboutTheGame -- The action of finding out more information about the game. (Short, ideally one word). + | MDLogoDescription -- A description of the Massive Decks logo (e.g: for blind users). + | RereadLogoDescription -- A description of the Reread Games logo (e.g: for blind users). + | MDProject -- A description of the action of visiting the development project for Massive Decks. + | License -- A description of the license the game is made available under. + | DevelopedByReread -- A description of the fact that the game was developed by Reread. + | RereadGames -- The name of "Reread Games" (https://www.rereadgames.com/). + | NameLabel -- A label for a user name text field. + | NameInUse -- An error indicating the name the user asked for is already in use and they should try another. + | RejoinTitle -- A title for a list of games the user was previously in and might be able to rejoin. + | RejoinGame { code : GameCode } -- A description of the action of attempting to rejoin a game the user was previously in. + -- Rules + | CardsAgainstHumanity -- The name of "Cards Against Humanity" (https://cardsagainsthumanity.com/). + | Rules -- The title for a DESCRIPTION of the rules. + | RulesHand -- The rules for the game about the player's hand. + | RulesCzar -- The rules for the game about being the czar. + | RulesPlaying -- The rules for the game about playing answers. + | RulesJudging -- The rules for the game about judging a round as czar. + | RulesPickTitle -- The title for the rules for calls with more than one slot. + | RulesPick -- The rules for calls with more than one slot. + | ExamplePickDescription -- The description for the example card showing the Pick mechanics. + | RulesDraw -- The rules for calls with lots of slots that let your draw additional cards. + | GameRulesTitle -- The title for the section on the core game rules. + | HouseRulesTitle -- The title for the section about house rules. + | HouseRules -- A description of what house rules are. + | HouseRuleReboot -- The name of the "Rebooting the Universe" house rule. + | HouseRuleRebootDescription { cost : Maybe Int } -- A description of the "Rebooting the Universe" house rule. + | HouseRuleRebootCost -- A description of the cost of redrawing your hand. + | HouseRuleRebootCostDescription -- + | HouseRulePackingHeat -- The name of the "Packing Heat" house rule. + | HouseRulePackingHeatDescription -- A description of the "Packing Heat" house rule. + -- Note that the below name is a pun on the Star Wars character "Lando Calrissian" and the words "Random" and + -- "Card". It may be better to be more literal, or make an alternative reference. This is essentially adding + -- a computer player that just plays a random card. + | HouseRuleRandoCardrissian -- The name of the "Rando Cardrissian" house rule. + | HouseRuleRandoCardrissianDescription -- A description of the "Rando Cardrissian" house rule. + | HouseRuleRandoCardrissianNumber -- A name of the setting for the number of bots added to the game. + | HouseRuleRandoCardrissianNumberDescription -- A description of the setting for the number of bots added to the game. + -- Settings + | SettingsTitle -- The title for the settings panel. + | LanguageSetting -- The label for the "Language" setting. + | MissingLanguage -- A question asking if the user doesn't see the language they want. + | TranslationBeg -- A request for help translating the game. + | CompactCardsSetting -- The label for the "Compact Cards" setting. + | CompactCardsExplanation -- An explanation of what compact cards does (makes cards square). + | SpeechSetting -- The label for the speech setting. + | SpeechExplanation -- An explanation of what the speech setting does (enables TTS on cards). + | NotificationsSetting -- The label for the notifications setting. + | NotificationsExplanation -- An explanation of what the notifications setting does (enables browser notifications). + | NotificationsBrowserPermissions -- An explanation that the user will need to give the game permission for notifications. + -- Terms + | Czar -- The name for the "Card Czar" (the player that judges the round). + | CzarDescription -- A short description of what the czar does. + | Call -- The name for a call card (a black card). + | CallDescription -- A short description of what a call is. + | Response -- The name for a response card (a white card). + | ResponseDescription -- A short description of what a response is. + | Point -- The name for a point in the game. + | PointDescription -- A short description of what a point is. + | GameCodeTerm -- The term for a unique code for a game that allows a user to find the game easily. + | GameCodeDescription -- A short description of what a game code is. + | GameCode { code : GameCode } -- Render a game code. + | GameCodeSpecificDescription -- A short description of what a specific game code and how to use it. + | GameCodeHowToAcquire -- A short description of how to get a game code. + | Deck -- The name for a deck of cards. + | Playing -- A term for a person who is in a round, but has not yet submitted a play. + | PlayingDescription -- A description of a person who is in a round, but has not yet submitted a play. + | Played -- A term for a person who is in a round, and has submitted a play. + | PlayedDescription -- A description of a person who is in a round, and has submitted a play. + -- Lobby Browser + | LobbyBrowserTitle -- A description of a tool to browse public games. + | NoPublicGames -- An explanation that there are no public games available to the user. + | PlayingGame -- A description of a game that is in-progress. + | SettingUpGame -- A description of a game that has not started yet. + | StartYourOwn -- A question asking if the player wants to start a new game. + -- Spectation + | JoinTheGame -- A short phrase encouraging a player to join the game using information that follows. + -- Cards + | Pick { numberOfCards : Int } -- The word describing the action of picking a card from their hand to play. Use NumberOfCards for the number. + | Draw { numberOfCards : Int } -- The word describing the action of drawing a card from the deck to their hand. Use NumberOfCards for the number. + | PickDescription { numberOfCards : Int } -- A description of game rules where the user must pick a given number of cards. + | DrawDescription { numberOfCards : Int } -- A description of game rules where the user gets to draw more cards. + | NumberOfCards { numberOfCards : Int } -- A number of cards as a single-digit number. This will be enhanced to render specially as a circle with the number in. + -- Lobby + | Invite -- A description of the action of inviting players to the game. + | InviteExplanation { gameCode : GameCode, password : Maybe String } -- An explanation of how players can join the game using the given game code and, potentially, password. + | InviteLinkHelp -- An explanation that the users can send the link to people to invite them to the game. + | Cast -- A description of the action of casting a view of the game to another device (e.g: a TV). + | CastConnecting -- A description of trying to connect to the casting device. + | CastConnected { deviceName : String } -- A description of being connected to the named casting device. + | Players -- A short term for a group of users who are playing in the game. + | PlayersDescription -- A description of a group of users who are playing in the game. + | Spectators -- A short term for a group of users who are only spectating the game. + | SpectatorsDescription -- A description of a group of users who are only spectating the game. + | Privileged -- The short term for a player who has privileges over the game. + | PrivilegedDescription -- A description of a player who has privileges over the game (e.g: can change settings) + | Ai -- The short term for a player who is controlled by the computer. + | AiDescription -- A description of a player who is controlled by the computer. + | Score { total : Int } -- A display of a score. + | ScoreDescription -- A description of a player's score. + | ToggleUserList -- A description of the action of showing or hiding the user list. + | GameMenu -- A description of the game menu. + | UnknownUser -- A name for a user that doesn't have a known name. + | InvitePlayers -- A short term for inviting players to the game. + | SetAway -- A short term for leaving the game temporarily. + | LeaveGame -- A short term for the action of leaving the game permanently. + | KickUser -- A short term for the action of forcing a user to leave the game permanently. + | Promote -- A short term for the action of allowing a user to edit the game configuration. + -- Notifications + | UserJoined { username : String } -- A notification that a user has joined the game. + | UserLeft { username : String } -- A notification that a user has left the game. + | UserConnected { username : String } -- A notification that a user has reconnected to the game. + | UserDisconnected { username : String } -- A notification that a user has disconnected from the game. + | Dismiss -- The action of dismissing a notification. + -- Configure + | ConfigureTitle -- A title for the game configuration screen. + | NoDecks -- A description of the situation where the user has no decks added to the game configuration. + | NoDecksHint -- A hint explaining that the user needs to add at least one deck, and offering the CaH deck. + | WaitForDecks -- A hint that the user has to wait for decks to load before starting the game. + | MissingCardType { cardType : MdString } -- An error explaining that the user needs a deck with the given type of card (call/response). + | AddDeck -- A description of the action of adding a deck to the game configuration. + | RemoveDeck -- A description of the action of removing a deck from the game configuration. + | CardcastPlayCode -- A term referring to the play code that identifies a deck on Cardcast. + | CardcastEmptyPlayCode -- A description of the problem of the entered Cardcast play code being empty. + | DeckAlreadyAdded -- A description of the problem of the deck already being added to the game configuration. + | ConfigureDecks -- A name for the section of the configuration screen for changing the decks for the game. + | ConfigureRules -- A name for the section of the configuration screen for changing the rules for the game. + | ConfigureGame -- A name for the section of the configuration screen for changing the settings for the game. + | HandSize -- The name of the rule defining how many cards a player can hold in their hand. + | HandSizeDescription -- The description of the above rule. + | ScoreLimit -- The name of the rule defining how many points a player has to accumulate to win the game. + | ScoreLimitDescription -- The description of the above rule. + | NeedAtLeastOneDeck -- A description of the problem that the game needs at least one deck to start. + | NeedAtLeastThreePlayers -- A description of the problem that the game needs at least three players to start. + | PasswordNotSecured -- A warning that game passwords are not stored securely and should not be used elsewhere. + | GamePassword -- A short label for the game password. + | GamePasswordDescription -- A description of a password to stop random people entering your game. + | StartGame -- A short description of the action of starting the game. + -- Game + | SubmitPlay -- A description of the action of submitting the play for the czar to judge. + | TakeBackPlay -- A description of the action of taking back a previously submitted play. + | JudgePlay -- A description of the action of choosing a play to win the round. + | LikePlay -- A description of the action of liking a play. + -- Instructions + | PlayInstruction { numberOfCards : Int } -- Instruction to the player on how to play cards. + | SubmitInstruction -- Instruction to the player on how to submit their play. + | WaitingForPlaysInstruction -- Instruction to the player that they need to wait for other players to play. + | CzarsDontPlayInstruction -- Instruction to the player that as Czar they don't play into the round. + | RevealPlaysInstruction -- Instruction to reveal plays for the round. + | WaitingForCzarInstruction -- Instruction to wait for the czar to reveal plays and pick a winner. + -- 404 Unknown + | UnknownPageTitle -- A title explaining the page the user tried to go to doesn't exist. + | GoBackHome -- The action to go back to the main page of the application. + -- Actions + | Refresh -- The action to refresh the page with newer information. + -- Errors + | Error -- A title for a generic error (something having gone wrong). + | ErrorHelp -- A message telling the user that an error has occurred and what to do. + | ErrorHelpTitle -- A title saying something went wrong. + | ReportError -- The action to report an error with the application to a developer. + | ReportErrorBody -- An explanation of how to report an error to the developer. + | BadUrlError -- An error where the server url wasn't valid. + | TimeoutError -- An error where the server didn't respond for a long time. + | NetworkError -- An error where the user's internet connection failed. + | ServerDownError -- An error where the game server wasn't available. + | BadStatusError -- An error where the server gave a response we didn't expect. + | BadPayloadError -- An error where the server gave a response we didn't understand. + | CastError -- An error where we the cast device couldn't connect to the game. + -- Language Names + | English -- The name of the English language (no specific dialect). + | BritishEnglish -- The name of the British dialect of the English language. diff --git a/client/src/elm/MassiveDecks/Strings/Languages.elm b/client/src/elm/MassiveDecks/Strings/Languages.elm new file mode 100644 index 0000000..1a049df --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings/Languages.elm @@ -0,0 +1,166 @@ +module MassiveDecks.Strings.Languages exposing + ( alt + , autonym + , code + , currentLanguage + , defaultLanguage + , findBestMatch + , fromCode + , givenLanguageString + , html + , label + , languageName + , languages + , placeholder + , string + , title + ) + +{-| The primary entry point to I18N for Massive Decks. +-} + +import Dict exposing (Dict) +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Strings exposing (MdString(..)) +import MassiveDecks.Strings.Languages.En as EnLang +import MassiveDecks.Strings.Languages.Model exposing (..) +import MassiveDecks.Strings.Render as Render +import MassiveDecks.Strings.Translation as Translation +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.String as String +import Weightless.Attributes as WlA + + +{-| A list of all the languages enabled in the application, in the order they will be presented to the end-user. +-} +languages : List Language +languages = + [ En + ] + + +{-| The IETF language tag for the language. +-} +code : Language -> String +code language = + language |> pack |> .code + + +{-| Get the language from an IETF language tag. This only finds exact matches. +-} +fromCode : String -> Maybe Language +fromCode c = + languagesDict |> Dict.get c + + +{-| Every language must have a string that describes itself. +-} +languageName : Language -> MdString +languageName language = + language |> pack |> .name + + +{-| The given language's name for itself. +-} +autonym : Language -> String +autonym language = + languageName language |> givenLanguageString language + + +{-| The language the user is currently seeing the page in. +-} +currentLanguage : Shared -> Language +currentLanguage shared = + Maybe.first [ shared.settings.settings.chosenLanguage, shared.browserLanguage ] |> Maybe.withDefault defaultLanguage + + +{-| The language selected by default. +-} +defaultLanguage : Language +defaultLanguage = + En + + +{-| Takes a list of IETF language tags and gives back the best language match, if any. +-} +findBestMatch : List String -> Maybe Language +findBestMatch codes = + -- TODO: Standardising & Fuzzy matching like https://github.com/LuminosoInsight/langcodes + Maybe.first (codes |> List.map fromCode) + + +{-| Build an actual string from an `MdString` in the user's language. +-} +string : Shared -> MdString -> String +string shared mdString = + mdString |> givenLanguageString (currentLanguage shared) + + +{-| Build an actual string in the given language. +-} +givenLanguageString : Language -> MdString -> String +givenLanguageString lang mdString = + mdString |> Render.asString ( lang, translate lang ) + + +{-| An HTML text node from the given `MdString`. Note this is more than just convenience - we enhance some strings +with rich HTML content (e.g: links, icons, etc...) when rendered as HTML. +-} +html : Shared -> MdString -> Html msg +html shared mdString = + let + lang = + currentLanguage shared + in + mdString |> Render.asHtml ( lang, translate lang ) + + +{-| Convenience for an HTML `placeholder` attribute from the given `MdString`. +-} +placeholder : Shared -> MdString -> Html.Attribute msg +placeholder shared = + string shared >> String.capitalise >> HtmlA.placeholder + + +{-| Convenience for an HTML `title` attribute from the given `MdString`. +-} +title : Shared -> MdString -> Html.Attribute msg +title shared = + string shared >> String.capitalise >> HtmlA.title + + +{-| Convenience for an HTML `alt` attribute from the given `MdString`. +-} +alt : Shared -> MdString -> Html.Attribute msg +alt shared = + string shared >> String.capitalise >> HtmlA.alt + + +{-| Convenience for an Weightless `label` attribute from the given `MdString`. +-} +label : Shared -> MdString -> Html.Attribute msg +label shared = + string shared >> String.capitalise >> WlA.label + + + +{- Private -} + + +translate : Language -> MdString -> List Translation.Result +translate lang mdString = + mdString |> (pack lang |> .translate) + + +languagesDict : Dict String Language +languagesDict = + Dict.fromList (languages |> List.map (\l -> ( code l, l ))) + + +pack : Language -> Translation.Pack +pack language = + case language of + En -> + EnLang.pack diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En.elm b/client/src/elm/MassiveDecks/Strings/Languages/En.elm new file mode 100644 index 0000000..219e214 --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings/Languages/En.elm @@ -0,0 +1,746 @@ +module MassiveDecks.Strings.Languages.En exposing (pack) + +{-| General English-language translation. +This is the primary language, strings here are the canonical representation, and are suitable to translate from. +-} + +import MassiveDecks.Pages.Lobby.GameCode as GameCode +import MassiveDecks.Strings exposing (MdString(..)) +import MassiveDecks.Strings.Translation as Translation exposing (Result(..)) + + +pack : Translation.Pack +pack = + { code = "en" + , name = English + , translate = translate + } + + + +{- Private -} + + +{-| The English translation +-} +translate : MdString -> List Translation.Result +translate mdString = + case mdString of + -- General + MassiveDecks -> + [ Text "Massive Decks" ] + + Close -> + [ Text "Close" ] + + -- Special + Plural { singular, amount } -> + if amount == Just 1 then + [ Raw singular ] + + else + [ Raw singular, Text "s" ] + + -- Start screen. + Version { versionNumber } -> + [ Text "Version “", Text versionNumber, Text "”" ] + + ShortGameDescription -> + [ Text "A comedy party game." ] + + WhatIsThis -> + [ Text "What is ", Ref MassiveDecks, Text "?" ] + + GameDescription -> + [ Ref MassiveDecks + , Text " is a comedy party game based on " + , Ref CardsAgainstHumanity + , Text ", developed by " + , Ref RereadGames + , Text " and other contributors—the game is open source under " + , Ref License + , Text ", so you can help improve the game, access the source code, or just find out more at " + , Ref MDProject + , Text "." + ] + + NewGame -> + [ Text "New" ] + + FindPublicGame -> + [ Text "Find" ] + + JoinPrivateGame -> + [ Text "Join" ] + + PlayGame -> + [ Text "Play" ] + + AboutTheGame -> + [ Text "About" ] + + MDLogoDescription -> + [ Text "A ", Ref Call, Text " and a ", Ref Response, Text " marked with an “M” and a “D”." ] + + RereadLogoDescription -> + [ Text "A book circled by a recycling arrow." ] + + MDProject -> + [ Text "the GitHub project" ] + + License -> + [ Text "the AGPLv3 license" ] + + DevelopedByReread -> + [ Text "Developed by ", Ref RereadGames, Text "." ] + + RereadGames -> + [ Text "Reread Games" ] + + NameLabel -> + [ Text "Your Name" ] + + NameInUse -> + [ Text "Someone else is using this name in the game—please try a different one." ] + + RejoinTitle -> + [ Text "Rejoin Game" ] + + RejoinGame { code } -> + [ Text "Rejoin “", GameCode { code = code } |> Ref, Text "”." ] + + -- Rules + CardsAgainstHumanity -> + [ Text "Cards Against Humanity" ] + + Rules -> + [ Text "How to play." ] + + RulesHand -> + [ Text "Each player has a hand of ", Ref (Plural { singular = Response, amount = Nothing }), Text "." ] + + RulesCzar -> + [ Text "The first player begins as the " + , Ref Czar + , Text ". the " + , Ref Czar + , Text " reads the question or fill-in-the-blank phrase on the " + , Ref Call + , Text " out loud." + ] + + RulesPlaying -> + [ Text "Everyone else answers the question or fills in the blank by choosing a " + , Ref Response + , Text " from their hand to play for the round." + ] + + RulesJudging -> + [ Text "The answers are then shuffled and the " + , Ref Czar + , Text " reads them out to the other players—for full effect, the " + , Ref Czar + , Text " should usually re-read the " + , Ref Call + , Text " before presenting each answer. The " + , Ref Czar + , Text " then picks the funniest play, and whoever played it gets one " + , Ref Point + , Text "." + ] + + RulesPickTitle -> + [ Ref (Pick { numberOfCards = 2 }) ] + + RulesPick -> + [ Text "Some cards will need more than one " + , Ref Response + , Text " as an answer. Play the cards in the order the " + , Ref Czar + , Text " should read them—the order matters." + ] + + ExamplePickDescription -> + [ Ref (Plural { singular = Call, amount = Nothing }) + , Text " like this will require picking more " + , Ref (Plural { singular = Response, amount = Nothing }) + , Text ", but give you more to pick from." + ] + + RulesDraw -> + [ Text "Some " + , Ref (Plural { singular = Call, amount = Nothing }) + , Text " will need even more " + , Ref (Plural { singular = Response, amount = Nothing }) + , Text "—these will say " + , Ref (Draw { numberOfCards = 2 }) + , Text " or more, and you’ll get that many extra cards before you play." + ] + + GameRulesTitle -> + [ Text "Game Rules" ] + + HouseRulesTitle -> + [ Text "House Rules" ] + + HouseRules -> + [ Text "You can change the way the game is played in a variety of ways. While setting up the game, choose " + , Text "as many house rules as you would like to use." + ] + + HouseRuleReboot -> + [ Text "Rebooting the Universe" ] + + HouseRuleRebootDescription { cost } -> + [ Text "At any time, players may trade in " + , Text (an cost) + , Ref (Plural { singular = Point, amount = cost }) + , Text " to discard their hand and draw a new one." + ] + + HouseRuleRebootCost -> + [ Ref Point, Text " Cost" ] + + HouseRuleRebootCostDescription -> + [ Text "How many ", Ref (Plural { singular = Point, amount = Nothing }), Text " it costs to redraw." ] + + HouseRulePackingHeat -> + [ Text "Packing Heat" ] + + HouseRulePackingHeatDescription -> + [ Text "Any " + , Ref (Plural { singular = Call, amount = Nothing }) + , Text " with " + , Ref (Pick { numberOfCards = 2 }) + , Text " also get " + , Ref (Draw { numberOfCards = 1 }) + , Text ", so everyone has more options." + ] + + HouseRuleRandoCardrissian -> + [ Text "Rando Cardrissian" ] + + HouseRuleRandoCardrissianDescription -> + [ Text "Every round, the first " + , Ref Response + , Text " in the deck will be played as an answer. This play belongs to an AI player named " + , Text "Rando Cardrissian, and if he wins the game, all players go home in a state of everlasting shame." + ] + + HouseRuleRandoCardrissianNumber -> + [ Text "Number of Randos" ] + + HouseRuleRandoCardrissianNumberDescription -> + [ Text "The number of AI players that will be in the game." ] + + SettingsTitle -> + [ Text "Settings" ] + + LanguageSetting -> + [ Text "Language" ] + + MissingLanguage -> + [ Text "Don’t see your language? ", Ref TranslationBeg ] + + TranslationBeg -> + [ Text "Help translate " + , Ref MassiveDecks + , Text "!" + ] + + CompactCardsSetting -> + [ Text "Compact Cards" ] + + CompactCardsExplanation -> + [ Text "Show cards as square—this is useful on small screens to fit more cards on at once." ] + + SpeechSetting -> + [ Text "Text To Speech" ] + + SpeechExplanation -> + [ Text "Read out cards using text to speech." ] + + NotificationsSetting -> + [ Text "Browser Notifications" ] + + NotificationsExplanation -> + [ Text "Alert you when you need to do something in the game using browser notifications. " + , Text "Useful when you need to be tabbed out of the game." + ] + + NotificationsBrowserPermissions -> + [ Text "You will need to give permission for " + , Ref MassiveDecks + , Text " to notify you. This will only be used while the game is open and while you have this enabled." + ] + + -- Terms + Czar -> + [ Text "Card Czar" ] + + CzarDescription -> + [ Text "The player judging the round." ] + + Call -> + [ Text "Black Card" ] + + CallDescription -> + [ Text "A black card with a question or fill-in-the-blank phrase." ] + + Response -> + [ Text "White Card" ] + + ResponseDescription -> + [ Text "A white card with a phrase played into rounds." ] + + Point -> + [ Text "Awesome Point" ] + + PointDescription -> + [ Text "A point—having more means winning." ] + + GameCodeTerm -> + [ Text "Game Code" ] + + GameCodeDescription -> + [ Text "A code that lets other people find and join your game." ] + + GameCode { code } -> + [ Text (GameCode.toString code) ] + + GameCodeSpecificDescription -> + [ Text "Give this game code to people and they can join the game." ] + + GameCodeHowToAcquire -> + [ Text "Ask the person who invited you for the game’s ", Ref GameCodeTerm, Text "." ] + + Deck -> + [ Text "Deck" ] + + Playing -> + [ Text "Playing" ] + + PlayingDescription -> + [ Text "This player is in the round, but has not yet submitted a play." ] + + Played -> + [ Text "Played" ] + + PlayedDescription -> + [ Text "This player has submitted their play for the round." ] + + -- Lobby Browser + LobbyBrowserTitle -> + [ Text "Public Games" ] + + NoPublicGames -> + [ Text "No public games available." ] + + PlayingGame -> + [ Text "Games that are in progress." ] + + SettingUpGame -> + [ Text "Games that have not yet started." ] + + StartYourOwn -> + [ Text "Start a new game?" ] + + -- Spectation + JoinTheGame -> + [ Text "Join the game!" ] + + -- Cards + Pick numberOfCards -> + [ Text "Pick", Ref (NumberOfCards numberOfCards) ] + + Draw numberOfCards -> + [ Text "Draw", Ref (NumberOfCards numberOfCards) ] + + PickDescription { numberOfCards } -> + [ Text "You need to play " + , Text (asWord numberOfCards) + , Text " " + , Ref (Plural { singular = Response, amount = Just numberOfCards }) + , Text "." + ] + + DrawDescription { numberOfCards } -> + [ Text "You get " + , Text (asWord numberOfCards) + , Text " extra " + , Ref (Plural { singular = Response, amount = Just numberOfCards }) + , Text " before playing." + ] + + NumberOfCards { numberOfCards } -> + [ Text (String.fromInt numberOfCards) ] + + -- Lobby + Invite -> + [ Text "Invite players to the game." ] + + InviteLinkHelp -> + [ Text "Send this link to players to invite them to the game, or let them scan the QR code below." ] + + InviteExplanation { gameCode, password } -> + let + extra = + password + |> Maybe.map + (\p -> + [ Text " and the game password “" + , Text p + , Text "”" + ] + ) + |> Maybe.withDefault [] + in + List.concat + [ [ Text "Your game code is " + , Ref (GameCode { code = gameCode }) + , Text ". Players can join the game by loading " + , Ref MassiveDecks + , Text " and entering that code" + ] + , extra + , [ Text "." + ] + ] + + Cast -> + [ Text "Cast to TV." ] + + CastConnecting -> + [ Text "Connecting…" ] + + CastConnected { deviceName } -> + [ Text "Casting to ", Text deviceName, Text "." ] + + Players -> + [ Text "Players" ] + + PlayersDescription -> + [ Text "Users playing the game." ] + + Spectators -> + [ Text "Audience" ] + + SpectatorsDescription -> + [ Text "Users watching the game without playing." ] + + Privileged -> + [ Text "Owner" ] + + PrivilegedDescription -> + [ Text "This user can adjust settings in the game." ] + + Ai -> + [ Text "AI" ] + + AiDescription -> + [ Text "This player is controlled by the computer." ] + + Score { total } -> + [ Text (String.fromInt total) ] + + ScoreDescription -> + [ Text "The number of " + , Ref (Plural { singular = Point, amount = Nothing }) + , Text " this player has." + ] + + ToggleUserList -> + [ Text "Show or hide the scoreboard." ] + + GameMenu -> + [ Text "Game menu." ] + + UnknownUser -> + [ Text "An unknown user" ] + + InvitePlayers -> + [ Text "Invite Players" ] + + SetAway -> + [ Text "Set Away" ] + + LeaveGame -> + [ Text "Leave Game" ] + + KickUser -> + [ Text "Kick" ] + + Promote -> + [ Text "Promote" ] + + -- Notifications + UserConnected { username } -> + [ Text username, Text " has reconnected to the game." ] + + UserDisconnected { username } -> + [ Text username, Text " has disconnected from the game." ] + + UserJoined { username } -> + [ Text username, Text " has joined the game." ] + + UserLeft { username } -> + [ Text username, Text " has left the game." ] + + Dismiss -> + [ Text "Dismiss" ] + + -- Configuration + ConfigureTitle -> + [ Text "Game Setup" ] + + NoDecks -> + [ Segment [ Text "No decks. " ] + , Text " " + , Segment [ Text "You will need to add at least one to the game." ] + ] + + NoDecksHint -> + [ Text "Not sure? Add the original ", Raw CardsAgainstHumanity, Text " deck." ] + + WaitForDecks -> + [ Text "The decks must load before you can start the game." ] + + MissingCardType { cardType } -> + [ Text "None of your decks contain any " + , Ref (Plural { singular = cardType, amount = Nothing }) + , Text ". You need a deck that does to start the game." + ] + + AddDeck -> + [ Text "Add deck." ] + + RemoveDeck -> + [ Text "Remove deck." ] + + CardcastPlayCode -> + [ Text "Cardcast Play Code" ] + + CardcastEmptyPlayCode -> + [ Text "Enter a ", Ref CardcastPlayCode, Text " for the deck you want to add." ] + + DeckAlreadyAdded -> + [ Text "This deck is already in the game." ] + + ConfigureDecks -> + [ Text "Decks" ] + + ConfigureRules -> + [ Text "Rules" ] + + ConfigureGame -> + [ Text "Game" ] + + HandSize -> + [ Text "Hand Size" ] + + HandSizeDescription -> + [ Text "The base number of cards each player has in their hand during the game." ] + + ScoreLimit -> + [ Ref Point, Text " Limit" ] + + ScoreLimitDescription -> + [ Segment + [ Text "The number of " + , Ref (Plural { singular = Point, amount = Nothing }) + , Text " a player needs to win the game." + ] + , Text " " + , Segment [ Text "If disabled, the game continues indefinitely." ] + ] + + NeedAtLeastOneDeck -> + [ Text "You need a deck of cards added to start the game." ] + + NeedAtLeastThreePlayers -> + [ Text "You need at least three players to start the game." ] + + PasswordNotSecured -> + [ Text "Please note that game passwords are " + , Em [ Text "not" ] + , Text " stored securely and are shared with everyone in the lobby" + , Text "—given this, please " + , Em [ Text "do not" ] + , Text " use passwords you use elsewhere." + ] + + GamePassword -> + [ Text "Game Password" ] + + GamePasswordDescription -> + [ Text "A password to users must enter before they can join the game." ] + + StartGame -> + [ Text "Start Game" ] + + -- Game + SubmitPlay -> + [ Text "Give these cards to the ", Ref Czar, Text " as your play for the round." ] + + TakeBackPlay -> + [ Text "Take back your cards to change your play for the round." ] + + JudgePlay -> + [ Text "Pick this play as the winner for the round." ] + + LikePlay -> + [ Text "Add a like to this play." ] + + -- Instructions + PlayInstruction { numberOfCards } -> + [ Text "You need to choose " + , Text (asWord numberOfCards) + , Text " more " + , Ref (Plural { singular = Response, amount = Just numberOfCards }) + , Text " from your hand into this round before you can submit your play." + ] + + SubmitInstruction -> + [ Text "You need to submit your play for this round." ] + + WaitingForPlaysInstruction -> + [ Text "You are waiting for other players to play into the round." ] + + CzarsDontPlayInstruction -> + [ Text "You are the " + , Ref Czar + , Text " for the round - you don't submit any " + , Ref (Plural { singular = Response, amount = Nothing }) + , Text ". Instead you choose the winner once everyone else has submitted theirs." + ] + + RevealPlaysInstruction -> + [ Text "Click on the plays to flip them, then pick the one you think is best." ] + + WaitingForCzarInstruction -> + [ Text "You ware waiting for the ", Ref Czar, Text " to reveal the plays and pick a winner for the round." ] + + -- 404 Unknown + UnknownPageTitle -> + [ Text "404 Error: Unknown page." ] + + GoBackHome -> + [ Text "Go to the main page." ] + + -- Actions + Refresh -> + [ Text "Refresh" ] + + -- Errors + Error -> + [ Text "Error" ] + + ErrorHelp -> + [ Text "The game server might be down, or this might be a bug. Refreshing the page should get you going " + , Text "again. More details can be found below." + ] + + ErrorHelpTitle -> + [ Text "Sorry, something went wrong." ] + + ReportError -> + [ Text "Report Bug" ] + + ReportErrorBody -> + [ Text "I was [replace with a short explanation of what you were doing] when I got the following error:" ] + + BadUrlError -> + [ Text "We tried to make a request to an invalid page." ] + + TimeoutError -> + [ Text "The server didn’t respond for too long. It may be down, please try again after a short delay." ] + + NetworkError -> + [ Text "Your internet connection was interrupted." ] + + ServerDownError -> + [ Text "The game server is currently offline. Please try again later." ] + + BadStatusError -> + [ Text "The server gave a response we didn’t expect." ] + + BadPayloadError -> + [ Text "The server gave a response we didn’t understand." ] + + CastError -> + [ Text "Sorry, something went wrong trying to connect to the game." ] + + -- Language Names + English -> + [ Text "English" ] + + BritishEnglish -> + [ Text "English (British)" ] + + +an : Maybe Int -> String +an amount = + case amount of + Just 1 -> + "an " + + _ -> + "" + + +a : Maybe Int -> String +a amount = + case amount of + Just 1 -> + "a " + + _ -> + "" + + +{-| Take a number and give back the name of that number. Falls back to the number when it gets too big. +-} +asWord : Int -> String +asWord number = + case number of + 0 -> + "zero" + + 1 -> + "one" + + 2 -> + "two" + + 3 -> + "three" + + 4 -> + "four" + + 5 -> + "five" + + 6 -> + "six" + + 7 -> + "seven" + + 8 -> + "eight" + + 9 -> + "nine" + + 10 -> + "ten" + + 11 -> + "eleven" + + 12 -> + "twelve" + + other -> + String.fromInt other diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Model.elm b/client/src/elm/MassiveDecks/Strings/Languages/Model.elm new file mode 100644 index 0000000..6dc5bca --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings/Languages/Model.elm @@ -0,0 +1,20 @@ +module MassiveDecks.Strings.Languages.Model exposing + ( Language(..) + , Translator + ) + +import MassiveDecks.Strings exposing (MdString) + + +{-| Languages. These should be IETF language tags, capitalised as-per Elm's standard. +-} +type Language + = En + + +{-| It makes sense to apply the language and pass around the function, so we give that a nice name. +These should only be used from views - otherwise we'll store translated text in the model, so if they change the +language it won't update. +-} +type alias Translator = + MdString -> String diff --git a/client/src/elm/MassiveDecks/Strings/Render.elm b/client/src/elm/MassiveDecks/Strings/Render.elm new file mode 100644 index 0000000..b0acb34 --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings/Render.elm @@ -0,0 +1,216 @@ +module MassiveDecks.Strings.Render exposing (asHtml, asString) + +import FontAwesome.Icon as Icon exposing (Icon) +import FontAwesome.Regular as RegularIcon +import FontAwesome.Solid as Icon +import Html as Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Strings exposing (..) +import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) +import MassiveDecks.Strings.Translation as Translation +import MassiveDecks.Util.Html as Html + + +type alias Context = + ( Language, MdString -> List Translation.Result ) + + +{-| Build an actual string from an `MdString` in the user's language. +-} +asString : Context -> MdString -> String +asString context mdString = + [ Translation.Ref mdString ] |> resultsToString context + + +{-| An HTML text node from the given `MdString`. Note this is more than just convenience - we enhance some strings +with rich HTML content (e.g: links, icons, etc...) when rendered as HTML. +-} +asHtml : Context -> MdString -> Html msg +asHtml context mdString = + [ Translation.Ref mdString ] |> resultsToHtml context |> Html.span [] + + + +{- Private -} + + +resultsToString : Context -> List Translation.Result -> String +resultsToString context results = + results |> List.map (resultToString context) |> String.concat + + +resultToString : Context -> Translation.Result -> String +resultToString context result = + let + ( _, translate ) = + context + + mdStringToString = + \mdString -> mdString |> translate |> resultsToString context + + partsToString = + List.map (resultToString context) >> String.join "" + in + case result of + Translation.Text text -> + text + + Translation.Ref mdString -> + mdStringToString mdString + + Translation.Raw mdString -> + mdStringToString mdString + + Translation.Em emphasised -> + partsToString emphasised + + Translation.Segment segment -> + partsToString segment + + +resultsToHtml : Context -> List Translation.Result -> List (Html msg) +resultsToHtml context results = + results |> List.concatMap (resultToHtml context) + + +resultToHtml : Context -> Translation.Result -> List (Html msg) +resultToHtml context result = + let + ( _, translate ) = + context + in + case result of + Translation.Ref mdString -> + mdString |> translate |> resultsToHtml context |> enhanceHtml context mdString + + Translation.Text text -> + [ Html.text text ] + + Translation.Raw mdString -> + [ mdString |> translate |> resultsToString context |> Html.text ] + + Translation.Em emphasised -> + [ Html.strong [] (emphasised |> List.concatMap (resultToHtml context)) ] + + Translation.Segment cluster -> + [ Html.span [ HtmlA.class "segment" ] (cluster |> List.concatMap (resultToHtml context)) ] + + +enhanceHtml : Context -> MdString -> List (Html msg) -> List (Html msg) +enhanceHtml context mdString unenhanced = + case mdString of + Plural { singular } -> + enhanceHtml context singular unenhanced + + Czar -> + term context CzarDescription Icon.gavel unenhanced + + Call -> + term context CallDescription Icon.square unenhanced + + Response -> + term context ResponseDescription RegularIcon.square unenhanced + + Point -> + term context PointDescription Icon.star unenhanced + + GameCodeTerm -> + term context GameCodeDescription Icon.qrcode unenhanced + + GameCode { code } -> + [ Html.span + [ HtmlA.class "game-code", GameCodeSpecificDescription |> asString context |> HtmlA.title ] + unenhanced + ] + + CardsAgainstHumanity -> + [ Html.blankA [ HtmlA.href "https://cardsagainsthumanity.com/" ] unenhanced ] + + Pick numberOfCards -> + [ Html.span [ HtmlA.class "instruction", PickDescription numberOfCards |> asString context |> HtmlA.title ] unenhanced ] + + Draw numberOfCards -> + [ Html.span [ HtmlA.class "instruction", DrawDescription numberOfCards |> asString context |> HtmlA.title ] unenhanced ] + + Players -> + term context PlayersDescription Icon.chessPawn unenhanced + + Spectators -> + term context SpectatorsDescription Icon.eye unenhanced + + Privileged -> + term context PrivilegedDescription Icon.userCog unenhanced + + Ai -> + term context AiDescription Icon.robot unenhanced + + Score _ -> + [ Html.span [ HtmlA.class "no-wrap", (ScoreDescription |> asString context) |> HtmlA.title ] (suffixed unenhanced Icon.star) ] + + NumberOfCards _ -> + [ Html.span [ HtmlA.class "amount" ] unenhanced ] + + HouseRulePackingHeat -> + prefixed unenhanced Icon.parachuteBox + + HouseRuleReboot -> + prefixed unenhanced Icon.redo + + HouseRuleRandoCardrissian -> + prefixed unenhanced Icon.robot + + RereadGames -> + [ Html.blankA [ HtmlA.class "no-wrap", HtmlA.href "https://www.rereadgames.com/" ] unenhanced ] + + MDProject -> + [ Html.blankA [ HtmlA.class "no-wrap", HtmlA.href "https://github.com/Lattyware/massivedecks" ] unenhanced ] + + License -> + [ Html.blankA [ HtmlA.href "https://github.com/Lattyware/massivedecks/blob/master/LICENSE" ] unenhanced ] + + TranslationBeg -> + [ Html.blankA [ HtmlA.href "https://github.com/Lattyware/massivedecks/wiki/Translation" ] unenhanced ] + + Error -> + prefixed unenhanced Icon.exclamationTriangle + + ErrorHelpTitle -> + prefixed unenhanced Icon.carCrash + + ReportError -> + prefixed unenhanced Icon.bug + + SettingsTitle -> + prefixed unenhanced Icon.cog + + CardcastPlayCode -> + [ Html.blankA [ HtmlA.href "https://www.cardcastgame.com/browse" ] unenhanced ] + + Playing -> + term context PlayingDescription Icon.clock unenhanced + + Played -> + term context PlayedDescription Icon.check unenhanced + + _ -> + unenhanced + + +prefixed : List (Html msg) -> Icon -> List (Html msg) +prefixed base prefix = + Html.span [ HtmlA.class "icon-prefix" ] [ Icon.view prefix, Html.text " " ] :: base + + +suffixed : List (Html msg) -> Icon -> List (Html msg) +suffixed base suffix = + base ++ [ Html.span [ HtmlA.class "icon-suffix" ] [ Html.text " ", Icon.view suffix ] ] + + +term : Context -> MdString -> Icon -> List (Html msg) -> List (Html msg) +term context description icon unenhanced = + [ Html.span + [ HtmlA.class "term", description |> asString context |> HtmlA.title ] + [ Html.span [ HtmlA.class "full" ] unenhanced + , Html.span [ HtmlA.class "icon-suffix" ] [ Html.text " ", Icon.view icon ] + ] + ] diff --git a/client/src/elm/MassiveDecks/Strings/Translation.elm b/client/src/elm/MassiveDecks/Strings/Translation.elm new file mode 100644 index 0000000..3205254 --- /dev/null +++ b/client/src/elm/MassiveDecks/Strings/Translation.elm @@ -0,0 +1,30 @@ +module MassiveDecks.Strings.Translation exposing + ( Pack + , Result(..) + ) + +import MassiveDecks.Strings exposing (MdString) + + +{-| A result of translating an `MdString`. +-} +type Result + = Ref MdString -- A reference to another string. + | Raw MdString -- A reference to another string as raw text - no enhancement will be done. Generally avoid. + | Text String -- The given static text. + | Em (List Result) -- The contained text will be emphasised if possible (e.g: bold). + | Segment (List Result) -- Indicates a "line" of text that will be wrapped independently (but not actively line broken). + + +{-| The details of a language. +-} +type alias Pack = + -- The function to translate `MdString`s for the language. + { translate : MdString -> List Result + + -- The IETF language tag for the language. + , code : String + + -- The name that describes the language. + , name : MdString + } diff --git a/client/src/elm/MassiveDecks/User.elm b/client/src/elm/MassiveDecks/User.elm new file mode 100644 index 0000000..b5d48cb --- /dev/null +++ b/client/src/elm/MassiveDecks/User.elm @@ -0,0 +1,64 @@ +module MassiveDecks.User exposing + ( Connection(..) + , Id + , Presence(..) + , Privilege(..) + , Registration + , Role(..) + , User + ) + +{-| Operations and models for a user in the game. +-} + + +{-| A unique Id for a user. +-} +type alias Id = + String + + +{-| The level of privilege a user has. +-} +type Privilege + = Privileged + | Unprivileged + + +{-| The role the user has in games in the lobby. +-} +type Role + = Player + | Spectator + + +{-| If the user is actively a part of the lobby. +-} +type Presence + = Joined + | Left + + +{-| The state of connection to the lobby. +-} +type Connection + = Connected + | Disconnected + + +{-| A request to register a new user in a lobby. +-} +type alias Registration = + { name : String + } + + +{-| A user in the lobby. +-} +type alias User = + { name : String + , presence : Presence + , connection : Connection + , privilege : Privilege + , role : Role + } diff --git a/client/src/elm/MassiveDecks/Util.elm b/client/src/elm/MassiveDecks/Util.elm new file mode 100644 index 0000000..fa34e25 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util.elm @@ -0,0 +1,29 @@ +module MassiveDecks.Util exposing + ( lift + , messageLift + , modelLift + ) + +{-| General utility methods. +-} + + +{-| Take a `(model, cmd)` for a specific component and turn it into a more general one. +-} +lift : (subModel -> model) -> (subMsg -> msg) -> ( subModel, Cmd subMsg ) -> ( model, Cmd msg ) +lift liftModel liftMsg ( subModel, subCmd ) = + ( liftModel subModel, subCmd |> Cmd.map liftMsg ) + + +{-| Take a `(model, cmd)` for a specific component and turn the message into a more general one. +-} +messageLift : (subMsg -> msg) -> ( model, Cmd subMsg ) -> ( model, Cmd msg ) +messageLift liftMsg ( model, subCmd ) = + ( model, subCmd |> Cmd.map liftMsg ) + + +{-| Take a `(model, cmd)` for a specific component and turn the model into a more general one. +-} +modelLift : (subModel -> model) -> ( subModel, Cmd msg ) -> ( model, Cmd msg ) +modelLift liftModel ( subModel, cmd ) = + ( liftModel subModel, cmd ) diff --git a/client/src/elm/MassiveDecks/Util/Cmd.elm b/client/src/elm/MassiveDecks/Util/Cmd.elm new file mode 100644 index 0000000..d97d8c7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Cmd.elm @@ -0,0 +1,14 @@ +module MassiveDecks.Util.Cmd exposing (after) + +{-| Utility methods for commands. +-} + +import Process +import Task + + +{-| Trigger the given message after the given (in milliseconds) amount of time. +-} +after : Int -> msg -> Cmd msg +after delay message = + delay |> toFloat |> Process.sleep |> Task.perform (always message) diff --git a/client/src/elm/MassiveDecks/Util/Html.elm b/client/src/elm/MassiveDecks/Util/Html.elm new file mode 100644 index 0000000..781074d --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Html.elm @@ -0,0 +1,25 @@ +module MassiveDecks.Util.Html exposing + ( blankA + , nothing + ) + +{-| Utility methods for Html +-} + +import Html exposing (Html) +import Html.Attributes as HtmlA + + +{-| A link that opens in a new tab/window by setting the target to `_blank`. This also sets `rel` to `noopener` as a +security measure. +-} +blankA : List (Html.Attribute msg) -> List (Html msg) -> Html msg +blankA attributes children = + Html.a (attributes ++ [ HtmlA.target "_blank", HtmlA.rel "noopener" ]) children + + +{-| An element to use when you want to show nothing in a case where you otherwise show HTMl. +-} +nothing : Html msg +nothing = + Html.text "" diff --git a/client/src/elm/MassiveDecks/Util/Html/Attributes.elm b/client/src/elm/MassiveDecks/Util/Html/Attributes.elm new file mode 100644 index 0000000..4dfda5f --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Html/Attributes.elm @@ -0,0 +1,12 @@ +module MassiveDecks.Util.Html.Attributes exposing (nothing) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import Json.Encode as Json + + +{-| An attribute to use when you want to have nothing in a case where you otherwise show have an attribute. +-} +nothing : Html.Attribute msg +nothing = + Json.null |> HtmlA.property "" diff --git a/client/src/elm/MassiveDecks/Util/List.elm b/client/src/elm/MassiveDecks/Util/List.elm new file mode 100644 index 0000000..b3bd9aa --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/List.elm @@ -0,0 +1,78 @@ +module MassiveDecks.Util.List exposing + ( any + , find + , map2Long + , mappedIntersperse + , merge + ) + +{-| Utility functions for lists. +-} + +import MassiveDecks.Util.Maybe as Maybe + + +{-| Returns `True` if any value in the list matches the given predicate. +-} +any : (a -> Bool) -> List a -> Bool +any predicate list = + find predicate list /= Nothing + + +{-| Find the first element of the given list that fulfils the given predicate. +-} +find : (a -> Bool) -> List a -> Maybe a +find predicate list = + case list of + head :: tail -> + Maybe.first [ Maybe.validate predicate head, find predicate tail ] + + [] -> + Nothing + + +{-| Map two items together, getting `Nothing`s when the shorter ends. +Note that if longer is shorter, shorter will be cut off to match. +-} +map2Long : (Maybe a -> b -> c) -> List a -> List b -> List c +map2Long f shorter longer = + let + diff = + List.length longer - List.length shorter + + shorterJust = + shorter |> List.map Just + + shorterExtended = + shorterJust ++ List.repeat diff Nothing + in + List.map2 f shorterExtended longer + + +{-| Intersperse an item into a list, building the item from it's surrounding items. +-} +mappedIntersperse : (a -> a -> a) -> List a -> List a +mappedIntersperse f items = + case items of + a :: b :: rest -> + a :: f a b :: b :: mappedIntersperse f rest + + otherwise -> + otherwise + + +{-| Combine two lists, zipping them together until one runs out, then finishing the other. +-} +merge : List a -> List a -> List a +merge xs ys = + case xs of + x :: xRest -> + case ys of + y :: yRest -> + x :: y :: merge xRest yRest + + [] -> + xs + + [] -> + ys diff --git a/client/src/elm/MassiveDecks/Util/Maybe.elm b/client/src/elm/MassiveDecks/Util/Maybe.elm new file mode 100644 index 0000000..74bd662 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Maybe.elm @@ -0,0 +1,100 @@ +module MassiveDecks.Util.Maybe exposing + ( decompose + , first + , isJust + , isNothing + , justIf + , toList + , transformWith + , validate + ) + +{-| Utility functions for optional values. +-} + + +{-| Turn a maybe of a pair into two maybes. +-} +decompose : Maybe ( a, b ) -> ( Maybe a, Maybe b ) +decompose pair = + case pair of + Just ( a, b ) -> + ( Just a, Just b ) + + Nothing -> + ( Nothing, Nothing ) + + +{-| Get the first value that isn't Nothing, if any. +-} +first : List (Maybe a) -> Maybe a +first values = + case values of + (Just value) :: _ -> + Just value + + Nothing :: rest -> + first rest + + [] -> + Nothing + + +{-| Give the value if the predicate is true, or nothing if the predicate is false. +-} +justIf : Bool -> a -> Maybe a +justIf predicate value = + if predicate then + Just value + + else + Nothing + + +{-| Create a singleton list of the value, or an empty list. +-} +toList : Maybe a -> List a +toList value = + case value of + Just item -> + [ item ] + + Nothing -> + [] + + +{-| Apply a given transformation to the value if the given maybe holds the necessary data +-} +transformWith : b -> (b -> a -> b) -> Maybe a -> b +transformWith value transform maybe = + case maybe of + Just op -> + transform value op + + Nothing -> + value + + +{-| Return `Just` the value if it fulfills the predicate, otherwise `Nothing`. +-} +validate : (a -> Bool) -> a -> Maybe a +validate predicate item = + if predicate item then + Just item + + else + Nothing + + +{-| Return if the given maybe doesn't hold a value. +-} +isNothing : Maybe a -> Bool +isNothing = + (==) Nothing + + +{-| Return if the given maybe holds a value. +-} +isJust : Maybe a -> Bool +isJust = + (/=) Nothing diff --git a/client/src/elm/MassiveDecks/Util/Random.elm b/client/src/elm/MassiveDecks/Util/Random.elm new file mode 100644 index 0000000..f4df198 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Random.elm @@ -0,0 +1,18 @@ +module MassiveDecks.Util.Random exposing (disparateList) + +{-| Utility methods for random generation. +-} + +import Random + + +{-| Take a `List` of `Generator` and give a `Generator` of a `List` of the results. +-} +disparateList : List (Random.Generator a) -> Random.Generator (List a) +disparateList generators = + case generators of + head :: tail -> + Random.map2 (\firstGen -> \restGen -> firstGen :: restGen) head (Random.lazy (\() -> disparateList tail)) + + [] -> + Random.constant [] diff --git a/client/src/elm/MassiveDecks/Util/Result.elm b/client/src/elm/MassiveDecks/Util/Result.elm new file mode 100644 index 0000000..72fb876 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Result.elm @@ -0,0 +1,47 @@ +module MassiveDecks.Util.Result exposing + ( error + , isError + , isOk + , unify + ) + +{-| Utility functions for results. +-} + + +{-| Get the error of a result as a maybe. +-} +error : Result err ok -> Maybe err +error result = + case result of + Ok _ -> + Nothing + + Err err -> + Just err + + +{-| If a result is an error. +-} +isError : Result a b -> Bool +isError result = + error result /= Nothing + + +{-| If a result is an error. +-} +isOk : Result a b -> Bool +isOk result = + Result.toMaybe result /= Nothing + + +{-| Turn a result that gives the same type in both cases into that result. +-} +unify : Result a a -> a +unify result = + case result of + Ok value -> + value + + Err value -> + value diff --git a/client/src/elm/MassiveDecks/Util/String.elm b/client/src/elm/MassiveDecks/Util/String.elm new file mode 100644 index 0000000..4beb6a7 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/String.elm @@ -0,0 +1,16 @@ +module MassiveDecks.Util.String exposing (capitalise) + +{-| Utility methods for strings. +-} + + +{-| Make the first letter of a word a capital if it isn't. +-} +capitalise : String -> String +capitalise str = + case String.uncons str of + Just ( firstChar, rest ) -> + String.cons (Char.toLocaleUpper firstChar) rest + + Nothing -> + "" diff --git a/client/src/elm/MassiveDecks/Util/Url.elm b/client/src/elm/MassiveDecks/Util/Url.elm new file mode 100644 index 0000000..d17d862 --- /dev/null +++ b/client/src/elm/MassiveDecks/Util/Url.elm @@ -0,0 +1,25 @@ +module MassiveDecks.Util.Url exposing (origin) + +{-| Utility methods for urls. +-} + +import Url exposing (Url) + + +{-| Get an origin from a URL (combination of protocol, host and port). +-} +origin : Url -> String +origin url = + let + protocol = + case url.protocol of + Url.Http -> + "http" + + Url.Https -> + "https" + in + protocol + ++ "://" + ++ url.host + ++ (url.port_ |> Maybe.map (\p -> ":" ++ String.fromInt p) |> Maybe.withDefault "") diff --git a/client/src/elm/MassiveDecks/Version.elm b/client/src/elm/MassiveDecks/Version.elm new file mode 100644 index 0000000..c22157f --- /dev/null +++ b/client/src/elm/MassiveDecks/Version.elm @@ -0,0 +1,11 @@ +module MassiveDecks.Version exposing (version) + +{-| This file is injected into by the build process. +-} + + +{-| The version of the game. +-} +version : String +version = + "dev" diff --git a/client/src/elm/MassiveDecks/index.d.ts b/client/src/elm/MassiveDecks/index.d.ts new file mode 100644 index 0000000..e54a550 --- /dev/null +++ b/client/src/elm/MassiveDecks/index.d.ts @@ -0,0 +1,70 @@ +type Token = string; + +interface Settings { + tokens: Token[]; + lastUsedName: string | null; + recentDecks: string[]; + chosenLanguage: string | null; +} + +interface Flags { + settings: Settings; + browserLanguages: string[]; +} + +interface CastFlags { + token: Token; +} + +interface CastStatus { + status: string; + name?: string; +} + +interface OpenCommand { + gameCode: string; + token: string; +} + +interface MessageCommand { + message: string; +} + +interface CloseCommand {} + +type ConnectionCommand = OpenCommand | MessageCommand | CloseCommand; + +export interface InboundPort { + subscribe(callback: (data: T) => void): void; +} + +export interface OutboundPort { + send(data: T): void; +} + +export namespace Elm { + namespace MassiveDecks { + export interface App { + ports: { + storeSettings: InboundPort; + tryCast: InboundPort; + castStatus: OutboundPort; + serverRecv: OutboundPort; + serverSend: InboundPort; + copyText: InboundPort; + }; + } + export function init(options: { + node?: HTMLElement | null; + flags: Flags; + }): Elm.MassiveDecks.App; + + namespace Cast { + export interface App {} + export function init(options: { + node?: HTMLElement | null; + flags: CastFlags; + }): Elm.MassiveDecks.App; + } + } +} diff --git a/client/src/elm/Reread.elm b/client/src/elm/Reread.elm new file mode 100644 index 0000000..88ac08c --- /dev/null +++ b/client/src/elm/Reread.elm @@ -0,0 +1,13 @@ +module Reread exposing (icon, mdIcon) + +import FontAwesome.Icon as Icon exposing (Icon) + + +icon : Icon +icon = + Icon "fab" "reread" 512 512 "M258 16c-62 0-124 24-171 71-95 94-94 246 0 339 95 93 250 93 345 0l12-12-15-14-12 12-10-10c-82 80-213 80-295 0-82-81-82-210-1-291 81-80 212-81 294-3l-38 35 129 25-1-3-26-117-39 36c-48-46-110-68-172-68zm-79 159c-46 1-70 10-70 10v133s24-9 70-9c50 0 70 20 70 20V196s-20-21-70-21zm160 0c-50 0-70 21-70 21v133s20-20 70-20c46 0 70 9 70 9V185s-24-9-70-10zM179 323c-46 1-70 10-70 10v16s24-9 70-9c50 0 81 31 81 31s30-31 80-31c46 0 70 9 70 9v-16s-24-9-70-10c-50 0-80 32-80 32s-31-32-81-32z" + + +mdIcon : Icon +mdIcon = + Icon "fab" "massivedecks" 512 512 "M273 20c-11 0-21 9-23 21l-9 88h-8L39 163c-12 2-21 15-19 27l50 283c2 12 15 21 27 19l194-34c12-3 21-15 18-27l-13-73 140 14c13 2 25-8 26-20l30-286c1-13-8-24-21-25L276 20h-3zm0 16h1l196 21c4 0 6 3 6 7l-30 286c0 4-4 7-8 6l-144-15-35-193-3-9 10-97c1-3 4-6 7-6zm47 106l-10 99 39 4c15 2 25 0 32-8 9-8 15-21 16-37 2-15-1-29-8-39-6-9-15-13-30-15zm19 20l18 1c16 2 22 14 20 35-3 22-11 32-27 31l-18-2zm-135 91l17 98-20 4-13-77-4 80-20 3-32-73 14 76-20 4-18-98 31-6 31 75 4-81z" diff --git a/client/src/elm/Weightless.elm b/client/src/elm/Weightless.elm new file mode 100644 index 0000000..a35f329 --- /dev/null +++ b/client/src/elm/Weightless.elm @@ -0,0 +1,89 @@ +module Weightless exposing + ( button + , card + , expansion + , listItem + , navigationBar + , popover + , popoverCard + , select + , switch + , tab + , tabGroup + , textArea + , textField + , tooltip + ) + +import Html exposing (Html) +import Weightless.Attributes as WlA + + +button : List (Html.Attribute msg) -> List (Html msg) -> Html msg +button = + Html.node "wl-button" + + +card : List (Html.Attribute msg) -> List (Html msg) -> Html msg +card = + Html.node "wl-card" + + +expansion : List (Html.Attribute msg) -> List (Html msg) -> Html msg +expansion attrs = + Html.node "wl-expansion" (WlA.icon "" :: attrs) + + +listItem : List (Html.Attribute msg) -> List (Html msg) -> Html msg +listItem = + Html.node "wl-list-item" + + +tabGroup : List (Html.Attribute msg) -> List (Html msg) -> Html msg +tabGroup = + Html.node "wl-tab-group" + + +tab : List (Html.Attribute msg) -> List (Html msg) -> Html msg +tab = + Html.node "wl-tab" + + +switch : List (Html.Attribute msg) -> Html msg +switch attrs = + Html.node "wl-switch" attrs [] + + +textField : List (Html.Attribute msg) -> List (Html msg) -> Html msg +textField = + Html.node "wl-textfield" + + +textArea : List (Html.Attribute msg) -> List (Html msg) -> Html msg +textArea = + Html.node "wl-textarea" + + +popover : List (Html.Attribute msg) -> List (Html msg) -> Html msg +popover = + Html.node "wl-popover" + + +popoverCard : List (Html.Attribute msg) -> List (Html msg) -> Html msg +popoverCard = + Html.node "wl-popover-card" + + +navigationBar : List (Html.Attribute msg) -> List (Html msg) -> Html msg +navigationBar = + Html.node "wl-nav" + + +select : List (Html.Attribute msg) -> List (Html msg) -> Html msg +select = + Html.node "wl-select" + + +tooltip : List (Html.Attribute msg) -> List (Html msg) -> Html msg +tooltip = + Html.node "wl-tooltip" diff --git a/client/src/elm/Weightless/Attributes.elm b/client/src/elm/Weightless/Attributes.elm new file mode 100644 index 0000000..ca52ff2 --- /dev/null +++ b/client/src/elm/Weightless/Attributes.elm @@ -0,0 +1,420 @@ +module Weightless.Attributes exposing + ( Alignment(..) + , ExpansionSlot(..) + , InputType(..) + , ListItemSlot(..) + , NavSlot(..) + , TextFieldSlot(..) + , XOrigin(..) + , YOrigin(..) + , active + , align + , anchor + , anchorCloseEvents + , anchorOpenEvents + , anchorOrigin + , checked + , clickable + , disableFocusTrap + , disabled + , expansionSlot + , fab + , filled + , fixed + , flat + , hoverable + , icon + , inverted + , label + , list + , listItemSlot + , max + , maxLength + , min + , minLength + , name + , navSlot + , outlined + , readonly + , selected + , textFieldSlot + , transformOrigin + , type_ + , value + , vertical + ) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import Json.Encode as Json + + +min : Int -> Html.Attribute msg +min = + numberAttr "min" + + +max : Int -> Html.Attribute msg +max = + numberAttr "max" + + +maxLength : Int -> Html.Attribute msg +maxLength = + numberAttr "maxLength" + + +minLength : Int -> Html.Attribute msg +minLength = + numberAttr "minLength" + + +anchorOpenEvents : List String -> Html.Attribute msg +anchorOpenEvents = + stringArrayProperty "anchorOpenEvents" + + +anchorCloseEvents : List String -> Html.Attribute msg +anchorCloseEvents = + stringArrayProperty "anchorCloseEvents" + + +anchor : String -> Html.Attribute msg +anchor id = + stringAttr "anchor" ("#" ++ id) + + +icon : String -> Html.Attribute msg +icon icon_name = + stringAttr "icon" icon_name + + +name : String -> Html.Attribute msg +name group = + stringAttr "name" group + + +type NavSlot + = Title + | Left + | Right + + +navSlot : NavSlot -> Html.Attribute msg +navSlot slot = + let + textSlot = + case slot of + Title -> + "title" + + Left -> + "left" + + Right -> + "right" + in + stringAttr "slot" textSlot + + +type ExpansionSlot + = ETitle + | EDescription + + +expansionSlot : ExpansionSlot -> Html.Attribute msg +expansionSlot slot = + let + textSlot = + case slot of + ETitle -> + "title" + + EDescription -> + "description" + in + stringAttr "slot" textSlot + + +type ListItemSlot + = BeforeItem + | AfterItem + + +listItemSlot : ListItemSlot -> Html.Attribute msg +listItemSlot slot = + let + textSlot = + case slot of + BeforeItem -> + "before" + + AfterItem -> + "after" + in + stringAttr "slot" textSlot + + +type TextFieldSlot + = BeforeText + | AfterText + + +textFieldSlot : TextFieldSlot -> Html.Attribute msg +textFieldSlot slot = + let + textSlot = + case slot of + BeforeText -> + "before" + + AfterText -> + "after" + in + stringAttr "slot" textSlot + + +value : String -> Html.Attribute msg +value text = + stringAttr "value" text + + +type Alignment + = Start + | Center + | End + + +align : Alignment -> Html.Attribute msg +align alignment = + let + textAlignment = + case alignment of + Start -> + "start" + + Center -> + "center" + + End -> + "end" + in + stringAttr "align" textAlignment + + +list : String -> Html.Attribute msg +list datalistSelector = + stringAttr "list" datalistSelector + + +type InputType + = Password + | Email + | Number + | Color + | Date + | Search + | Tel + | File + + +type_ : InputType -> Html.Attribute msg +type_ inputType = + let + textInputType = + case inputType of + Password -> + "password" + + Email -> + "email" + + Number -> + "number" + + Color -> + "color" + + Date -> + "date" + + Search -> + "search" + + Tel -> + "tel" + + File -> + "file" + in + stringAttr "type" textInputType + + +label : String -> Html.Attribute msg +label text = + stringAttr "label" text + + +inverted : Html.Attribute msg +inverted = + presentAttribute "inverted" + + +vertical : Html.Attribute msg +vertical = + presentAttribute "vertical" + + +filled : Html.Attribute msg +filled = + presentAttribute "filled" + + +outlined : Html.Attribute msg +outlined = + presentAttribute "outlined" + + +flat : Html.Attribute msg +flat = + presentAttribute "flat" + + +fab : Html.Attribute msg +fab = + presentAttribute "fab" + + +hoverable : Html.Attribute msg +hoverable = + presentAttribute "hoverable" + + +disabled : Html.Attribute msg +disabled = + presentAttribute "disabled" + + +readonly : Html.Attribute msg +readonly = + presentAttribute "readonly" + + +active : Html.Attribute msg +active = + presentAttribute "active" + + +clickable : Html.Attribute msg +clickable = + presentAttribute "clickable" + + +checked : Html.Attribute msg +checked = + presentAttribute "checked" + + +fixed : Html.Attribute msg +fixed = + presentAttribute "fixed" + + +selected : Html.Attribute msg +selected = + presentAttribute "selected" + + +disableFocusTrap : Html.Attribute msg +disableFocusTrap = + presentAttribute "disablefocustrap" + + +type XOrigin + = XLeft + | XCenter + | XRight + + +type YOrigin + = YTop + | YCenter + | YBottom + + +anchorOrigin : XOrigin -> YOrigin -> List (Html.Attribute msg) +anchorOrigin = + origin "anchor" + + +transformOrigin : XOrigin -> YOrigin -> List (Html.Attribute msg) +transformOrigin = + origin "transform" + + + +{- Private -} + + +origin : String -> XOrigin -> YOrigin -> List (Html.Attribute msg) +origin prefix xOrigin yOrigin = + let + ( textXOrigin, textYOrigin ) = + textOrigin xOrigin yOrigin + in + [ stringAttr (prefix ++ "originx") textXOrigin + , stringAttr (prefix ++ "originy") textYOrigin + ] + + +textOrigin : XOrigin -> YOrigin -> ( String, String ) +textOrigin xOrigin yOrigin = + let + textXOrigin = + case xOrigin of + XLeft -> + "left" + + XCenter -> + "center" + + XRight -> + "right" + + textYOrigin = + case yOrigin of + YTop -> + "top" + + YCenter -> + "center" + + YBottom -> + "bottom" + in + ( textXOrigin, textYOrigin ) + + +presentAttribute : String -> Html.Attribute msg +presentAttribute attribute = + HtmlA.attribute attribute attribute + + +stringArrayProperty : String -> List String -> Html.Attribute msg +stringArrayProperty property values = + HtmlA.property property (values |> Json.list Json.string) + + +stringAttr : String -> String -> Html.Attribute msg +stringAttr attribute stringValue = + HtmlA.attribute attribute stringValue + + +numberAttr : String -> Int -> Html.Attribute msg +numberAttr attribute intValue = + intValue |> String.fromInt |> HtmlA.attribute attribute + + +numberProp : String -> Int -> Html.Attribute msg +numberProp property intValue = + intValue |> Json.int |> HtmlA.property property diff --git a/client/src/html/cast.html b/client/src/html/cast.html new file mode 100644 index 0000000..b25293a --- /dev/null +++ b/client/src/html/cast.html @@ -0,0 +1,23 @@ + + + + + + Massive Decks + + + + + + + + + + + + + +

Loading...
+ + + diff --git a/client/src/html/index.html b/client/src/html/index.html new file mode 100644 index 0000000..2d3412b --- /dev/null +++ b/client/src/html/index.html @@ -0,0 +1,24 @@ + + + + + + Massive Decks + + + + + + + + + + + + + + + + + + diff --git a/client/src/scss/animation.scss b/client/src/scss/animation.scss new file mode 100644 index 0000000..d6c8a7f --- /dev/null +++ b/client/src/scss/animation.scss @@ -0,0 +1,12 @@ +.animation { + transition: transform 200ms ease-in, opacity 200ms ease-in; + opacity: 0; +} + +.animation.entering { + opacity: 1; +} + +.animation.exiting { + transition: transform 200ms ease-out, opacity 200ms ease-out; +} diff --git a/client/src/scss/card/sources.scss b/client/src/scss/card/sources.scss new file mode 100644 index 0000000..696f9bd --- /dev/null +++ b/client/src/scss/card/sources.scss @@ -0,0 +1,10 @@ +.cardcast-logo { + display: inline-block; + width: 1em; + height: 1em; + font-size: inherit; + vertical-align: -0.125em; + background-image: url(../../assets/images/cardcast.png); + background-size: contain; + margin-right: 0.5em; +} diff --git a/client/src/scss/card/variables.scss b/client/src/scss/card/variables.scss new file mode 100644 index 0000000..c7a5aea --- /dev/null +++ b/client/src/scss/card/variables.scss @@ -0,0 +1,33 @@ +$call-color: #ffffff; +$call-background: #000000; + +$response-color: $call-background; +$response-background: $call-color; + +$card-width: 18em; +$card-margin: 0.5em; +$card-overlap: 2em; + +$card-ratio: 5/7; +$card-compact-ratio: 1; + +@mixin pad-to-aspect-ratio($ratio) { + padding-top: #{calc(1 / #{$ratio} * 100%)}; +} + +@function strip-unit($value) { + @return $value / ($value * 0 + 1); +} + +@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) { + font-size: $min-font-size; + @media screen and (min-width: $min-vw) { + font-size: calc( + #{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} * + ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)}) + ); + } + @media screen and (min-width: $max-vw) { + font-size: $max-font-size; + } +} diff --git a/client/src/scss/cards.scss b/client/src/scss/cards.scss new file mode 100644 index 0000000..ba8a6a1 --- /dev/null +++ b/client/src/scss/cards.scss @@ -0,0 +1,194 @@ +@import "./card/variables"; +@import "./colors"; +@import "./fonts"; +@import "./card/sources"; + +.compact-cards .card-area { + --card-aspect-ratio: #{$card-compact-ratio}; +} + +.card-area { + --card-aspect-ratio: #{$card-ratio}; + @include fluid-type(20rem, 64rem, 0.5rem, 0.8rem); +} + +.game-card { + width: $card-width; + margin: $card-margin; + perspective: 100vw; + + text-rendering: optimizeLegibility; + font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif; + font-weight: bold; + + p { + margin: 0; + } + + &.response { + --bg: #{$response-background}; + --fg: #{$response-color}; + } + + &.call { + --bg: #{$call-background}; + --fg: #{$call-color}; + + p { + display: flex; + flex-flow: row wrap; + align-items: baseline; + align-content: flex-start; + justify-content: flex-start; + + span { + white-space: pre-wrap; + overflow-wrap: break-word; + word-break: break-all; + } + + .slot { + display: contents; + + span { + text-decoration: underline; + } + + &:empty { + display: block; + height: 1em; + flex-grow: 1; + flex-basis: 2em; + border-bottom: 0.1em solid var(--fg); + } + } + } + } + + .aspect { + transition: transform 0.75s; + transform-style: preserve-3d; + position: relative; + height: 0; + width: 100%; + @include pad-to-aspect-ratio(var(--card-aspect-ratio)); + + & > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + + &.face-down .aspect { + transform: rotateY(180deg); + } + + .side { + box-sizing: border-box; + background: var(--bg); + color: var(--fg); + border-radius: 0.75em; + border: 0.3em solid var(--bg); + box-shadow: 0.15em 0.15em 0.4em rgba(0, 0, 0, 0.3), + -0.05em -0.05em 0.4em rgba(0, 0, 0, 0.3); + padding: 1em; + backface-visibility: hidden; + transform: rotateX(0deg); + + &.back { + transform: rotateY(180deg); + + .content p { + line-height: 1em; + font-size: 2em; + } + } + + &.front { + display: flex; + flex-flow: column nowrap; + + .content { + flex-grow: 1; + overflow-x: hidden; + overflow-y: auto; + } + } + + p { + color: var(--fg); + } + } + + .content { + font-size: 1.6em; + font-weight: 700; + line-height: 1.4em; + } + + textarea { + display: block; + vertical-align: top; + font: inherit; + background-color: inherit; + resize: none; + border: 0; + margin: 0; + padding: 0; + width: 100%; + height: 100%; + } + + .info { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-top: 0.5em; + + .source { + font-size: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &:before { + content: ""; + background-image: url(../../assets/images/deck.svg); + background-size: 100% 100%; + display: inline-block; + width: 2.8em; + height: 2em; + } + } + + .instructions { + list-style: none; + margin: 0; + padding: 0; + text-align: right; + + li { + display: flex; + justify-content: flex-end; + align-items: baseline; + margin-top: 0.2em; + font-size: 1.4em; + } + } + } + + &.error .side { + border-color: $error; + } +} + +.picked { + position: relative; + + .side { + border-color: $primary; + } +} diff --git a/client/src/scss/cast.scss b/client/src/scss/cast.scss new file mode 100644 index 0000000..11d574c --- /dev/null +++ b/client/src/scss/cast.scss @@ -0,0 +1,9 @@ +.loading { + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + align-content: center; + justify-items: center; +} diff --git a/client/src/scss/colors.scss b/client/src/scss/colors.scss new file mode 100644 index 0000000..1b907cf --- /dev/null +++ b/client/src/scss/colors.scss @@ -0,0 +1,22 @@ +$primary: #42a5f5; +$on-primary: #212121; +$primary-light: #80d6ff; +$on-primary-light: #212121; +$primary-dark: #0077c2; +$on-primary-dark: #f5f5f5; + +$secondary: #ff6122; +$on-secondary: #212121; +$secondary-light: #ff8a50; +$on-secondary-light: #212121; +$secondary-dark: #c41c00; +$on-secondary-dark: #f5f5f5; + +$error: $secondary-dark; +$on-error: $on-secondary-dark; + +$background: #e2e2e2; + +$secondary-background: #cccccc; + +$transparent: rgba(#ffffff, 0); diff --git a/client/src/scss/error.scss b/client/src/scss/error.scss new file mode 100644 index 0000000..69f600a --- /dev/null +++ b/client/src/scss/error.scss @@ -0,0 +1,55 @@ +@import "./colors"; + +.error-overlay { + z-index: 100; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background-color: transparentize($secondary-background, 0.6); + + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + + padding: 3em; + + .actions { + display: flex; + flex-direction: row; + justify-content: center; + } + + .help { + margin-bottom: 0.5em; + } + + .errors { + overflow-y: auto; + } +} + +.error { + --expansion-color: #{$on-error}; + --expansion-header-description-color: #{$on-error}; + --expansion-bg: #{$error}; + + padding: 0.25em; + + .report { + .body { + margin: 0 1em; + --input-color: #{$on-error}; + } + + .link { + margin-top: 1em; + display: flex; + justify-content: flex-end; + width: 100%; + } + } +} diff --git a/client/src/scss/fonts.scss b/client/src/scss/fonts.scss new file mode 100644 index 0000000..8bbeb06 --- /dev/null +++ b/client/src/scss/fonts.scss @@ -0,0 +1,56 @@ +@font-face { + font-family: "Nimbus Sans L"; + font-style: normal; + font-weight: 400; + src: local("Nimbus Sans L"), local("NimbusSansL"), + url("../../assets/fonts/nimbus-sans-l-regular.woff2") format("woff2"); +} + +@font-face { + font-family: "Nimbus Sans L"; + font-style: normal; + font-weight: 700; + src: local("Nimbus Sans L Bold"), local("NimbusSansL-Bold"), + url("../../assets/fonts/nimbus-sans-l-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + src: local("Source Code Pro"), local("SourceCodePro-Regular"), + url("../../assets/fonts/source-code-pro-v8-latin-regular.woff2") + format("woff2"); +} + +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url("../../assets/fonts/roboto-v18-latin-regular.woff2") format("woff2"); +} + +@font-face { + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url("../../assets/fonts/roboto-v18-latin-italic.woff2") format("woff2"); +} + +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 700; + src: local("Roboto Bold"), local("Roboto-Bold"), + url("../../assets/fonts/roboto-v18-latin-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Roboto"; + font-style: italic; + font-weight: 700; + src: local("Roboto Bold Italic"), local("Roboto-BoldItalic"), + url("../../assets/fonts/roboto-v18-latin-700italic.woff2") format("woff2"); +} diff --git a/client/src/scss/game.scss b/client/src/scss/game.scss new file mode 100644 index 0000000..8d10dac --- /dev/null +++ b/client/src/scss/game.scss @@ -0,0 +1,262 @@ +@import "card/variables"; +@import "colors"; +@import "~weightless/style/base/elevation"; + +#game { + height: 100%; + position: relative; + + display: flex; + flex-direction: column; + align-items: stretch; + + .card-area { + position: relative; + } + + .context-help { + position: fixed; + top: 4em; + right: 0; + z-index: 1; + + background-color: $secondary-background; + padding: 0.5em; + border-bottom-left-radius: 1em; + + display: flex; + align-items: center; + align-self: stretch; + + transition: transform 200ms ease-out; + transform: translate(calc(100% - 2em), 0); + + &:hover, + &:active { + transform: translate(0, 0); + transition: transform 200ms ease-in; + } + + & > svg { + margin-right: 0.5em; + } + } + + .scroll-top { + pointer-events: none; + display: flex; + flex-direction: column; + justify-content: flex-end; + + position: absolute; + left: 0; + top: 0; + bottom: 0; + + wl-button { + pointer-events: all; + position: sticky; + bottom: 0.5em; + margin: 0.5em; + z-index: 1; + + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + } + + // This pushes the scroll-top button down below the page content. + .scroll-top-spacer { + height: 6em; + } + + .action { + position: fixed; + // Hack because the debugger covers it up. + bottom: 5rem; + //bottom: 1em; + right: 1rem; + + z-index: 100; + + --button-fab-size: 4rem; + font-size: 1.5rem; + } + + // A style for actions that the game is waiting on - high contrast to grab attention. + .important { + --button-bg: #{$secondary}; + --button-color: #{$on-secondary}; + --button-bg-hover: #{$secondary-light}; + --button-color-hover: #{$on-secondary-light}; + --button-bg-active: #{$secondary-dark}; + --button-color-acitve: #{$on-secondary-dark}; + } +} + +.play-area { + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + flex-basis: $card-width; + min-width: $card-width; + padding: 2em 0; + + .cards-in-play { + display: flex; + justify-content: center; + } +} + +.hand { + margin-top: 0.5em; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + + position: relative; + + &.not-playing { + pointer-events: none; + + .game-card .aspect::after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 0.75em; + background-color: #000000; + opacity: 0.1; + } + } +} + +.background-plays { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: -10; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-evenly; + + .play { + position: relative; + transform: translateY(-100%); + transition: transform 500ms ease-in; + width: $card-width + $card-margin * 2; + @include pad-to-aspect-ratio(var(--card-aspect-ratio)); + + .game-card { + transform: rotate(0); + transition: transform 500ms ease-in; + transform-origin: center; + + position: absolute; + top: 0; + right: 0; + left: 0; + } + + &.played { + transform: translateY(0); + transition: transform 500ms ease-out; + + .game-card { + transform: rotate(var(--rotation)); + transition: transform 500ms ease-out; + } + } + } +} + +.plays { + list-style: none; + padding: 0; + + margin-top: 0.5em; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + + position: relative; + + .play-with-byline { + display: flex; + flex-direction: column; + align-items: center; + } + + .byline { + font-size: 2em; + text-align: center; + + svg { + margin-right: 0.5em; + } + } + + .play { + display: flex; + margin: 1em; + + .overlap { + flex-shrink: 1; + flex-grow: 0; + flex-basis: $card-width; + overflow: visible; + z-index: 0; + + &:last-of-type { + width: $card-width; + } + + &:hover { + z-index: 1; + } + + .game-card { + margin: 0 $card-margin; + } + } + } +} + +.liked { + .side::after { + position: absolute; + bottom: 1em; + right: 1em; + width: 8em; + height: 8em; + transform: rotate(-0.125turn); + + display: block; + content: ""; + opacity: 0.3; + //noinspection CssInvalidFunction + background: inline-svg( + "./assets/images/certificate-thumbs-up.svg", + ( + path: ( + fill: $primary + ) + ) + ); + } +} diff --git a/client/src/scss/massive-decks.scss b/client/src/scss/massive-decks.scss new file mode 100644 index 0000000..2390f5e --- /dev/null +++ b/client/src/scss/massive-decks.scss @@ -0,0 +1,239 @@ +@import "./colors"; +@import "./card/variables"; +@import "./fonts"; +@import "./animation.scss"; +@import "./variables.scss"; + +@mixin no-wrap() { + white-space: nowrap; +} + +.no-wrap { + @include no-wrap(); +} + +body { + color: $on-primary; + background: $background url(../../assets/images/background.svg) repeat; + margin: 0; + padding: 0; + font-size: 18px; + line-height: 1.2; + font-family: "Roboto", sans-serif; + min-width: $min-page-width; +} + +.game-code-input { + --input-font-family: "Source Code Pro", monospace; +} + +.game-code { + font-family: "Source Code Pro", monospace; + .svg-inline--fa { + margin-left: 0.3em; + } + white-space: nowrap; + color: #ffffff; + background-color: #000000; + padding: 0.1em 0.5em; +} + +h1 { + font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif; + font-weight: 700; + margin: 0.5em; +} + +.link-button { + background: none; + border: none; + font-size: 0.8em; + cursor: pointer; + outline: none; + padding: 0; + margin: 0; + display: inline-block; + text-align: left; +} + +a, +.link-button { + color: inherit; + text-decoration: none; + + &:not(.tab) { + background: linear-gradient(to top, $secondary 50%, $transparent 50%) + no-repeat 0 10%; + background-size: 100% 200%; + transition: background-position 0.3s cubic-bezier(0.64, 0.9, 0.8, 1); + } + + &:hover { + background-position: 0 100%; + } +} + +.page { + padding: 2em; +} + +.term { + @include no-wrap(); + font-variant: small-caps; + letter-spacing: 0.05em; + text-rendering: optimizeLegibility; + + .icon-prefix { + display: none; + } + + .icon-suffix { + display: none; + } +} + +.compressed-terms { + .term { + .full { + display: none; + } + .icon-prefix { + display: inline; + } + .icon-suffix { + display: inline; + } + } +} + +.instruction { + @include no-wrap(); + + text-transform: uppercase; + letter-spacing: 0.05em; + text-rendering: optimizeLegibility; + + &:before { + content: "["; + } + + &:after { + content: "]"; + } +} + +.game-card .instruction { + &:before, + &:after { + content: none; + } +} + +.amount { + @include no-wrap(); + display: inline-flex; + align-items: center; + align-content: center; + justify-content: center; + text-align: center; + width: 1.2em; + height: 1.2em; + margin-left: 0.3em; + border-radius: 50%; + background: var(--fg, $call-background); + color: var(--bg, $call-color); +} + +// TODO: Is this not used any more? +.form-chunk { + margin: 0 0 0.5em 0; + + display: flex; + flex-direction: column; + + align-self: stretch; +} + +// TODO: Move to Elm asset loader when one works. +.forceLoad { + background: url(../../assets/images/reread-icon.svg); +} + +.empty-info { + text-align: center; + padding: 1em; + font-size: 1.2em; + opacity: 0.3; + + .svg-inline--fa { + margin-right: 0.3em; + } +} + +.form-section { + margin: 1em 0; + + display: flex; + flex-direction: column; + + .multipart { + display: flex; + align-items: center; + + min-height: 2em; + + .primary { + flex-grow: 1; + } + + label { + margin-left: 0.5em; + } + } +} + +.segment { + display: inline-block; +} + +@mixin message { + font-size: 0.75em; + padding: 0.25em; + display: flex; + align-items: center; + + .svg-inline--fa { + margin-right: 0.3em; + } +} + +.info { + @include message; +} + +.inline-error { + @include message; + color: $error; +} + +.warning { + @include message; + color: #6f6401; +} + +.remove-from-flow { + position: absolute; +} + +@import "./fonts"; +@import "./cards"; +@import "./error"; +@import "./settings"; +@import "./game"; +@import "./pages/start"; +@import "./pages/unknown-page"; +@import "./pages/spectate"; +@import "./pages/lobby"; +@import "./weightless"; + +@import "@fortawesome/fontawesome-free/css/svg-with-js.min.css"; diff --git a/client/src/scss/pages/lobby.scss b/client/src/scss/pages/lobby.scss new file mode 100644 index 0000000..6341c89 --- /dev/null +++ b/client/src/scss/pages/lobby.scss @@ -0,0 +1,248 @@ +@import "../colors"; +@import "../card/variables"; +@import "../game"; +@import "./lobby/configure"; +@import "./lobby/invite"; +@import "~weightless/style/base/elevation"; + +$users-width: 15em; + +@keyframes fadeIn { + from { + opacity: 0.1; + } +} + +.lobby { + box-sizing: border-box; + height: 100vh; + padding-top: 4em; + overflow: hidden; + + .content-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + min-width: fit-content; + overflow: hidden; + height: 100%; + width: 100%; + } +} + +#top-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + + z-index: 10; + + height: 4em; + padding: 0 1em; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + + color: $call-color; + background-color: $call-background; + + .left { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + } + + .title-call { + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + + margin: 0 1em; + + white-space: pre; + font-size: 1em; + text-rendering: optimizeLegibility; + font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif; + font-weight: 700; + + display: flex; + justify-content: center; + + .slot { + display: inline-block; + flex-shrink: 0; + flex-basis: 3em; + border-bottom: 0.1em solid $call-color; + } + + &::before { + content: "“"; + } + + &::after { + content: "”"; + } + } +} + +#scroll-frame { + flex-grow: 100; + flex-shrink: 1; + flex-basis: $card-width; + max-height: 100%; + max-width: 100%; + min-width: $card-width; + + overflow: auto; +} + +.outlined-solid-icon { + path { + fill: none; + stroke: black; + stroke-width: 32px; + } +} + +.loading { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.collapsed-users .users { + flex-grow: 0; + flex-basis: 0; +} + +.context-help { + left: $users-width; +} + +.collapsed-users .context-help { + left: 0; +} + +.users { + font-size: 1rem; + overflow-y: auto; + overflow-x: hidden; + + z-index: 2; + + border-radius: 0; + + padding: 0; + + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + + min-height: 100%; + + flex-basis: $users-width; + + transition: flex-basis 0.3s, flex-grow 0.3s, padding-left 0.3s, + padding-right 0.3s; + + --list-item-padding: 0.5em; + + .collapsible { + padding: 1em; + } + + .user { + position: relative; + display: flex; + + .term { + margin-left: 0.5em; + } + } + + ol { + list-style: none; + margin: 0; + padding: 0; + } + + li { + margin-top: 0.5em; + } +} + +.cast-button { + &.connecting { + animation: fadeIn 1s infinite alternate; + } + + &.connected { + color: $secondary; + } +} + +.you { + font-weight: bold; +} + +.notifications { + position: fixed; + left: 0; + bottom: 0; + list-style: none; + z-index: 100; +} + +.notification { + position: fixed; + left: 0; + bottom: 0; + margin: 1em; + padding: 0; + transform: translateY(100%); + + --card-bg: #{$call-background}; + --card-color: #{$call-color}; + + &.entering { + transform: translateY(0%); + } + + .content { + display: flex; + align-items: center; + padding: 0.05em; + + .icon { + margin: 0 0.5em 0 1em; + } + + .action { + margin-left: 0.5em; + } + } +} + +.menu { + --list-item-border-radius: 0; + padding: 0; + + ul { + list-style: none; + padding: 0; + margin: 0; + + wl-list-item { + width: 100%; + } + } +} diff --git a/client/src/scss/pages/lobby/configure.scss b/client/src/scss/pages/lobby/configure.scss new file mode 100644 index 0000000..72bd39f --- /dev/null +++ b/client/src/scss/pages/lobby/configure.scss @@ -0,0 +1,129 @@ +@import "../../colors"; +@import "../../card/variables"; + +.configure { + padding: 1em; + height: 100%; + overflow: auto; + + wl-card { + max-width: 30em; + margin: 1em auto; + } + + .title { + display: flex; + justify-content: space-between; + align-items: baseline; + + .game-code { + font-size: 1.5em; + } + } + + h2, + h3 { + margin: 0.5em 0 0 0; + } + + .multipart { + wl-switch { + margin-right: 0.5em; + } + } +} + +.decks table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1em; + + th { + text-align: center; + padding: 1em 0.5em; + } + + th.deck-name { + text-align: left; + } + + tr { + border-top: 1px solid $secondary-background; + } + + .deck-row .name { + display: flex; + min-height: 3.5rem; + align-items: center; + } + + thead tr { + border-top: none; + } + + .deck-name { + width: auto; + } + + .count { + width: 2em; + text-align: right; + } + + .remove-button { + display: none; + margin: 0 0 0 0.1em; + } + + tr:hover .remove-button { + display: inline-flex; + } +} + +#source-selector { + flex-basis: 7em; + flex-shrink: 1; + height: 100%; + margin-right: 0.5em; + --input-padding-top-bottom: 0.9em; +} + +.house-rules { + margin-top: 2em; +} + +.house-rule { + margin: 1em 0; + opacity: 0.6; + + &:first-of-type { + margin: 0 0 1em 0; + } + + h4 { + margin: 0; + } + + &.enabled { + opacity: 1; + + .house-rule-settings { + transition: height 200ms ease-in; + height: fit-content; + } + } + + .form-section { + margin: 0; + } + + .house-rule-settings { + height: 0; + overflow: hidden; + transition: height 200ms ease-out; + + .form-section { + margin: 0 1.5em; + } + } +} diff --git a/client/src/scss/pages/lobby/invite.scss b/client/src/scss/pages/lobby/invite.scss new file mode 100644 index 0000000..73d1bd5 --- /dev/null +++ b/client/src/scss/pages/lobby/invite.scss @@ -0,0 +1,49 @@ +#invite-dialog { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + min-width: min-content; + + z-index: 200000; + + opacity: 0; + + pointer-events: none; + + overflow: hidden; + + background-color: transparentize(#cccccc, 0.2); + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transition: opacity 200ms ease-in; + + #invite-link-field { + border: 0; + border-bottom: 2px solid #cccccc; + flex-grow: 1; + text-align: center; + + &:focus { + outline: none; + } + } + + &.open { + opacity: 1; + bottom: 0; + transition: opacity 200ms ease-out; + pointer-events: all; + } + + wl-card { + display: flex; + flex-direction: column; + align-items: center; + max-width: 20em; + } +} diff --git a/client/src/scss/pages/spectate.scss b/client/src/scss/pages/spectate.scss new file mode 100644 index 0000000..c065efc --- /dev/null +++ b/client/src/scss/pages/spectate.scss @@ -0,0 +1,79 @@ +//@import "../colors"; +//@import "../card/variables"; +// +//.spectate { +// width: 100vw; +// height: 100vh; +// +// min-width: 320px; +// min-height: 320px; +// +// padding: 0; +// margin: 0; +// +// display: flex; +// align-items: center; +// justify-content: center; +// +// font-size: 1.7vmin; +// +// overflow: hidden; +// +// .middle { +// flex-basis: max-content; +// position: relative; +// } +// +// .player { +// position: absolute; +// left: 0; +// top: 0; +// transition-property: transform, left, top; +// transition-duration: 0.5s; +// +// display: flex; +// align-items: center; +// justify-content: center; +// } +// +// .play { +// display: flex; +// +// margin-left: calc(-#{$card-overlap} * var(--cards-in-play) - 1); +// +// > *:not(:first-child) { +// margin-left: -($card-min-width - $card-overlap); +// } +// } +//} +// +//.join-info { +// position: fixed; +// right: 0; +// top: 0; +// margin: 1em; +// font-size: 2.5vmin; +// +// opacity: 0.9; +// font-weight: bold; +// +// p { +// margin: 0.2em; +// text-align: center; +// } +// +// .game-code { +// font-size: 1.5em; +// background-color: $primary-dark; +// color: $on-primary-dark; +// } +//} +// +//.qr-code { +// position: fixed; +// left: 0; +// bottom: 0; +// +// opacity: 0.7; +// margin: 1em; +//} diff --git a/client/src/scss/pages/start.scss b/client/src/scss/pages/start.scss new file mode 100644 index 0000000..7262b47 --- /dev/null +++ b/client/src/scss/pages/start.scss @@ -0,0 +1,143 @@ +@import "./start/lobby-browser"; +@import "../card/variables"; +@import "../colors"; + +.start { + @include fluid-type(20rem, 64rem, 0.8rem, 1rem); + --card-aspect-ratio: #{$card-ratio}; + padding: 0.5em 2em 2em 2em; + + display: flex; + flex-direction: column; + align-items: stretch; + + min-width: min-content; + width: 60%; + + @media (max-width: 1280px) { + width: auto; + } + + margin: 0 auto; + + nav { + display: flex; + } + + header { + display: flex; + flex-wrap: wrap; + justify-content: center; + overflow: hidden; + padding: 4em 0 0 2em; + + h1, + .subtitle { + flex-basis: $card-width; + font-size: 1em; + } + + .card-slicer { + height: 6em; + } + + .game-card { + margin: 0 0 0 auto; + transition: transform 1s; + transform: rotateZ(-4deg) translateY(-2em); + + &:hover { + transform: rotateZ(-2deg) translateY(-3em); + } + } + + .subtitle { + .game-card { + margin: 0 auto 0 0; + transform: rotateZ(4deg) translateY(0.5em) translateX(-2em); + + &:hover { + transform: rotateZ(5deg) translateY(-1em) translateX(-1.5em); + } + } + } + } + + .start-tab { + margin: 0.5em auto 0 auto; + width: 100%; + max-width: 20em; + display: flex; + flex-direction: column; + align-items: center; + } + + .rejoin { + align-self: stretch; + } + + .example-card { + float: right; + width: $card-width; + margin: 0.5em; + font-size: 0.7em; + color: #666677; + hyphens: none; + + figcaption { + display: block; + width: 100%; + padding: 0.5em; + } + } + + .rule-list { + margin: 0.5em; + + h3 { + margin-bottom: 0.3em; + } + + p { + margin-top: 0.3em; + margin-left: 0.5em; + } + } + + footer { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 1em; + + .logos { + display: flex; + } + + .logo { + display: block; + margin: 0.5em; + padding: 1em; + + img, + svg { + width: 3em; + height: 3em; + } + } + + .version { + opacity: 0.3; + } + } + + .rejoin ul { + list-style: none; + padding: 0; + + li { + display: inline-block; + margin-right: 0.5em; + } + } +} diff --git a/client/src/scss/pages/start/lobby-browser.scss b/client/src/scss/pages/start/lobby-browser.scss new file mode 100644 index 0000000..963a84a --- /dev/null +++ b/client/src/scss/pages/start/lobby-browser.scss @@ -0,0 +1,17 @@ +.lobby-browser { + .top { + display: flex; + justify-content: space-between; + align-items: center; + } + + a { + .svg-inline--fa { + margin-right: 0.3em; + } + } + + .subheader { + font-size: 0.8em; + } +} diff --git a/client/src/scss/pages/unknown-page.scss b/client/src/scss/pages/unknown-page.scss new file mode 100644 index 0000000..99c7dbe --- /dev/null +++ b/client/src/scss/pages/unknown-page.scss @@ -0,0 +1,3 @@ +.unknown-page { + width: 30em; +} diff --git a/client/src/scss/settings.scss b/client/src/scss/settings.scss new file mode 100644 index 0000000..c2e7f10 --- /dev/null +++ b/client/src/scss/settings.scss @@ -0,0 +1,50 @@ +@import "colors"; + +.start-settings { + position: fixed; + top: 0; + left: 0; + right: 0; + margin: 0.5em; + display: flex; + justify-content: flex-end; + z-index: 10; +} + +.settings-panel { + display: none; + position: absolute; + top: 4em; + right: 0; + margin: 0.5em; + + z-index: 10; + + flex-direction: column; + align-items: stretch; + padding: 0 1em 1em 1em; + border-radius: 0.5em; + + .title { + display: flex; + align-items: center; + justify-content: flex-end; + font-size: 1.6em; + + h3 { + margin: 0 0.25em 0 0; + } + } + + .body { + padding: 0 0.5em; + } + + &.open { + display: flex; + } + + .setting-help { + font-size: 0.8em; + } +} diff --git a/client/src/scss/variables.scss b/client/src/scss/variables.scss new file mode 100644 index 0000000..53bb7b4 --- /dev/null +++ b/client/src/scss/variables.scss @@ -0,0 +1 @@ +$min-page-width: 320px; diff --git a/client/src/scss/weightless.scss b/client/src/scss/weightless.scss new file mode 100644 index 0000000..fd2e826 --- /dev/null +++ b/client/src/scss/weightless.scss @@ -0,0 +1,80 @@ +@import "./colors"; +@import "./cards"; +@import "./fonts"; + +@function toHSL($color) { + @return #{hue($color) / 1deg}, #{saturation($color)}, #{lightness($color)}; +} + +body { + --primary-400: #{toHSL($primary-light)}; + --primary-500: #{toHSL($primary)}; + --primary-600: #{toHSL($primary-dark)}; + + --secondary-400: #{toHSL($secondary-light)}; + --secondary-500: #{toHSL($secondary)}; + --secondary-600: #{toHSL($secondary-dark)}; + + --font-family-serif: "Helvetica Neue", "Nimbus Sans L", sans-serif; + --font-family-sans: "Roboto", sans-serif; +} + +wl-card { + padding: 1em; +} + +wl-button { + transform-origin: center center; + margin: 0.5em; + + .svg-inline--fa { + margin-right: 0.4em; + } + + flex-shrink: 0; +} + +wl-select { + flex-shrink: 0; +} + +wl-tab { + --tab-padding: 1em; + + .svg-inline--fa { + margin-right: 0.25em; + } +} + +wl-button[fab] { + line-height: 1; + + &:focus, + &::-moz-focus-inner { + outline: none; + border: 0; + } + + .svg-inline--fa { + margin-right: 0; + } + + display: inline-flex; + justify-content: center; + + transition: opacity 15ms linear 30ms, + transform 0.27s cubic-bezier(0, 0, 0.2, 1) 0ms; + + &.exited { + transition: opacity 15ms linear 0.15s, + transform 0.18s cubic-bezier(0.4, 0, 1, 1) 0ms; + + transform: scale(0); + opacity: 0; + } + + &.large { + --button-fab-size: 3em; + font-size: 1.2em; + } +} diff --git a/client/src/ts/cast.ts b/client/src/ts/cast.ts new file mode 100644 index 0000000..cc80011 --- /dev/null +++ b/client/src/ts/cast.ts @@ -0,0 +1,10 @@ +import "../html/cast.html"; +import { Server as CastServer } from "./chromecast"; + +import(/* webpackChunkName: "massive-decks" */ "../elm/MassiveDecks").then( + ({ Elm: elm }) => { + new CastServer(castFlags => + elm.MassiveDecks.Cast.init({ flags: castFlags }) + ); + } +); diff --git a/client/src/ts/chromecast.ts b/client/src/ts/chromecast.ts new file mode 100644 index 0000000..bc18921 --- /dev/null +++ b/client/src/ts/chromecast.ts @@ -0,0 +1,150 @@ +import { + CastFlags, + CastStatus, + InboundPort, + OutboundPort +} from "../elm/MassiveDecks"; + +const channel = "urn:x-cast:com.rereadgames.massivedecks"; + +declare const cast: any; + +declare global { + interface Window { + __onGCastApiAvailable: any; + } +} + +let whenAvailable: (() => void)[] | null = []; + +window["__onGCastApiAvailable"] = function(isAvailable: Boolean) { + if (isAvailable) { + cast.framework.CastContext.getInstance().setOptions({ + receiverApplicationId: "A6799922", + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED + }); + if (whenAvailable != null) { + for (const callback of whenAvailable) { + callback(); + } + whenAvailable = null; + } else { + console.warn("Cast API initialized twice."); + } + } +}; + +abstract class WithCastApi { + protected constructor() { + if (whenAvailable == null) { + this.onceCastApiAvailable(); + } else { + whenAvailable.push(() => this.onceCastApiAvailable()); + } + } + + abstract onceCastApiAvailable(): void; +} + +/** + * The client that lives on the MassiveDecks client. + */ +export class Client extends WithCastApi { + readonly tryCast: InboundPort; + readonly status: OutboundPort; + flags: CastFlags | null; + + constructor( + tryCast: InboundPort, + status: OutboundPort + ) { + super(); + this.tryCast = tryCast; + this.status = status; + this.flags = null; + } + + onceCastApiAvailable(): void { + this.tryCast.subscribe((flags: CastFlags) => this.toggleCast(flags)); + const context = cast.framework.CastContext.getInstance(); + context.addEventListener( + cast.framework.CastContextEventType.CAST_STATE_CHANGED, + (event: cast.framework.CastStateEventData) => + this.onCastStateChanged(event) + ); + context.addEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + (event: cast.framework.SessionStateEventData) => + this.onSessionStateChanged(event) + ); + this.status.send(context.getCastState()); + } + + toggleCast(flags: CastFlags): void { + this.flags = flags; + const context = cast.framework.CastContext.getInstance(); + if (context.getCastState() === cast.framework.CastState.CONNECTED) { + context.endCurrentSession(true); + } else { + context.requestSession().then((e: chrome.cast.ErrorCode) => { + if (e) { + console.log(e); + } + }); + } + } + + onCastStateChanged(event: cast.framework.CastStateEventData): void { + let status: CastStatus; + if (event.castState === cast.framework.CastState.NO_DEVICES_AVAILABLE) { + status = { status: "NoDevicesAvailable" }; + } else if (event.castState === cast.framework.CastState.NOT_CONNECTED) { + status = { status: "NotConnected" }; + } else if (event.castState === cast.framework.CastState.CONNECTING) { + status = { status: "Connecting" }; + } else if (event.castState === cast.framework.CastState.CONNECTED) { + status = { status: "Connected" }; + const session = cast.framework.CastContext.getInstance().getCurrentSession(); + if (session) { + status.name = session.getCastDevice().friendlyName; + } + } else { + throw new Error("Unknown cast state."); + } + this.status.send(status); + } + + onSessionStateChanged(event: cast.framework.SessionStateEventData): void { + if ( + !event.errorCode && + this.flags != null && + event.sessionState === cast.framework.SessionState.SESSION_STARTED + ) { + event.session + .sendMessage(channel, JSON.stringify(this.flags)) + .catch((e: Error) => console.log(e)); + } + } +} + +/** + * The server that lives on the chromecast. + */ +export class Server extends WithCastApi { + readonly launch: (castFlags: CastFlags) => void; + + constructor(launch: (castFlags: CastFlags) => void) { + super(); + this.launch = launch; + } + + onceCastApiAvailable(): void { + const context = cast.framework.CastReceiverContext.getInstance(); + + context.addCustomMessageListener(channel, (customEvent: any) => + this.launch(customEvent.data) + ); + + context.start(); + } +} diff --git a/client/src/ts/index.ts b/client/src/ts/index.ts new file mode 100644 index 0000000..1fb0835 --- /dev/null +++ b/client/src/ts/index.ts @@ -0,0 +1,74 @@ +import "../html/index.html"; +import "./weightless"; +import { Client as CastClient } from "./chromecast"; +import { OutboundPort, Settings, Token } from "../elm/MassiveDecks"; +import { ServerConnection } from "./server-connection"; + +/** + * Settings storage/retrieval in local storage. + */ +class SettingsStorage { + static readonly storage = window.localStorage; + static readonly key: string = "settings"; + static readonly default: Settings = { + tokens: [], + lastUsedName: null, + recentDecks: [], + chosenLanguage: null + }; + + /** + * Load settings from local storage. + */ + static load() { + const rawSettings = SettingsStorage.storage.getItem(SettingsStorage.key); + return rawSettings ? JSON.parse(rawSettings) : SettingsStorage.default; + } + + /** + * Save the given settings to local storage. + * @param settings The settings. + */ + static save(settings: Settings) { + SettingsStorage.storage.setItem( + SettingsStorage.key, + JSON.stringify(settings) + ); + } +} + +// This lets us chunk off our elm code separately, which is important so we can +// use optimisations that will only be valid on pure code. +import(/* webpackChunkName: "massive-decks" */ "../elm/MassiveDecks").then( + ({ Elm: elm }) => { + const app = elm.MassiveDecks.init({ + flags: { + settings: SettingsStorage.load(), + browserLanguages: navigator.languages.concat() + } + }); + + // Settings + app.ports.storeSettings.subscribe((settings: Settings) => + SettingsStorage.save(settings) + ); + + new ServerConnection(app.ports.serverRecv, app.ports.serverSend); + + new CastClient(app.ports.tryCast, app.ports.castStatus); + + app.ports.copyText.subscribe(async (id: string) => { + const textField = document.getElementById(id); + if (textField !== null && textField instanceof HTMLInputElement) { + textField.select(); + const value = textField.value; + textField.setSelectionRange(0, value.length); + try { + await navigator.clipboard.writeText(value); + } catch (err) { + document.execCommand("copy"); + } + } + }); + } +); diff --git a/client/src/ts/server-connection.ts b/client/src/ts/server-connection.ts new file mode 100644 index 0000000..236db33 --- /dev/null +++ b/client/src/ts/server-connection.ts @@ -0,0 +1,151 @@ +import { + CloseCommand, + ConnectionCommand, + InboundPort, + MessageCommand, + OpenCommand, + OutboundPort +} from "../elm/MassiveDecks"; + +function isOpen(command: ConnectionCommand): command is OpenCommand { + return command.hasOwnProperty("gameCode"); +} + +function isMessage(command: ConnectionCommand): command is MessageCommand { + return command.hasOwnProperty("message"); +} + +function isClose(command: CloseCommand): command is CloseCommand { + return !isOpen(command) && !isMessage(command); +} + +interface BaseUrl { + protocol: string; + host: string; + path: string; +} + +export class ServerConnection { + static readonly apiUrl = "api/games/"; + readonly base: string; + readonly out: OutboundPort; + lobbyConnection: LobbyConnection | null; + + constructor( + out: OutboundPort, + inbound: InboundPort + ) { + const baseElement = document.querySelector("base"); + const baseUrl = + baseElement !== null + ? ServerConnection.baseFromHtml(baseElement) + : ServerConnection.baseFromLocation(); + const slash = baseUrl.path.endsWith("/") ? "" : "/"; + this.base = + (baseUrl.protocol === "http:" ? "ws:" : "wss:") + + ("//" + baseUrl.host + baseUrl.path + slash + ServerConnection.apiUrl); + this.out = out; + this.lobbyConnection = null; + inbound.subscribe(command => this.handleMessage(command)); + } + + static baseFromHtml(baseElement: HTMLBaseElement): BaseUrl { + const url = new URL(baseElement.href); + return { + protocol: url.protocol, + host: url.host, + path: url.pathname + }; + } + + // Fallback - we should always have a base element so this should never get + // hit. Note that we can't work out the base path from this. + static baseFromLocation(): BaseUrl { + const url = window.location; + return { + protocol: url.protocol, + host: url.host, + path: "" + }; + } + + handleMessage(command: ConnectionCommand) { + if (isOpen(command)) { + if (this.lobbyConnection !== null) { + this.lobbyConnection.close(); + } + this.lobbyConnection = new LobbyConnection( + this, + this.base + command.gameCode, + command.token + ); + } else if (isMessage(command)) { + if (this.lobbyConnection !== null) { + this.lobbyConnection.send(command.message); + } + } else if (isClose(command)) { + if (this.lobbyConnection !== null) { + this.lobbyConnection.close(); + } + } + } +} + +class LobbyConnection { + static readonly oneSecond = 1000; + static readonly oneMinute = LobbyConnection.oneSecond * 60; + + static readonly initialDelay = LobbyConnection.oneSecond / 2; + static readonly maxDelay = LobbyConnection.oneMinute * 10; + + socket: WebSocket; + delay: number = LobbyConnection.initialDelay; + closed: boolean = false; + readonly token: string; + + constructor(parent: ServerConnection, url: string, token: string) { + this.socket = this.open(parent, url); + this.token = token; + } + + open(parent: ServerConnection, url: string) { + this.closed = false; + const socket = new WebSocket(url); + socket.addEventListener("message", event => { + if (parent.lobbyConnection === this) { + parent.out.send(event.data); + } + }); + socket.addEventListener("open", event => { + this.delay = LobbyConnection.initialDelay; + this.send( + JSON.stringify({ + action: "Authenticate", + token: this.token + }) + ); + }); + socket.addEventListener("close", event => { + if (!this.closed) { + setTimeout(() => { + if (!this.closed) { + if (parent.lobbyConnection === this) { + this.delay = Math.min(LobbyConnection.maxDelay, this.delay * 2); + this.socket = this.open(parent, url); + } + } + }, this.delay); + } + }); + return socket; + } + + send(message: string) { + this.socket.send(message); + } + + close() { + this.closed = true; + this.socket.close(); + } +} diff --git a/client/src/ts/weightless.ts b/client/src/ts/weightless.ts new file mode 100644 index 0000000..9b7fe98 --- /dev/null +++ b/client/src/ts/weightless.ts @@ -0,0 +1,15 @@ +import "@webcomponents/webcomponentsjs/webcomponents-loader"; +import "weightless/button"; +import "weightless/card"; +import "weightless/expansion"; +import "weightless/list-item"; +import "weightless/nav"; +import "weightless/popover"; +import "weightless/popover-card"; +import "weightless/select"; +import "weightless/switch"; +import "weightless/tab"; +import "weightless/tab-group"; +import "weightless/textfield"; +import "weightless/textarea"; +import "weightless/tooltip"; diff --git a/client/src/webmanifest/manifest.webmanifest b/client/src/webmanifest/manifest.webmanifest new file mode 100644 index 0000000..6cecfc9 --- /dev/null +++ b/client/src/webmanifest/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Massive Decks", + "short_name": "Massive Decks", + "lang": "en", + "start_url": "/", + "display": "fullscreen", + "background_color": "#cccccc", + "orientation": "natural", + "description": "A comedy party game.", + "icons": [ + { + "src": "../../assets/images/icon.svg", + "type": "image/svg+xml", + "sizes": "48x48 72x72 96x96 128x128 256x256" + } + ] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..f4f8345 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "esNext", + "target": "es5", + "noImplicitAny": true, + "preserveConstEnums": true, + "outDir": "dist", + "strict": true, + "sourceMap": true, + "baseUrl": ".", + "moduleResolution": "node", + "lib": [ "es2015", "dom" ] + }, + "include": [ + "src/ts/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/client/webpack.config.js b/client/webpack.config.js new file mode 100644 index 0000000..f014f16 --- /dev/null +++ b/client/webpack.config.js @@ -0,0 +1,261 @@ +const webpack = require("webpack"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); +const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); +const path = require("path"); +const inliner = require("sass-inline-svg"); + +module.exports = (env, argv) => { + // WebStorm doesn't give any arguments, causing this to blow up without the check. + const mode = typeof argv === "undefined" ? "production" : argv.mode; + + const prod = mode === "production"; + const dev = mode === "development"; + + const dist = path.resolve(__dirname, "dist"); + const src = path.resolve(__dirname, "src"); + + const cssLoaders = [ + // Extract to separate file. + { + loader: "file-loader", + options: { + name: "[name].css", + outputPath: "assets/styles" + } + }, + { + loader: "extract-loader" + }, + // Load CSS to inline styles. + { + loader: "css-loader", + options: { sourceMap: dev, importLoaders: 2 } + }, + // Transform CSS for compatibility. + { + loader: "postcss-loader", + options: { + sourceMap: dev, + ident: "postcss", + plugins: loader => [ + require("postcss-import")({ root: loader.resourcePath }), + require("postcss-preset-env")(), + require("cssnano")() + ] + } + }, + // Load SASS to CSS. + { + loader: "sass-loader", + options: { + sourceMap: dev, + includePaths: ["node_modules"], + functions: { + "inline-svg": inliner("./", { encodingFormat: "uri" }) + } + } + } + ]; + + const elmLoaders = [ + // Load elm to JS. + { + loader: "elm-webpack-loader", + options: { + files: [ + path.resolve(src, "elm/MassiveDecks.elm"), + path.resolve(src, "elm/MassiveDecks/Cast.elm") + ], + optimize: prod, + debug: dev, + forceWatch: dev, + cwd: __dirname + } + } + ]; + + const plugins = []; + + if (dev) { + // Load CSS without refreshing in a dev env. + cssLoaders.unshift({ + loader: "css-hot-loader", + options: { + reloadAll: true + } + }); + // Load elm without refreshing in a dev env. + // Disable if working with chromecasts. + elmLoaders.unshift({ loader: "elm-hot-webpack-loader" }); + plugins.push(new webpack.HotModuleReplacementPlugin()); + } + + if (prod) { + // If we are in production, ensure we don't have old files lying around. + plugins.push(new CleanWebpackPlugin()); + } + + return { + context: path.resolve(__dirname), + entry: { + // Main entry point. + index: "./src/ts/index.ts", + // Chromecast entry point. + cast: "./src/ts/cast.ts" + }, + // Source maps only in development. + devtool: prod ? "none" : "inline-source-map", + output: { + path: dist, + publicPath: "/", + filename: "assets/scripts/[name].js" + }, + module: { + rules: [ + // HTML + { + test: /\.html$/, + exclude: [/elm-stuff/, /node_modules/], + use: [ + { + loader: "file-loader", + options: { + name: "[name].[ext]" + } + }, + { + loader: "extract-loader" + }, + { + loader: "html-loader", + options: { attrs: ["img:src", "link:href"] } + } + ] + }, + // Elm scripts. + { + test: /\.elm$/, + exclude: [/elm-stuff/, /node_modules/], + use: elmLoaders + }, + // Typescript scripts. + { + test: /\.ts$/, + exclude: [/elm-stuff/, /node_modules/], + use: "ts-loader", + include: src + }, + // Styles. + { + test: /\.s?css$/, + exclude: [/elm-stuff/, /node_modules/], + use: cssLoaders + }, + // Image assets. + { + test: /\.(jpg|png|svg)$/, + loader: "file-loader", + options: { + name: "assets/images/[name].[ext]" + } + }, + // Font assets. + { + test: /\.(woff2)$/, + loader: "file-loader", + options: { + name: "assets/fonts/[name].[ext]", + publicPath: "/" + } + }, + // App manifest. + { + test: /\.webmanifest$/, + use: [ + { + loader: "file-loader", + options: { + name: "[name].[ext]", + publicPath: "/" + } + }, + { + loader: "app-manifest-loader" + } + ] + } + ] + }, + resolve: { + extensions: [".js", ".ts", ".elm", ".scss"], + modules: ["node_modules"] + }, + plugins: plugins, + optimization: { + minimizer: [ + // Typescript + new TerserPlugin({ + test: /assets\/scripts\/(index|cast)\.js$/, + sourceMap: dev, + parallel: true, + terserOptions: { + output: { + comments: false + } + } + }), + // Elm - we can do otherwise dangerous optimisation thanks to the purity. + new UglifyJsPlugin({ + test: /assets\/scripts\/massive-decks\.js$/, + uglifyOptions: { + compress: { + pure_funcs: [ + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9" + ], + pure_getters: true, + keep_fargs: false, + unsafe_comps: true, + unsafe: true, + passes: 3 + }, + mangle: true + } + }) + ] + }, + devServer: { + hot: true, + proxy: { + // Forward to the server. + "/api/**": { + target: "http://localhost:8081", + ws: true + }, + // As we are an SPA, this lets us route all requests to the index. + "**": { + target: "http://localhost:8080", + pathRewrite: { + "cast.html": "cast.html", + ".*": "" + } + } + } + } + }; +}; diff --git a/conf/application.conf b/conf/application.conf deleted file mode 100644 index e7f8c16..0000000 --- a/conf/application.conf +++ /dev/null @@ -1,39 +0,0 @@ -// Play Framework Settings - -// In a production environment, set the APPLICATION_SECRET environment variable to a random secret, or -// modify the value here. -play.crypto.secret = "changeme" -play.crypto.secret = ${?APPLICATION_SECRET} - -play.i18n.langs = [ "en" ] - -play.cache.bindCaches = ["lobby", "cardcast"] - -play.modules.enabled += "massivedecks.CoreModule" -play.http.errorHandler = "massivedecks.controllers.ErrorHandler" - - -// Massive Decks Settings - -// Canonical URL settings. These should be set in a production environment to ensure a canonical URL for the -// application. If you have multiple domains pointing to the application, you should have them redirect, or -// this will result in cross-domain requests. -// You can set the environment variables listed, or change these settings to literal values in this file. - -// The protocol for the application (e.g: "https"). -// Defaults to the value of the X-Forwarded-Proto header in the request, or the protocol used for the request. -md_protocol = ${?MD_PROTOCOL} -// The host for the application, including a port if needed (e.g: "www.example.com"). -// Defaults to the host used in the request. -md_host = ${?MD_HOST} -// The path to the application, with a leading / (e.g: "/massivedecks"). -// Defaults to "/" -md_path = ${?MD_PATH} - - -// The version of Massive Decks (displayed in the footer on the main page, and given in bug reports). -// md_release_version="0.1.0-beta" - -// If md_release_version is unset, the git sha1 hash or short hash will be taken here, cut to 7 characters, and be -// suffixed with -dev to show a development build. If this is unset too, no version will be used. -md_git_version = ${?SOURCE_VERSION} diff --git a/conf/lobbies.routes b/conf/lobbies.routes deleted file mode 100644 index cc5a99a..0000000 --- a/conf/lobbies.routes +++ /dev/null @@ -1,11 +0,0 @@ -# Lobby APIs - -POST / massivedecks.controllers.API.createLobby() -GET /:gameCode massivedecks.controllers.API.getLobby(gameCode: String) -GET /:gameCode/notifications massivedecks.controllers.API.notifications(gameCode: String) -GET /:gameCode/history massivedecks.controllers.API.getHistory(gameCode: String) -POST /:gameCode massivedecks.controllers.API.command(gameCode: String) -POST /:gameCode/players massivedecks.controllers.API.newPlayer(gameCode: String) -POST /:gameCode/players/newAi massivedecks.controllers.API.newAi(gameCode: String) -POST /:gameCode/players/:playerId massivedecks.controllers.API.getPlayer(gameCode: String, playerId: Int) -POST /:gameCode/players/:playerId/leave massivedecks.controllers.API.leave(gameCode: String, playerId: Int) diff --git a/conf/logback.xml b/conf/logback.xml deleted file mode 100644 index 6045363..0000000 --- a/conf/logback.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - %coloredLevel - %logger - %message%n%xException - - - - - - - - - - - - diff --git a/conf/routes b/conf/routes deleted file mode 100644 index 09d3a46..0000000 --- a/conf/routes +++ /dev/null @@ -1,14 +0,0 @@ -#Pages -### NoDocs ### -GET / massivedecks.controllers.Pages.index() -### NoDocs ### -GET /manifest.json massivedecks.controllers.Pages.manifest() - - -# API --> /api/lobbies lobbies.Routes - - -# Static -### NoDocs ### -GET /*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..387b8ef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3" + +networks: + internal: + +services: + server: + image: "massivedecks/server:latest" + networks: + - internal + restart: unless-stopped + + client: + image: "massivedecks/client:latest" + ports: + - "8080:8080" + networks: + - internal + restart: unless-stopped diff --git a/docker_push b/docker_push new file mode 100644 index 0000000..6f4b48e --- /dev/null +++ b/docker_push @@ -0,0 +1,3 @@ +#!/bin/bash +echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin +docker push "massivedecks/${1}:${2}" diff --git a/elm-package.json b/elm-package.json deleted file mode 100644 index 09dd9e7..0000000 --- a/elm-package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "1.0.0", - "summary": "A web game based on Cards against Humanity.", - "repository": "https://github.com/Lattyware/massivedecks.git", - "license": "AGPLv3", - "source-directories": [ - "client/src" - ], - "exposed-modules": [], - "dependencies": { - "elm-lang/animation-frame": "1.0.1 <= v < 2.0.0", - "elm-lang/core": "5.1.1 <= v < 6.0.0", - "elm-lang/html": "2.0.0 <= v < 3.0.0", - "elm-lang/http": "1.0.0 <= v < 2.0.0", - "elm-lang/navigation": "2.1.0 <= v < 3.0.0", - "elm-lang/websocket": "1.0.2 <= v < 2.0.0", - "elm-lang/window": "1.0.1 <= v < 2.0.0" - }, - "elm-version": "0.18.0 <= v < 0.19.0" -} diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 8059ba4..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,62 +0,0 @@ -var gulp = require('gulp-help')(require('gulp')); -var elm = require('gulp-elm'); -var gutil = require('gulp-util'); -var del = require('del'); - -var spawn = require('child_process').spawn; - -gulp.task('default', 'Watches for changes while serving to a local server.', ['watch', 'serve'], function() { -}); - -gulp.task('make', 'Compiles all code.', ['make:elm', 'make:scala'], function() { -}); - -gulp.task('make:elm', 'Compiles client source code.', function() { - return gulp - .src('client/src/MassiveDecks.elm') - .pipe( - elm({filetype: 'js'}) - ).on('error', gutil.log) - .pipe( - gulp.dest('public/javascripts') - ) -}); - -gulp.task('make:scala', 'Compiles server source code.', function() { - spawn('activator', ['compile'], {stdio: 'inherit'}); -}); - -gulp.task('test', 'Run all tests.', ['test:scala'], function() { - spawn('activator', ['test'], {stdio: 'inherit'}); -}); - -gulp.task('test:scala', 'Run server tests.', function() { - spawn('activator', ['test'], {stdio: 'inherit'}); -}); - -gulp.task('watch', 'Compiles, then watches client code, compiling on changes.', ['make:elm'], function() { - gulp.watch('client/src/**/*.elm', ['make:elm']); -}); - -gulp.task('serve', 'Run a local server, compiling server code on changes.', function(cb) { - spawn('activator', ['~run'], {stdio: 'inherit'}); -}); - -gulp.task('clean', 'Delete compiled, generated & downloaded files.', ['clean:elm', 'clean:scala'], - function(cb) { -}); - -gulp.task('clean:scala', 'Delete compiled, generated & downloaded scala files.', function(cb) { - return del([ - 'target/**', - 'project/target/**', - 'project/project/target/**' - ]); -}); - -gulp.task('clean:elm', 'Delete compiled, generated & downloaded elm files.', function(cb) { - return del([ - 'elm-stuff/**', - 'public/javascripts/MassiveDecks.js' - ]); -}); diff --git a/package.json b/package.json deleted file mode 100644 index a0e824d..0000000 --- a/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "massivedecks", - "version": "1.0.0", - "devDependencies": { - "del": "^2.2.1", - "gulp": "^3.9.1", - "gulp-elm": "^0.4.4", - "gulp-help": "^1.6.1", - "gulp-util": "^3.0.7" - } -} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 35c88ba..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=0.13.12 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index b151e32..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1,10 +0,0 @@ -// The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.4") - -// web plugins - -addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.1") -addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7") -addSbtPlugin("com.typesafe.sbt" % "sbt-uglify" % "1.0.3") -addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.0") diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png deleted file mode 100644 index 827f4d2b63a0045dffb3bcefee8f78da4c8a3156..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9484 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 diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index c60ba7bbb6871676f83b6d51a66e02c40f31cfff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 diff --git a/public/images/bgnoise_lg.png b/public/images/bgnoise_lg.png deleted file mode 100644 index caacc6feadce5ffcd6b75ff95335effc1412943c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5425 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 - - - - - - - - - image/svg+xml - - - - - - - diff --git a/public/images/icons/fast-forward.png b/public/images/icons/fast-forward.png deleted file mode 100644 index ef972b6a0871a799796d1171c431f3cfb171311a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI zppNSx%;=;sy8@(JDg6P%Gl?&n0bHkAwK6q4ig;DDx?H>j&k4GE$S^KlaMNU)i_W!slAD($s)&E9 z`c}J1K<@C;2fRB1zujT(Z(C!Xz*)oa{s40WyWP|do%gdf7&q@(Z}ClLP7ULBuLkyc zq79Xt51uA$QC2;UlTXOX(yN72|8P_yUT$s8I2`>>*kkq>}YC4S)g3;OXk;vd$@? F2>{yrq*ed` diff --git a/public/images/icons/gavel.png b/public/images/icons/gavel.png deleted file mode 100644 index e60c3182cd0d6125eddbf4a3566ca66bfa852e21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 485 zcmV-KNGY#taFHkg+73AorguVvC&shPG3ZP0~?m$ASvMcfUxhpPsBz| zgnyO384Ti0Q~Vp)0uGh_$$(?qmB^*r)o&iIsKmO!%!YiPKr7~ZJg2;f~n)Mr%lMeuSXiq+X=!KvkF?)%`_*d6NGNuxrXqh>vbVK zTM!SL5EnMePynyM4zS;{_Rr>TyaznCRN&P1MaBc3YQ(R{y3YB)b4C1S>>KqOa1Pu8 bXX=B0l^9fxj-bP?00000NkvXXu0mjf>EYIg diff --git a/public/images/icons/hourglass.png b/public/images/icons/hourglass.png deleted file mode 100644 index 2ae4dd09e7b0a78a2a553cd5e807c70022e24e29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI zppNSx%;=;sy8d0` zix``jp!EI2w>-|+b;`~^l(S^<+Rmv5#SJD*dwStSr`-2$I8C`hcfuZ~JE81sPRFzpzGq%=*zn_5sz$bh zUc$W9H*xbe?cGvWpR6xxf8RVN#&B%^v%F7F zSDm;Tqbd&&(m=l+(n+1)*s-(-1*i+o+r),h=t*s+2*l-p,v=Math.min(m,h)),{height:p+"px",top:i+"px",scrollTop:v}}var o=15,r=32,s=42,l=8;t.exports={getMenuPositionalCSS:i}},{}],4:[function(e,t,n){"use strict";function i(e,t){if(t&&e.setAttribute){for(var n,i=h(e),o=t.split(" "),r=0;r-1:!1}function s(e){if(void 0===e)return"undefined";var t=Object.prototype.toString.call(e);if(0===t.indexOf("[object "))return t.slice(8,-1).toLowerCase();throw new Error("MUI: Could not understand type: "+t)}function l(e,t,n,i){i=void 0===i?!1:i;var o=e._muiEventCache=e._muiEventCache||{};t.split(" ").map(function(t){e.addEventListener(t,n,i),o[t]=o[t]||[],o[t].push([n,i])})}function a(e,t,n,i){i=void 0===i?!1:i;var o,r,s,l=e._muiEventCache=e._muiEventCache||{};t.split(" ").map(function(t){for(o=l[t]||[],s=o.length;s--;)r=o[s],(void 0===n||r[0]===n&&r[1]===i)&&(o.splice(s,1),e.removeEventListener(t,r[0],r[1]))})}function c(e,t,n,i){t.split(" ").map(function(t){l(e,t,function o(i){n&&n.apply(this,arguments),a(e,t,o)},i)})}function u(e,t){var n=window;if(void 0===t){if(e===n){var i=document.documentElement;return(n.pageXOffset||i.scrollLeft)-(i.clientLeft||0)}return e.scrollLeft}e===n?n.scrollTo(t,d(n)):e.scrollLeft=t}function d(e,t){var n=window;if(void 0===t){if(e===n){var i=document.documentElement;return(n.pageYOffset||i.scrollTop)-(i.clientTop||0)}return e.scrollTop}e===n?n.scrollTo(u(n),t):e.scrollTop=t}function f(e){var t=window,n=e.getBoundingClientRect(),i=d(t),o=u(t);return{top:n.top+i,left:n.left+o,height:n.height,width:n.width}}function p(e){var t=!1,n=!0,i=document,o=i.defaultView,r=i.documentElement,s=i.addEventListener?"addEventListener":"attachEvent",l=i.addEventListener?"removeEventListener":"detachEvent",a=i.addEventListener?"":"on",c=function(n){"readystatechange"==n.type&&"complete"!=i.readyState||(("load"==n.type?o:i)[l](a+n.type,c,!1),!t&&(t=!0)&&e.call(o,n.type||n))},u=function(){try{r.doScroll("left")}catch(e){return void setTimeout(u,50)}c("poll")};if("complete"==i.readyState)e.call(o,"lazy");else{if(i.createEventObject&&r.doScroll){try{n=!o.frameElement}catch(d){}n&&u()}i[s](a+"DOMContentLoaded",c,!1),i[s](a+"readystatechange",c,!1),o[s](a+"load",c,!1)}}function m(e,t){if(t&&e.setAttribute){for(var n,i=h(e),o=t.split(" "),r=0;r=0;)i=i.replace(" "+n+" "," ");e.setAttribute("class",i.trim())}}function h(e){var t=(e.getAttribute("class")||"").replace(/[\n\t]/g,"");return" "+t+" "}function v(e){return e.replace(g,function(e,t,n,i){return i?n.toUpperCase():n}).replace(E,"Moz$1")}function b(e,t,n){var i;return i=n.getPropertyValue(t),""!==i||e.ownerDocument||(i=e.style[v(t)]),i}var y,g=/([\:\-\_]+(.))/g,E=/^moz([A-Z])/;y={multiple:!0,selected:!0,checked:!0,disabled:!0,readonly:!0,required:!0,open:!0},t.exports={addClass:i,css:o,hasClass:r,off:a,offset:f,on:l,one:c,ready:p,removeClass:m,type:s,scrollLeft:u,scrollTop:d}},{}],5:[function(e,t,n){"use strict";function i(){var e=window;if(b.debug&&"undefined"!=typeof e.console)try{e.console.log.apply(e.console,arguments)}catch(t){var n=Array.prototype.slice.call(arguments);e.console.log(n.join("\n"))}}function o(e){var t,n=document;t=n.head||n.getElementsByTagName("head")[0]||n.documentElement;var i=n.createElement("style");return i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(n.createTextNode(e)),t.insertBefore(i,t.firstChild),i}function r(e,t){if(!t)throw new Error("MUI: "+e);"undefined"!=typeof console&&console.error("MUI Warning: "+e)}function s(e){if(g.push(e),void 0===g._initialized){var t=document,n="animationstart mozAnimationStart webkitAnimationStart";y.on(t,n,l),g._initialized=!0}}function l(e){if("mui-node-inserted"===e.animationName)for(var t=e.target,n=g.length-1;n>=0;n--)g[n](t)}function a(e){var t="";for(var n in e)t+=e[n]?n+" ":"";return t.trim()}function c(){if(void 0!==v)return v;var e=document.createElement("x");return e.style.cssText="pointer-events:auto",v="auto"===e.style.pointerEvents}function u(e,t){return function(){e[t].apply(e,arguments)}}function d(e,t,n,i,o){var r,s=document.createEvent("HTMLEvents"),n=void 0!==n?n:!0,i=void 0!==i?i:!0;if(s.initEvent(t,n,i),o)for(r in o)s[r]=o[r];return e&&e.dispatchEvent(s),s}function f(){if(E+=1,1===E){var e=window,t=document;h={left:y.scrollLeft(e),top:y.scrollTop(e)},y.addClass(t.body,C),e.scrollTo(h.left,h.top)}}function p(){if(0!==E&&(E-=1,0===E)){var e=window,t=document;y.removeClass(t.body,C),e.scrollTo(h.left,h.top)}}function m(e){var t=window.requestAnimationFrame;t?t(e):setTimeout(e,0)}var h,v,b=e("../config"),y=e("./jqLite"),g=[],E=0,C="mui-body--scroll-lock";t.exports={callback:u,classNames:a,disableScrollLock:p,dispatchEvent:d,enableScrollLock:f,log:i,loadStyle:o,onNodeInserted:s,raiseError:r,requestAnimationFrame:m,supportsPointerEvents:c}},{"../config":2,"./jqLite":4}],6:[function(e,t,n){"use strict";function i(e){e._muiDropdown!==!0&&(e._muiDropdown=!0,e.hasAttribute("type")||(e.type="button"),s.on(e,"click",o))}function o(e){if(0===e.button){var t=this;null===t.getAttribute("disabled")&&r(t)}}function r(e){function t(){s.removeClass(o,u),s.off(r,"click",t)}function n(){var n=i.getBoundingClientRect(),l=e.getBoundingClientRect(),a=l.top-n.top+l.height;s.css(o,"top",a+"px"),s.addClass(o,u),setTimeout(function(){s.on(r,"click",t)},0)}var i=e.parentNode,o=e.nextElementSibling,r=i.ownerDocument;return o&&s.hasClass(o,d)?void(s.hasClass(o,u)?t():n()):l.raiseError("Dropdown menu element not found")}var s=e("./lib/jqLite"),l=e("./lib/util"),a="data-mui-toggle",c='[data-mui-toggle="dropdown"]',u="mui--is-open",d="mui-dropdown__menu";t.exports={initListeners:function(){for(var e=document,t=e.querySelectorAll(c),n=t.length-1;n>=0;n--)i(t[n]);l.onNodeInserted(function(e){"dropdown"===e.getAttribute(a)&&i(e)})}}},{"./lib/jqLite":4,"./lib/util":5}],7:[function(e,t,n){t.exports=e(4)},{}],8:[function(e,t,n){"use strict";function i(e){var t;if("on"===e){for(var n,i,s,l=arguments.length-1;l>0;l--)n=arguments[l],"object"===p.type(n)&&(i=n),n instanceof Element&&1===n.nodeType&&(s=n);i=i||{},void 0===i.keyboard&&(i.keyboard=!0),void 0===i["static"]&&(i["static"]=!1),t=o(i,s)}else"off"===e?t=r():f.raiseError("Expecting 'on' or 'off'");return t}function o(e,t){var n=document.body,i=document.getElementById(m);if(f.enableScrollLock(),i){for(;i.firstChild;)i.removeChild(i.firstChild);t&&i.appendChild(t)}else i=document.createElement("div"),i.setAttribute("id",m),t&&i.appendChild(t),n.appendChild(i);return h.test(navigator.userAgent)&&p.css(i,"cursor","pointer"),e.keyboard?s():l(),e["static"]?u(i):c(i),i.muiOptions=e,i}function r(){var e,t=document.getElementById(m);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);t.parentNode.removeChild(t),e=t.muiOptions.onclose,u(t)}return f.disableScrollLock(),l(),e&&e(),t}function s(){p.on(document,"keyup",a)}function l(){p.off(document,"keyup",a)}function a(e){27===e.keyCode&&r()}function c(e){p.on(e,"click",d)}function u(e){p.off(e,"click",d)}function d(e){e.target.id===m&&r()}var f=e("./lib/util"),p=e("./lib/jqLite"),m="mui-overlay",h=/(iPad|iPhone|iPod)/g;t.exports=i},{"./lib/jqLite":4,"./lib/util":5}],9:[function(e,t,n){"use strict";function i(e){e._muiRipple!==!0&&(e._muiRipple=!0,"INPUT"!==e.tagName&&l.on(e,f,o))}function o(e){if("mousedown"!==e.type||0===e.button){var t=this;if(t.disabled!==!0){t.muiMouseUp||(l.on(t,p,r),t.muiMouseUp=!0);var n=s(e,t);t.appendChild(n),a.requestAnimationFrame(function(){l.addClass(n,"mui--animate-in mui--active")})}}}function r(e){for(var t,n=this.children,i=n.length,o=[];i--;)t=n[i],l.hasClass(t,u)&&(l.addClass(t,"mui--animate-out"),o.push(t));o.length&&setTimeout(function(){for(var e,t,n=o.length;n--;)e=o[n],t=e.parentNode,t&&t.removeChild(e)},m)}function s(e,t){var n,i,o,r=l.offset(t),s="touchstart"===e.type?e.touches[0]:e,a=s.pageX-r.left,c=s.pageY-r.top;return n=2*Math.sqrt(r.width*r.width+r.height*r.height),o=document.createElement("div"),o.className=u,i=n/2,l.css(o,{height:n+"px",width:n+"px",top:c-i+"px",left:a-i+"px"}),o}var l=e("./lib/jqLite"),a=e("./lib/util"),c="mui-btn",u="mui-ripple-effect",d="ontouchstart"in document.documentElement,f=d?"touchstart":"mousedown",p=d?"touchend":"mouseup mouseleave",m=600;t.exports={initListeners:function(){for(var e=document,t=e.getElementsByClassName(c),n=t.length-1;n>=0;n--)i(t[n]);a.onNodeInserted(function(e){l.hasClass(e,c)&&i(e)})}}},{"./lib/jqLite":4,"./lib/util":5}],10:[function(e,t,n){"use strict";function i(e){e._muiSelect!==!0&&(e._muiSelect=!0,"ontouchstart"in p.documentElement||new o(e))}function o(e){this.selectEl=e,this.wrapperEl=e.parentNode,this.useDefault=!1,s.on(e,"mousedown",l.callback(this,"mousedownHandler")),s.on(e,"focus",l.callback(this,"focusHandler")),s.on(e,"click",l.callback(this,"clickHandler")),this.wrapperEl.tabIndex=-1;var t=l.callback(this,"wrapperFocusHandler");s.on(this.wrapperEl,"focus",t)}function r(e,t){l.enableScrollLock(),this.origIndex=null,this.currentIndex=null,this.selectEl=t,this.menuEl=this._createMenuEl(e,t),this.clickCallbackFn=l.callback(this,"clickHandler"),this.keydownCallbackFn=l.callback(this,"keydownHandler"),this.destroyCallbackFn=l.callback(this,"destroy"),e.appendChild(this.menuEl),s.scrollTop(this.menuEl,this.menuEl._muiScrollTop),setTimeout(function(){"body"!==p.activeElement.nodeName.toLowerCase()&&p.activeElement.blur()},0),s.on(this.menuEl,"click",this.clickCallbackFn),s.on(p,"keydown",this.keydownCallbackFn),s.on(m,"resize",this.destroyCallbackFn);var n=this.destroyCallbackFn;setTimeout(function(){s.on(p,"click",n)},0)}var s=e("./lib/jqLite"),l=e("./lib/util"),a=e("./lib/forms"),c="mui-select",u=".mui-select > select",d="mui-select__menu",f="mui--is-selected",p=document,m=window;o.prototype.mousedownHandler=function(e){0===e.button&&this.useDefault!==!0&&e.preventDefault()},o.prototype.focusHandler=function(e){if(this.useDefault!==!0){var t=this.selectEl,n=this.wrapperEl,i=t.tabIndex,o=l.callback(this,"keydownHandler");s.on(p,"keydown",o),t.tabIndex=-1,s.one(n,"blur",function(){t.tabIndex=i,s.off(p,"keydown",o)}),n.focus()}},o.prototype.keydownHandler=function(e){var t=e.keyCode;32!==t&&38!==t&&40!==t||(e.preventDefault(),this.selectEl.disabled!==!0&&this.renderMenu())},o.prototype.wrapperFocusHandler=function(){return this.selectEl.disabled?this.wrapperEl.blur():void 0},o.prototype.clickHandler=function(e){0===e.button&&this.renderMenu()},o.prototype.renderMenu=function(){return this.useDefault===!0?this.useDefault=!1:void new r(this.wrapperEl,this.selectEl)},r.prototype._createMenuEl=function(e,t){var n,i,o,r=p.createElement("div"),l=t.children,c=l.length,u=0;for(r.className=d,o=0;c>o;o++)n=l[o],i=p.createElement("div"),i.textContent=n.textContent,i._muiPos=o,n.selected&&(i.setAttribute("class",f),u=o),r.appendChild(i);this.origIndex=u,this.currentIndex=u;var m=a.getMenuPositionalCSS(e,c,u);return s.css(r,m),r._muiScrollTop=m.scrollTop,r},r.prototype.keydownHandler=function(e){var t=e.keyCode;return 9===t?this.destroy():(27!==t&&40!==t&&38!==t&&13!==t||e.preventDefault(),void(27===t?this.destroy():40===t?this.increment():38===t?this.decrement():13===t&&(this.selectCurrent(),this.destroy())))},r.prototype.clickHandler=function(e){e.stopPropagation();var t=e.target._muiPos;void 0!==t&&(this.currentIndex=t,this.selectCurrent(),this.destroy())},r.prototype.increment=function(){if(this.currentIndex!==this.menuEl.children.length-1){var e=this.menuEl.children;s.removeClass(e[this.currentIndex],f),this.currentIndex+=1,s.addClass(e[this.currentIndex],f)}},r.prototype.decrement=function(){if(0!==this.currentIndex){var e=this.menuEl.children;s.removeClass(e[this.currentIndex],f),this.currentIndex-=1,s.addClass(e[this.currentIndex],f)}},r.prototype.selectCurrent=function(){if(this.currentIndex!==this.origIndex){var e=this.selectEl.children;e[this.origIndex].selected=!1,e[this.currentIndex].selected=!0,l.dispatchEvent(this.selectEl,"change")}},r.prototype.destroy=function(){var e=this.menuEl.parentNode;e&&e.removeChild(this.menuEl),this.selectEl.focus(),l.disableScrollLock(),s.off(this.menuEl,"click",this.clickCallbackFn),s.off(p,"keydown",this.keydownCallbackFn),s.off(p,"click",this.destroyCallbackFn),s.off(m,"resize",this.destroyCallbackFn)},t.exports={initListeners:function(){for(var e=p.querySelectorAll(u),t=e.length-1;t>=0;t--)i(e[t]);l.onNodeInserted(function(e){"SELECT"===e.tagName&&s.hasClass(e.parentNode,c)&&i(e)})}}},{"./lib/forms":3,"./lib/jqLite":4,"./lib/util":5}],11:[function(e,t,n){"use strict";function i(e){e._muiTabs!==!0&&(e._muiTabs=!0,l.on(e,"click",o))}function o(e){if(0===e.button){var t=this;null===t.getAttribute("disabled")&&r(t)}}function r(e){var t,n,i,o,r,c,u,b,y,g=e.parentNode,E=e.getAttribute(d),C=document.getElementById(E);l.hasClass(g,f)||(C||a.raiseError('Tab pane "'+E+'" not found'),n=s(C),i=n.id,y="["+d+'="'+i+'"]',o=document.querySelectorAll(y)[0],t=o.parentNode,r={paneId:E,relatedPaneId:i},c={paneId:i,relatedPaneId:E},u=a.dispatchEvent(o,h,!0,!0,c),b=a.dispatchEvent(e,p,!0,!0,r),setTimeout(function(){u.defaultPrevented||b.defaultPrevented||(t&&l.removeClass(t,f),n&&l.removeClass(n,f),l.addClass(g,f),l.addClass(C,f),a.dispatchEvent(o,v,!0,!1,c),a.dispatchEvent(e,m,!0,!1,r))},0))}function s(e){for(var t,n=e.parentNode.children,i=n.length,o=null;i--&&!o;)t=n[i],t!==e&&l.hasClass(t,f)&&(o=t);return o}var l=e("./lib/jqLite"),a=e("./lib/util"),c="data-mui-toggle",u="["+c+'="tab"]',d="data-mui-controls",f="mui--is-active",p="mui.tabs.showstart",m="mui.tabs.showend",h="mui.tabs.hidestart",v="mui.tabs.hideend";t.exports={initListeners:function(){for(var e=document.querySelectorAll(u),t=e.length-1;t>=0;t--)i(e[t]);a.onNodeInserted(function(e){"tab"===e.getAttribute(c)&&i(e)})},api:{activate:function(e){var t="["+d+"="+e+"]",n=document.querySelectorAll(t);n.length||a.raiseError('Tab control for pane "'+e+'" not found'),r(n[0])}}}},{"./lib/jqLite":4,"./lib/util":5}],12:[function(e,t,n){"use strict";function i(e){e._muiTextfield!==!0&&(e._muiTextfield=!0,e.value.length?r.addClass(e,c):r.addClass(e,a),r.on(e,"input change",o),r.on(e,"focus",function(){r.addClass(this,u)}))}function o(){var e=this;e.value.length?(r.removeClass(e,a),r.addClass(e,c)):(r.removeClass(e,c),r.addClass(e,a)),r.addClass(e,u)}var r=e("./lib/jqLite"),s=e("./lib/util"),l=".mui-textfield > input, .mui-textfield > textarea",a="mui--is-empty",c="mui--is-not-empty",u="mui--is-dirty",d="mui-textfield--float-label";t.exports={initialize:i,initListeners:function(){for(var e=document,t=e.querySelectorAll(l),n=t.length-1;n>=0;n--)i(t[n]);s.onNodeInserted(function(e){"INPUT"!==e.tagName&&"TEXTAREA"!==e.tagName||i(e)}),setTimeout(function(){var e=".mui-textfield.mui-textfield--float-label > label {"+["-webkit-transition","-moz-transition","-o-transition","transition",""].join(":all .15s ease-out;")+"}";s.loadStyle(e)},150),s.supportsPointerEvents()===!1&&r.on(document,"click",function(e){var t=e.target;if("LABEL"===t.tagName&&r.hasClass(t.parentNode,d)){var n=t.previousElementSibling;n&&n.focus()}})}}},{"./lib/jqLite":4,"./lib/util":5}]},{},[1]); \ No newline at end of file diff --git a/public/javascripts/qrcode.min.js b/public/javascripts/qrcode.min.js deleted file mode 100644 index d5f3ca8..0000000 --- a/public/javascripts/qrcode.min.js +++ /dev/null @@ -1 +0,0 @@ -var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); diff --git a/public/javascripts/util.js b/public/javascripts/util.js deleted file mode 100644 index a4353f6..0000000 --- a/public/javascripts/util.js +++ /dev/null @@ -1,64 +0,0 @@ -function toggleWarningDrawer() { - document.getElementById('warning-drawer').classList.toggle('shut'); -} - -function getExistingGames() { - var existingGames = localStorage.getItem("existing-games"); - try { - existingGames = JSON.parse(existingGames); - } catch (error) { - existingGames = null; - } - if (existingGames == null) { - existingGames = []; - } - return existingGames; -} - -function start(url, version) { - var initialState = { - version: version, - url: url, - existingGames : getExistingGames(), - seed: new Date().getTime().toString(), // Doesn't like int flags, change when that changes. - browserNotificationsSupported: ("Notification" in window) - }; - - var game = Elm.MassiveDecks.fullscreen(initialState); - - game.ports.title.subscribe(function (title) { - document.title = title; - }); - - game.ports.store.subscribe(function (existingGames) { - localStorage.setItem("existing-games", JSON.stringify(existingGames)); - }); - - if ('speechSynthesis' in window) { - game.ports.say.subscribe(function (text) { - window.speechSynthesis.cancel(); - var utterance = new SpeechSynthesisUtterance(text); - // We should do something better here - other languages are likely. - utterance.lang = "en-GB"; - window.speechSynthesis.speak(utterance); - }); - } - - game.ports.qr.subscribe(function (idAndValue) { - new QRCode(document.getElementById(idAndValue.id), { - text: idAndValue.value, - width: 200, - height: 200 - }); - }); - - game.ports.requestPermission.subscribe(function (nothing) { - Notification.requestPermission(function (permission) { - game.ports.permissions.send(permission); - }); - }); - - game.ports.notifications.subscribe(function (notification) { - new Notification(notification.title, {icon: notification.icon}); - }); -} diff --git a/public/mstile.png b/public/mstile.png deleted file mode 100644 index cdbe60946aba023173e426a5f35d113738a6b9eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7994 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 diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg deleted file mode 100644 index 4e4af83..0000000 --- a/public/safari-pinned-tab.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - diff --git a/public/stylesheets/mui.min.css b/public/stylesheets/mui.min.css deleted file mode 100644 index 416de6e..0000000 --- a/public/stylesheets/mui.min.css +++ /dev/null @@ -1 +0,0 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{box-sizing:border-box}:after,:before{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Arial,Verdana,Tahoma;font-size:14px;font-weight:400;line-height:1.429;color:rgba(0,0,0,.87);background-color:#FFF}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#2196F3;text-decoration:none}a:focus,a:hover{color:#1976D2;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}p{margin:0 0 10px}ol,ul{margin-top:0;margin-bottom:10px}figure{margin:0}img{vertical-align:middle}hr{margin-top:20px;margin-bottom:20px;border:0;height:1px;background-color:rgba(0,0,0,.12)}legend{display:block;width:100%;padding:0;margin-bottom:10px;font-size:21px;color:rgba(0,0,0,.87);line-height:inherit;border:0}input[type=search]{box-sizing:border-box;-webkit-appearance:none}input[type=checkbox]:focus,input[type=radio]:focus,input[type=file]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}strong{font-weight:700}abbr[title]{cursor:help;border-bottom:1px dotted #2196F3}h1,h2,h3{margin-top:20px;margin-bottom:10px}h4,h5,h6{margin-top:10px;margin-bottom:10px}.mui--appbar-height{height:56px}.mui--appbar-min-height,.mui-appbar{min-height:56px}.mui--appbar-line-height{line-height:56px}.mui--appbar-top{top:56px}@media (orientation:landscape) and (max-height:480px){.mui--appbar-height{height:48px}.mui--appbar-min-height,.mui-appbar{min-height:48px}.mui--appbar-line-height{line-height:48px}.mui--appbar-top{top:48px}}@media (min-width:480px){.mui--appbar-height{height:64px}.mui--appbar-min-height,.mui-appbar{min-height:64px}.mui--appbar-line-height{line-height:64px}.mui--appbar-top{top:64px}}.mui-appbar{background-color:#2196F3;color:#FFF}.mui-btn{animation-duration:.1ms;animation-name:mui-node-inserted;font-weight:500;font-size:14px;line-height:18px;text-transform:uppercase;color:rgba(0,0,0,.87);background-color:#FFF;transition:all .2s ease-in-out;display:inline-block;height:36px;padding:0 26px;margin-top:6px;margin-bottom:6px;border:none;border-radius:2px;cursor:pointer;-ms-touch-action:manipulation;touch-action:manipulation;background-image:none;text-align:center;line-height:36px;vertical-align:middle;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:14px;letter-spacing:.03em;position:relative;overflow:hidden}.mui-btn:active,.mui-btn:focus,.mui-btn:hover{color:rgba(0,0,0,.87);background-color:#fff}.mui-btn[disabled]:active,.mui-btn[disabled]:focus,.mui-btn[disabled]:hover{color:rgba(0,0,0,.87);background-color:#FFF}.mui-btn.mui-btn--flat{color:rgba(0,0,0,.87);background-color:transparent}.mui-btn.mui-btn--flat:active,.mui-btn.mui-btn--flat:focus,.mui-btn.mui-btn--flat:hover{color:rgba(0,0,0,.87);background-color:#f2f2f2}.mui-btn.mui-btn--flat[disabled]:active,.mui-btn.mui-btn--flat[disabled]:focus,.mui-btn.mui-btn--flat[disabled]:hover{color:rgba(0,0,0,.87);background-color:transparent}.mui-btn:active,.mui-btn:focus,.mui-btn:hover{outline:0;text-decoration:none;color:rgba(0,0,0,.87)}.mui-btn:focus,.mui-btn:hover{box-shadow:0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-btn:focus,.mui-btn:hover{box-shadow:0 -1px 2px rgba(0,0,0,.12),-1px 0 2px rgba(0,0,0,.12),0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2)}}.mui-btn:active:hover{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-btn:active:hover{box-shadow:0 -1px 2px rgba(0,0,0,.12),-1px 0 2px rgba(0,0,0,.12),0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}}.mui-btn.mui--is-disabled,.mui-btn:disabled{cursor:not-allowed;pointer-events:none;opacity:.6;box-shadow:none}.mui-btn+.mui-btn{margin-left:8px}.mui-btn--flat{background-color:transparent}.mui-btn--flat:active,.mui-btn--flat:active:hover,.mui-btn--flat:focus,.mui-btn--flat:hover{box-shadow:none;background-color:#f2f2f2}.mui-btn--fab,.mui-btn--raised{box-shadow:0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-btn--fab,.mui-btn--raised{box-shadow:0 -1px 2px rgba(0,0,0,.12),-1px 0 2px rgba(0,0,0,.12),0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2)}}.mui-btn--fab:active,.mui-btn--raised:active{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-btn--fab:active,.mui-btn--raised:active{box-shadow:0 -1px 2px rgba(0,0,0,.12),-1px 0 2px rgba(0,0,0,.12),0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}}.mui-btn--fab{position:relative;padding:0;width:55px;height:55px;line-height:55px;border-radius:50%;z-index:1}.mui-btn--primary{color:#FFF;background-color:#2196F3}.mui-btn--primary:active,.mui-btn--primary:focus,.mui-btn--primary:hover{color:#FFF;background-color:#39a1f4}.mui-btn--primary[disabled]:active,.mui-btn--primary[disabled]:focus,.mui-btn--primary[disabled]:hover{color:#FFF;background-color:#2196F3}.mui-btn--primary.mui-btn--flat{color:#2196F3;background-color:transparent}.mui-btn--primary.mui-btn--flat:active,.mui-btn--primary.mui-btn--flat:focus,.mui-btn--primary.mui-btn--flat:hover{color:#2196F3;background-color:#f2f2f2}.mui-btn--primary.mui-btn--flat[disabled]:active,.mui-btn--primary.mui-btn--flat[disabled]:focus,.mui-btn--primary.mui-btn--flat[disabled]:hover{color:#2196F3;background-color:transparent}.mui-btn--dark{color:#FFF;background-color:#424242}.mui-btn--dark:active,.mui-btn--dark:focus,.mui-btn--dark:hover{color:#FFF;background-color:#4f4f4f}.mui-btn--dark[disabled]:active,.mui-btn--dark[disabled]:focus,.mui-btn--dark[disabled]:hover{color:#FFF;background-color:#424242}.mui-btn--dark.mui-btn--flat{color:#424242;background-color:transparent}.mui-btn--dark.mui-btn--flat:active,.mui-btn--dark.mui-btn--flat:focus,.mui-btn--dark.mui-btn--flat:hover{color:#424242;background-color:#f2f2f2}.mui-btn--dark.mui-btn--flat[disabled]:active,.mui-btn--dark.mui-btn--flat[disabled]:focus,.mui-btn--dark.mui-btn--flat[disabled]:hover{color:#424242;background-color:transparent}.mui-btn--danger{color:#FFF;background-color:#F44336}.mui-btn--danger:active,.mui-btn--danger:focus,.mui-btn--danger:hover{color:#FFF;background-color:#f55a4e}.mui-btn--danger[disabled]:active,.mui-btn--danger[disabled]:focus,.mui-btn--danger[disabled]:hover{color:#FFF;background-color:#F44336}.mui-btn--danger.mui-btn--flat{color:#F44336;background-color:transparent}.mui-btn--danger.mui-btn--flat:active,.mui-btn--danger.mui-btn--flat:focus,.mui-btn--danger.mui-btn--flat:hover{color:#F44336;background-color:#f2f2f2}.mui-btn--danger.mui-btn--flat[disabled]:active,.mui-btn--danger.mui-btn--flat[disabled]:focus,.mui-btn--danger.mui-btn--flat[disabled]:hover{color:#F44336;background-color:transparent}.mui-btn--accent{color:#FFF;background-color:#FF4081}.mui-btn--accent:active,.mui-btn--accent:focus,.mui-btn--accent:hover{color:#FFF;background-color:#ff5a92}.mui-btn--accent[disabled]:active,.mui-btn--accent[disabled]:focus,.mui-btn--accent[disabled]:hover{color:#FFF;background-color:#FF4081}.mui-btn--accent.mui-btn--flat{color:#FF4081;background-color:transparent}.mui-btn--accent.mui-btn--flat:active,.mui-btn--accent.mui-btn--flat:focus,.mui-btn--accent.mui-btn--flat:hover{color:#FF4081;background-color:#f2f2f2}.mui-btn--accent.mui-btn--flat[disabled]:active,.mui-btn--accent.mui-btn--flat[disabled]:focus,.mui-btn--accent.mui-btn--flat[disabled]:hover{color:#FF4081;background-color:transparent}.mui-btn--small{height:30.6px;line-height:30.6px;padding:0 16px;font-size:13px}.mui-btn--large{height:54px;line-height:54px;padding:0 26px;font-size:14px}.mui-btn--fab.mui-btn--small{width:44px;height:44px;line-height:44px}.mui-btn--fab.mui-btn--large{width:75px;height:75px;line-height:75px}.mui-checkbox,.mui-radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.mui-checkbox>label,.mui-radio>label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.mui-checkbox--inline>label>input[type=checkbox],.mui-checkbox>label>input[type=checkbox],.mui-radio--inline>label>input[type=radio],.mui-radio>label>input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px}.mui-checkbox+.mui-checkbox,.mui-radio+.mui-radio{margin-top:-5px}.mui-checkbox--inline,.mui-radio--inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.mui-checkbox--inline>input[type=checkbox],.mui-checkbox--inline>input[type=radio],.mui-checkbox--inline>label>input[type=checkbox],.mui-checkbox--inline>label>input[type=radio],.mui-radio--inline>input[type=checkbox],.mui-radio--inline>input[type=radio],.mui-radio--inline>label>input[type=checkbox],.mui-radio--inline>label>input[type=radio]{margin:4px 0 0;line-height:normal}.mui-checkbox--inline+.mui-checkbox--inline,.mui-radio--inline+.mui-radio--inline{margin-top:0;margin-left:10px}.mui-container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.mui-container:after,.mui-container:before{content:" ";display:table}.mui-container:after{clear:both}@media (min-width:544px){.mui-container{max-width:570px}}@media (min-width:768px){.mui-container{max-width:740px}}@media (min-width:992px){.mui-container{max-width:960px}}@media (min-width:1200px){.mui-container{max-width:1170px}}.mui-container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.mui-container-fluid:after,.mui-container-fluid:before{content:" ";display:table}.mui-container-fluid:after{clear:both}.mui-divider{display:block;height:1px;background-color:rgba(0,0,0,.12)}.mui--divider-top{border-top:1px solid rgba(0,0,0,.12)}.mui--divider-bottom{border-bottom:1px solid rgba(0,0,0,.12)}.mui--divider-left{border-left:1px solid rgba(0,0,0,.12)}.mui--divider-right{border-right:1px solid rgba(0,0,0,.12)}.mui-dropdown{display:inline-block;position:relative}[data-mui-toggle=dropdown]{animation-duration:.1ms;animation-name:mui-node-inserted;outline:0}.mui-dropdown__menu{position:absolute;top:100%;left:0;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#FFF;border-radius:2px;z-index:1;background-clip:padding-box}.mui-dropdown__menu.mui--is-open{display:block}.mui-dropdown__menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.429;color:rgba(0,0,0,.87);white-space:nowrap}.mui-dropdown__menu>li>a:focus,.mui-dropdown__menu>li>a:hover{text-decoration:none;color:rgba(0,0,0,.87);background-color:#EEE}.mui-dropdown__menu>.mui--is-disabled>a,.mui-dropdown__menu>.mui--is-disabled>a:focus,.mui-dropdown__menu>.mui--is-disabled>a:hover{color:#EEE}.mui-dropdown__menu>.mui--is-disabled>a:focus,.mui-dropdown__menu>.mui--is-disabled>a:hover{text-decoration:none;background-color:transparent;background-image:none;cursor:not-allowed}.mui-dropdown__menu--right{left:auto;right:0}@media (min-width:544px){.mui-form--inline>.mui-textfield{display:inline-block;margin-bottom:0}.mui-form--inline>.mui-checkbox,.mui-form--inline>.mui-radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.mui-form--inline>.mui-checkbox>label,.mui-form--inline>.mui-radio>label{padding-left:0}.mui-form--inline>.mui-checkbox>label>input[type=checkbox],.mui-form--inline>.mui-radio>label>input[type=radio]{position:relative;margin-left:0}.mui-form--inline>.mui-select{display:inline-block}.mui-form--inline>.mui-btn{margin-bottom:0;margin-top:0;vertical-align:bottom}}.mui-row{margin-left:-15px;margin-right:-15px}.mui-row:after,.mui-row:before{content:" ";display:table}.mui-row:after{clear:both}.mui-col-lg-1,.mui-col-lg-10,.mui-col-lg-11,.mui-col-lg-12,.mui-col-lg-2,.mui-col-lg-3,.mui-col-lg-4,.mui-col-lg-5,.mui-col-lg-6,.mui-col-lg-7,.mui-col-lg-8,.mui-col-lg-9,.mui-col-md-1,.mui-col-md-10,.mui-col-md-11,.mui-col-md-12,.mui-col-md-2,.mui-col-md-3,.mui-col-md-4,.mui-col-md-5,.mui-col-md-6,.mui-col-md-7,.mui-col-md-8,.mui-col-md-9,.mui-col-sm-1,.mui-col-sm-10,.mui-col-sm-11,.mui-col-sm-12,.mui-col-sm-2,.mui-col-sm-3,.mui-col-sm-4,.mui-col-sm-5,.mui-col-sm-6,.mui-col-sm-7,.mui-col-sm-8,.mui-col-sm-9,.mui-col-xs-1,.mui-col-xs-10,.mui-col-xs-11,.mui-col-xs-12,.mui-col-xs-2,.mui-col-xs-3,.mui-col-xs-4,.mui-col-xs-5,.mui-col-xs-6,.mui-col-xs-7,.mui-col-xs-8,.mui-col-xs-9{min-height:1px;padding-left:15px;padding-right:15px}.mui-col-xs-1,.mui-col-xs-10,.mui-col-xs-11,.mui-col-xs-12,.mui-col-xs-2,.mui-col-xs-3,.mui-col-xs-4,.mui-col-xs-5,.mui-col-xs-6,.mui-col-xs-7,.mui-col-xs-8,.mui-col-xs-9{float:left}.mui-col-xs-1{width:8.33333%}.mui-col-xs-2{width:16.66667%}.mui-col-xs-3{width:25%}.mui-col-xs-4{width:33.33333%}.mui-col-xs-5{width:41.66667%}.mui-col-xs-6{width:50%}.mui-col-xs-7{width:58.33333%}.mui-col-xs-8{width:66.66667%}.mui-col-xs-9{width:75%}.mui-col-xs-10{width:83.33333%}.mui-col-xs-11{width:91.66667%}.mui-col-xs-12{width:100%}.mui-col-xs-offset-0{margin-left:0}.mui-col-xs-offset-1{margin-left:8.33333%}.mui-col-xs-offset-2{margin-left:16.66667%}.mui-col-xs-offset-3{margin-left:25%}.mui-col-xs-offset-4{margin-left:33.33333%}.mui-col-xs-offset-5{margin-left:41.66667%}.mui-col-xs-offset-6{margin-left:50%}.mui-col-xs-offset-7{margin-left:58.33333%}.mui-col-xs-offset-8{margin-left:66.66667%}.mui-col-xs-offset-9{margin-left:75%}.mui-col-xs-offset-10{margin-left:83.33333%}.mui-col-xs-offset-11{margin-left:91.66667%}.mui-col-xs-offset-12{margin-left:100%}@media (min-width:544px){.mui-col-sm-1,.mui-col-sm-10,.mui-col-sm-11,.mui-col-sm-12,.mui-col-sm-2,.mui-col-sm-3,.mui-col-sm-4,.mui-col-sm-5,.mui-col-sm-6,.mui-col-sm-7,.mui-col-sm-8,.mui-col-sm-9{float:left}.mui-col-sm-1{width:8.33333%}.mui-col-sm-2{width:16.66667%}.mui-col-sm-3{width:25%}.mui-col-sm-4{width:33.33333%}.mui-col-sm-5{width:41.66667%}.mui-col-sm-6{width:50%}.mui-col-sm-7{width:58.33333%}.mui-col-sm-8{width:66.66667%}.mui-col-sm-9{width:75%}.mui-col-sm-10{width:83.33333%}.mui-col-sm-11{width:91.66667%}.mui-col-sm-12{width:100%}.mui-col-sm-offset-0{margin-left:0}.mui-col-sm-offset-1{margin-left:8.33333%}.mui-col-sm-offset-2{margin-left:16.66667%}.mui-col-sm-offset-3{margin-left:25%}.mui-col-sm-offset-4{margin-left:33.33333%}.mui-col-sm-offset-5{margin-left:41.66667%}.mui-col-sm-offset-6{margin-left:50%}.mui-col-sm-offset-7{margin-left:58.33333%}.mui-col-sm-offset-8{margin-left:66.66667%}.mui-col-sm-offset-9{margin-left:75%}.mui-col-sm-offset-10{margin-left:83.33333%}.mui-col-sm-offset-11{margin-left:91.66667%}.mui-col-sm-offset-12{margin-left:100%}}@media (min-width:768px){.mui-col-md-1,.mui-col-md-10,.mui-col-md-11,.mui-col-md-12,.mui-col-md-2,.mui-col-md-3,.mui-col-md-4,.mui-col-md-5,.mui-col-md-6,.mui-col-md-7,.mui-col-md-8,.mui-col-md-9{float:left}.mui-col-md-1{width:8.33333%}.mui-col-md-2{width:16.66667%}.mui-col-md-3{width:25%}.mui-col-md-4{width:33.33333%}.mui-col-md-5{width:41.66667%}.mui-col-md-6{width:50%}.mui-col-md-7{width:58.33333%}.mui-col-md-8{width:66.66667%}.mui-col-md-9{width:75%}.mui-col-md-10{width:83.33333%}.mui-col-md-11{width:91.66667%}.mui-col-md-12{width:100%}.mui-col-md-offset-0{margin-left:0}.mui-col-md-offset-1{margin-left:8.33333%}.mui-col-md-offset-2{margin-left:16.66667%}.mui-col-md-offset-3{margin-left:25%}.mui-col-md-offset-4{margin-left:33.33333%}.mui-col-md-offset-5{margin-left:41.66667%}.mui-col-md-offset-6{margin-left:50%}.mui-col-md-offset-7{margin-left:58.33333%}.mui-col-md-offset-8{margin-left:66.66667%}.mui-col-md-offset-9{margin-left:75%}.mui-col-md-offset-10{margin-left:83.33333%}.mui-col-md-offset-11{margin-left:91.66667%}.mui-col-md-offset-12{margin-left:100%}}@media (min-width:992px){.mui-col-lg-1,.mui-col-lg-10,.mui-col-lg-11,.mui-col-lg-12,.mui-col-lg-2,.mui-col-lg-3,.mui-col-lg-4,.mui-col-lg-5,.mui-col-lg-6,.mui-col-lg-7,.mui-col-lg-8,.mui-col-lg-9{float:left}.mui-col-lg-1{width:8.33333%}.mui-col-lg-2{width:16.66667%}.mui-col-lg-3{width:25%}.mui-col-lg-4{width:33.33333%}.mui-col-lg-5{width:41.66667%}.mui-col-lg-6{width:50%}.mui-col-lg-7{width:58.33333%}.mui-col-lg-8{width:66.66667%}.mui-col-lg-9{width:75%}.mui-col-lg-10{width:83.33333%}.mui-col-lg-11{width:91.66667%}.mui-col-lg-12{width:100%}.mui-col-lg-offset-0{margin-left:0}.mui-col-lg-offset-1{margin-left:8.33333%}.mui-col-lg-offset-2{margin-left:16.66667%}.mui-col-lg-offset-3{margin-left:25%}.mui-col-lg-offset-4{margin-left:33.33333%}.mui-col-lg-offset-5{margin-left:41.66667%}.mui-col-lg-offset-6{margin-left:50%}.mui-col-lg-offset-7{margin-left:58.33333%}.mui-col-lg-offset-8{margin-left:66.66667%}.mui-col-lg-offset-9{margin-left:75%}.mui-col-lg-offset-10{margin-left:83.33333%}.mui-col-lg-offset-11{margin-left:91.66667%}.mui-col-lg-offset-12{margin-left:100%}}@media (min-width:1200px){.mui-col-xl-1,.mui-col-xl-10,.mui-col-xl-11,.mui-col-xl-12,.mui-col-xl-2,.mui-col-xl-3,.mui-col-xl-4,.mui-col-xl-5,.mui-col-xl-6,.mui-col-xl-7,.mui-col-xl-8,.mui-col-xl-9{float:left}.mui-col-xl-1{width:8.33333%}.mui-col-xl-2{width:16.66667%}.mui-col-xl-3{width:25%}.mui-col-xl-4{width:33.33333%}.mui-col-xl-5{width:41.66667%}.mui-col-xl-6{width:50%}.mui-col-xl-7{width:58.33333%}.mui-col-xl-8{width:66.66667%}.mui-col-xl-9{width:75%}.mui-col-xl-10{width:83.33333%}.mui-col-xl-11{width:91.66667%}.mui-col-xl-12{width:100%}.mui-col-xl-offset-0{margin-left:0}.mui-col-xl-offset-1{margin-left:8.33333%}.mui-col-xl-offset-2{margin-left:16.66667%}.mui-col-xl-offset-3{margin-left:25%}.mui-col-xl-offset-4{margin-left:33.33333%}.mui-col-xl-offset-5{margin-left:41.66667%}.mui-col-xl-offset-6{margin-left:50%}.mui-col-xl-offset-7{margin-left:58.33333%}.mui-col-xl-offset-8{margin-left:66.66667%}.mui-col-xl-offset-9{margin-left:75%}.mui-col-xl-offset-10{margin-left:83.33333%}.mui-col-xl-offset-11{margin-left:91.66667%}.mui-col-xl-offset-12{margin-left:100%}}.mui-panel{padding:15px;margin-bottom:20px;border-radius:0;background-color:#FFF;box-shadow:0 2px 2px 0 rgba(0,0,0,.16),0 0 2px 0 rgba(0,0,0,.12)}.mui-panel:after,.mui-panel:before{content:" ";display:table}.mui-panel:after{clear:both}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-panel{box-shadow:0 -1px 2px 0 rgba(0,0,0,.12),-1px 0 2px 0 rgba(0,0,0,.12),0 2px 2px 0 rgba(0,0,0,.16),0 0 2px 0 rgba(0,0,0,.12)}}.mui-select{display:block;padding-top:15px;margin-bottom:20px;position:relative}.mui-select:focus{outline:0}.mui-select:focus>select{height:33px;margin-bottom:-1px;border-color:#2196F3;border-width:2px}.mui-select>select{animation-duration:.1ms;animation-name:mui-node-inserted;display:block;height:32px;width:100%;appearance:none;-webkit-appearance:none;-moz-appearance:none;outline:0;border:none;border-bottom:1px solid rgba(0,0,0,.26);border-radius:0;box-shadow:none;background-color:transparent;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNiIgd2lkdGg9IjEwIj48cG9seWdvbiBwb2ludHM9IjAsMCAxMCwwIDUsNiIgc3R5bGU9ImZpbGw6cmdiYSgwLDAsMCwuMjQpOyIvPjwvc3ZnPg==);background-repeat:no-repeat;background-position:right center;cursor:pointer;color:rgba(0,0,0,.87);font-size:16px;padding:0 25px 0 0}.mui-select>select::-ms-expand{display:none}.mui-select>select:focus{outline:0;height:33px;margin-bottom:-1px;border-color:#2196F3;border-width:2px}.mui-select>select:disabled{color:rgba(0,0,0,.38);cursor:not-allowed;background-color:transparent;opacity:1}.mui-select>label{position:absolute;top:0;display:block;width:100%;color:rgba(0,0,0,.54);font-size:12px;font-weight:400;line-height:15px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.mui-select:focus>label,.mui-select>select:focus~label{color:#2196F3}.mui-select__menu{position:absolute;z-index:2;min-width:100%;overflow-y:auto;padding:8px 0;background-color:#FFF;font-size:16px}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.mui-select__menu{border-left:1px solid rgba(0,0,0,.12);border-top:1px solid rgba(0,0,0,.12)}}.mui-select__menu>div{padding:0 22px;height:42px;line-height:42px;cursor:pointer;white-space:nowrap}.mui-select__menu>div:hover{background-color:#E0E0E0}.mui-select__menu>div.mui--is-selected{background-color:#EEE}th{text-align:left}.mui-table{width:100%;max-width:100%;margin-bottom:20px}.mui-table>tbody>tr>td,.mui-table>tbody>tr>th,.mui-table>tfoot>tr>td,.mui-table>tfoot>tr>th,.mui-table>thead>tr>td,.mui-table>thead>tr>th{padding:10px;line-height:1.429}.mui-table>thead>tr>th{border-bottom:2px solid rgba(0,0,0,.12);font-weight:700}.mui-table>tbody+tbody{border-top:2px solid rgba(0,0,0,.12)}.mui-table.mui-table--bordered>tbody>tr>td{border-bottom:1px solid rgba(0,0,0,.12)}.mui-tabs__bar{list-style:none;padding-left:0;margin-bottom:0;background-color:transparent;white-space:nowrap;overflow-x:auto}.mui-tabs__bar>li{display:inline-block}.mui-tabs__bar>li>a{display:block;white-space:nowrap;text-transform:uppercase;font-weight:500;font-size:14px;color:rgba(0,0,0,.87);cursor:default;height:48px;line-height:48px;padding-left:24px;padding-right:24px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mui-tabs__bar>li>a:hover{text-decoration:none}.mui-tabs__bar>li.mui--is-active{border-bottom:2px solid #2196F3}.mui-tabs__bar>li.mui--is-active>a{color:#2196F3}.mui-tabs__bar.mui-tabs__bar--justified{display:table;width:100%;table-layout:fixed}.mui-tabs__bar.mui-tabs__bar--justified>li{display:table-cell}.mui-tabs__bar.mui-tabs__bar--justified>li>a{text-align:center;padding-left:0;padding-right:0}.mui-tabs__pane{display:none}.mui-tabs__pane.mui--is-active{display:block}[data-mui-toggle=tab]{animation-duration:.1ms;animation-name:mui-node-inserted}.mui-textfield{display:block;padding-top:15px;margin-bottom:20px;position:relative}.mui-textfield>label{position:absolute;top:0;display:block;width:100%;color:rgba(0,0,0,.54);font-size:12px;font-weight:400;line-height:15px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.mui-textfield>textarea{padding-top:5px}.mui-textfield>input,.mui-textfield>textarea{display:block}.mui-textfield>input:focus~label,.mui-textfield>textarea:focus~label{color:#2196F3}.mui-textfield--float-label>label{position:absolute;transform:translate(0,15px);font-size:16px;line-height:32px;color:rgba(0,0,0,.26);text-overflow:clip;cursor:text;pointer-events:none}.mui-textfield--float-label>input:focus~label,.mui-textfield--float-label>textarea:focus~label{transform:translate(0,0);font-size:12px;line-height:15px;text-overflow:ellipsis}.mui-textfield--float-label>input:not(:focus).mui--is-not-empty~label,.mui-textfield--float-label>input:not(:focus):not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield--float-label>input:not(:focus)[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield--float-label>textarea:not(:focus).mui--is-not-empty~label,.mui-textfield--float-label>textarea:not(:focus):not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield--float-label>textarea:not(:focus)[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label{color:rgba(0,0,0,.54);font-size:12px;line-height:15px;transform:translate(0,0);text-overflow:ellipsis}.mui-textfield--wrap-label{display:table;width:100%;padding-top:0}.mui-textfield--wrap-label:not(.mui-textfield--float-label)>label{display:table-header-group;position:static;white-space:normal;overflow-x:visible}.mui-textfield>input,.mui-textfield>textarea{animation-duration:.1ms;animation-name:mui-node-inserted;display:block;background-color:transparent;color:rgba(0,0,0,.87);border:none;border-bottom:1px solid rgba(0,0,0,.26);outline:0;width:100%;font-size:16px;padding:0;box-shadow:none;border-radius:0;background-image:none}.mui-textfield>input:focus,.mui-textfield>textarea:focus{border-color:#2196F3;border-width:2px}.mui-textfield>input:-moz-read-only,.mui-textfield>input:disabled,.mui-textfield>textarea:-moz-read-only,.mui-textfield>textarea:disabled{cursor:not-allowed;background-color:transparent;opacity:1}.mui-textfield>input:disabled,.mui-textfield>input:read-only,.mui-textfield>textarea:disabled,.mui-textfield>textarea:read-only{cursor:not-allowed;background-color:transparent;opacity:1}.mui-textfield>input::-webkit-input-placeholder,.mui-textfield>textarea::-webkit-input-placeholder{color:rgba(0,0,0,.26);opacity:1}.mui-textfield>input::-moz-placeholder,.mui-textfield>textarea::-moz-placeholder{color:rgba(0,0,0,.26);opacity:1}.mui-textfield>input:-ms-input-placeholder,.mui-textfield>textarea:-ms-input-placeholder{color:rgba(0,0,0,.26);opacity:1}.mui-textfield>input::placeholder,.mui-textfield>textarea::placeholder{color:rgba(0,0,0,.26);opacity:1}.mui-textfield>input{height:32px}.mui-textfield>input:focus{height:33px;margin-bottom:-1px}.mui-textfield>textarea{min-height:64px}.mui-textfield>textarea[rows]:not([rows="2"]):focus{margin-bottom:-1px}.mui-textfield>input:focus{height:33px;margin-bottom:-1px}.mui-textfield>input:invalid:not(:focus):not(:required),.mui-textfield>input:invalid:not(:focus):required.mui--is-empty.mui--is-dirty,.mui-textfield>input:invalid:not(:focus):required.mui--is-not-empty,.mui-textfield>input:invalid:not(:focus):required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:invalid:not(:focus):required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:not(:focus).mui--is-invalid:not(:required),.mui-textfield>input:not(:focus).mui--is-invalid:required.mui--is-empty.mui--is-dirty,.mui-textfield>input:not(:focus).mui--is-invalid:required.mui--is-not-empty,.mui-textfield>input:not(:focus).mui--is-invalid:required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:not(:focus).mui--is-invalid:required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>textarea:invalid:not(:focus):not(:required),.mui-textfield>textarea:invalid:not(:focus):required.mui--is-empty.mui--is-dirty,.mui-textfield>textarea:invalid:not(:focus):required.mui--is-not-empty,.mui-textfield>textarea:invalid:not(:focus):required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>textarea:invalid:not(:focus):required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>textarea:not(:focus).mui--is-invalid:not(:required),.mui-textfield>textarea:not(:focus).mui--is-invalid:required.mui--is-empty.mui--is-dirty,.mui-textfield>textarea:not(:focus).mui--is-invalid:required.mui--is-not-empty,.mui-textfield>textarea:not(:focus).mui--is-invalid:required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>textarea:not(:focus).mui--is-invalid:required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty){border-color:#F44336;border-width:2px}.mui-textfield>input:invalid:not(:focus):not(:required),.mui-textfield>input:invalid:not(:focus):required.mui--is-empty.mui--is-dirty,.mui-textfield>input:invalid:not(:focus):required.mui--is-not-empty,.mui-textfield>input:invalid:not(:focus):required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:invalid:not(:focus):required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:not(:focus).mui--is-invalid:not(:required),.mui-textfield>input:not(:focus).mui--is-invalid:required.mui--is-empty.mui--is-dirty,.mui-textfield>input:not(:focus).mui--is-invalid:required.mui--is-not-empty,.mui-textfield>input:not(:focus).mui--is-invalid:required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty),.mui-textfield>input:not(:focus).mui--is-invalid:required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty){height:33px;margin-bottom:-1px}.mui-textfield>input:invalid:not(:focus):not(:required)~label,.mui-textfield>input:invalid:not(:focus):required.mui--is-not-empty~label,.mui-textfield>input:invalid:not(:focus):required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>input:invalid:not(:focus):required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>input:not(:focus).mui--is-invalid:not(:required)~label,.mui-textfield>input:not(:focus).mui--is-invalid:required.mui--is-not-empty~label,.mui-textfield>input:not(:focus).mui--is-invalid:required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>input:not(:focus).mui--is-invalid:required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>textarea:invalid:not(:focus):not(:required)~label,.mui-textfield>textarea:invalid:not(:focus):required.mui--is-not-empty~label,.mui-textfield>textarea:invalid:not(:focus):required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>textarea:invalid:not(:focus):required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>textarea:not(:focus).mui--is-invalid:not(:required)~label,.mui-textfield>textarea:not(:focus).mui--is-invalid:required.mui--is-not-empty~label,.mui-textfield>textarea:not(:focus).mui--is-invalid:required:not(:empty):not(.mui--is-empty):not(.mui--is-not-empty)~label,.mui-textfield>textarea:not(:focus).mui--is-invalid:required[value]:not([value=""]):not(.mui--is-empty):not(.mui--is-not-empty)~label{color:#F44336}.mui-textfield:not(.mui-textfield--float-label)>input:invalid:not(:focus):required.mui--is-empty.mui--is-dirty~label,.mui-textfield:not(.mui-textfield--float-label)>input:not(:focus).mui--is-invalid:required.mui--is-empty.mui--is-dirty~label,.mui-textfield:not(.mui-textfield--float-label)>textarea:invalid:not(:focus):required.mui--is-empty.mui--is-dirty~label,.mui-textfield:not(.mui-textfield--float-label)>textarea:not(:focus).mui--is-invalid:required.mui--is-empty.mui--is-dirty~label{color:#F44336}@keyframes mui-node-inserted{from{opacity:.99}to{opacity:1}}.mui--no-transition{transition:none!important}.mui--no-user-select{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mui-caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.mui--text-left{text-align:left!important}.mui--text-right{text-align:right!important}.mui--text-center{text-align:center!important}.mui--text-justify{text-align:justify!important}.mui--text-nowrap{white-space:nowrap!important}.mui--align-baseline{vertical-align:baseline!important}.mui--align-top{vertical-align:top!important}.mui--align-middle{vertical-align:middle!important}.mui--align-bottom{vertical-align:bottom!important}.mui--text-dark{color:rgba(0,0,0,.87)}.mui--text-dark-secondary{color:rgba(0,0,0,.54)}.mui--text-dark-hint{color:rgba(0,0,0,.38)}.mui--text-light{color:#FFF}.mui--text-light-secondary{color:rgba(255,255,255,.7)}.mui--text-light-hint{color:rgba(255,255,255,.3)}.mui--text-accent{color:rgba(255,64,129,.87)}.mui--text-accent-secondary{color:rgba(255,64,129,.54)}.mui--text-accent-hint{color:rgba(255,64,129,.38)}.mui--text-black{color:#000}.mui--text-white{color:#FFF}.mui--text-danger{color:#F44336}.mui-list--unstyled{padding-left:0;list-style:none}.mui-list--inline{padding-left:0;list-style:none;margin-left:-5px}.mui-list--inline>li{display:inline-block;padding-left:5px;padding-right:5px}.mui--z1,.mui-dropdown__menu,.mui-select__menu{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}.mui--z2{box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}.mui--z3{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}.mui--z4{box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22)}.mui--z5{box-shadow:0 19px 38px rgba(0,0,0,.3),0 15px 12px rgba(0,0,0,.22)}.mui--clearfix:after,.mui--clearfix:before{content:" ";display:table}.mui--clearfix:after{clear:both}.mui--pull-right{float:right!important}.mui--pull-left{float:left!important}.mui--hide{display:none!important}.mui--show{display:block!important}.mui--invisible{visibility:hidden}.mui--overflow-hidden{overflow:hidden!important}.mui--overflow-hidden-x{overflow-x:hidden!important}.mui--overflow-hidden-y{overflow-y:hidden!important}.mui--visible-lg-block,.mui--visible-lg-inline,.mui--visible-lg-inline-block,.mui--visible-md-block,.mui--visible-md-inline,.mui--visible-md-inline-block,.mui--visible-sm-block,.mui--visible-sm-inline,.mui--visible-sm-inline-block,.mui--visible-xl-block,.mui--visible-xl-inline,.mui--visible-xl-inline-block,.mui--visible-xs-block,.mui--visible-xs-inline,.mui--visible-xs-inline-block{display:none!important}@media (max-width:543px){.mui-visible-xs{display:block!important}table.mui-visible-xs{display:table}tr.mui-visible-xs{display:table-row!important}td.mui-visible-xs,th.mui-visible-xs{display:table-cell!important}.mui--visible-xs-block{display:block!important}.mui--visible-xs-inline{display:inline!important}.mui--visible-xs-inline-block{display:inline-block!important}}@media (min-width:544px) and (max-width:767px){.mui-visible-sm{display:block!important}table.mui-visible-sm{display:table}tr.mui-visible-sm{display:table-row!important}td.mui-visible-sm,th.mui-visible-sm{display:table-cell!important}.mui--visible-sm-block{display:block!important}.mui--visible-sm-inline{display:inline!important}.mui--visible-sm-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.mui-visible-md{display:block!important}table.mui-visible-md{display:table}tr.mui-visible-md{display:table-row!important}td.mui-visible-md,th.mui-visible-md{display:table-cell!important}.mui--visible-md-block{display:block!important}.mui--visible-md-inline{display:inline!important}.mui--visible-md-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.mui-visible-lg{display:block!important}table.mui-visible-lg{display:table}tr.mui-visible-lg{display:table-row!important}td.mui-visible-lg,th.mui-visible-lg{display:table-cell!important}.mui--visible-lg-block{display:block!important}.mui--visible-lg-inline{display:inline!important}.mui--visible-lg-inline-block{display:inline-block!important}}@media (min-width:1200px){.mui-visible-xl{display:block!important}table.mui-visible-xl{display:table}tr.mui-visible-xl{display:table-row!important}td.mui-visible-xl,th.mui-visible-xl{display:table-cell!important}.mui--visible-xl-block{display:block!important}.mui--visible-xl-inline{display:inline!important}.mui--visible-xl-inline-block{display:inline-block!important}}@media (max-width:543px){.mui--hidden-xs{display:none!important}}@media (min-width:544px) and (max-width:767px){.mui--hidden-sm{display:none!important}}@media (min-width:768px) and (max-width:991px){.mui--hidden-md{display:none!important}}@media (min-width:992px) and (max-width:1199px){.mui--hidden-lg{display:none!important}}@media (min-width:1200px){.mui--hidden-xl{display:none!important}}body.mui-body--scroll-lock{overflow:hidden!important}#mui-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:99999999;background-color:rgba(0,0,0,.2);overflow:auto}.mui-ripple-effect{position:absolute;border-radius:50%;pointer-events:none;opacity:.4;transform:scale(.0001)}.mui-ripple-effect.mui--animate-in{opacity:0;transform:scale(1);transition:transform .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1)}.mui-ripple-effect.mui--active{opacity:.3}.mui-ripple-effect.mui--animate-out{opacity:0;transition:opacity .6s cubic-bezier(0,0,.2,1)}.mui-btn>.mui-ripple-effect{background-color:#a6a6a6}.mui-btn--primary>.mui-ripple-effect{background-color:#FFF}.mui-btn--dark>.mui-ripple-effect{background-color:#FFF}.mui-btn--danger>.mui-ripple-effect{background-color:#FFF}.mui-btn--accent>.mui-ripple-effect{background-color:#FFF}.mui-btn--flat>.mui-ripple-effect{background-color:#a6a6a6}.mui--text-display4{font-weight:300;font-size:112px;line-height:112px}.mui--text-display3{font-weight:400;font-size:56px;line-height:56px}.mui--text-display2{font-weight:400;font-size:45px;line-height:48px}.mui--text-display1,h1{font-weight:400;font-size:34px;line-height:40px}.mui--text-headline,h2{font-weight:400;font-size:24px;line-height:32px}.mui--text-title,h3{font-weight:400;font-size:20px;line-height:28px}.mui--text-subhead,h4{font-weight:400;font-size:16px;line-height:24px}.mui--text-body2,h5{font-weight:500;font-size:14px;line-height:24px}.mui--text-body1{font-weight:400;font-size:14px;line-height:20px}.mui--text-caption{font-weight:400;font-size:12px;line-height:16px}.mui--text-menu{font-weight:500;font-size:13px;line-height:17px}.mui--text-button{font-weight:500;font-size:14px;line-height:18px;text-transform:uppercase} \ No newline at end of file diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..5922255 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,4 @@ +.idea/ +dist/ +node_modules/ +.eslintrc.json/ diff --git a/server/.eslintrc.json b/server/.eslintrc.json new file mode 100644 index 0000000..8dfa7c5 --- /dev/null +++ b/server/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "no-template-curly-in-string": "warn", + "guard-for-in": "error", + "no-eval": "error", + "arrow-body-style": "error", + + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "none", + "argsIgnorePattern": "^_$", + "varsIgnorePattern": "^_$" + } + ], + + "@typescript-eslint/explicit-function-return-type": [ + "error", + { + "allowExpressions": true, + "allowTypedFunctionExpressions": true, + "allowHigherOrderFunctions": true + } + ] + } +} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..50ecc7c --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,34 @@ +FROM node:11-alpine + +WORKDIR /md + +COPY ["./src", "./src"] +COPY ["./package.json", "./package-lock.json", "./tsconfig.json", "./config.json5", "./"] +RUN ["npm", "ci"] +RUN ["npm", "run", "build"] + +FROM node:11-alpine +LABEL maintainer="team@rereadgames.com" + +ARG BUILD_DATE +ARG VCS_REF +ARG VERSION + +LABEL org.label-schema.name="massivedecks/server" +LABEL org.label-schema.description="The web server for Massive Decks, a comedy party game." +LABEL org.label-schema.url="https://github.com/Lattyware/massivedecks" +LABEL org.label-schema.vcs-url="https://github.com/Lattyware/massivedecks" +LABEL org.label-schema.vendor="Reread Games" +LABEL org.label-schema.schema-version="1.0" +LABEL org.label-schema.build-date=$BUILD_DATE +LABEL org.label-schema.vcs-ref=$VCS_REF +LABEL org.label-schema.version=$VERSION + +WORKDIR /md + +COPY --from=0 ["/md/dist", "./"] +COPY ["./package.json", "./package-lock.json", "./config.json5", "./"] +RUN ["npm", "ci", "--only=production"] + +EXPOSE 8081 +CMD ["node", "./index.js"] diff --git a/server/config.json5 b/server/config.json5 new file mode 100644 index 0000000..53201a8 --- /dev/null +++ b/server/config.json5 @@ -0,0 +1,91 @@ +{ + // In a production environment, be sure to change this value to a random secret for security. + // You can use `npm run generate-secret` to generate a suitable value. + // Note that changes to this value will kick all users from all games. + secret: "CHANGE ME", + + // The port to serve on. This should be behind a proxy. + port: 8081, + + // The path this is hosted behind - only relevant when behind a reverse proxy, this won't actually host on a different + // path. This should always have a trailing slash if it isn't empty, and never one at the start. + basePath: "", + + // Timeouts determine how long the server waits before taking certain actions. + // These values are all ISO 8601 durations. + timeouts: { + // How long to wait in between checking timeouts. + // Note that this will effectively determine the resolution of all other timeouts, so should be kept low. + // Increasing the value will decrease server load, however. + // Zero-durations will result in instant execution, regardless of frequency here. + timeoutCheckFrequency: "PT1S", + + // How long to wait before marking a player as disconnected. + disconnectionGracePeriod: "PT5S", + + // How long to wait after everyone has played to begin the revealing phase. Having this as a non-zero value lets + // the last person playing + finishedPlayingDelay: "PT2S", + + // How long to wait after a winner for the round is decided to move to the next round. + // Note the client will let users look at the winner for longer than this, this just means users can't advance and + // the round timer won't start for this long after a winner is picked, as such zero is a perfectly valid setting + // here. + nextRoundDelay: "PT5S" + }, + + // How to store game data. + storage: { + // Example of a configuration using in-memory storage. Note this means server restart wipes all data. + type: "InMemory", + + // type: "PostgreSQL", + // connection: { + // host: "localhost", + // port: 5432, + // user: "postgres", + // database: "postgres", + // password: "md", + // keepAlive: false + // }, + + //// Example of a configuration using a postgres database. + // type: "PostgreSQL", + // connection: { + // host: "postgres", + // port: 5432 , + // user: "massivedecks", + // database: "massivedecks", + // password: "massivedecks", + // keepAlive: false, + // }, + + // How often to delete abandoned/finished games (an ISO 8601 duration). + garbageCollectionFrequency: "PT5M" + }, + + // How to cache game data. + cache: { + // Example of a configuration using in-memory storage. + type: "InMemory", + + //// Example of a configuration using a postgres database. + // type: "PostgreSQL", + // connection: { + // host: "postgres", + // port: 5432 , + // user: "massivedecks", + // database: "massivedecks", + // password: "massivedecks", + // keepAlive: false, + // }, + + // How old a cached value should be before we check to see if we should update it on a hit. + checkAfter: "PT5M" + }, + + // The version of Massive Decks (displayed in the footer on the main page, and given in bug reports). + // Please don't change this unless you are running a custom version, and if so use a unique prefix, so it is clear + // in bug reports. + version: "dev" +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..eefa3e9 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,4785 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/ajv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-1.0.0.tgz", + "integrity": "sha1-T7JEB0Ly9sMOf7B5e4OfxvaWaCo=", + "dev": true, + "requires": { + "ajv": "*" + } + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cross-spawn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz", + "integrity": "sha512-evp2ZGsFw9YKprDbg8ySgC9NA15g3YgiI8ANkGmKKvvi0P2aDGYLPxQIC5qfeKNUOe3TjABVGuah6omPRpIYhg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/express": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", + "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.4.tgz", + "integrity": "sha512-x/8h6FHm14rPWnW2HP5likD/rsqJ3t/77OWx2PLxym0hXbeBWQmcPyHmwX+CtCQpjIfgrUdEoDFcLPwPZWiqzQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/express-winston": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/express-winston/-/express-winston-3.0.3.tgz", + "integrity": "sha512-dSzZ0lHEpP2JxLubsLs3P+JWcFKiO4bKWnHAhPkpL0YdtmDrQU+JemVAAUvAIhn0RDuNcUDetQOA8NzHLzL1UQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/logform": "*", + "winston": "^3.0.0" + } + }, + "@types/express-ws": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.0.tgz", + "integrity": "sha512-GxsWec7Vp6h7sJuK0PwnZHeXNZnOwQn8kHAbCfvii66it5jXHTWzSg5cgHVtESwJfBLOe9SJ5wmM7C6gsDoyQw==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, + "@types/generic-pool": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@types/generic-pool/-/generic-pool-3.1.9.tgz", + "integrity": "sha512-IkXMs8fhV6+E4J8EWv8iL7mLvApcLLQUH4m1Rex3KCPRqT+Xya0DDHIeGAokk/6VXe9zg8oTWyr+FGyeuimEYQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/hashids": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@types/hashids/-/hashids-1.0.30.tgz", + "integrity": "sha512-ESXJxz/GRY+SGa3n0WuWZLR0xM3k8LTr4+2xOoj/a0ZThglF/UfEeKVaMQNNFvFQ+mw3ng8LbC3gmJx/GmR5EQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-stable-stringify": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz", + "integrity": "sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw==", + "dev": true + }, + "@types/json5": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz", + "integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==", + "dev": true + }, + "@types/jsonwebtoken": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.2.tgz", + "integrity": "sha512-Mkjljd9DTpkPlrmGfTJvcP4aBU7yO2QmW7wNVhV4/6AEUxYoacqU7FJU/N0yFEHTsIrE4da3rUrjrR5ejicFmA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/logform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/logform/-/logform-1.2.0.tgz", + "integrity": "sha512-E8cLzW+PmqHI//4HLR+ZvE3cyzqVjsncYBx1O14P07oVJj3ezhmiL/azlgkXKLFyCWAeKsPQdjHNg/NEhBF5Ow==", + "dev": true + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "11.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz", + "integrity": "sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/source-map-support": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.0.tgz", + "integrity": "sha512-OrnAz5K5dXDgMdeRRoXIjDAvkodQ9ESvVJCyzrhzUJKmCkXgmYx/KLUBcVFe5eS4FiohfcY7YPxsdkmSwJz9wA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=", + "dev": true + }, + "@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", + "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/wu": { + "version": "2.1.39", + "resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.39.tgz", + "integrity": "sha512-GcWc+khCfE3yBonhADArZxvWqgxp3HzzjG720JSB+5s5jFlN0OMcTIOUCBmnPx8tSzj6HNKty7S5zOtZ6VKKSA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.10.2.tgz", + "integrity": "sha512-7449RhjE1oLFIy5E/5rT4wG5+KsfPzakJuhvpzXJ3C46lq7xywY0/Rjo9ZBcwrfbk0nRZ5xmUHkk7DZ67tSBKw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "1.10.2", + "eslint-utils": "^1.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.7.0" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.10.2.tgz", + "integrity": "sha512-Hf5lYcrnTH5Oc67SRrQUA7KuHErMvCf5RlZsyxXPIT6AXa8fKTyfFO6vaEnUmlz48RpbxO4f0fY3QtWkuHZNjg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "1.10.2", + "eslint-scope": "^4.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.10.2.tgz", + "integrity": "sha512-xWDWPfZfV0ENU17ermIUVEVSseBBJxKfqBcRCMZ8nAjJbfA5R7NWMZmFFHYnars5MjK4fPjhu4gwQv526oZIPQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.10.2", + "@typescript-eslint/typescript-estree": "1.10.2", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.10.2.tgz", + "integrity": "sha512-Kutjz0i69qraOsWeI8ETqYJ07tRLvD9URmdrMoF10bG8y8ucLmPtSxROvVejWvlJUGl2et/plnMiKRDW+rhEhw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "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-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "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", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "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", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "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", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "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 + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "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", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "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" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "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" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "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 + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "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 + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "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 + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "env-variable": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", + "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz", + "integrity": "sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz", + "integrity": "sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "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", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==" + }, + "express-winston": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-3.2.0.tgz", + "integrity": "sha512-AcVSV016U7aPs1Uknoil962SU124UgNkdakP564X6w+lH0oS1FhV3uctUj2/dOog7np4uvsAHhBmSoICjZMDmA==", + "requires": { + "chalk": "^2.4.1", + "lodash": "^4.17.10" + } + }, + "express-ws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", + "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", + "requires": { + "ws": "^5.2.0" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.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" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "generic-pool": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.7.1.tgz", + "integrity": "sha512-ug6DAZoNgWm6q5KhPFA+hzXfBLFQu5sTXxPpv44DmE0A2g+CiHoq9LTVdkXpZMkYVMoGw83F6W+WT0h0MFMK/w==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://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", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "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.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "got": { + "version": "6.7.1", + "resolved": "https://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", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hashids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.2.2.tgz", + "integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-status-codes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.3.2.tgz", + "integrity": "sha512-nDUtj0ltIt08tGi2VWSpSzNNFye0v3YSe9lX3lIqLTuVvvRiYCvs4QQBSHo0eomFYw1wlUuofurUAlTm+vHnXg==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "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 + }, + "import-fresh": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", + "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.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=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "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 + }, + "inquirer": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", + "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.11", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "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", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "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-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=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "requires": { + "colornames": "^1.1.1" + } + }, + "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" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nodemon": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.1.tgz", + "integrity": "sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==", + "dev": true, + "requires": { + "chokidar": "^2.1.5", + "debug": "^3.1.0", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.6", + "semver": "^5.5.0", + "supports-color": "^5.2.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "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-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "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-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.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" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "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-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "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" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "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-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "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=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "prettier": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pstree.remy": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", + "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "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" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "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" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "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.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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==" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "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=", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://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 + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.0.tgz", + "integrity": "sha512-nHFDrxmbrkU7JAFKqKbDJXfzrX2UBsWmrieXFTGxiI5e4ncg3VqsZeI4EzNmX0ncp4XNGVeoxIWJXfCIXwrsvw==", + "dev": true, + "requires": { + "ajv": "^6.9.1", + "lodash": "^4.17.11", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "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" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "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 + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "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" + } + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "ts-postgres": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.0.2.tgz", + "integrity": "sha512-WdCth7Q00KrcL2FwxiQq5FM2aUFa2fXqrcOssd537zb8RPa1omhtIZ6Jpi2oL2GjqXEADqar3YPQ4OUbIzjESA==", + "requires": { + "ts-typed-events": "^1.1.1" + } + }, + "ts-typed-events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ts-typed-events/-/ts-typed-events-1.1.1.tgz", + "integrity": "sha512-jftubiJvjHWcHIFUgLOGB1NjybQwKpAWX1Rb9FeVGw9R6LvqjdBdkowFQUTTjcpaVmmJXXvuwjZFhDuqnr9xQQ==" + }, + "tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "requires": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tsutils": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", + "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "dev": true + }, + "typescript-json-schema": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.38.3.tgz", + "integrity": "sha512-+13qUoBUQwOXqxUoYQWtLA9PEM7ojfv8r+hYc2ebeqqVwVM4+yI5JSlsYRBlJKKewc9q1FHqrMR6L6d9TNX9Dw==", + "dev": true, + "requires": { + "glob": "~7.1.4", + "json-stable-stringify": "^1.0.1", + "typescript": "^3.5.1", + "yargs": "^13.2.4" + } + }, + "typescript-json-validator": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/typescript-json-validator/-/typescript-json-validator-2.2.0.tgz", + "integrity": "sha512-F+swgGAA5CrwjL6cUHsRzA9e29g2LWZaszhoJTXn70ks/KCYVG0RuvkdeKe+IcbKNzSfD9lwEGgRo0cinZ3YCw==", + "dev": true, + "requires": { + "@types/ajv": "^1.0.0", + "@types/cross-spawn": "^6.0.0", + "@types/glob": "^7.1.1", + "@types/json-stable-stringify": "^1.0.32", + "@types/minimatch": "^3.0.3", + "cross-spawn": "^6.0.5", + "glob": "^7.1.3", + "json-stable-stringify": "^1.0.1", + "minimatch": "^3.0.4", + "tsconfig": "^7.0.0", + "typescript-json-schema": "^0.38.3", + "yargs": "^13.2.4" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "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" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "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", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "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", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "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", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "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" + } + }, + "winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "requires": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "requires": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "wu": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wu/-/wu-2.1.0.tgz", + "integrity": "sha1-fnLj+6I+D/Zp3dU34chX09SxYU8=" + }, + "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 + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..f974fe8 --- /dev/null +++ b/server/package.json @@ -0,0 +1,61 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "Massive Decks server.", + "main": "dist/index.js", + "scripts": { + "generate-json-validators": "npx typescript-json-validator src/ts/action/validation.ts --collection --noExtraProps--format=full --aliasRefs --ignoreErrors && sed -i '1s;^;/* eslint-disable */\\n;' src/ts/action/validation.validator.ts", + "build": "tsc", + "prestart": "npm run build", + "dev": "nodemon .", + "debug": "nodemon --inspect .", + "generate-secret": "npm run prestart && node dist/secret.js" + }, + "author": "Reread Games ", + "license": "AGPL-3.0-or-later", + "private": true, + "engines": { + "node": ">=0.12" + }, + "dependencies": { + "ajv": "^6.10.0", + "axios": "^0.19.0", + "body-parser": "^1.19.0", + "express": "^4.17.1", + "express-async-errors": "^3.1.1", + "express-winston": "^3.2.0", + "express-ws": "^4.0.0", + "generic-pool": "^3.7.1", + "hashids": "^1.2.2", + "http-status-codes": "^1.3.2", + "json5": "^2.1.0", + "jsonwebtoken": "^8.5.1", + "moment": "^2.24.0", + "source-map-support": "^0.5.12", + "ts-postgres": "^1.0.2", + "uuid": "^3.3.2", + "winston": "^3.2.1", + "wu": "^2.1.0" + }, + "devDependencies": { + "@types/express": "^4.17.0", + "@types/express-winston": "^3.0.3", + "@types/express-ws": "^3.0.0", + "@types/generic-pool": "^3.1.9", + "@types/hashids": "^1.0.30", + "@types/json5": "0.0.30", + "@types/jsonwebtoken": "^8.3.2", + "@types/source-map-support": "^0.5.0", + "@types/uuid": "^3.4.4", + "@types/wu": "latest", + "@typescript-eslint/eslint-plugin": "^1.10.2", + "@typescript-eslint/parser": "^1.10.2", + "eslint": "^5.16.0", + "eslint-config-prettier": "^4.3.0", + "eslint-plugin-prettier": "^3.1.0", + "nodemon": "^1.19.1", + "prettier": "1.18.2", + "typescript": "^3.5.2", + "typescript-json-validator": "^2.2.0" + } +} diff --git a/server/src/ts/action.ts b/server/src/ts/action.ts new file mode 100644 index 0000000..82e126f --- /dev/null +++ b/server/src/ts/action.ts @@ -0,0 +1,39 @@ +import { Handler } from "./action/handler"; +import * as validation from "./action/validation.validator"; +import * as authenticate from "./action/authenticate"; +import { Authenticate } from "./action/authenticate"; +import * as gameAction from "./action/game-action"; +import { GameAction } from "./action/game-action"; +import * as privileged from "./action/privileged"; +import { Privileged } from "./action/privileged"; +import { AlreadyAuthenticatedError } from "./errors/authentication"; +import { InvalidActionError } from "./errors/validation"; +import * as util from "./util"; + +/** + * An action a user takes to affect the game in some way, received via a + * websocket (therefore with no hints as to what the action will be). + */ +export type Action = Authenticate | GameAction | Privileged; + +const _validateAction = validation.validate("Action"); +export const validate = (action: object): Action => { + try { + return _validateAction(action); + } catch (e) { + throw new InvalidActionError(e.message); + } +}; + +export const handle: Handler = (auth, lobby, action, config) => { + const validated = validate(action); + if (authenticate.is(validated)) { + throw new AlreadyAuthenticatedError(); + } else if (gameAction.is(validated)) { + return gameAction.handle(auth, lobby, validated, config); + } else if (privileged.is(validated)) { + return privileged.handle(auth, lobby, validated, config); + } else { + return util.assertNever(validated); + } +}; diff --git a/server/src/ts/action/authenticate.ts b/server/src/ts/action/authenticate.ts new file mode 100644 index 0000000..3baf069 --- /dev/null +++ b/server/src/ts/action/authenticate.ts @@ -0,0 +1,61 @@ +import { InvalidAuthenticationError } from "../errors/authentication"; +import * as event from "../event"; +import { ServerState } from "../server-state"; +import { GameCode } from "../lobby/game-code"; +import { User } from "../user"; +import * as token from "../user/token"; +import { Token } from "../user/token"; +import { Action } from "../action"; +import * as change from "../lobby/change"; +import * as connectionChanged from "../events/lobby-event/connection-changed"; + +/** + * Authenticate with the game. + */ +export interface Authenticate { + action: NameType; + token: Token; +} + +type NameType = "Authenticate"; +const name: NameType = "Authenticate"; + +/** + * Check if an action is an authenticate action. + * @param action The action to check. + */ +export const is = (action: Action): action is Authenticate => + action.action === name; + +/** + * Handle the authentication, returning the validated claim if successful. + * @param server The server state. + * @param authenticate The action. + * @param gameCode The game code for the lobby to validate in. + */ +export async function handle( + server: ServerState, + authenticate: Authenticate, + gameCode: GameCode +): Promise { + const claims = token.validate( + authenticate.token, + await server.store.id(), + server.config.secret + ); + if (claims.gc !== gameCode) { + throw new InvalidAuthenticationError("wrong game code"); + } + await change.apply(server, gameCode, lobby => { + const user = lobby.users.get(claims.uid) as User; + if (user.connection !== "Connected") { + user.connection = "Connected"; + return { + events: [event.target(connectionChanged.connected(claims.uid))] + }; + } else { + return {}; + } + }); + return claims; +} diff --git a/server/src/ts/action/game-action.ts b/server/src/ts/action/game-action.ts new file mode 100644 index 0000000..c73830d --- /dev/null +++ b/server/src/ts/action/game-action.ts @@ -0,0 +1,88 @@ +import { Action } from "../action"; +import { + GameNotStartedError, + IncorrectPlayerRoleError +} from "../errors/action-execution-error"; +import { Game } from "../games/game"; +import * as player from "../games/player"; +import * as gameLobby from "../lobby"; +import * as token from "../user/token"; +import * as util from "../util"; +import * as czar from "./game-action/czar"; +import { Czar } from "./game-action/czar"; +import * as playerAction from "./game-action/player"; +import { Player as PlayerAction } from "./game-action/player"; +import * as redraw from "./game-action/redraw"; +import { Redraw } from "./game-action/redraw"; +import * as handler from "./handler"; +import { Handler } from "./handler"; +import wu = require("wu"); + +/** + * An action only a player can perform. + */ +export type GameAction = PlayerAction | Czar | Redraw; + +/** + * A handler for game actions. + */ +export type Handler = handler.Custom< + T, + gameLobby.WithActiveGame +>; + +const possible = new Set([playerAction.is, czar.is, redraw.is]); + +/** + * Check if an action is a configure action. + * @param action The action to check. + */ +export const is = (action: Action): action is GameAction => + wu(possible).some(is => is(action)); + +export const handle: Handler = (auth, lobby, action, server) => { + if (gameLobby.hasActiveGame(lobby)) { + if (czar.is(action)) { + return czar.handle(auth, lobby, action, server); + } else if (playerAction.is(action)) { + return playerAction.handle(auth, lobby, action, server); + } else if (redraw.is(action)) { + return redraw.handle(auth, lobby, action, server); + } else { + return util.assertNever(action); + } + } else { + throw new GameNotStartedError(action); + } +}; + +export function expectRole( + auth: token.Claims, + action: Czar, + game: Game, + expected: "Czar" +): void; +export function expectRole( + auth: token.Claims, + action: PlayerAction, + game: Game, + expected: "Player" +): void; +/** + * Expect a given role. + * @param auth The claims for the user attempting to perform the action. + * @param action The action being performed. + * @param game The game being played. + * @param expected The expected role for the user. + */ +export function expectRole( + auth: token.Claims, + action: GameAction, + game: Game, + expected: player.Role +): void { + const playerRole = player.role(game, auth.uid); + if (playerRole !== expected) { + throw new IncorrectPlayerRoleError(action, playerRole, expected); + } +} diff --git a/server/src/ts/action/game-action/czar.ts b/server/src/ts/action/game-action/czar.ts new file mode 100644 index 0000000..33083d2 --- /dev/null +++ b/server/src/ts/action/game-action/czar.ts @@ -0,0 +1,38 @@ +import { Action } from "../../action"; +import * as util from "../../util"; +import * as gameAction from "../game-action"; +import * as judge from "./czar/judge"; +import { Judge } from "./czar/judge"; +import * as reveal from "./czar/reveal"; +import { Reveal } from "./czar/reveal"; +import wu = require("wu"); + +/** + * An action only the czar can perform. + */ +export type Czar = Judge | Reveal; + +const possible = new Set([judge.is, reveal.is]); + +/** + * Check if an action is a player action. + * @param action The action to check. + */ +export const is = (action: Action): action is Czar => + wu(possible).some(is => is(action)); + +export const handle: gameAction.Handler = ( + auth, + lobby, + action, + server +) => { + gameAction.expectRole(auth, action, lobby.game, "Czar"); + if (judge.is(action)) { + return judge.handle(auth, lobby, action, server); + } else if (reveal.is(action)) { + return reveal.handle(auth, lobby, action, server); + } else { + return util.assertNever(action); + } +}; diff --git a/server/src/ts/action/game-action/czar/judge.ts b/server/src/ts/action/game-action/czar/judge.ts new file mode 100644 index 0000000..ee3e166 --- /dev/null +++ b/server/src/ts/action/game-action/czar/judge.ts @@ -0,0 +1,59 @@ +import { Action } from "../../../action"; +import { InvalidActionError } from "../../../errors/validation"; +import * as roundFinished from "../../../events/game-event/round-finished"; +import * as play from "../../../games/cards/play"; +import * as round from "../../../games/game/round"; +import * as gameAction from "../../game-action"; +import * as event from "../../../event"; +import * as roundStart from "../../../timeout/round-start"; + +/** + * A user declares the winning play for a round. + */ +export interface Judge { + action: NameType; + winner: play.Id; +} + +type NameType = "Judge"; +const name: NameType = "Judge"; + +export const is = (action: Action): action is Judge => action.action === name; + +export const handle: gameAction.Handler = ( + auth, + lobby, + action, + server +) => { + const lobbyRound = lobby.game.round; + const plays = lobbyRound.plays; + if (round.verifyStage(action, lobbyRound, "Judging")) { + const play = plays.find(play => play.id === action.winner); + if (play === undefined) { + throw new InvalidActionError("Given play doesn't exist."); + } + const player = lobby.game.players.get(play.playedBy); + if (player !== undefined) { + player.score += 1; + } + const completedRound: round.Complete = { + ...lobbyRound, + stage: "Complete", + winner: play.playedBy + }; + lobby.game.round = completedRound; + return { + lobby, + events: [event.target(roundFinished.of(completedRound))], + timeouts: [ + { + timeout: roundStart.of(), + after: server.config.timeouts.nextRoundDelay + } + ] + }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/game-action/czar/reveal.ts b/server/src/ts/action/game-action/czar/reveal.ts new file mode 100644 index 0000000..234a96c --- /dev/null +++ b/server/src/ts/action/game-action/czar/reveal.ts @@ -0,0 +1,56 @@ +import { Action } from "../../../action"; +import { InvalidActionError } from "../../../errors/validation"; +import * as event from "../../../event"; +import * as playRevealed from "../../../events/game-event/play-revealed"; +import * as play from "../../../games/cards/play"; +import * as gameAction from "../../game-action"; +import * as round from "../../../games/game/round"; + +/** + * A user judges the winning play for a round. + */ +export interface Reveal { + action: NameType; + play: play.Id; +} + +type NameType = "Reveal"; +const name: NameType = "Reveal"; + +/** + * Check if an action is a reveal action. + * @param action The action to check. + */ +export const is = (action: Action): action is Reveal => action.action === name; + +/** + * Handle a Judge action. + * @param auth The claims for the user attempting to perform the action. + * @param lobby The lobby the user is attempting to perform the action in. + * @param action The action. + */ +export const handle: gameAction.Handler = (auth, lobby, action) => { + const lobbyRound = lobby.game.round; + if (round.verifyStage(action, lobbyRound, "Revealing")) { + const play = lobbyRound.plays.find(play => play.id === action.play); + if (play === undefined) { + throw new InvalidActionError("Given play doesn't exist."); + } + if (play.revealed) { + throw new InvalidActionError("Given play is already revealed."); + } + play.revealed = true; + if (round.allStoredPlaysAreRevealed(lobbyRound)) { + lobby.game.round = { + ...lobbyRound, + stage: "Judging" + }; + } + return { + lobby, + events: [event.target(playRevealed.of(play.id, play.play))] + }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/game-action/player.ts b/server/src/ts/action/game-action/player.ts new file mode 100644 index 0000000..385811d --- /dev/null +++ b/server/src/ts/action/game-action/player.ts @@ -0,0 +1,37 @@ +import { Action } from "../../action"; +import * as util from "../../util"; +import * as gameAction from "../game-action"; +import * as submit from "./player/submit"; +import { Submit } from "./player/submit"; +import * as takeBack from "./player/take-back"; +import { TakeBack } from "./player/take-back"; +import wu from "wu"; + +/** + * An action only the czar can perform. + */ +export type Player = Submit | TakeBack; + +const possible = new Set([submit.is, takeBack.is]); + +/** + * Check if an action is a non-czar action. + * @param action The action to check. + */ +export const is = (action: Action): action is Player => + wu(possible).some(is => is(action)); + +export const handle: gameAction.Handler = ( + auth, + lobby, + action, + server +) => { + if (submit.is(action)) { + return submit.handle(auth, lobby, action, server); + } else if (takeBack.is(action)) { + return takeBack.handle(auth, lobby, action, server); + } else { + return util.assertNever(action); + } +}; diff --git a/server/src/ts/action/game-action/player/submit.ts b/server/src/ts/action/game-action/player/submit.ts new file mode 100644 index 0000000..98a60d8 --- /dev/null +++ b/server/src/ts/action/game-action/player/submit.ts @@ -0,0 +1,86 @@ +import wu from "wu"; +import { Action } from "../../../action"; +import { IncorrectUserRoleError } from "../../../errors/action-execution-error"; +import { InvalidActionError } from "../../../errors/validation"; +import * as event from "../../../event"; +import * as playSubmitted from "../../../events/game-event/play-submitted"; +import * as card from "../../../games/cards/card"; +import * as play from "../../../games/cards/play"; +import { Play } from "../../../games/cards/play"; +import * as finishedPlaying from "../../../timeout/finished-playing"; +import * as gameAction from "../../game-action"; +import * as round from "../../../games/game/round"; + +/** + * A player plays a white card into a round. + */ +export interface Submit { + action: "Submit"; + play: card.Id[]; +} + +type NameType = "Submit"; +const name: NameType = "Submit"; + +/** + * Check if an action is a submit action. + * @param action The action to check. + */ +export const is = (action: Action): action is Submit => action.action === name; + +export const handle: gameAction.Handler = ( + auth, + lobby, + action, + server +) => { + const lobbyRound = lobby.game.round; + if (round.verifyStage(action, lobbyRound, "Playing")) { + const playId = play.id(); + const plays = lobbyRound.plays; + if (plays.find(play => play.playedBy === auth.uid)) { + throw new InvalidActionError("Already played into round."); + } + const playLength = action.play.length; + const slotCount = card.slotCount(lobbyRound.call); + if (playLength !== slotCount) { + throw new InvalidActionError( + "The play must have the same number of responses as the call " + + `has slots (expected ${slotCount}, got ${playLength}).` + ); + } + const player = lobby.game.players.get(auth.uid); + if (player === undefined) { + throw new IncorrectUserRoleError(action, "Spectator", "Player"); + } + const ids = new Set(action.play); + const extractedPlay: Play = []; + for (const playedId of ids) { + const played = player.hand.find(card => card.id === playedId); + if (played === undefined) { + throw new InvalidActionError( + "The given card doesn't exist or isn't in the player's hand." + ); + } + extractedPlay.push(played); + } + plays.push({ + id: playId, + play: extractedPlay, + playedBy: auth.uid, + revealed: false + }); + const events = [event.target(playSubmitted.of(auth.uid))]; + const timeouts = []; + const timeout = finishedPlaying.ifNeeded(lobbyRound); + if (timeout !== undefined) { + timeouts.push({ + timeout: timeout, + after: server.config.timeouts.nextRoundDelay + }); + } + return { lobby, events, timeouts }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/game-action/player/take-back.ts b/server/src/ts/action/game-action/player/take-back.ts new file mode 100644 index 0000000..40e89ae --- /dev/null +++ b/server/src/ts/action/game-action/player/take-back.ts @@ -0,0 +1,37 @@ +import { Action } from "../../../action"; +import { InvalidActionError } from "../../../errors/validation"; +import * as event from "../../../event"; +import * as playTakenBack from "../../../events/game-event/play-taken-back"; +import * as gameAction from "../../game-action"; +import * as round from "../../../games/game/round"; + +/** + * A player plays a white card into a round. + */ +export interface TakeBack { + action: "TakeBack"; +} + +type NameType = "TakeBack"; +const name: NameType = "TakeBack"; + +/** + * Check if an action is a take back action. + * @param action The action to check. + */ +export const is = (action: Action): action is TakeBack => + action.action === name; + +export const handle: gameAction.Handler = (auth, lobby, action) => { + if (round.verifyStage(action, lobby.game.round, "Playing")) { + const plays = lobby.game.round.plays; + const playIndex = plays.findIndex(play => play.playedBy === auth.uid); + if (playIndex < 0) { + throw new InvalidActionError("No play to take back."); + } + plays.splice(playIndex, 1); + return { lobby, events: [event.target(playTakenBack.of(auth.uid))] }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/game-action/redraw.ts b/server/src/ts/action/game-action/redraw.ts new file mode 100644 index 0000000..d0dfe4d --- /dev/null +++ b/server/src/ts/action/game-action/redraw.ts @@ -0,0 +1,48 @@ +import { Action } from "../../action"; +import { IncorrectUserRoleError } from "../../errors/action-execution-error"; +import { InvalidActionError } from "../../errors/validation"; +import * as event from "../../event"; +import * as handRedrawn from "../../events/game-event/hand-redrawn"; +import * as gameAction from "../game-action"; + +/** + * A player plays a white card into a round. + */ +export interface Redraw { + action: "Redraw"; +} + +type NameType = "Redraw"; +const name: NameType = "Redraw"; + +/** + * Check if an action is a take back action. + * @param action The action to check. + */ +export const is = (action: Action): action is Redraw => action.action === name; + +export const handle: gameAction.Handler = (auth, lobby, action) => { + const game = lobby.game; + const reboot = game.rules.houseRules.reboot; + if (reboot === undefined) { + throw new InvalidActionError("Redraw house rule not enabled."); + } + const cost = reboot.cost; + const player = game.players.get(auth.uid); + if (player === undefined) { + throw new IncorrectUserRoleError(action, "Spectator", "Player"); + } + if (player.score < cost) { + throw new InvalidActionError("Can't afford to redraw."); + } + player.score -= cost; + player.hand = game.decks.responses.replace(...player.hand); + return { + lobby, + events: event.targetByPlayer( + auth.uid, + handRedrawn.of(auth.uid, player.hand), + handRedrawn.censor + ) + }; +}; diff --git a/server/src/ts/action/handler.ts b/server/src/ts/action/handler.ts new file mode 100644 index 0000000..f7c80b1 --- /dev/null +++ b/server/src/ts/action/handler.ts @@ -0,0 +1,21 @@ +import { Action } from "../action"; +import { Change } from "../lobby/change"; +import { Lobby } from "../lobby"; +import * as token from "../user/token"; +import { ServerState } from "../server-state"; + +/** + * A handler for a given type of action where the lobby is customised. + * This can let us avoid making the same checks down the line. + */ +export type Custom
= ( + auth: token.Claims, + lobby: L, + action: A, + server: ServerState +) => Change; + +/** + * A handler for a given type of action. + */ +export type Handler = Custom; diff --git a/server/src/ts/action/initial/check-alive.ts b/server/src/ts/action/initial/check-alive.ts new file mode 100644 index 0000000..44a7e84 --- /dev/null +++ b/server/src/ts/action/initial/check-alive.ts @@ -0,0 +1,19 @@ +import { InvalidActionError } from "../../errors/validation"; +import { Token } from "../../user/token"; +import * as validation from "../validation.validator"; + +/** + * Previously obtained tokens to check the validity of. + */ +export interface CheckAlive { + tokens: Token[]; +} + +const _validateCheckAlive = validation.validate("CheckAlive"); +export const validate = (action: object): CheckAlive => { + try { + return _validateCheckAlive(action); + } catch (e) { + throw new InvalidActionError(e.message); + } +}; diff --git a/server/src/ts/action/initial/create-lobby.ts b/server/src/ts/action/initial/create-lobby.ts new file mode 100644 index 0000000..ca805d1 --- /dev/null +++ b/server/src/ts/action/initial/create-lobby.ts @@ -0,0 +1,26 @@ +import { InvalidActionError } from "../../errors/validation"; +import * as validation from "../validation.validator"; +import { RegisterUser } from "./register-user"; + +/** + * The details needed to create a new lobby. + */ +export interface CreateLobby { + /** + * The name of the lobby, if not given, will default to "Name's Game". + */ + name?: string; + /** + * The registration for the owner of the lobby. + */ + owner: RegisterUser; +} + +const _validateCreateLobby = validation.validate("CreateLobby"); +export const validate = (action: object): CreateLobby => { + try { + return _validateCreateLobby(action); + } catch (e) { + throw new InvalidActionError(e.message); + } +}; diff --git a/server/src/ts/action/initial/register-user.ts b/server/src/ts/action/initial/register-user.ts new file mode 100644 index 0000000..0a3e539 --- /dev/null +++ b/server/src/ts/action/initial/register-user.ts @@ -0,0 +1,26 @@ +import { InvalidActionError } from "../../errors/validation"; +import * as user from "../../user"; +import * as validation from "../validation.validator"; + +/** + * The details to register a new user for a lobby. + */ +export interface RegisterUser { + /** + * The name the user wishes to use. + */ + name: user.Name; + /** + * The lobby password, if there is one, this must be given and correct. + */ + password?: string; +} + +const _validateRegisterUser = validation.validate("RegisterUser"); +export const validate = (action: object): RegisterUser => { + try { + return _validateRegisterUser(action); + } catch (e) { + throw new InvalidActionError(e.message); + } +}; diff --git a/server/src/ts/action/privileged.ts b/server/src/ts/action/privileged.ts new file mode 100644 index 0000000..797b135 --- /dev/null +++ b/server/src/ts/action/privileged.ts @@ -0,0 +1,36 @@ +import wu from "wu"; +import { Action } from "../action"; +import { UnprivilegedError } from "../errors/action-execution-error"; +import * as util from "../util"; +import { Handler } from "./handler"; +import { Configure } from "./privileged/configure"; +import * as configure from "./privileged/configure"; +import * as startGame from "./privileged/start-game"; +import { StartGame } from "./privileged/start-game"; + +/** + * An action only a privileged user can perform. + */ +export type Privileged = Configure | StartGame; + +const possible = new Set([configure.is, startGame.is]); + +/** + * Check if an action is a configure action. + * @param action The action to check. + */ +export const is = (action: Action): action is Privileged => + wu(possible).some(is => is(action)); + +export const handle: Handler = (auth, lobby, action, server) => { + if (auth.pvg !== "Privileged") { + throw new UnprivilegedError(action); + } + if (configure.is(action)) { + return configure.handle(auth, lobby, action, server); + } else if (startGame.is(action)) { + return startGame.handle(auth, lobby, action, server); + } else { + return util.assertNever(action); + } +}; diff --git a/server/src/ts/action/privileged/configure.ts b/server/src/ts/action/privileged/configure.ts new file mode 100644 index 0000000..70ecf6e --- /dev/null +++ b/server/src/ts/action/privileged/configure.ts @@ -0,0 +1,69 @@ +import wu from "wu"; +import { Action } from "../../action"; +import { ConfigEditConflictError } from "../../errors/action-execution-error"; +import { Handler } from "../handler"; +import { ChangeDecks } from "./configure/change-decks"; +import * as changeDecks from "./configure/change-decks"; +import * as setHandSize from "./configure/set-hand-size"; +import { SetHandSize } from "./configure/set-hand-size"; +import * as setPassword from "./configure/set-password"; +import { SetPassword } from "./configure/set-password"; +import * as setScoreLimit from "./configure/set-score-limit"; +import { SetScoreLimit } from "./configure/set-score-limit"; +import * as changeHouseRule from "./configure/change-house-rule"; +import { ChangeHouseRule } from "./configure/change-house-rule"; + +/** + * An action to change the configuration of the lobby. + */ +export type Configure = + | SetPassword + | SetHandSize + | SetScoreLimit + | ChangeDecks + | ChangeHouseRule; + +const possible = new Set([ + setPassword.is, + setHandSize.is, + setScoreLimit.is, + changeDecks.is, + changeHouseRule.is +]); + +/** + * Check if an action is a configure action. + * @param action The action to check. + */ +export const is = (action: Action): action is Configure => + wu(possible).some(is => is(action)); + +/** + * A base for all config actions. + */ +export interface Base { + /** + * If the config version doesn't match this, the operation will be rejected. + * This avoids users accidentally overwriting each other's changes. + */ + if: string; +} + +export const handle: Handler = (auth, lobby, action, config) => { + const version = lobby.config.version.toString(); + if (action.if !== version) { + throw new ConfigEditConflictError(action, action.if, version); + } + switch (action.action) { + case "SetPassword": + return setPassword.handle(auth, lobby, action, config); + case "SetHandSize": + return setHandSize.handle(auth, lobby, action, config); + case "SetScoreLimit": + return setScoreLimit.handle(auth, lobby, action, config); + case "ChangeDecks": + return changeDecks.handle(auth, lobby, action, config); + case "ChangeHouseRule": + return changeHouseRule.handle(auth, lobby, action, config); + } +}; diff --git a/server/src/ts/action/privileged/configure/change-decks.ts b/server/src/ts/action/privileged/configure/change-decks.ts new file mode 100644 index 0000000..340c5b0 --- /dev/null +++ b/server/src/ts/action/privileged/configure/change-decks.ts @@ -0,0 +1,72 @@ +import { Action } from "../../../action"; +import * as event from "../../../event"; +import { DecksChanged } from "../../../events/lobby-event/configured/decks-changed"; +import * as source from "../../../games/cards/source"; +import * as sources from "../../../games/cards/sources"; +import * as config from "../../../lobby/config"; +import { LoadDeckSummary } from "../../../task/load-deck-summary"; +import { Handler } from "../../handler"; +import * as configure from "../configure"; + +/** + * Make a change to the configuration of decks for the lobby. + */ +export interface ChangeDecks extends configure.Base { + action: NameType; + deck: source.External; + change: config.PlayerDriven; +} + +type NameType = "ChangeDecks"; +const name: NameType = "ChangeDecks"; + +/** + * Check if an action is an change decks action. + * @param action The action to check. + */ +export const is = (action: Action): action is ChangeDecks => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + const config = lobby.config; + const version = config.version + 1; + let change: undefined | config.PlayerDriven = undefined; + const deckSource = action.deck; + deckSource.playCode = deckSource.playCode.toUpperCase(); + const resolver = sources.limitedResolver(deckSource); + switch (action.change) { + case "Add": + if ( + config.decks.find(deck => resolver.equals(deck.source)) === undefined + ) { + change = "Add"; + config.decks.push({ source: deckSource }); + } + break; + case "Remove": + const index = config.decks.findIndex(deck => + resolver.equals(deck.source) + ); + if (index > -1) { + change = "Remove"; + config.decks.splice(index, 1); + } + break; + } + if (change === undefined) { + return {}; + } else { + config.version = version; + const decksChanged: DecksChanged = { + event: "DecksChanged", + version: version.toString(), + deck: deckSource, + change: change + }; + return { + lobby, + events: [event.target(decksChanged)], + tasks: [new LoadDeckSummary(auth.gc, deckSource)] + }; + } +}; diff --git a/server/src/ts/action/privileged/configure/change-house-rule.ts b/server/src/ts/action/privileged/configure/change-house-rule.ts new file mode 100644 index 0000000..3670d5a --- /dev/null +++ b/server/src/ts/action/privileged/configure/change-house-rule.ts @@ -0,0 +1,47 @@ +import { Action } from "../../../action"; +import * as event from "../../../event"; +import * as rules from "../../../games/rules"; +import { Handler } from "../../handler"; +import * as configure from "../configure"; +import * as houseRuleChanged from "../../../events/lobby-event/configured/house-rule-changed"; + +/** + * Set the hand size for the lobby. + */ +export interface ChangeHouseRule extends configure.Base { + action: NameType; + change: rules.Change; +} + +type NameType = "ChangeHouseRule"; +const name: NameType = "ChangeHouseRule"; + +/** + * Check if an action is an change decks action. + * @param action The action to check. + */ +export const is = (action: Action): action is ChangeHouseRule => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + const houseRules = lobby.config.rules.houseRules; + switch (action.change.houseRule) { + case "PackingHeat": + houseRules.packingHeat = action.change.settings; + break; + case "Rando": + houseRules.rando = action.change.settings; + break; + case "Reboot": + houseRules.reboot = action.change.settings; + break; + } + lobby.config.version += 1; + + return { + lobby, + events: [ + event.target(houseRuleChanged.of(action.change, lobby.config.version)) + ] + }; +}; diff --git a/server/src/ts/action/privileged/configure/set-hand-size.ts b/server/src/ts/action/privileged/configure/set-hand-size.ts new file mode 100644 index 0000000..c60c1ab --- /dev/null +++ b/server/src/ts/action/privileged/configure/set-hand-size.ts @@ -0,0 +1,47 @@ +import { Action } from "../../../action"; +import * as event from "../../../event"; +import { Handler } from "../../handler"; +import * as configure from "../configure"; + +/** + * Set the hand size for the lobby. + */ +export interface SetHandSize extends configure.Base { + action: NameType; + /** + * The number of cards in each player's hand. + * @TJS-type integer + * @minimum 3 + */ + handSize: number; +} + +type NameType = "SetHandSize"; +const name: NameType = "SetHandSize"; + +/** + * Check if an action is an change decks action. + * @param action The action to check. + */ +export const is = (action: Action): action is SetHandSize => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + const config = lobby.config; + if (config.rules.handSize !== action.handSize) { + config.rules.handSize = action.handSize; + config.version += 1; + return { + lobby, + events: [ + event.target({ + event: "HandSizeSet", + handSize: action.handSize, + version: config.version.toString() + }) + ] + }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/privileged/configure/set-password.ts b/server/src/ts/action/privileged/configure/set-password.ts new file mode 100644 index 0000000..beafca0 --- /dev/null +++ b/server/src/ts/action/privileged/configure/set-password.ts @@ -0,0 +1,50 @@ +import { Action } from "../../../action"; +import * as event from "../../../event"; +import * as passwordSet from "../../../events/lobby-event/configured/password-set"; +import { PasswordSet } from "../../../events/lobby-event/configured/password-set"; +import { Handler } from "../../handler"; +import * as configure from "../configure"; + +/** + * Set (or unset) the password for the lobby. + */ +export interface SetPassword extends configure.Base { + action: NameType; + /** + * @maxLength 100 + * @minLength 1 + */ + password?: string; +} + +type NameType = "SetPassword"; +const name: NameType = "SetPassword"; + +/** + * Check if an action is an change decks action. + * @param action The action to check. + */ +export const is = (action: Action): action is SetPassword => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + const config = lobby.config; + if (action.password !== config.password) { + const version = config.version + 1; + const resultEvent: PasswordSet = { + event: "PasswordSet", + version: version.toString() + }; + if (action.password !== undefined) { + config.password = action.password; + resultEvent.password = action.password; + } else { + delete config.password; + } + config.version = version; + const events = event.targetByPrivilege(resultEvent, passwordSet.censor); + return { lobby, events: events }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/privileged/configure/set-score-limit.ts b/server/src/ts/action/privileged/configure/set-score-limit.ts new file mode 100644 index 0000000..c8e87cc --- /dev/null +++ b/server/src/ts/action/privileged/configure/set-score-limit.ts @@ -0,0 +1,50 @@ +import { Action } from "../../../action"; +import * as event from "../../../event"; +import { ScoreLimitSet } from "../../../events/lobby-event/configured/score-limit-set"; +import { Handler } from "../../handler"; +import * as configure from "../configure"; + +/** + * (Un)Set the score limit for the lobby. + */ +export interface SetScoreLimit extends configure.Base { + action: NameType; + /** + * The score threshold for the game - when a player hits this they win. + * If not set, then there is end - the game goes on infinitely. + * @TJS-type integer + * @minimum 1 + */ + scoreLimit?: number; +} + +type NameType = "SetScoreLimit"; +const name: NameType = "SetScoreLimit"; + +/** + * Check if an action is an change decks action. + * @param action The action to check. + */ +export const is = (action: Action): action is SetScoreLimit => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + const config = lobby.config; + if (config.rules.scoreLimit !== action.scoreLimit) { + const version = config.version + 1; + const scoreLimitSet: ScoreLimitSet = { + event: "ScoreLimitSet", + version: version.toString() + }; + if (action.scoreLimit !== undefined) { + config.rules.scoreLimit = action.scoreLimit; + scoreLimitSet.scoreLimit = action.scoreLimit; + } else { + delete config.rules.scoreLimit; + } + config.version = version; + return { lobby, events: [event.target(scoreLimitSet)] }; + } else { + return {}; + } +}; diff --git a/server/src/ts/action/privileged/start-game.ts b/server/src/ts/action/privileged/start-game.ts new file mode 100644 index 0000000..3976005 --- /dev/null +++ b/server/src/ts/action/privileged/start-game.ts @@ -0,0 +1,27 @@ +import { Action } from "../../action"; +import { StartGame as StartGameTask } from "../../task/start-game"; +import { Handler } from "../handler"; + +/** + * Start a game in the lobby if possible. + */ +export interface StartGame { + action: NameType; +} + +type NameType = "StartGame"; +const name: NameType = "StartGame"; + +export const is = (action: Action): action is StartGame => + action.action === name; + +export const handle: Handler = (auth, lobby, action) => { + // If a game is already started, two uses probably hit the button at the same + // time. Harmless and no correction needed, so just drop it. + if (lobby.game !== undefined) { + return {}; + } + return { + tasks: [new StartGameTask(auth.gc, lobby.config.decks.map(s => s.source))] + }; +}; diff --git a/server/src/ts/action/validation.ts b/server/src/ts/action/validation.ts new file mode 100644 index 0000000..edf431c --- /dev/null +++ b/server/src/ts/action/validation.ts @@ -0,0 +1,9 @@ +import { Action as ActionType } from "../action"; +import { CreateLobby as CreateLobbyType } from "../action/initial/create-lobby"; +import { RegisterUser as RegisterUserType } from "../action/initial/register-user"; +import { CheckAlive as CheckAliveType } from "../action/initial/check-alive"; + +export type Action = ActionType; +export type CreateLobby = CreateLobbyType; +export type RegisterUser = RegisterUserType; +export type CheckAlive = CheckAliveType; diff --git a/server/src/ts/action/validation.validator.ts b/server/src/ts/action/validation.validator.ts new file mode 100644 index 0000000..b6aa457 --- /dev/null +++ b/server/src/ts/action/validation.validator.ts @@ -0,0 +1,529 @@ +/* eslint-disable */ +/* tslint:disable */ +// generated by typescript-json-validator +import Ajv = require("ajv"); +import { RegisterUser, CreateLobby, Action, CheckAlive } from "./validation"; +export const ajv = new Ajv({ + allErrors: true, + coerceTypes: false, + format: "fast", + nullable: true, + unicode: true, + uniqueItems: true, + useDefaults: true +}); + +ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + +export { RegisterUser, CreateLobby, Action, CheckAlive }; +export const Schema = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { + Action: { + anyOf: [ + { + $ref: "#/definitions/Judge" + }, + { + $ref: "#/definitions/Reveal" + }, + { + $ref: "#/definitions/Authenticate" + }, + { + $ref: "#/definitions/Submit" + }, + { + $ref: "#/definitions/TakeBack" + }, + { + $ref: "#/definitions/Redraw" + }, + { + $ref: "#/definitions/ChangeDecks" + }, + { + $ref: "#/definitions/SetHandSize" + }, + { + $ref: "#/definitions/SetPassword" + }, + { + $ref: "#/definitions/SetScoreLimit" + }, + { + $ref: "#/definitions/ChangeHouseRule" + }, + { + $ref: "#/definitions/StartGame" + } + ] + }, + Authenticate: { + defaultProperties: [], + description: "Authenticate with the game.", + properties: { + action: { + $ref: "#/definitions/NameType_2" + }, + token: { + $ref: "#/definitions/Token" + } + }, + required: ["action", "token"], + type: "object" + }, + Cardcast: { + defaultProperties: [], + description: "A source for Cardcast.", + properties: { + playCode: { + $ref: "#/definitions/PlayCode" + }, + source: { + enum: ["Cardcast"], + type: "string" + } + }, + required: ["playCode", "source"], + type: "object" + }, + Change: { + anyOf: [ + { + $ref: '#/definitions/ChangeBase<"PackingHeat",PackingHeat>' + }, + { + $ref: '#/definitions/ChangeBase<"Rando",Rando>' + }, + { + $ref: '#/definitions/ChangeBase<"Reboot",Reboot>' + } + ] + }, + "ChangeBase.HouseRule": { + $ref: "#/definitions/PackingHeat" + }, + "ChangeBase.HouseRule_1": { + $ref: "#/definitions/Rando" + }, + "ChangeBase.HouseRule_2": { + $ref: "#/definitions/Reboot" + }, + "ChangeBase.Name": { + enum: ["PackingHeat"], + type: "string" + }, + "ChangeBase.Name_1": { + enum: ["Rando"], + type: "string" + }, + "ChangeBase.Name_2": { + enum: ["Reboot"], + type: "string" + }, + 'ChangeBase<"PackingHeat",PackingHeat>': { + defaultProperties: [], + properties: { + houseRule: { + $ref: "#/definitions/ChangeBase.Name" + }, + settings: { + $ref: "#/definitions/ChangeBase.HouseRule" + } + }, + required: ["houseRule"], + type: "object" + }, + 'ChangeBase<"Rando",Rando>': { + defaultProperties: [], + properties: { + houseRule: { + $ref: "#/definitions/ChangeBase.Name_1" + }, + settings: { + $ref: "#/definitions/ChangeBase.HouseRule_1" + } + }, + required: ["houseRule"], + type: "object" + }, + 'ChangeBase<"Reboot",Reboot>': { + defaultProperties: [], + properties: { + houseRule: { + $ref: "#/definitions/ChangeBase.Name_2" + }, + settings: { + $ref: "#/definitions/ChangeBase.HouseRule_2" + } + }, + required: ["houseRule"], + type: "object" + }, + ChangeDecks: { + defaultProperties: [], + description: "Make a change to the configuration of decks for the lobby.", + properties: { + action: { + $ref: "#/definitions/NameType_3" + }, + change: { + $ref: "#/definitions/PlayerDriven" + }, + deck: { + $ref: "#/definitions/External" + }, + if: { + description: + "If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.", + type: "string" + } + }, + required: ["action", "change", "deck", "if"], + type: "object" + }, + ChangeHouseRule: { + defaultProperties: [], + description: "Set the hand size for the lobby.", + properties: { + action: { + $ref: "#/definitions/NameType_7" + }, + change: { + $ref: "#/definitions/Change" + }, + if: { + description: + "If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.", + type: "string" + } + }, + required: ["action", "change", "if"], + type: "object" + }, + CheckAlive: { + defaultProperties: [], + description: "Previously obtained tokens to check the validity of.", + properties: { + tokens: { + items: { + type: "string" + }, + type: "array" + } + }, + required: ["tokens"], + type: "object" + }, + CreateLobby: { + defaultProperties: [], + description: "The details needed to create a new lobby.", + properties: { + name: { + description: + 'The name of the lobby, if not given, will default to "Name\'s Game".', + type: "string" + }, + owner: { + $ref: "#/definitions/RegisterUser", + description: "The registration for the owner of the lobby." + } + }, + required: ["owner"], + type: "object" + }, + External: { + $ref: "#/definitions/Cardcast", + description: "A source for Cardcast." + }, + Id: { + description: "A unique id for a play.", + format: "uuid", + type: "string" + }, + Judge: { + defaultProperties: [], + description: "A user declares the winning play for a round.", + properties: { + action: { + $ref: "#/definitions/NameType" + }, + winner: { + $ref: "#/definitions/Id" + } + }, + required: ["action", "winner"], + type: "object" + }, + Name: { + description: "The name the user goes by.", + maxLength: 100, + minLength: 1, + type: "string" + }, + NameType: { + enum: ["Judge"], + type: "string" + }, + NameType_1: { + enum: ["Reveal"], + type: "string" + }, + NameType_2: { + enum: ["Authenticate"], + type: "string" + }, + NameType_3: { + enum: ["ChangeDecks"], + type: "string" + }, + NameType_4: { + enum: ["SetHandSize"], + type: "string" + }, + NameType_5: { + enum: ["SetPassword"], + type: "string" + }, + NameType_6: { + enum: ["SetScoreLimit"], + type: "string" + }, + NameType_7: { + enum: ["ChangeHouseRule"], + type: "string" + }, + NameType_8: { + enum: ["StartGame"], + type: "string" + }, + PackingHeat: { + defaultProperties: [], + description: 'Configuration for the "Packing Heat" house rule.', + type: "object" + }, + PlayCode: { + description: "A Cardcast play code for a deck.", + type: "string" + }, + PlayerDriven: { + enum: ["Add", "Remove"], + type: "string" + }, + Rando: { + defaultProperties: [], + properties: { + number: { + description: "The number of AI players to add to the game.", + maximum: 10, + minimum: 1, + type: "integer" + } + }, + required: ["number"], + type: "object" + }, + Reboot: { + defaultProperties: [], + description: + 'Configuration for the "Reboot the Universe" house rule.\nThis rule allows players to draw a new hand by sacrificing a given number\nof points.', + properties: { + cost: { + description: "The cost to redrawing.", + maximum: 50, + minimum: 1, + type: "integer" + } + }, + required: ["cost"], + type: "object" + }, + Redraw: { + defaultProperties: [], + description: "A player plays a white card into a round.", + properties: { + action: { + enum: ["Redraw"], + type: "string" + } + }, + required: ["action"], + type: "object" + }, + RegisterUser: { + defaultProperties: [], + description: "The details to register a new user for a lobby.", + properties: { + name: { + $ref: "#/definitions/Name", + description: "The name the user wishes to use." + }, + password: { + description: + "The lobby password, if there is one, this must be given and correct.", + type: "string" + } + }, + required: ["name"], + type: "object" + }, + Reveal: { + defaultProperties: [], + description: "A user judges the winning play for a round.", + properties: { + action: { + $ref: "#/definitions/NameType_1" + }, + play: { + $ref: "#/definitions/Id" + } + }, + required: ["action", "play"], + type: "object" + }, + SetHandSize: { + defaultProperties: [], + description: "Set the hand size for the lobby.", + properties: { + action: { + $ref: "#/definitions/NameType_4" + }, + handSize: { + description: "The number of cards in each player's hand.", + minimum: 3, + type: "integer" + }, + if: { + description: + "If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.", + type: "string" + } + }, + required: ["action", "handSize", "if"], + type: "object" + }, + SetPassword: { + defaultProperties: [], + description: "Set (or unset) the password for the lobby.", + properties: { + action: { + $ref: "#/definitions/NameType_5" + }, + if: { + description: + "If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.", + type: "string" + }, + password: { + maxLength: 100, + minLength: 1, + type: "string" + } + }, + required: ["action", "if"], + type: "object" + }, + SetScoreLimit: { + defaultProperties: [], + description: "(Un)Set the score limit for the lobby.", + properties: { + action: { + $ref: "#/definitions/NameType_6" + }, + if: { + description: + "If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.", + type: "string" + }, + scoreLimit: { + description: + "The score threshold for the game - when a player hits this they win.\nIf not set, then there is end - the game goes on infinitely.", + minimum: 1, + type: "integer" + } + }, + required: ["action", "if"], + type: "object" + }, + StartGame: { + defaultProperties: [], + description: "Start a game in the lobby if possible.", + properties: { + action: { + $ref: "#/definitions/NameType_8" + } + }, + required: ["action"], + type: "object" + }, + Submit: { + defaultProperties: [], + description: "A player plays a white card into a round.", + properties: { + action: { + enum: ["Submit"], + type: "string" + }, + play: { + items: { + type: "string" + }, + type: "array" + } + }, + required: ["action", "play"], + type: "object" + }, + TakeBack: { + defaultProperties: [], + description: "A player plays a white card into a round.", + properties: { + action: { + enum: ["TakeBack"], + type: "string" + } + }, + required: ["action"], + type: "object" + }, + Token: { + description: "A token that contains the encoded claims of a user.", + type: "string" + } + } +}; +ajv.addSchema(Schema, "Schema"); +export function validate( + typeName: "RegisterUser" +): (value: unknown) => RegisterUser; +export function validate( + typeName: "CreateLobby" +): (value: unknown) => CreateLobby; +export function validate(typeName: "Action"): (value: unknown) => Action; +export function validate( + typeName: "CheckAlive" +): (value: unknown) => CheckAlive; +export function validate(typeName: string): (value: unknown) => any { + const validator: any = ajv.getSchema(`Schema#/definitions/${typeName}`); + return (value: unknown): any => { + if (!validator) { + throw new Error( + `No validator defined for Schema#/definitions/${typeName}` + ); + } + + const valid = validator(value); + + if (!valid) { + throw new Error( + "Invalid " + + typeName + + ": " + + ajv.errorsText(validator.errors, { dataVar: typeName }) + ); + } + + return value as any; + }; +} diff --git a/server/src/ts/cache.ts b/server/src/ts/cache.ts new file mode 100644 index 0000000..3934a1a --- /dev/null +++ b/server/src/ts/cache.ts @@ -0,0 +1,157 @@ +import * as serverConfig from "./config"; +import * as decks from "./games/cards/decks"; +import * as deckSource from "./games/cards/source"; +import * as logging from "./logging"; + +/** + * A tag is used to check if there is a need to refresh the data in the cache. + */ +export type Tag = string; + +/** + * A type that can have a tag attached to indicate freshness. + */ +export interface Tagged { + /** + * A tag which should change if the underlying templates change. + */ + tag?: Tag; +} + +/** + * The age of the cached value. + */ +export type Age = number; + +/** + * A cached item with it's age. + */ +export interface Aged { + cached: T; + age: Age; +} + +/** + * A cache for data that is stored for efficiency, but can be retrieved again. + */ +export abstract class Cache { + /** + * The configuration for this cache. + */ + public abstract readonly config: serverConfig.Cache; + + private async get( + source: deckSource.Resolver, + getCachedAlways: ( + source: deckSource.Resolver + ) => Promise | undefined>, + cacheAlways: (source: deckSource.Resolver, value: Always) => Promise, + cacheSometimes: ( + source: deckSource.Resolver, + value: Sometimes + ) => Promise, + extract: (result: Result) => [Always, Sometimes | undefined], + miss: () => Promise + ): Promise { + const cached = await getCachedAlways(source); + if (cached !== undefined && !(await this.cacheExpired(source, cached))) { + return cached.cached; + } else { + const [always, sometimes] = extract(await miss()); + await cacheAlways(source, always); + if (sometimes !== undefined) { + cacheSometimes(source, sometimes).catch(error => + logging.logException("Error while caching:", error) + ); + } + return always; + } + } + + private async cacheExpired( + source: deckSource.Resolver, + cached: Aged + ): Promise { + if ( + cached.age >= Date.now() + this.config.checkAfter && + cached.cached.tag !== undefined + ) { + return (await source.getTag()) !== cached.cached.tag; + } + return false; + } + + /** + * Get the summary from the cache, using the miss function to populate the + * cache if missing. + * @param source The resolver for the source being cached. + * @param miss The function to actively get the summary from the source. + * Note this function can also give the templates if efficient + * to do so. + */ + public async getSummary( + source: deckSource.Resolver, + miss: () => Promise + ): Promise { + return this.get( + source, + s => this.getCachedSummary(s), + (s, summary) => this.cacheSummary(s, summary), + (s, templates: decks.Templates) => this.cacheTemplates(s, templates), + result => [result.summary, result.templates], + miss + ); + } + + /** + * Get the templates from the cache, using the miss function to populate the + * cache if missing. + * @param source The resolver for the source being cached. + * @param miss The function to actively get the templates from the source. + * Note this function can also give the summary if efficient + * to do so. + */ + public async getTemplates( + source: deckSource.Resolver, + miss: () => Promise + ): Promise { + return this.get( + source, + s => this.getCachedTemplates(s), + (s, templates) => this.cacheTemplates(s, templates), + (s, summary: deckSource.Summary) => this.cacheSummary(s, summary), + result => [result.templates, result.summary], + miss + ); + } + + /** + * Get the given summary from the cache. + */ + public abstract async getCachedSummary( + source: deckSource.Resolver + ): Promise | undefined>; + + /** + * Store the given summary in the cache. + */ + public abstract async cacheSummary( + source: deckSource.Resolver, + summary: deckSource.Summary + ): Promise; + + /** + * Get the given deck templates from the cache. + */ + public abstract async getCachedTemplates( + source: deckSource.Resolver + ): Promise | undefined>; + + /** + * Store the given deck templates in the cache. + */ + public abstract async cacheTemplates( + source: deckSource.Resolver, + templates: decks.Templates + ): Promise; +} diff --git a/server/src/ts/caches.ts b/server/src/ts/caches.ts new file mode 100644 index 0000000..4cd6db2 --- /dev/null +++ b/server/src/ts/caches.ts @@ -0,0 +1,13 @@ +import * as serverConfig from "./config"; +import { Cache } from "./cache"; +import { InMemoryCache } from "./caches/in-memory"; +//import { PostgresStore } from "./caches/postgres"; + +export async function from(config: serverConfig.Cache): Promise { + switch (config.type) { + case "InMemory": + return new InMemoryCache(config); + case "PostgreSQL": + throw new Error("Not Implemented"); + } +} diff --git a/server/src/ts/caches/in-memory.ts b/server/src/ts/caches/in-memory.ts new file mode 100644 index 0000000..b8f99be --- /dev/null +++ b/server/src/ts/caches/in-memory.ts @@ -0,0 +1,61 @@ +import * as cache from "../cache"; +import { Cache } from "../cache"; +import * as config from "../config"; +import * as decks from "../games/cards/decks"; +import * as source from "../games/cards/source"; + +/** + * An in-memory cache. + */ +export class InMemoryCache extends Cache { + public readonly config: config.InMemoryCache; + private readonly cache: { + summaries: Map<[string, string], cache.Aged>; + templates: Map<[string, string], cache.Aged>; + }; + + public constructor(config: config.InMemoryCache) { + super(); + this.config = config; + this.cache = { + summaries: new Map(), + templates: new Map() + }; + } + + private static key(source: source.Resolver): [string, string] { + return [source.id(), source.deckId()]; + } + + public async cacheSummary( + source: source.Resolver, + summary: source.Summary + ): Promise { + this.cache.summaries.set(InMemoryCache.key(source), { + cached: summary, + age: Date.now() + }); + } + + public async cacheTemplates( + source: source.Resolver, + templates: decks.Templates + ): Promise { + this.cache.templates.set(InMemoryCache.key(source), { + cached: templates, + age: Date.now() + }); + } + + public async getCachedSummary( + source: source.Resolver + ): Promise | undefined> { + return this.cache.summaries.get(InMemoryCache.key(source)); + } + + public async getCachedTemplates( + source: source.Resolver + ): Promise | undefined> { + return this.cache.templates.get(InMemoryCache.key(source)); + } +} diff --git a/server/src/ts/config.ts b/server/src/ts/config.ts new file mode 100644 index 0000000..70514ea --- /dev/null +++ b/server/src/ts/config.ts @@ -0,0 +1,105 @@ +import moment from "moment"; +import * as util from "./util"; + +type Duration = UnparsedDuration | ParsedDuration; +type UnparsedDuration = string; +type ParsedDuration = number; + +export interface Config { + secret: string; + port: number; + basePath: string; + version: string; + timeouts: Timeouts; + storage: BaseStorage; + cache: BaseCache; +} + +export type Parsed = Config; +export type Unparsed = Config; + +type Timeouts = { + timeoutCheckFrequency: D; + disconnectionGracePeriod: D; + nextRoundDelay: D; +} & { [key: string]: D }; + +type BaseStorage = BaseInMemory | BasePostgreSQL; +export type Storage = BaseStorage; + +export interface PostgreSqlConnection { + host?: string; + port?: number; + user?: string; + database?: string; + password?: string; + keepAlive?: boolean; +} + +interface StorageBase { + type: string; + garbageCollectionFrequency: D; +} + +interface BaseInMemory extends StorageBase { + type: "InMemory"; +} +export type InMemory = BaseInMemory; + +interface BasePostgreSQL extends StorageBase { + type: "PostgreSQL"; + connection: PostgreSqlConnection; +} +export type PostgreSQL = BasePostgreSQL; + +type BaseCache = + | BaseInMemoryCache + | BasePostgreSQLCache; +export type Cache = BaseCache; + +interface CacheBase { + type: string; + checkAfter: D; +} + +interface BaseInMemoryCache extends CacheBase { + type: "InMemory"; +} +export type InMemoryCache = BaseInMemoryCache; + +interface BasePostgreSQLCache extends CacheBase { + type: "PostgreSQL"; + connection: PostgreSqlConnection; +} +export type PostgreSQLCache = BasePostgreSQL; + +const parseDuration = (unparsed: UnparsedDuration): ParsedDuration => + moment.duration(unparsed).asMilliseconds(); + +export const parseStorage = ( + storage: BaseStorage +): BaseStorage => ({ + ...storage, + garbageCollectionFrequency: parseDuration(storage.garbageCollectionFrequency) +}); + +export const parseCache = ( + cache: BaseCache +): BaseCache => ({ + ...cache, + checkAfter: parseDuration(cache.checkAfter) +}); + +export const parseTimeouts = ( + timeouts: Timeouts +): Timeouts => + util.mapObjectValues(timeouts, (key: string, value: UnparsedDuration) => + parseDuration(value) + ); + +export const parse = (config: Unparsed): Parsed => ({ + ...config, + timeouts: parseTimeouts(config.timeouts), + storage: parseStorage(config.storage), + cache: parseCache(config.cache) +}); diff --git a/server/src/ts/errors.ts b/server/src/ts/errors.ts new file mode 100644 index 0000000..49b1a7f --- /dev/null +++ b/server/src/ts/errors.ts @@ -0,0 +1,15 @@ +/** + * The base for JSON payload for errors. + */ +export interface Details { + error: string; +} + +/** + * An error specific to Massive Decks. These can be sent as JSON loads or HTTP + * responses. + */ +export abstract class MassiveDecksError extends Error { + public abstract readonly status: number; + public abstract details(): T; +} diff --git a/server/src/ts/errors/action-execution-error.ts b/server/src/ts/errors/action-execution-error.ts new file mode 100644 index 0000000..de339d0 --- /dev/null +++ b/server/src/ts/errors/action-execution-error.ts @@ -0,0 +1,170 @@ +import HttpStatus from "http-status-codes"; +import { Action } from "../action"; +import { GameAction } from "../action/game-action"; +import { Privileged } from "../action/privileged"; +import * as errors from "../errors"; +import * as round from "../games/game/round"; +import * as player from "../games/player"; +import * as user from "../user"; + +abstract class ActionExecutionError extends errors.MassiveDecksError< + errors.Details +> { + public readonly status: number = HttpStatus.BAD_REQUEST; + public readonly action: Action; + + protected constructor(message: string, action: Action) { + super(message); + this.action = action; + Error.captureStackTrace(this, ActionExecutionError); + } +} + +// Could happen if the game ends. +export class GameNotStartedError extends ActionExecutionError { + public constructor(action: GameAction) { + super( + `The game must be started for this action:\n ${JSON.stringify(action)}`, + action + ); + Error.captureStackTrace(this, GameNotStartedError); + } + + public details = (): errors.Details => ({ + error: "GameNotStarted" + }); +} + +// Could happen if the user has privileges removed. +export class UnprivilegedError extends ActionExecutionError { + public readonly status = HttpStatus.FORBIDDEN; + + public constructor(action: Privileged) { + super( + `The user does not have the privilege to perform this action:\n` + + `${JSON.stringify(action)}`, + action + ); + Error.captureStackTrace(this, UnprivilegedError); + } + + public details = (): errors.Details => ({ + error: "Unprivileged" + }); +} + +interface IncorrectPlayerRoleDetails extends errors.Details { + role: player.Role; + expected: player.Role; +} + +// Could happen if the round changes unexpectedly (e.g: czar leaves game). +export class IncorrectPlayerRoleError extends ActionExecutionError { + public readonly role: player.Role; + public readonly expected: player.Role; + + public constructor(action: Action, role: player.Role, expected: player.Role) { + super( + `For this action the player must be ${expected} but is ${role}:\n` + + `${JSON.stringify(action)}`, + action + ); + this.role = role; + this.expected = expected; + Error.captureStackTrace(this, UnprivilegedError); + } + + public details = (): IncorrectPlayerRoleDetails => ({ + error: "IncorrectPlayerRole", + role: this.role, + expected: this.expected + }); +} + +interface IncorrectUserRoleDetails extends errors.Details { + role: user.Role; + expected: user.Role; +} + +// Could happen if the user's role changes. +export class IncorrectUserRoleError extends ActionExecutionError { + public readonly role: user.Role; + public readonly expected: user.Role; + + public constructor(action: Action, role: user.Role, expected: user.Role) { + super( + `For this action the user must be ${expected} but is ${role}:\n` + + `${JSON.stringify(action)}`, + action + ); + this.role = role; + this.expected = expected; + Error.captureStackTrace(this, IncorrectUserRoleError); + } + + public details = (): IncorrectUserRoleDetails => ({ + error: "IncorrectUserRole", + role: this.role, + expected: this.expected + }); +} + +interface IncorrectRoundStageDetails extends errors.Details { + stage: round.Stage; + expected: round.Stage; +} + +// Could happen if the round changes unexpectedly (e.g: czar leaves game). +export class IncorrectRoundStageError extends ActionExecutionError { + public readonly stage: round.Stage; + public readonly expected: round.Stage; + + public constructor( + action: Action, + stage: round.Stage, + expected: round.Stage + ) { + super( + `For this action the round must be in the ${expected} stage but is in ` + + `the ${stage} stage:\n ${JSON.stringify(action)}`, + action + ); + this.stage = stage; + this.expected = expected; + Error.captureStackTrace(this, IncorrectRoundStageError); + } + + public details = (): IncorrectRoundStageDetails => ({ + error: "IncorrectRoundStage", + stage: this.stage, + expected: this.expected + }); +} + +interface ConfigEditConflictDetails extends errors.Details { + version: string; + expected: string; +} + +// Could happen if two users edit the configuration at the same time. +export class ConfigEditConflictError extends ActionExecutionError { + public readonly version: string; + public readonly expected: string; + + public constructor(action: Action, version: string, expected: string) { + super( + `The configuration is at version ${expected}, but the client's edit ` + + `was made to version ${version}:\n ${JSON.stringify(action)}`, + action + ); + this.version = version; + this.expected = expected; + Error.captureStackTrace(this, ConfigEditConflictError); + } + + public details = (): ConfigEditConflictDetails => ({ + error: "ConfigEditConflict", + version: this.version, + expected: this.expected + }); +} diff --git a/server/src/ts/errors/authentication.ts b/server/src/ts/errors/authentication.ts new file mode 100644 index 0000000..3d81cd6 --- /dev/null +++ b/server/src/ts/errors/authentication.ts @@ -0,0 +1,82 @@ +import HttpStatus from "http-status-codes"; +import * as errors from "../errors"; + +export type Reason = + | "IncorrectIssuer" + | "NotAuthenticated" + | "AlreadyAuthenticated" + | "InvalidAuthentication" + | "InvalidLobbyPassword"; + +export interface Details extends errors.Details { + error: "AuthenticationFailure"; + reason: Reason; +} + +abstract class AuthenticationFailureError extends errors.MassiveDecksError< + Details +> { + public readonly status = HttpStatus.FORBIDDEN; + abstract readonly reason: Reason; + + protected constructor(reason: string) { + super(`Could not authenticate for the game, ${reason}.`); + Error.captureStackTrace(this, AuthenticationFailureError); + } + + public details = (): Details => ({ + error: "AuthenticationFailure", + reason: this.reason + }); +} + +// Could happen if the server database is lost. +export class IncorrectIssuerError extends AuthenticationFailureError { + public readonly reason = "IncorrectIssuer"; + + public constructor() { + super( + "the authentication was not for this server or the server data store " + + "has been wiped" + ); + Error.captureStackTrace(this, IncorrectIssuerError); + } +} + +export class NotAuthenticatedError extends AuthenticationFailureError { + public readonly reason = "NotAuthenticated"; + + public constructor() { + super("the player is not authenticated"); + Error.captureStackTrace(this, NotAuthenticatedError); + } +} + +export class AlreadyAuthenticatedError extends AuthenticationFailureError { + public readonly reason = "AlreadyAuthenticated"; + + public constructor() { + super("the player is already authenticated"); + Error.captureStackTrace(this, AlreadyAuthenticatedError); + } +} + +// Could happen if the server database is lost. +export class InvalidAuthenticationError extends AuthenticationFailureError { + public readonly reason = "InvalidAuthentication"; + + public constructor(reason: string) { + super(`the given authentication was not valid (${reason})`); + Error.captureStackTrace(this, InvalidAuthenticationError); + } +} + +// If the user gets the password wrong. +export class InvalidLobbyPasswordError extends AuthenticationFailureError { + public readonly reason = "InvalidLobbyPassword"; + + public constructor() { + super("the given lobby password was wrong"); + Error.captureStackTrace(this, InvalidLobbyPasswordError); + } +} diff --git a/server/src/ts/errors/game-state-error.ts b/server/src/ts/errors/game-state-error.ts new file mode 100644 index 0000000..1508c49 --- /dev/null +++ b/server/src/ts/errors/game-state-error.ts @@ -0,0 +1,24 @@ +import HttpStatus from "http-status-codes"; +import * as errors from "../errors"; + +abstract class GameStateError extends errors.MassiveDecksError { + public readonly status: number = HttpStatus.INTERNAL_SERVER_ERROR; + + protected constructor(message: string) { + super(message); + Error.captureStackTrace(this, GameStateError); + } +} + +// Can happen with calls with large numbers of slots or if the user selects +// decks with very few cards. +export class OutOfCardsError extends GameStateError { + public constructor() { + super("Ran out of cards in the game."); + Error.captureStackTrace(this, OutOfCardsError); + } + + public details = (): errors.Details => ({ + error: "OutOfCards" + }); +} diff --git a/server/src/ts/errors/lobby.ts b/server/src/ts/errors/lobby.ts new file mode 100644 index 0000000..27968c8 --- /dev/null +++ b/server/src/ts/errors/lobby.ts @@ -0,0 +1,51 @@ +import HttpStatus from "http-status-codes"; +import * as errors from "../errors"; +import { GameCode } from "../lobby/game-code"; + +export type Reason = "Closed" | "DoesNotExist"; + +export interface Details extends errors.Details { + reason: Reason; + gameCode: GameCode; +} + +// Could happen on user typo. +export abstract class LobbyNotFoundError extends errors.MassiveDecksError< + Details +> { + abstract readonly reason: Reason; + public readonly gameCode: GameCode; + + protected constructor(gameCode: GameCode, reason: string) { + super(`The lobby "${gameCode}" could not be found, ${reason}.`); + this.gameCode = gameCode; + Error.captureStackTrace(this, LobbyNotFoundError); + } + + public details: () => Details = () => ({ + error: "LobbyNotFound", + gameCode: this.gameCode, + reason: this.reason + }); +} + +// Could happen on user trying to come back to old game. +export class LobbyClosedError extends LobbyNotFoundError { + public readonly status = HttpStatus.GONE; + public readonly reason = "Closed"; + + public constructor(gameCode: GameCode) { + super(gameCode, "the lobby has been closed"); + Error.captureStackTrace(this, LobbyClosedError); + } +} + +export class LobbyDoesNotExistError extends LobbyNotFoundError { + public readonly status = HttpStatus.NOT_FOUND; + public readonly reason = "DoesNotExist"; + + public constructor(gameCode: GameCode) { + super(gameCode, "the lobby never existed"); + Error.captureStackTrace(this, LobbyDoesNotExistError); + } +} diff --git a/server/src/ts/errors/validation.ts b/server/src/ts/errors/validation.ts new file mode 100644 index 0000000..83be0ad --- /dev/null +++ b/server/src/ts/errors/validation.ts @@ -0,0 +1,22 @@ +import HttpStatus from "http-status-codes"; +import * as errors from "../errors"; + +export interface Details extends errors.Details { + reason: string; +} + +export class InvalidActionError extends errors.MassiveDecksError
{ + public readonly status = HttpStatus.BAD_REQUEST; + public readonly reason: string; + + public constructor(reason: string) { + super(`Bad request: ${reason}`); + this.reason = reason; + Error.captureStackTrace(this, InvalidActionError); + } + + public details: () => Details = () => ({ + error: "InvalidAction", + reason: this.reason + }); +} diff --git a/server/src/ts/event.ts b/server/src/ts/event.ts new file mode 100644 index 0000000..7ed2172 --- /dev/null +++ b/server/src/ts/event.ts @@ -0,0 +1,170 @@ +import WebSocket from "ws"; +import wu from "wu"; +import { GameEvent } from "./events/game-event"; +import { LobbyEvent } from "./events/lobby-event"; +import { UserEvent } from "./events/user-event"; +import { Game } from "./games/game"; +import * as player from "./games/player"; +import { Lobby } from "./lobby"; +import * as logging from "./logging"; +import { SocketManager } from "./socket-manager"; +import * as user from "./user"; +import { User } from "./user"; +import * as util from "./util"; + +/** + * An event send to clients to update them on the state of the game. + */ +export type Event = LobbyEvent | UserEvent | GameEvent; + +/** + * An event with who it should be sent to. + */ +export interface Targeted { + targets?: Set | ((id: user.Id, user: User) => boolean); + event: Event; +} + +/** + * Give the event a target of all users in the lobby that fulfil the given + * targeting predicate. If the predicate is not given, it will be sent to all. + * @param event The event. + * @param targets The function that determines if the event will be sent to + * a given user. + */ +export const target = ( + event: Event, + targets?: (id: user.Id, user: User) => boolean +): Targeted => ({ targets, event }); + +/** + * Give the event a target of the given users. + * @param event The event. + * @param targets The users to send the event to. + */ +export const targetSpecifically = ( + event: Event, + ...targets: user.Id[] +): Targeted => ({ targets: new Set(targets), event }); + +/** + * Target the given event by privilege. Sending only the event as given to + * privileged users, and optionally a censored version to unprivileged users. + * @param event + * @param censor + */ +export function* targetByPrivilege( + event: T, + censor?: (event: T) => T +): Iterable { + yield target(event, (_, user) => user.privilege === "Privileged"); + if (censor !== undefined) { + yield target(censor(event), (_, user) => user.privilege === "Unprivileged"); + } +} + +/** + * Target the given event by player role. Sending only the event as given to + * the czar, and optionally a censored version to other players. + * @param game The game at hand. + * @param event The event to send to the czar. + * @param censor The function to censor the event for players. + */ +export function* targetByPlayerRole( + game: Game, + event: T, + censor?: (event: T) => T +): Iterable { + yield target(event, (id, _) => player.role(game, id) === "Czar"); + if (censor !== undefined) { + yield target(event, (id, _) => player.role(game, id) === "Player"); + } +} + +/** + * Target the given event to only one player, but send a censored version to + * everyone else. + * @param player The player to send the initial event to. + * @param event The event to send. + * @param censor The function to censor the event for others. + */ +export function* targetByPlayer( + player: user.Id, + event: T, + censor: (event: T) => U +): Iterable { + yield targetSpecifically(event, player); + if (censor !== undefined) { + yield target(event, (id, _) => id !== player); + } +} + +function sendHelper( + sockets: Map, + serializedEvent: string, + event: Event +): (user: user.Id) => void { + return user => { + try { + const socket = sockets.get(user); + if (socket) { + socket.send(serializedEvent); + logging.logger.info("WebSocket send:", { + user: user, + event: event + }); + } + } catch (error) {} + }; +} + +/** + * Send the given events to the targets given with them. + * @param sockets The sockets to send with. + * @param lobby The lobby as context to send the events in. + * @param firstEvent The first event to send (there must be at least one). + * @param restEvents Any further events to send. + */ +export function send( + sockets: SocketManager, + lobby: Lobby, + firstEvent: Targeted, + ...restEvents: Targeted[] +): void; +/** + * Send the given events to the targets given with them. + * @param sockets The sockets to send with. + * @param lobby The lobby as context to send the events in. + * @param events An iterable of events with targets. + */ +export function send( + sockets: SocketManager, + lobby: Lobby, + events: Iterable +): void; +export function send( + sockets: SocketManager, + lobby: Lobby, + firstEvent: Targeted | Iterable, + ...restEvents: Targeted[] +): void { + const events = util.isIterable(firstEvent) + ? firstEvent + : [firstEvent, ...restEvents]; + for (const event of events) { + const sendToUser = sendHelper( + sockets.sockets, + JSON.stringify(event.event), + event.event + ); + const targets = event.targets; + (targets === undefined + ? wu(lobby.users.keys()) + : targets instanceof Function + ? wu(lobby.users.entries()) + .filter(([id, user]) => targets(id, user)) + .map(([id]) => id) + : targets + ).forEach(sendToUser); + } +} diff --git a/server/src/ts/events/game-event.ts b/server/src/ts/events/game-event.ts new file mode 100644 index 0000000..f2d8359 --- /dev/null +++ b/server/src/ts/events/game-event.ts @@ -0,0 +1,18 @@ +import { GameStarted } from "./game-event/game-started"; +import { HandRedrawn } from "./game-event/hand-redrawn"; +import { PlayRevealed } from "./game-event/play-revealed"; +import { PlaySubmitted } from "./game-event/play-submitted"; +import { PlayTakenBack } from "./game-event/play-taken-back"; +import { RoundFinished } from "./game-event/round-finished"; +import { RoundStarted } from "./game-event/round-started"; +import { StartRevealing } from "./game-event/start-revealing"; + +export type GameEvent = + | GameStarted + | StartRevealing + | RoundStarted + | RoundFinished + | PlaySubmitted + | PlayRevealed + | PlayTakenBack + | HandRedrawn; diff --git a/server/src/ts/events/game-event/game-started.ts b/server/src/ts/events/game-event/game-started.ts new file mode 100644 index 0000000..58e4e2c --- /dev/null +++ b/server/src/ts/events/game-event/game-started.ts @@ -0,0 +1,20 @@ +import * as card from "../../games/cards/card"; +import * as publicRound from "../../games/game/round/public"; + +/** + * Indicated a game has started in the lobby. + */ +export interface GameStarted { + event: "GameStarted"; + round: publicRound.Playing; + hand?: card.Response[]; +} + +export const of = ( + round: publicRound.Playing, + hand: card.Response[] +): GameStarted => ({ + event: "GameStarted", + round, + hand +}); diff --git a/server/src/ts/events/game-event/hand-redrawn.ts b/server/src/ts/events/game-event/hand-redrawn.ts new file mode 100644 index 0000000..a3e8cee --- /dev/null +++ b/server/src/ts/events/game-event/hand-redrawn.ts @@ -0,0 +1,25 @@ +import * as card from "../../games/cards/card"; +import * as user from "../../user"; + +/** + * Indicates a player has paid to redraw their hand under the Reboot house rule. + */ +export interface HandRedrawn extends Public { + hand?: card.Response[]; +} + +export interface Public { + event: "HandRedrawn"; + player: user.Id; +} + +export const of = (player: user.Id, hand: card.Response[]): HandRedrawn => ({ + event: "HandRedrawn", + player, + hand +}); + +export const censor = (event: HandRedrawn): Public => ({ + event: event.event, + player: event.player +}); diff --git a/server/src/ts/events/game-event/play-revealed.ts b/server/src/ts/events/game-event/play-revealed.ts new file mode 100644 index 0000000..0e3afdf --- /dev/null +++ b/server/src/ts/events/game-event/play-revealed.ts @@ -0,0 +1,17 @@ +import * as play from "../../games/cards/play"; +import { Play } from "../../games/cards/play"; + +/** + * Indicates the czar revealed a play for the round. + */ +export interface PlayRevealed { + event: "PlayRevealed"; + id: play.Id; + play: Play; +} + +export const of = (id: play.Id, play: Play): PlayRevealed => ({ + event: "PlayRevealed", + id, + play +}); diff --git a/server/src/ts/events/game-event/play-submitted.ts b/server/src/ts/events/game-event/play-submitted.ts new file mode 100644 index 0000000..d41ce6c --- /dev/null +++ b/server/src/ts/events/game-event/play-submitted.ts @@ -0,0 +1,14 @@ +import * as user from "../../user"; + +/** + * Indicates a player has submitted a play for the round. + */ +export interface PlaySubmitted { + event: "PlaySubmitted"; + by: user.Id; +} + +export const of = (by: user.Id): PlaySubmitted => ({ + event: "PlaySubmitted", + by +}); diff --git a/server/src/ts/events/game-event/play-taken-back.ts b/server/src/ts/events/game-event/play-taken-back.ts new file mode 100644 index 0000000..02961de --- /dev/null +++ b/server/src/ts/events/game-event/play-taken-back.ts @@ -0,0 +1,14 @@ +import * as user from "../../user"; + +/** + * Indicates a player has taken back their play for the round. + */ +export interface PlayTakenBack { + event: "PlayTakenBack"; + by: user.Id; +} + +export const of = (by: user.Id): PlayTakenBack => ({ + event: "PlayTakenBack", + by +}); diff --git a/server/src/ts/events/game-event/round-finished.ts b/server/src/ts/events/game-event/round-finished.ts new file mode 100644 index 0000000..743ab5e --- /dev/null +++ b/server/src/ts/events/game-event/round-finished.ts @@ -0,0 +1,18 @@ +import * as gameRound from "../../games/game/round"; +import * as user from "../../user"; + +/** + * Indicates players have finished playing into the round and now the czar + * should judge the winner. + */ +export interface RoundFinished { + event: "RoundFinished"; + winner: user.Id; + playedBy: { [id: string]: user.Id }; +} + +export const of = (round: gameRound.Complete): RoundFinished => ({ + event: "RoundFinished", + winner: round.winner, + playedBy: gameRound.playedBy(round) +}); diff --git a/server/src/ts/events/game-event/round-started.ts b/server/src/ts/events/game-event/round-started.ts new file mode 100644 index 0000000..e4741c0 --- /dev/null +++ b/server/src/ts/events/game-event/round-started.ts @@ -0,0 +1,30 @@ +import * as card from "../../games/cards/card"; +import * as user from "../../user"; +import * as round from "../../games/game/round"; + +/** + * Indicates a new round has started. + */ +export interface RoundStarted { + event: "RoundStarted"; + id: string; + czar: user.Id; + players: user.Id[]; + call: card.Call; + drawn?: card.Response[]; +} + +export const of = ( + id: round.Id, + czar: user.Id, + players: user.Id[], + call: card.Call, + drawn?: card.Response[] +): RoundStarted => ({ + event: "RoundStarted", + id: id.toString(), + czar, + players, + call, + ...(drawn === undefined ? {} : { drawn }) +}); diff --git a/server/src/ts/events/game-event/start-revealing.ts b/server/src/ts/events/game-event/start-revealing.ts new file mode 100644 index 0000000..c573be3 --- /dev/null +++ b/server/src/ts/events/game-event/start-revealing.ts @@ -0,0 +1,15 @@ +import * as play from "../../games/cards/play"; + +/** + * Indicates players have finished playing into the round and now the czar + * should reveal the plays. + */ +export interface StartRevealing { + event: "StartRevealing"; + plays: play.Id[]; +} + +export const of = (plays: play.Id[]): StartRevealing => ({ + event: "StartRevealing", + plays +}); diff --git a/server/src/ts/events/lobby-event.ts b/server/src/ts/events/lobby-event.ts new file mode 100644 index 0000000..26ef40b --- /dev/null +++ b/server/src/ts/events/lobby-event.ts @@ -0,0 +1,5 @@ +import { Configured } from "./lobby-event/configured"; +import { ConnectionChanged } from "./lobby-event/connection-changed"; +import { PresenceChanged } from "./lobby-event/presence-changed"; + +export type LobbyEvent = Configured | ConnectionChanged | PresenceChanged; diff --git a/server/src/ts/events/lobby-event/configured.ts b/server/src/ts/events/lobby-event/configured.ts new file mode 100644 index 0000000..3d648f2 --- /dev/null +++ b/server/src/ts/events/lobby-event/configured.ts @@ -0,0 +1,23 @@ +import { DecksChanged } from "./configured/decks-changed"; +import { HandSizeSet } from "./configured/hand-size-set"; +import { HouseRuleChanged } from "./configured/house-rule-changed"; +import { PasswordSet } from "./configured/password-set"; +import { ScoreLimitSet } from "./configured/score-limit-set"; + +/** + * An event for when connection state for a user changes. + */ +export type Configured = + | PasswordSet + | HandSizeSet + | ScoreLimitSet + | DecksChanged + | HouseRuleChanged; + +export interface Base { + event: string; + /** + * The version the config is at once this change is applied. + */ + version: string; +} diff --git a/server/src/ts/events/lobby-event/configured/decks-changed.ts b/server/src/ts/events/lobby-event/configured/decks-changed.ts new file mode 100644 index 0000000..3e4c39e --- /dev/null +++ b/server/src/ts/events/lobby-event/configured/decks-changed.ts @@ -0,0 +1,23 @@ +import * as source from "../../../games/cards/source"; +import * as config from "../../../lobby/config"; +import * as configured from "../configured"; + +/** + * A change was made to the configuration of decks for the lobby. + */ +export interface DecksChanged extends configured.Base { + event: "DecksChanged"; + deck: source.External; + change: config.DeckChange; +} + +export const of = ( + deck: source.External, + change: config.DeckChange, + version: config.Version +): DecksChanged => ({ + event: "DecksChanged", + deck, + change, + version: version.toString() +}); diff --git a/server/src/ts/events/lobby-event/configured/hand-size-set.ts b/server/src/ts/events/lobby-event/configured/hand-size-set.ts new file mode 100644 index 0000000..769ee49 --- /dev/null +++ b/server/src/ts/events/lobby-event/configured/hand-size-set.ts @@ -0,0 +1,9 @@ +import * as configured from "../configured"; + +/** + * The hand size in the rules for the lobby is changed. + */ +export interface HandSizeSet extends configured.Base { + event: "HandSizeSet"; + handSize: number; +} diff --git a/server/src/ts/events/lobby-event/configured/house-rule-changed.ts b/server/src/ts/events/lobby-event/configured/house-rule-changed.ts new file mode 100644 index 0000000..ac504a4 --- /dev/null +++ b/server/src/ts/events/lobby-event/configured/house-rule-changed.ts @@ -0,0 +1,20 @@ +import * as config from "../../../lobby/config"; +import * as configured from "../configured"; +import * as rules from "../../../games/rules"; + +/** + * A change was made to a house rule in the lobby. + */ +export interface HouseRuleChanged extends configured.Base { + event: "HouseRuleChanged"; + change: rules.Change; +} + +export const of = ( + change: rules.Change, + version: config.Version +): HouseRuleChanged => ({ + event: "HouseRuleChanged", + change, + version: version.toString() +}); diff --git a/server/src/ts/events/lobby-event/configured/password-set.ts b/server/src/ts/events/lobby-event/configured/password-set.ts new file mode 100644 index 0000000..cffa4e7 --- /dev/null +++ b/server/src/ts/events/lobby-event/configured/password-set.ts @@ -0,0 +1,15 @@ +import * as configured from "../configured"; + +/** + * The lobby password is (un)set. + */ +export interface PasswordSet extends configured.Base { + event: "PasswordSet"; + password?: string | boolean; +} + +export const censor = (passwordSet: PasswordSet): PasswordSet => ({ + event: "PasswordSet", + version: passwordSet.version, + password: passwordSet.password !== undefined +}); diff --git a/server/src/ts/events/lobby-event/configured/score-limit-set.ts b/server/src/ts/events/lobby-event/configured/score-limit-set.ts new file mode 100644 index 0000000..d9fb55e --- /dev/null +++ b/server/src/ts/events/lobby-event/configured/score-limit-set.ts @@ -0,0 +1,9 @@ +import * as configured from "../configured"; + +/** + * The score limit in the rules for the lobby is (un)set. + */ +export interface ScoreLimitSet extends configured.Base { + event: "ScoreLimitSet"; + scoreLimit?: number; +} diff --git a/server/src/ts/events/lobby-event/connection-changed.ts b/server/src/ts/events/lobby-event/connection-changed.ts new file mode 100644 index 0000000..665a3eb --- /dev/null +++ b/server/src/ts/events/lobby-event/connection-changed.ts @@ -0,0 +1,34 @@ +import * as user from "../../user"; + +/** + * An event for when connection state for a user changes. + */ +export type ConnectionChanged = Connected | Disconnected; + +interface Base { + user: user.Id; +} + +/** + * A user connects to the lobby. + */ +export interface Connected extends Base { + event: "Connected"; +} + +export const connected = (user: user.Id): Connected => ({ + event: "Connected", + user +}); + +/** + * A user disconnects from the lobby. + */ +export interface Disconnected extends Base { + event: "Disconnected"; +} + +export const disconnected = (user: user.Id): Disconnected => ({ + event: "Disconnected", + user +}); diff --git a/server/src/ts/events/lobby-event/presence-changed.ts b/server/src/ts/events/lobby-event/presence-changed.ts new file mode 100644 index 0000000..c274eee --- /dev/null +++ b/server/src/ts/events/lobby-event/presence-changed.ts @@ -0,0 +1,25 @@ +import * as user from "../../user"; + +/** + * An event for when connection state for a user changes. + */ +export type PresenceChanged = Joined | Left; + +interface Base { + user: user.Id; +} + +/** + * A user connects to the lobby. + */ +export interface Joined extends Base { + event: "Joined"; + name: user.Name; +} + +/** + * A user disconnects from the lobby. + */ +export interface Left extends Base { + event: "Left"; +} diff --git a/server/src/ts/events/user-event.ts b/server/src/ts/events/user-event.ts new file mode 100644 index 0000000..a1bc99e --- /dev/null +++ b/server/src/ts/events/user-event.ts @@ -0,0 +1,6 @@ +import { Sync } from "./user-event/sync"; + +/** + * An event that should be sent to all users. + */ +export type UserEvent = Sync; diff --git a/server/src/ts/events/user-event/sync.ts b/server/src/ts/events/user-event/sync.ts new file mode 100644 index 0000000..f48aaba --- /dev/null +++ b/server/src/ts/events/user-event/sync.ts @@ -0,0 +1,24 @@ +import * as card from "../../games/cards/card"; +import { Hand } from "../../games/cards/hand"; +import * as lobby from "../../lobby"; + +/** + * Synchronise the game state. + */ +export interface Sync { + event: "Sync"; + state: lobby.Public; + hand?: Hand; + play?: card.Id[]; +} + +export const of = ( + state: lobby.Public, + hand?: Hand, + play?: card.Id[] +): Sync => ({ + event: "Sync", + state, + ...(hand !== undefined ? { hand } : {}), + ...(play !== undefined ? { play } : {}) +}); diff --git a/server/src/ts/games/cards/card.ts b/server/src/ts/games/cards/card.ts new file mode 100644 index 0000000..2ea4b08 --- /dev/null +++ b/server/src/ts/games/cards/card.ts @@ -0,0 +1,73 @@ +import { v4 as uuid } from "uuid"; +import { Source } from "./source"; +import wu = require("wu"); + +/** + * A game card. + */ +export type Card = Call | Response; + +/** + * A call for plays. Some text with blank slots to be filled with responses. + */ +export interface Call extends BaseCard { + /** The text and slots on the call.*/ + parts: Part[][]; +} + +/** + * A response (some text) played into slots. + */ +export interface Response extends BaseCard { + /** The text on the response.*/ + text: string; +} + +/** A unique id for an instance of a card.*/ +export type Id = string; + +/** Values shared by all cards.*/ +export interface BaseCard { + /** A unique id for a card.*/ + id: Id; + /** Where the card came from.*/ + source: Source; +} + +/** An empty slot for responses to be played into.*/ +export interface Slot { + /** + * Defines a transformation over the content the slot is filled with. + */ + transform?: "UpperCase" | "Capitalize"; +} + +export const isSlot = (part: Part): part is Slot => typeof part !== "string"; + +/** Either text or a slot.*/ +export type Part = string | Slot; + +/** + * Create a new user id. + */ +export const id: () => Id = uuid; + +/** + * If the given card is a call. + */ +export const isCall = (card: Card): card is Call => + (card as Call).parts !== undefined; + +/** + * If the given card is a response. + */ +export const isResponse = (card: Card): card is Response => + (card as Response).text !== undefined; + +/** + * The number of slots the given call. + */ +export const slotCount = (call: Call): number => + wu(call.parts) + .flatten(true) + .reduce((count, part) => count + (isSlot(part) ? 1 : 0), 0); diff --git a/server/src/ts/games/cards/decks.ts b/server/src/ts/games/cards/decks.ts new file mode 100644 index 0000000..9794141 --- /dev/null +++ b/server/src/ts/games/cards/decks.ts @@ -0,0 +1,105 @@ +import * as cache from "../../cache"; +import { OutOfCardsError } from "../../errors/game-state-error"; +import * as card from "./card"; +import * as util from "../../util"; +import wu = require("wu"); + +const union = (sets: Iterable>): Set => { + const result = new Set(); + for (const set of sets) { + for (const element of set) { + result.add(element); + } + } + return result; +}; + +/** + * A deck of cards. + */ +export class Deck { + /** + * The cards in the deck. + */ + public readonly cards: C[]; + /** + * Cards drawn from this deck that have since been discarded (used for reshuffling). + */ + public readonly discarded: Set; + + public constructor(template: Iterable>) { + this.cards = []; + this.discarded = union(template); + this.reshuffle(); + } + + public discard(cards: Iterable): void; + public discard(firstCard: C, ...cards: C[]): void; + public discard(firstCard: C | Iterable, ...cards: C[]): void { + const resolvedCards: Iterable = util.isIterable(firstCard) + ? cards + : [firstCard, ...cards]; + for (const c of resolvedCards) { + this.discarded.add(c); + } + } + + public draw(cards: number): C[] { + const cardsLeft = this.cards.length; + const toDraw = Math.min(cardsLeft, cards); + const result = this.cards.splice(0, toDraw); + if (toDraw < cards) { + this.reshuffle(); + return [...result, ...this.draw(cards - toDraw)]; + } else { + return result; + } + } + + public replace(...cards: C[]): C[] { + this.discard(cards); + return this.draw(cards.length); + } + + private reshuffle(): void { + if (this.discarded.size < 1) { + throw new OutOfCardsError(); + } + this.cards.push(...util.shuffled(this.discarded)); + this.discarded.clear(); + } +} + +/** + * The two decks needed for a game. + */ +export interface Decks { + calls: Deck; + responses: Deck; +} + +/** + * A template for a deck. + */ +export type Template = Set; + +/** + * Templates for the two decks needed for a game. + */ +export interface Templates extends cache.Tagged { + calls: Template; + responses: Template; +} + +/** + * An empty pair of templates. + */ +export const emptyTemplates = (): Templates => ({ + calls: new Set(), + responses: new Set() +}); + +export const decks = (templates: Iterable): Decks => ({ + calls: new Deck(wu(templates).map(template => template.calls)), + responses: new Deck(wu(templates).map(template => template.responses)) +}); diff --git a/server/src/ts/games/cards/hand.ts b/server/src/ts/games/cards/hand.ts new file mode 100644 index 0000000..62fe6ec --- /dev/null +++ b/server/src/ts/games/cards/hand.ts @@ -0,0 +1,6 @@ +import * as card from "./card"; + +/** + * A hand of cards. + */ +export type Hand = card.Response[]; diff --git a/server/src/ts/games/cards/play.ts b/server/src/ts/games/cards/play.ts new file mode 100644 index 0000000..3fb4f2e --- /dev/null +++ b/server/src/ts/games/cards/play.ts @@ -0,0 +1,29 @@ +import { v4 as uuid } from "uuid"; +import { Response } from "./card"; + +/** A series of cards played into a round.*/ +export type Play = Response[]; + +/** + * A unique id for a play. + * @TJS-format uuid + */ +export type Id = string; + +/** + * An id or id with play. + */ +export interface PotentiallyRevealed { + id: Id; + play?: Play; +} + +/** + * A play with its id. + */ +export type Revealed = PotentiallyRevealed & { play: Play }; + +/** + * Create a new user id. + */ +export const id: () => Id = uuid; diff --git a/server/src/ts/games/cards/source.ts b/server/src/ts/games/cards/source.ts new file mode 100644 index 0000000..b259e30 --- /dev/null +++ b/server/src/ts/games/cards/source.ts @@ -0,0 +1,248 @@ +import * as cache from "../../cache"; +import { Cache } from "../../cache"; +import * as logging from "../../logging"; +import * as decks from "./decks"; +import { Cardcast } from "./sources/cardcast"; +import { Custom } from "./sources/custom"; + +/** + * A source for a card or deck. + */ +export type Source = External | Custom; + +/** + * An external source for a card or deck. + */ +export type External = Cardcast; + +/** + * More information that can be looked up given a source. + */ +export interface Details { + /** + * A name for the source. + */ + name: string; + /** + * A link to more information about the source. + */ + url?: string; +} + +export interface Summary extends cache.Tagged { + /** + * Details about the deck. + */ + details: Details; + /** + * The number of calls in the deck. + * @TJS-type integer + */ + calls: number; + /** + * The number of responses in the deck. + * @TJS-type integer + */ + responses: number; +} + +export interface AtLeastSummary { + summary: Summary; + templates?: decks.Templates; +} + +export interface AtLeastTemplates { + templates: decks.Templates; + summary?: Summary; +} + +/** + * Resolve information about the given source. + */ +export abstract class Resolver implements LimitedResolver { + /** + * A unique id for the source as a whole. + */ + public abstract id(): string; + + /** + * A unique id for the deck. + */ + public abstract deckId(): string; + + /** + * The temporary details to use for the source while loading. + */ + public abstract loadingDetails(): Details; + + /** + * If the given source represents the same deck. + */ + public abstract equals(source: External): boolean; + + /** + * Go to the remote source to get a tag for the source, if possible. + * Note that if you have a fresh summary, you should check if that has a + * tag first. + */ + public abstract async getTag(): Promise; + + /** + * The summary for the source, and potentially the templates if efficient to + * return both. + */ + public abstract async atLeastSummary(): Promise; + + /** + * The summary for the source. + */ + public async summary(): Promise { + return (await this.atLeastSummary()).summary; + } + + /** + * The details for the source. + */ + public async details(): Promise
{ + return (await this.summary()).details; + } + + /** + * The deck templates for the source, and potentially the summary if + * efficient to return both. + */ + public abstract async atLeastTemplates(): Promise; + + /** + * The deck templates for the source. + */ + public async templates(): Promise { + return (await this.atLeastTemplates()).templates; + } + + /** + * Get both the summary and templates efficiently. This will issue two + * separate requests if necessary, but only one if possible. + */ + public abstract summaryAndTemplates(): { + summary: Promise; + templates: Promise; + }; +} + +/** + * A resolver that only allows access to properties that don't require store + * access. + */ +export interface LimitedResolver { + id: () => string; + deckId: () => string; + loadingDetails: () => Details; + equals: (source: External) => boolean; +} + +/** + * A resolver that caches expensive responses in the store. + */ +export class CachedResolver extends Resolver { + private readonly resolver: Resolver; + private readonly cache: Cache; + + public constructor(cache: Cache, resolver: Resolver) { + super(); + this.cache = cache; + this.resolver = resolver; + } + + public id(): string { + return this.resolver.id(); + } + + public deckId(): string { + return this.resolver.deckId(); + } + + public loadingDetails(): Details { + return this.resolver.loadingDetails(); + } + + public equals(source: External): boolean { + return this.resolver.equals(source); + } + + public async getTag(): Promise { + // Caching the tag here would defy the point. + return this.resolver.getTag(); + } + + public async atLeastSummary(): Promise { + return { + summary: await this.cache.getSummary(this.resolver, () => + this.resolver.atLeastSummary() + ) + }; + } + + public async atLeastTemplates(): Promise { + return { + templates: await this.cache.getTemplates(this.resolver, () => + this.resolver.atLeastTemplates() + ) + }; + } + + public summaryAndTemplates(): { + summary: Promise; + templates: Promise; + } { + const promise = this.internalSummaryAndTemplates(); + return { + summary: promise.then(async value => await value.summary), + templates: promise.then(async value => await value.templates) + }; + } + + private async internalSummaryAndTemplates(): Promise<{ + summary: Promise; + templates: Promise; + }> { + return this.internalSummaryAndTemplatesContent( + await this.cache.getCachedSummary(this.resolver), + await this.cache.getCachedTemplates(this.resolver) + ); + } + + private internalSummaryAndTemplatesContent( + cachedSummary?: cache.Aged, + cachedTemplates?: cache.Aged + ): { + summary: Promise; + templates: Promise; + } { + if (cachedSummary === undefined && cachedTemplates === undefined) { + const result = this.resolver.summaryAndTemplates(); + result.summary + .then(async s => await this.cache.cacheSummary(this.resolver, s)) + .catch(CachedResolver.logError); + result.templates + .then(async t => await this.cache.cacheTemplates(this.resolver, t)) + .catch(CachedResolver.logError); + return result; + } else { + return { + summary: + cachedSummary !== undefined + ? Promise.resolve(cachedSummary.cached) + : this.summary(), + templates: + cachedTemplates !== undefined + ? Promise.resolve(cachedTemplates.cached) + : this.templates() + }; + } + } + + private static logError(error: Error): void { + logging.logException("Error while caching:", error); + } +} diff --git a/server/src/ts/games/cards/sources.ts b/server/src/ts/games/cards/sources.ts new file mode 100644 index 0000000..e468b78 --- /dev/null +++ b/server/src/ts/games/cards/sources.ts @@ -0,0 +1,39 @@ +import { Cache } from "../../cache"; +import * as deckSource from "./source"; +import { Source } from "./source"; +import * as cardcast from "./sources/cardcast"; +import * as custom from "./sources/custom"; + +function uncachedResolver(source: deckSource.External): deckSource.Resolver { + switch (source.source) { + case "Cardcast": + return new cardcast.Resolver(source); + } +} + +/** + * Get the limited resolver for the given source. + */ +export const limitedResolver = ( + source: deckSource.External +): deckSource.LimitedResolver => uncachedResolver(source); + +/** + * Get the resolver for the given source. + */ +export const resolver = ( + cache: Cache, + source: deckSource.External +): deckSource.Resolver => + new deckSource.CachedResolver(cache, uncachedResolver(source)); + +/** + * Get the details for the given source. + */ +export const details = async ( + cache: Cache, + source: Source +): Promise => + source.source === "Custom" + ? custom.details(source) + : await resolver(cache, source).details(); diff --git a/server/src/ts/games/cards/sources/cardcast.ts b/server/src/ts/games/cards/sources/cardcast.ts new file mode 100644 index 0000000..4878d9d --- /dev/null +++ b/server/src/ts/games/cards/sources/cardcast.ts @@ -0,0 +1,211 @@ +import http, { AxiosRequestConfig } from "axios"; +import * as genericPool from "generic-pool"; +import * as card from "../card"; +import { Slot } from "../card"; +import * as decks from "../decks"; +import * as source from "../source"; + +interface CCSummary { + name: string; + code: string; + description: string; + unlisted: boolean; + created_at: string; + updated_at: string; + external_copyright: boolean; + copyright_holder_url: string; + category: string | null; + call_count: string; + response_count: string; + author: { + id: string; + username: string; + }; + rating: string; +} + +interface CCDeck { + calls: CCCard[]; + responses: CCCard[]; +} + +interface CCCard { + id: string; + text: string[]; + created_at: string; + nsfw: boolean; +} + +const config: AxiosRequestConfig = { + method: "GET", + baseURL: "https://api.cardcastgame.com/v1/", + timeout: 10000, + responseType: "json" +}; + +/** + * We pool requests to cardcast to stop us hitting them too hard (on top of + * caching). We only allow two simultaneous requests. + */ +const connectionPool = genericPool.createPool( + { + create: async () => http.create(config), + destroy: async _ => {} + }, + { max: 2 } +); + +const summaryUrl = (playCode: PlayCode): string => `decks/${playCode}`; +const deckUrl = (playCode: PlayCode): string => `${summaryUrl(playCode)}/cards`; +const humanViewUrl = (playCode: PlayCode): string => + `https://www.cardcastgame.com/browse/deck/${playCode}`; + +/** + * A source for Cardcast. + */ +export interface Cardcast { + source: "Cardcast"; + playCode: PlayCode; +} + +/** + * A Cardcast play code for a deck. + */ +export type PlayCode = string; + +const nextWordShouldBeCapitalizedRegex = /[.?!]\s*$/; +const nextWordShouldBeCapitalized = (previously: string): boolean => + previously.match(nextWordShouldBeCapitalizedRegex) !== null; + +/** + * Get the parts from Cardcast's representation. As Cardcast doesn't offer as + * much flexibility as we do, we use heuristics to try and do the right thing. + */ +// TODO: We probably want to offer some control over these heuristics. +function* parts(call: CCCard): Iterable { + let upper: Slot = call.text.every(text => text === text.toUpperCase()) + ? { transform: "UpperCase" } + : {}; + let first = true; + let previous: string | null = null; + for (const text of call.text) { + if (previous !== null) { + let capitalize: Slot = + nextWordShouldBeCapitalized(previous) || first + ? { transform: "Capitalize" } + : {}; + yield { ...capitalize, ...upper }; + } + if (text !== "") { + yield text; + } + previous = text; + if (first && (text !== "" || previous !== null)) { + first = false; + } + } +} + +const call = (source: Cardcast, call: CCCard): card.Call => ({ + id: card.id(), + parts: [Array.from(parts(call))], + source: source +}); + +const response = (source: Cardcast, response: CCCard): card.Response => ({ + id: card.id(), + text: response.text[0], + source: source +}); + +export class Resolver extends source.Resolver { + public readonly source: Cardcast; + + public constructor(source: Cardcast) { + super(); + this.source = source; + } + + public id(): string { + return "Cardcast"; + } + + public deckId(): string { + return this.source.playCode; + } + + public loadingDetails(): source.Details { + return { + name: `Cardcast ${this.source.playCode}`, + url: humanViewUrl(this.source.playCode) + }; + } + + public equals(source: source.External): boolean { + return ( + source.source === "Cardcast" && + this.source.playCode.toUpperCase() === source.playCode.toUpperCase() + ); + } + + public async getTag(): Promise { + return (await this.summary()).tag; + } + + public async atLeastSummary(): Promise { + const summary = (await Resolver.get( + summaryUrl(this.source.playCode) + )) as CCSummary; + try { + return { + summary: { + details: { + name: summary.name, + url: humanViewUrl(this.source.playCode) + }, + calls: Number.parseInt(summary.call_count, 10), + responses: Number.parseInt(summary.response_count, 10), + tag: summary.updated_at + } + }; + } catch (error) { + // TODO: Error wrapper for unexpected response shape. + throw error; + } + } + + public async atLeastTemplates(): Promise { + const deck = (await Resolver.get(deckUrl(this.source.playCode))) as CCDeck; + try { + return { + templates: { + calls: new Set(deck.calls.map(c => call(this.source, c))), + responses: new Set(deck.responses.map(r => response(this.source, r))) + } + }; + } catch (error) { + // TODO: Error wrapper for unexpected response shape. + throw error; + } + } + + private static async get(url: string): Promise { + const connection = await connectionPool.acquire(); + try { + return (await connection.get(url)).data; + } catch (error) { + // TODO: Error wrapper for connection to Cardcast. + throw error; + } finally { + connectionPool.release(connection); + } + } + + public summaryAndTemplates = (): { + summary: Promise; + templates: Promise; + } => ({ + summary: this.summary(), + templates: this.templates() + }); +} diff --git a/server/src/ts/games/cards/sources/custom.ts b/server/src/ts/games/cards/sources/custom.ts new file mode 100644 index 0000000..a96157b --- /dev/null +++ b/server/src/ts/games/cards/sources/custom.ts @@ -0,0 +1,15 @@ +import * as source from "../source"; + +/** + * A source for custom cards made during the game. + */ +export interface Custom { + source: "Custom"; +} + +/** + * Get the details for a given source. + */ +export const details = (_: Custom): source.Details => ({ + name: "Custom" +}); diff --git a/server/src/ts/games/game.ts b/server/src/ts/games/game.ts new file mode 100644 index 0000000..c9f30c6 --- /dev/null +++ b/server/src/ts/games/game.ts @@ -0,0 +1,86 @@ +import * as user from "../user"; +import * as util from "../util"; +import { Decks } from "./cards/decks"; +import * as decks from "./cards/decks"; +import * as round from "./game/round"; +import { Round } from "./game/round"; +import * as publicRound from "./game/round/public"; +import { Public as PublicRound } from "./game/round/public"; +import * as player from "./player"; +import { Player } from "./player"; +import * as rules from "./rules"; +import { Rules } from "./rules"; +import wu from "wu"; + +/** + * The state of a game. + */ +export interface Game { + round: Round; + history: round.Complete[]; + playerOrder: user.Id[]; + players: Map; + decks: Decks; + rules: Rules; + winner?: user.Id; +} + +export interface Public { + round: PublicRound; + history: publicRound.Complete[]; + playerOrder: user.Id[]; + players: { [id: string]: player.Public }; + rules: rules.Public; + winner?: user.Id; +} + +export const censor: (game: Game) => Public = game => ({ + round: round.censor(game.round), + history: game.history.map(complete => round.censor(complete)), + playerOrder: game.playerOrder, + players: util.mapObjectValues(util.mapToObject(game.players), player.censor), + rules: rules.censor(game.rules), + ...(game.winner === undefined ? {} : { winner: game.winner }) +}); + +const newPlayerForUser = (decks: Decks, rules: Rules): Player => ({ + hand: decks.responses.draw(rules.handSize), + control: "Human", + score: 0 +}); + +export const start = ( + templates: Iterable, + players: Iterable, + rules: Rules +): Game & { round: round.Playing } => { + const gameDecks = decks.decks(templates); + const playerOrder = Array.from(players); + const czar = playerOrder[0]; + const playerMap = new Map( + wu(playerOrder).map(id => [id, newPlayerForUser(gameDecks, rules)]) + ); + const [call] = gameDecks.calls.draw(1); + const playersInRound = new Set(wu(playerOrder).filter(id => id !== czar)); + return { + round: { + stage: "Playing", + id: 0, + czar, + players: playersInRound, + call, + plays: [] + }, + history: [], + playerOrder, + players: playerMap, + decks: gameDecks, + rules + }; +}; + +export const nextCzar = (game: Game): user.Id => { + const current = game.round.czar; + const index = game.playerOrder.findIndex(id => id === current) + 1; + return game.playerOrder[index >= game.playerOrder.length ? 0 : index]; +}; diff --git a/server/src/ts/games/game/round.ts b/server/src/ts/games/game/round.ts new file mode 100644 index 0000000..5539eba --- /dev/null +++ b/server/src/ts/games/game/round.ts @@ -0,0 +1,164 @@ +import { Action } from "../../action"; +import { IncorrectRoundStageError } from "../../errors/action-execution-error"; +import * as user from "../../user"; +import * as card from "../cards/card"; +import * as play from "../cards/play"; +import { Play } from "../cards/play"; +import * as publicRound from "./round/public"; +import { Public as PublicRound } from "./round/public"; + +export type Round = Playing | Revealing | Judging | Complete; + +export type Stage = "Playing" | "Revealing" | "Judging" | "Complete"; + +export type Id = number; + +interface Base { + stage: Stage; + id: Id; + czar: user.Id; + players: Set; + call: card.Call; + plays: StoredPlay[]; +} + +export interface Playing extends Base { + stage: "Playing"; +} + +export interface Revealing extends Base { + stage: "Revealing"; + plays: StoredPlay[]; +} + +export interface Judging extends Base { + stage: "Judging"; + plays: RevealedStoredPlay[]; +} + +export interface Complete extends Base { + stage: "Complete"; + plays: RevealedStoredPlay[]; + winner: user.Id; +} + +export interface StoredPlay { + id: play.Id; + playedBy: user.Id; + play: Play; + revealed: boolean; +} + +export type RevealedStoredPlay = StoredPlay & { revealed: true }; + +function* potentiallyRevealedPlays( + round: Revealing +): Iterable { + for (const roundPlay of round.plays) { + const potentiallyRevealed: play.PotentiallyRevealed = { id: roundPlay.id }; + if (roundPlay.revealed) { + potentiallyRevealed.play = roundPlay.play; + } + yield potentiallyRevealed; + } +} + +function* revealedPlays(round: Judging): Iterable { + for (const roundPlay of round.plays) { + yield { id: roundPlay.id, play: roundPlay.play }; + } +} + +export function playedBy(round: Complete): { [player: string]: user.Id } { + const obj: { [player: string]: user.Id } = {}; + for (const roundPlay of round.plays) { + obj[roundPlay.id] = roundPlay.playedBy; + } + return obj; +} + +export function playsObj(round: Complete): { [player: string]: Play } { + const obj: { [player: string]: Play } = {}; + for (const roundPlay of round.plays) { + obj[roundPlay.playedBy] = roundPlay.play; + } + return obj; +} + +export function censor(round: Playing): publicRound.Playing; +export function censor(round: Revealing): publicRound.Revealing; +export function censor(round: Judging): publicRound.Judging; +export function censor(round: Complete): publicRound.Complete; +export function censor(round: Round): PublicRound; +export function censor(round: Round): PublicRound { + switch (round.stage) { + case "Playing": + return { + stage: round.stage, + id: round.id.toString(), + czar: round.czar, + players: Array.from(round.players), + call: round.call, + played: round.plays.map(play => play.playedBy) + }; + case "Revealing": + return { + stage: round.stage, + id: round.id.toString(), + czar: round.czar, + players: Array.from(round.players), + call: round.call, + plays: Array.from(potentiallyRevealedPlays(round)) + }; + case "Judging": + return { + stage: round.stage, + id: round.id.toString(), + czar: round.czar, + players: Array.from(round.players), + call: round.call, + plays: Array.from(revealedPlays(round)) + }; + case "Complete": + return { + stage: round.stage, + id: round.id.toString(), + czar: round.czar, + players: Array.from(round.players), + call: round.call, + winner: round.winner, + plays: playsObj(round) + }; + } +} + +/** + * Verifies the user is on the right stage for this action, throwing if they + * are not. Note that you will need to give the generic type explicitly, as + * it. + * @param action + * @param round + * @param expected + */ +export function verifyStage( + action: Action, + round: Round, + expected: T["stage"] +): round is T { + if (round.stage !== expected) { + throw new IncorrectRoundStageError(action, round.stage, expected); + } + return true; +} + +const storedPlayIsRevealed = (play: StoredPlay): play is RevealedStoredPlay => + play.revealed; + +/** + * Checks if every stored play in an array is revealed or not. + */ +export function allStoredPlaysAreRevealed( + round: Round +): round is Round & { plays: RevealedStoredPlay[] } { + return round.plays.every(storedPlayIsRevealed); +} diff --git a/server/src/ts/games/game/round/public.ts b/server/src/ts/games/game/round/public.ts new file mode 100644 index 0000000..6cc5926 --- /dev/null +++ b/server/src/ts/games/game/round/public.ts @@ -0,0 +1,35 @@ +import * as user from "../../../user"; +import * as card from "../../cards/card"; +import { Play } from "../../cards/play"; +import * as play from "../../cards/play"; + +export type Public = Playing | Revealing | Judging | Complete; + +interface Base { + stage: string; + id: string; + czar: user.Id; + players: user.Id[]; + call: card.Call; +} + +export interface Playing extends Base { + stage: "Playing"; + played: user.Id[]; +} + +export interface Revealing extends Base { + stage: "Revealing"; + plays: play.PotentiallyRevealed[]; +} + +export interface Judging extends Base { + stage: "Judging"; + plays: play.Revealed[]; +} + +export interface Complete extends Base { + stage: "Complete"; + winner: user.Id; + plays: { [player: string]: Play }; +} diff --git a/server/src/ts/games/player.ts b/server/src/ts/games/player.ts new file mode 100644 index 0000000..94e2740 --- /dev/null +++ b/server/src/ts/games/player.ts @@ -0,0 +1,53 @@ +import * as user from "../user"; +import { Hand } from "./cards/hand"; +import { Game } from "./game"; + +/** + * A player in the game. + */ +export interface Player { + hand: Hand; + control: Control; + score: Score; +} + +/** + * A player containing only state all users can see. + */ +export interface Public { + control: Control; + score: Score; +} + +/** + * The role the player currently has in the game. + */ +export type Role = "Czar" | "Player"; + +/** + * Who controls the player. + */ +export type Control = "Human" | "Computer"; + +/** + * How many points the player has scored. + * @TJS-type integer + * @minimum 0 + */ +export type Score = number; + +/** + * Produce a public version of the given player. + */ +export const censor = (player: Player): Public => ({ + control: player.control, + score: player.score +}); + +/** + * Get the given player's role in the game. + * @param game The game. + * @param player The player's id. + */ +export const role = (game: Game, player: user.Id): Role => + game.round.czar === player ? "Czar" : "Player"; diff --git a/server/src/ts/games/rules.ts b/server/src/ts/games/rules.ts new file mode 100644 index 0000000..ff50ffb --- /dev/null +++ b/server/src/ts/games/rules.ts @@ -0,0 +1,86 @@ +/** The rules for a standard game.*/ +export interface Rules { + /** + * The number of cards in each player's hand. + * @TJS-type integer + * @minimum 3 + * @maximum 50 + */ + handSize: number; + /** + * The score threshold for the game - when a player hits this they win. + * If not set, then there is end - the game goes on infinitely. + * @TJS-type integer, undefined + * @minimum 1 + * @maximum 10000 + */ + scoreLimit?: number; + houseRules: HouseRules; +} + +export interface Public { + handSize: number; + scoreLimit?: number; + houseRules: HouseRules; +} + +export interface HouseRules { + packingHeat?: PackingHeat; + reboot?: Reboot; + rando?: Rando; +} + +/** + * Create a default set of rules. + */ +export const create = (): Rules => ({ + handSize: 10, + scoreLimit: 25, + houseRules: {} +}); + +/** + * Configuration for the "Packing Heat" house rule. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PackingHeat {} + +/** + * Configuration for the "Reboot the Universe" house rule. + * This rule allows players to draw a new hand by sacrificing a given number + * of points. + */ +export interface Reboot { + /** + * The cost to redrawing. + * @TJS-type integer + * @minimum 1 + * @maximum 50 + */ + cost: number; +} + +export interface Rando { + /** + * The number of AI players to add to the game. + * @TJS-type integer + * @minimum 1 + * @maximum 10 + */ + number: number; +} + +export const censor = (rules: Rules): Public => ({ + ...rules +}); + +export interface ChangeBase { + houseRule: Name; + settings?: HouseRule; +} + +export type ChangePackingHeat = ChangeBase<"PackingHeat", PackingHeat>; +export type ChangeRando = ChangeBase<"Rando", Rando>; +export type ChangeReboot = ChangeBase<"Reboot", Reboot>; + +export type Change = ChangePackingHeat | ChangeRando | ChangeReboot; diff --git a/server/src/ts/index.ts b/server/src/ts/index.ts new file mode 100644 index 0000000..7f6b488 --- /dev/null +++ b/server/src/ts/index.ts @@ -0,0 +1,198 @@ +import bodyParser from "body-parser"; +import express, { NextFunction, Request, Response } from "express"; +import "express-async-errors"; +import expressWinston from "express-winston"; +import ws from "express-ws"; +import fs from "fs"; +import HttpStatus from "http-status-codes"; +import JSON5 from "json5"; +import sourceMapSupport from "source-map-support"; +import { promisify } from "util"; +import * as createLobby from "./action/initial/create-lobby"; +import * as registerUser from "./action/initial/register-user"; +import * as serverConfig from "./config"; +import { MassiveDecksError } from "./errors"; +import { InvalidLobbyPasswordError } from "./errors/authentication"; +import * as event from "./event"; +import * as change from "./lobby/change"; +import { GameCode } from "./lobby/game-code"; +import * as logging from "./logging"; +import * as serverState from "./server-state"; +import * as timeout from "./timeout"; +import * as userDisconnect from "./timeout/user-disconnect"; +import * as user from "./user"; +import * as token from "./user/token"; +import * as checkAlive from "./action/initial/check-alive"; + +sourceMapSupport.install(); + +process.on("uncaughtException", function(error) { + logging.logException("Uncaught exception: ", error); + process.exit(1); +}); + +process.on("unhandledRejection", function(reason, promise) { + logging.logger.error(`Unhandled rejection at ${promise}: ${reason}`); + process.exit(1); +}); + +async function main(): Promise { + const config = serverConfig.parse(JSON5.parse( + (await promisify(fs.readFile)("config.json5")).toString() + ) as serverConfig.Unparsed); + + const { app } = ws(express()); + + app.set('trust proxy', true); + + const environment = app.get("env"); + + if (environment !== "development" && config.secret === "CHANGE ME") { + throw new Error( + "Secret not set - this should never be the case outside of " + + "development. Please set a real random secret value in 'config.json5'." + ); + } + + const state = await serverState.create(config); + + app.use(bodyParser.json()); + + app.use( + expressWinston.logger({ + winstonInstance: logging.logger + }) + ); + + app.get("/api/version", async (req, res) => res.json(config.version)); + + app.get("/api/games", async (req, res) => { + const result = []; + for await (const summary of state.store.lobbySummaries()) { + result.push(summary); + } + res.json(result); + }); + + app.post("/api/games", async (req, res) => { + const { gameCode, token } = await state.store.newLobby( + createLobby.validate(req.body), + config.secret + ); + res.append( + "Location", + `${req.protocol}://${req.hostname}/${config.basePath}api/games/${gameCode}` + ); + res.status(HttpStatus.CREATED).json(token); + }); + + app.post("/api/games/alive", async (req, res) => { + const result: { [key: string]: boolean } = {}; + for (const current of checkAlive.validate(req.body).tokens) { + try { + const claims = token.validate( + current, + await state.store.id(), + state.config.secret + ); + result[current] = await state.store.exists(claims.gc); + } catch (error) { + result[current] = false; + } + } + res.json(result); + }); + + app.post("/api/games/:gameCode", async (req, res) => { + const gameCode: GameCode = req.params.gameCode; + + const registration = registerUser.validate(req.body); + const newUser = user.create(registration); + const id = await change.applyAndReturn(state, gameCode, lobby => { + if (lobby.config.password !== registration.password) { + throw new InvalidLobbyPasswordError(); + } + const id = lobby.nextUserId.toString(); + lobby.nextUserId += 1; + lobby.users.set(id, newUser); + return { + change: { + lobby, + events: [ + event.target({ event: "Joined", user: id, name: newUser.name }) + ], + timeouts: [ + { + timeout: userDisconnect.of(id), + after: config.timeouts.disconnectionGracePeriod + } + ] + }, + returnValue: id + }; + }); + const claims: token.Claims = { + gc: gameCode, + uid: id, + pvg: "Unprivileged" + }; + res.json(token.create(claims, await state.store.id(), config.secret)); + }); + + app.ws("/api/games/:gameCode", async (socket, req) => { + const gameCode = req.params.gameCode; + state.socketManager.add(state, gameCode, socket); + }); + + app.use((error: Error, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + next(error); + } + if (error instanceof MassiveDecksError) { + logging.logger.warn("Bad request:", error.details()); + res.status(error.status).json(error.details()); + } else { + next(error); + } + }); + + app.use( + expressWinston.errorLogger({ + winstonInstance: logging.logger, + msg: "{{err.message}}" + }) + ); + + app.use((error: Error, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + next(error); + } + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "InternalServerError" }); + }); + + setInterval(async () => { + const lobbies = await state.store.garbageCollect(); + if (lobbies > 0) { + logging.logger.info(`Collected ${lobbies} ended/abandoned lobbies.`); + } + }, config.storage.garbageCollectionFrequency); + + setInterval(async () => { + await timeout.handle(state); + }, config.timeouts.timeoutCheckFrequency); + + state.tasks + .loadFromStore(state) + .catch(error => logging.logException("Error running store tasks:", error)); + + app.listen(config.port, () => + logging.logger.info(`Serving on port ${config.port}.`) + ); +} + +main().catch(error => { + logging.logException("Application exception:", error); + process.exit(1); +}); diff --git a/server/src/ts/lobby.ts b/server/src/ts/lobby.ts new file mode 100644 index 0000000..af77d6b --- /dev/null +++ b/server/src/ts/lobby.ts @@ -0,0 +1,131 @@ +import { CreateLobby } from "./action/initial/create-lobby"; +import * as game from "./games/game"; +import { Game } from "./games/game"; +import * as rules from "./games/rules"; +import * as config from "./lobby/config"; +import { Config } from "./lobby/config"; +import { GameCode } from "./lobby/game-code"; +import * as user from "./user"; +import { User } from "./user"; +import * as util from "./util"; +import * as token from "./user/token"; + +/** + * A game lobby. + */ +export interface Lobby { + name: string; + public: boolean; + nextUserId: number; + users: Map; + owner: user.Id; + config: Config; + game?: Game; +} + +/** + * A lobby with an active game. + */ +export type WithActiveGame = Lobby & { game: Game }; + +/** + * Return if the given lobby has an active game. + */ +export const hasActiveGame = (lobby: Lobby): lobby is WithActiveGame => + lobby.game !== undefined; + +/** + * A game lobby containing only state all users can see. + */ +export interface Public { + name: string; + public: boolean; + users: { [id: string]: user.Public }; + owner: user.Id; + config: config.Public; + game?: game.Public; +} + +/** + * The state of a lobby. + */ +export type State = "Playing" | "SettingUp"; + +/** + * A summary of a lobby. + */ +export interface Summary { + name: string; + gameCode: GameCode; + state: State; + users: { players: number; spectators: number }; +} + +/** + * Create a config with default values. + */ +export const defaultConfig = (): Config => ({ + version: 0, + rules: rules.create(), + decks: [] +}); + +/** + * Create a new lobby. + * @param creation The details of the lobby to create. + */ +export function create(creation: CreateLobby): Lobby { + const id = (0).toString(); + return { + name: creation.name ? creation.name : `${creation.owner.name}'s Game`, + public: false, + nextUserId: 1, + users: new Map([[id, user.create(creation.owner, "Privileged")]]), + owner: id, + config: defaultConfig() + }; +} + +/** + * The state of the given lobby. + */ +export const state = (lobby: Lobby): State => + lobby.game === null ? "SettingUp" : "Playing"; + +/** + * Get a summary of the given lobby. + * @param gameCode The game code for the lobby. + * @param lobby The lobby. + */ +export const summary = (gameCode: GameCode, lobby: Lobby): Summary => ({ + name: lobby.name, + gameCode: gameCode, + state: state(lobby), + users: util.counts(Object.values(lobby.users), { + players: user.isPlaying, + spectators: user.isSpectating + }) +}); + +/** + * If the given lobby has ended. + */ +export const ended = (lobby: Lobby): boolean => + lobby.game !== undefined && lobby.game.winner !== undefined; + +function usersObj(lobby: Lobby): { [id: string]: user.Public } { + const obj: { [id: string]: user.Public } = {}; + for (const [id, lobbyUser] of lobby.users.entries()) { + obj[id] = user.censor(lobbyUser); + } + return obj; +} + +export const censor = (lobby: Lobby, auth: token.Claims): Public => ({ + name: lobby.name, + public: lobby.public, + users: usersObj(lobby), + owner: lobby.owner, + config: config.censor(lobby.config, auth), + ...(lobby.game === undefined ? {} : { game: game.censor(lobby.game) }) +}); diff --git a/server/src/ts/lobby/change.ts b/server/src/ts/lobby/change.ts new file mode 100644 index 0000000..a3770dd --- /dev/null +++ b/server/src/ts/lobby/change.ts @@ -0,0 +1,122 @@ +import * as event from "../event"; +import { Lobby } from "../lobby"; +import { ServerState } from "../server-state"; +import { Task } from "../task"; +import * as timeout from "../timeout"; +import { GameCode } from "./game-code"; + +export interface Change { + lobby?: Lobby; + events?: Iterable; + timeouts?: Iterable; + tasks?: Iterable; +} + +export type Handler = (lobby: Lobby) => Change; +export type HandlerWithReturnValue = ( + lobby: Lobby +) => { change: Change; returnValue: T }; + +function internalApply( + server: ServerState, + originalLobby: Lobby, + change: Change +): { + lobby?: Lobby; + timeouts?: Iterable; + tasks?: Iterable; +} { + let lobbyUnchanged = change.lobby === undefined; + const lobby: Lobby = + change.lobby !== undefined ? change.lobby : originalLobby; + if (change.events !== undefined) { + event.send(server.socketManager, lobby, change.events); + } + let currentLobby = lobby; + const returnTasks = change.tasks !== undefined ? [...change.tasks] : []; + const futureTimeouts = []; + if (change.timeouts !== undefined) { + for (const timeoutAfter of change.timeouts) { + if (timeoutAfter.after > 0) { + futureTimeouts.push(timeoutAfter); + } else { + const chained = internalApply( + server, + currentLobby, + timeout.handler(server, timeoutAfter.timeout, currentLobby) + ); + if (chained.lobby !== undefined) { + lobbyUnchanged = false; + } + currentLobby = + chained.lobby !== undefined ? chained.lobby : currentLobby; + if (chained.timeouts !== undefined) { + futureTimeouts.push(...chained.timeouts); + } + if (chained.tasks !== undefined) { + returnTasks.push(...chained.tasks); + } + } + } + } + const lobbyResult = lobbyUnchanged ? {} : { lobby: currentLobby }; + const timeoutResult = + futureTimeouts.length === 0 ? {} : { timeouts: futureTimeouts }; + const tasksResult = returnTasks.length === 0 ? {} : { tasks: returnTasks }; + return { ...lobbyResult, ...timeoutResult, ...tasksResult }; +} + +/** + * Apply the given change and return a value. + * @param server The server state. + * @param gameCode The game code for the lobby to apply the change to. + * @param handler The handler that gives the change. + * @param timeoutId If given, the id of the timeout being executed. + */ +export async function applyAndReturn( + server: ServerState, + gameCode: GameCode, + handler: HandlerWithReturnValue, + timeoutId?: timeout.Id +): Promise { + const [tasks, returnValue] = await server.store.writeAndReturn( + gameCode, + lobby => { + const { change, returnValue } = handler(lobby); + const result = internalApply(server, lobby, change); + return { + transaction: { + lobby: result.lobby, + timeouts: result.timeouts, + executedTimeout: timeoutId + }, + result: [result.tasks, returnValue] + }; + } + ); + if (tasks !== undefined) { + for (const task of tasks) { + await server.tasks.enqueue(server, task); + } + } + return returnValue; +} + +/** + * Apply the given change. + * @param server The server state. + * @param gameCode The game code for the lobby to apply the change to. + * @param handler The handler that gives the change. + * @param timeoutId If given, the id of the timeout being executed. + */ +export async function apply( + server: ServerState, + gameCode: GameCode, + handler: Handler, + timeoutId?: timeout.Id +): Promise { + await applyAndReturn(server, gameCode, lobby => ({ + change: handler(lobby), + returnValue: undefined + })); +} diff --git a/server/src/ts/lobby/config.ts b/server/src/ts/lobby/config.ts new file mode 100644 index 0000000..6759e8f --- /dev/null +++ b/server/src/ts/lobby/config.ts @@ -0,0 +1,75 @@ +import * as source from "../games/cards/source"; +import { Rules } from "../games/rules"; +import * as rules from "../games/rules"; +import * as token from "../user/token"; + +/** + * Configuration for a lobby. + */ +export interface Config { + version: Version; + rules: Rules; + password?: string; + decks: SummarisedSource[]; +} + +export type Version = number; + +export interface Public { + version: string; + rules: rules.Public; + /** + * @maxLength 100 + * @minLength 1 + */ + password?: boolean | string; + decks: SummarisedSource[]; +} + +export interface SummarisedSource { + source: source.External; + summary?: source.Summary; +} + +export function censor(config: Config, auth: token.Claims): Public { + const privilegedPart: {} = + auth !== undefined && auth.pvg === "Privileged" + ? config.password === undefined + ? {} + : { password: config.password } + : { password: config.password !== undefined }; + return { + version: config.version.toString(), + rules: rules.censor(config.rules), + decks: config.decks, + ...privilegedPart + }; +} + +/** + * The way in which the decks configuration is changed. + */ +export type DeckChange = PlayerDriven | Load | Fail; + +export type PlayerDriven = "Add" | "Remove"; + +/** + * Signifies the given deck was successfully loaded. + */ +export interface Load { + change: "Load"; + summary: source.Summary; +} + +/** + * The reason a deck could not be loaded. + */ +export type FailReason = "SourceFailure" | "NotFound"; + +/** + * Signifies the given deck could not be loaded. + */ +export interface Fail { + change: "Fail"; + reason: FailReason; +} diff --git a/server/src/ts/lobby/game-code.ts b/server/src/ts/lobby/game-code.ts new file mode 100644 index 0000000..feda1ef --- /dev/null +++ b/server/src/ts/lobby/game-code.ts @@ -0,0 +1,34 @@ +// noinspection SpellCheckingInspection +import Hashids from "hashids"; + +/** + * A unique code for a lobby to identify it. + * @minLength 2 + */ +export type GameCode = string; + +/** + * A numeric id for a lobby. This is presented to the user as a game code. + */ +export type LobbyId = number; + +// noinspection SpellCheckingInspection +/** + * Game code methods. + */ +const hashIds = new Hashids( + "massivedecks", + 2, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +); + +/** + * Decode a game code to a lobby id. + */ +export const decode = (gameCode: GameCode): LobbyId => + hashIds.decode(gameCode)[0]; + +/** + * Decode a lobby id to a game code. + */ +export const encode = (lobbyId: LobbyId): GameCode => hashIds.encode(lobbyId); diff --git a/server/src/ts/logging.ts b/server/src/ts/logging.ts new file mode 100644 index 0000000..33a4572 --- /dev/null +++ b/server/src/ts/logging.ts @@ -0,0 +1,43 @@ +import * as winston from "winston"; + +const logFormat = winston.format.printf(info => { + const stringRest = JSON.stringify( + { + ...info, + level: undefined, + message: undefined, + splat: undefined + }, + null, + 2 + ); + + if (stringRest !== "{}") { + return `${info.level}: ${info.message}\n${stringRest}`; + } else { + return `${info.level}: ${info.message}`; + } +}); + +export const logger = winston.createLogger({ + transports: [new winston.transports.Console()], + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + logFormat + ) +}); + +const exceptionHandler = new winston.ExceptionHandler(logger); +export const exceptionToMeta = exceptionHandler.getAllInfo.bind( + exceptionHandler +); + +export const logException = ( + message: string, + error: Error, + data?: string +): void => { + const details = exceptionToMeta(error); + logger.error(message, details === undefined ? details : { details, data }); +}; diff --git a/server/src/ts/secret.ts b/server/src/ts/secret.ts new file mode 100644 index 0000000..1f06bbb --- /dev/null +++ b/server/src/ts/secret.ts @@ -0,0 +1,3 @@ +import crypto from "crypto"; + +console.log(crypto.randomBytes(256 / 8).toString("hex")); diff --git a/server/src/ts/server-state.ts b/server/src/ts/server-state.ts new file mode 100644 index 0000000..fe509f6 --- /dev/null +++ b/server/src/ts/server-state.ts @@ -0,0 +1,30 @@ +import { Cache } from "./cache"; +import * as caches from "./caches"; +import * as config from "./config"; +import { SocketManager } from "./socket-manager"; +import { Store } from "./store"; +import * as stores from "./store/stores"; +import * as backgroundTasks from "./task/tasks"; + +export interface ServerState { + config: config.Parsed; + store: Store; + cache: Cache; + socketManager: SocketManager; + tasks: backgroundTasks.Queue; +} + +export async function create(config: config.Parsed): Promise { + const store = await stores.from(config.storage); + const cache = await caches.from(config.cache); + const socketManager = new SocketManager(); + const tasks = new backgroundTasks.Queue(); + + return { + config, + store, + cache, + socketManager, + tasks + }; +} diff --git a/server/src/ts/socket-manager.ts b/server/src/ts/socket-manager.ts new file mode 100644 index 0000000..00c1425 --- /dev/null +++ b/server/src/ts/socket-manager.ts @@ -0,0 +1,150 @@ +import WebSocket from "ws"; +import * as action from "./action"; +import * as authenticate from "./action/authenticate"; +import { MassiveDecksError } from "./errors"; +import { NotAuthenticatedError } from "./errors/authentication"; +import { InvalidActionError } from "./errors/validation"; +import * as event from "./event"; +import * as gameLobby from "./lobby"; +import * as change from "./lobby/change"; +import { GameCode } from "./lobby/game-code"; +import * as logging from "./logging"; +import { ServerState } from "./server-state"; +import * as userDisconnect from "./timeout/user-disconnect"; +import * as user from "./user"; +import * as token from "./user/token"; +import * as sync from "./events/user-event/sync"; + +export type Sockets = Map; + +const parseJson = (raw: string): object => { + try { + return JSON.parse(raw); + } catch (error) { + throw new InvalidActionError(error.message); + } +}; + +export class SocketManager { + public readonly sockets: Sockets; + + public constructor() { + this.sockets = new Map(); + } + + private readonly errorWSHandler = ( + socket: WebSocket, + fn: (data: WebSocket.Data) => Promise + ): ((data: WebSocket.Data) => Promise) => async data => { + try { + return await fn(data); + } catch (error) { + const dataDescription = + typeof data === "string" ? data : `(${typeof data})`; + if (error instanceof MassiveDecksError) { + const details = error.details(); + logging.logger.warn("WebSocket bad request:", { + data: dataDescription, + details, + errorMessage: error.message + }); + socket.send(JSON.stringify(details)); + } else { + logging.logException("WebSocket error:", error, dataDescription); + socket.send(JSON.stringify({ error: "InternalServerError" })); + socket.close(); + } + return; + } + }; + + public add(server: ServerState, gameCode: GameCode, socket: WebSocket): void { + const sockets = this.sockets; + let auth: token.Claims | null = null; + socket.on("open", async () => {}); + socket.on( + "message", + this.errorWSHandler(socket, async data => { + if (typeof data !== "string") { + throw new InvalidActionError("Invalid message."); + } + const validated = action.validate(parseJson(data)); + if (auth === null) { + if (validated.action === "Authenticate") { + const knownAuth = await authenticate.handle( + server, + validated, + gameCode + ); + auth = knownAuth; + const uid = auth.uid; + sockets.set(auth.uid, socket); + await change.apply(server, auth.gc, lobby => { + let hand = undefined; + let play = undefined; + if (lobby.game !== undefined) { + const player = lobby.game.players.get(uid); + if (player !== undefined) { + hand = player.hand; + } + const potentialPlay = lobby.game.round.plays.find( + play => play.playedBy === uid + ); + if (potentialPlay !== undefined) { + play = potentialPlay.play.map(card => card.id); + } + } + const user = lobby.users.get(uid); + if (user === undefined) { + throw new Error( + "User doesn't exist, but we have a socket and auth?" + ); + } + user.connection = "Connected"; + return { + lobby, + events: [ + event.targetSpecifically( + sync.of(gameLobby.censor(lobby, knownAuth), hand, play), + uid + ) + ] + }; + }); + logging.logger.info("WebSocket connect:", { + user: auth.uid, + authenticate: validated + }); + } else { + throw new NotAuthenticatedError(); + } + } else { + const claims = auth; + await change.apply(server, auth.gc, lobby => + action.handle(claims, lobby, validated, server) + ); + logging.logger.info("WebSocket receive:", { + user: auth.uid, + action: validated + }); + } + }) + ); + socket.on("close", async () => { + if (auth) { + const uid = auth.uid; + sockets.delete(uid); + await change.apply(server, auth.gc, lobby => ({ + lobby, + timeouts: [ + { + timeout: userDisconnect.of(uid), + after: server.config.timeouts.disconnectionGracePeriod + } + ] + })); + logging.logger.info("WebSocket disconnect:", { user: auth.uid }); + } + }); + } +} diff --git a/server/src/ts/store.ts b/server/src/ts/store.ts new file mode 100644 index 0000000..2328869 --- /dev/null +++ b/server/src/ts/store.ts @@ -0,0 +1,114 @@ +import { CreateLobby } from "./action/initial/create-lobby"; +import * as serverConfig from "./config"; +import * as lobby from "./lobby"; +import { Lobby } from "./lobby"; +import { GameCode } from "./lobby/game-code"; +import * as timeout from "./timeout"; +import { Token } from "./user/token"; + +/** + * Represents a chunk of data that should be written as a single transaction, + * so if the server was restarted either all are applied or none are. + */ +export interface Transaction { + /** + * New lobby data that should replace what is currently in the store. + */ + lobby?: Lobby; + /** + * If given, timeouts that should be added to the store. + */ + timeouts?: Iterable; + /** + * If given, the id of the timeout that this resolves, and so should be + * removed from the store. + */ + executedTimeout?: timeout.Id; +} + +export type ReadOnlyTransaction = Transaction & { + lobby?: undefined; + timeouts?: Iterable }>; +}; + +export abstract class Store { + /** + * The configuration for this store. + */ + public abstract readonly config: serverConfig.Storage; + + /** + * A unique id for the store - this *must* be unique to the store, otherwise + * there is a chance reused lobby codes could be incorrectly accessed by users + * using old tokens. When this changes, it makes all previously issued tokens + * invalid, but if there is any security concern, change the application + * secret, not this. That will have the same effect securely. + */ + public abstract async id(): Promise; + + /** + * Returns if the given lobby exists. + */ + public abstract async exists(gameCode: GameCode): Promise; + + /** Create a new lobby. + * @return The game code for the new lobby and the user id for the owner. + */ + public abstract async newLobby( + creation: CreateLobby, + secret: string + ): Promise<{ gameCode: GameCode; token: Token }>; + + /** + * An operation that doesn't make any changes to the lobby. + * @param gameCode The game code for the lobby to modify. + * @param read The operation to perform. + */ + public async read( + gameCode: GameCode, + read: (lobby: Lobby) => { transaction: ReadOnlyTransaction; result: T } + ): Promise { + return this.writeAndReturn(gameCode, read); + } + + /** + * Perform a write operation on a lobby. + * If you make changes to the object as given, you must return it. + * @param gameCode The game code for the lobby to modify. + * @param write The operation to perform. + */ + public async write( + gameCode: GameCode, + write: (lobby: Lobby) => Transaction + ): Promise { + await this.writeAndReturn(gameCode, (lobby: Lobby) => ({ + transaction: write(lobby), + result: undefined + })); + } + + public abstract async writeAndReturn( + gameCode: GameCode, + write: (lobby: Lobby) => { transaction: Transaction; result: T } + ): Promise; + + /** Get a list of summaries for all the public lobbies in the store.*/ + public abstract lobbySummaries(): AsyncIterableIterator; + + /** + * Get all timed out timeouts from the store. + */ + public abstract timedOut(): AsyncIterableIterator; + + /** + * Delete the given lobby and all associated timeouts. + */ + public abstract async delete(gameCode: GameCode): Promise; + + /** + * Remove lobbies where the game is finished or everyone has been + * disconnected for some time. + * This should also clean up the cache as appropriate. + */ + public abstract async garbageCollect(): Promise; +} diff --git a/server/src/ts/store/in-memory.ts b/server/src/ts/store/in-memory.ts new file mode 100644 index 0000000..eba743c --- /dev/null +++ b/server/src/ts/store/in-memory.ts @@ -0,0 +1,157 @@ +import { v4 as uuid } from "uuid"; +import wu from "wu"; +import { CreateLobby } from "../action/initial/create-lobby"; +import * as serverConfig from "../config"; +import { LobbyClosedError, LobbyDoesNotExistError } from "../errors/lobby"; +import * as gameLobby from "../lobby"; +import { Lobby } from "../lobby"; +import * as lobbyGameCode from "../lobby/game-code"; +import { GameCode } from "../lobby/game-code"; +import { Store, Transaction } from "../store"; +import * as timeout from "../timeout"; +import { Timeout } from "../timeout"; +import * as token from "../user/token"; +import { Token } from "../user/token"; + +declare module "wu" { + // Fix incorrect types. + interface WuIterable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + spreadMap(fn: (...x: any[]) => U): WuIterable; + } +} + +interface TimeoutMeta { + timeout: Timeout; + lobby: GameCode; + after: number; +} + +/** + * A store where the data is stored in memory. + */ +export class InMemoryStore extends Store { + public readonly config: serverConfig.InMemory; + private readonly _id: string; + private readonly lobbies: Map; + private readonly timeouts: Map; + private nextLobby: number; + + public constructor(config: serverConfig.InMemory) { + super(); + this.config = config; + this._id = uuid(); + this.nextLobby = 0; + this.lobbies = new Map(); + this.timeouts = new Map(); + } + + public id = async (): Promise => this._id; + + public async exists(gameCode: string): Promise { + return this.lobbies.has(gameCode); + } + + public async *lobbySummaries(): AsyncIterableIterator { + for (const summary of wu(this.lobbies.entries()).spreadMap( + gameLobby.summary + )) { + yield summary; + } + } + + public async newLobby( + creation: CreateLobby, + secret: string + ): Promise<{ gameCode: GameCode; token: Token }> { + const lobby = gameLobby.create(creation); + const gameCode = lobbyGameCode.encode(this.nextLobby); + this.nextLobby += 1; + this.lobbies.set(gameCode, lobby); + return { + gameCode, + token: token.create( + { + gc: gameCode, + uid: lobby.owner, + pvg: "Privileged" + }, + this._id, + secret + ) + }; + } + + private async lobby(gameCode: GameCode): Promise { + const lobby = this.lobbies.get(gameCode.toUpperCase()); + if (lobby !== undefined) { + return lobby; + } else { + const lobbyNumber = lobbyGameCode.decode(gameCode); + if (lobbyNumber < this.nextLobby) { + throw new LobbyClosedError(gameCode); + } else { + throw new LobbyDoesNotExistError(gameCode); + } + } + } + + public async writeAndReturn( + gameCode: GameCode, + write: (lobby: Lobby) => { transaction: Transaction; result: T } + ): Promise { + const { transaction, result } = write(await this.lobby(gameCode)); + if (transaction.lobby !== undefined) { + this.lobbies.set(gameCode, transaction.lobby); + } + if (transaction.timeouts !== undefined) { + for (let timeout of transaction.timeouts) { + this.timeouts.set(uuid(), { + timeout: timeout.timeout, + lobby: gameCode, + after: Date.now() + timeout.after + }); + } + } + if (transaction.executedTimeout !== undefined) { + this.timeouts.delete(transaction.executedTimeout); + } + return result; + } + + public async garbageCollect(): Promise { + const toRemove = new Set(); + for (const [gameCode, lobby] of this.lobbies.entries()) { + // TODO: Also lobbies that have been abandoned for some time. + // TODO: Make ended a time, wait for config minutes before deleting. + if (gameLobby.ended(lobby)) { + toRemove.add(gameCode); + } + } + for (const gameCode of toRemove) { + this.delete(gameCode); + } + return toRemove.size; + } + + public async delete(gameCode: GameCode): Promise { + this.lobbies.delete(gameCode); + for (const [id, meta] of this.timeouts) { + if (meta.lobby === gameCode) { + this.timeouts.delete(id); + } + } + } + + public async *timedOut(): AsyncIterableIterator { + for (const [id, meta] of this.timeouts) { + if (Date.now() > meta.after) { + yield { + id, + timeout: meta.timeout, + lobby: meta.lobby + }; + } + } + } +} diff --git a/server/src/ts/store/postgres.ts b/server/src/ts/store/postgres.ts new file mode 100644 index 0000000..3424634 --- /dev/null +++ b/server/src/ts/store/postgres.ts @@ -0,0 +1,146 @@ +import * as genericPool from "generic-pool"; +import { Pool } from "generic-pool"; +import { Client } from "ts-postgres"; +import { CreateLobby } from "../action/initial/create-lobby"; +import * as config from "../config"; +import * as lobby from "../lobby"; +import { Lobby } from "../lobby"; +import { GameCode } from "../lobby/game-code"; +import * as logging from "../logging"; +import * as store from "../store"; +import { Store } from "../store"; +import * as timeout from "../timeout"; +import { Token } from "../user/token"; + +const idColumn = "id"; +const versionColumn = "version"; + +/** + * A store where the data is stored in a PostgreSQL database. + */ +export class PostgresStore extends Store { + private static readonly version = 0; + + public readonly config: config.PostgreSQL; + private readonly pool: Pool; + private readonly cachedId: string; + + public static async create( + config: config.PostgreSQL + ): Promise { + const client = await this.newClient(config.connection); + try { + const rows = client.query( + `SELECT version, ${idColumn} FROM massivedecks.meta` + ); + const row = await rows.one(); + const version = row.get(versionColumn); + if (version !== PostgresStore.version) { + // noinspection ExceptionCaughtLocallyJS + throw new Error( + `Database at incorrect version (expected ` + + `"${PostgresStore.version}" got "${version}"). Please upgrade.` + ); + } + return new PostgresStore(row.get(idColumn) as string, config); + } catch (error) { + logging.logger.error("Database is not configured correctly."); + throw error; + } finally { + this.destroyClient(client); + } + } + + // Only use statically, use the pool instead internally. + private static async newClient( + connection: config.PostgreSqlConnection + ): Promise { + const client = new Client(connection); + await client.connect(); + client.on("error", error => + logging.logger.error("Error from database client: ", error.message) + ); + return client; + } + + private static async destroyClient(client: Client): Promise { + await client.end(); + } + + private constructor(id: string, config: config.PostgreSQL) { + super(); + this.cachedId = id; + this.config = config; + this.pool = genericPool.createPool( + { + create: async () => + await PostgresStore.newClient(this.config.connection), + destroy: PostgresStore.destroyClient, + validate: async (client: Client) => !client.closed + }, + { testOnBorrow: true } + ); + } + + public async id(): Promise { + return this.cachedId; + } + + public async exists(gameCode: string): Promise { + return false; + } + + public async delete(gameCode: string): Promise { + // TODO: Impl. + return; + } + + public async garbageCollect(): Promise { + // TODO: Impl. + return 0; + } + + public async *lobbySummaries(): AsyncIterableIterator { + // TODO: Caching. + const client = await this.pool.acquire(); + try { + return; + } finally { + this.pool.release(client); + } + } + + public async newLobby( + creation: CreateLobby, + secret: string + ): Promise<{ gameCode: GameCode; token: Token }> { + const client = await this.pool.acquire(); + try { + return { gameCode: "", token: "" }; + } finally { + this.pool.release(client); + } + } + + public async *timedOut(): AsyncIterableIterator { + const client = await this.pool.acquire(); + try { + return; + } finally { + this.pool.release(client); + } + } + + public async writeAndReturn( + gameCode: string, + write: (lobby: Lobby) => { transaction: store.Transaction; result: T } + ): Promise { + const client = await this.pool.acquire(); + try { + const { result } = write((undefined as unknown) as Lobby); + return result; + } finally { + this.pool.release(client); + } + } +} diff --git a/server/src/ts/store/stores.ts b/server/src/ts/store/stores.ts new file mode 100644 index 0000000..bf34096 --- /dev/null +++ b/server/src/ts/store/stores.ts @@ -0,0 +1,13 @@ +import * as serverConfig from "../config"; +import { Store } from "../store"; +import { InMemoryStore } from "./in-memory"; +import { PostgresStore } from "./postgres"; + +export async function from(config: serverConfig.Storage): Promise { + switch (config.type) { + case "InMemory": + return new InMemoryStore(config); + case "PostgreSQL": + return PostgresStore.create(config); + } +} diff --git a/server/src/ts/task.ts b/server/src/ts/task.ts new file mode 100644 index 0000000..93e5592 --- /dev/null +++ b/server/src/ts/task.ts @@ -0,0 +1,35 @@ +import { Lobby } from "./lobby"; +import * as change from "./lobby/change"; +import { Change } from "./lobby/change"; +import { GameCode } from "./lobby/game-code"; +import { ServerState } from "./server-state"; + +/** + * A good base implementation of a task. + */ +export abstract class TaskBase implements Task { + private readonly gameCode: GameCode; + + protected constructor(gameCode: GameCode) { + this.gameCode = gameCode; + } + + protected abstract async begin(server: ServerState): Promise; + protected abstract resolve(lobby: Lobby, work: T): Change; + + public async handle(server: ServerState): Promise { + const work = await this.begin(server); + await change.apply(server, this.gameCode, lobby => + this.resolve(lobby, work) + ); + } +} + +/** + * A task is a long-running process for a lobby. + * It must be discoverable by inspecting the state of the lobby, as they are not + * stored in the store, but will be discovered on restart. + */ +export interface Task { + handle: (server: ServerState) => Promise; +} diff --git a/server/src/ts/task/load-deck-summary.ts b/server/src/ts/task/load-deck-summary.ts new file mode 100644 index 0000000..1ad37d1 --- /dev/null +++ b/server/src/ts/task/load-deck-summary.ts @@ -0,0 +1,61 @@ +import * as event from "../event"; +import * as decksChanged from "../events/lobby-event/configured/decks-changed"; +import * as deckSource from "../games/cards/source"; +import * as sources from "../games/cards/sources"; +import { Lobby } from "../lobby"; +import { Change } from "../lobby/change"; +import { GameCode } from "../lobby/game-code"; +import { ServerState } from "../server-state"; +import * as task from "../task"; + +export class LoadDeckSummary extends task.TaskBase { + private readonly source: deckSource.External; + + public constructor(gameCode: GameCode, source: deckSource.External) { + super(gameCode); + this.source = source; + } + + protected async begin(server: ServerState): Promise { + // We are intentionally ensuring the templates get cached here in advance, + // but don't actually need to do anything with them at this point. + return sources.resolver(server.cache, this.source).summaryAndTemplates() + .summary; + } + + protected resolve(lobby: Lobby, work: deckSource.Summary): Change { + const config = lobby.config; + const resolver = sources.limitedResolver(this.source); + const summarised = config.decks.find(deck => resolver.equals(deck.source)); + if (summarised !== undefined) { + summarised.summary = work; + config.version += 1; + return { + lobby, + events: [ + event.target( + decksChanged.of( + this.source, + { change: "Load", summary: work }, + config.version + ) + ) + ] + }; + } else { + return {}; + } + } + + public static *discover( + gameCode: GameCode, + lobby: Lobby + ): Iterable { + const config = lobby.config; + for (const deck of config.decks) { + if (deck.summary === undefined) { + yield new LoadDeckSummary(gameCode, deck.source); + } + } + } +} diff --git a/server/src/ts/task/start-game.ts b/server/src/ts/task/start-game.ts new file mode 100644 index 0000000..393fd51 --- /dev/null +++ b/server/src/ts/task/start-game.ts @@ -0,0 +1,50 @@ +import wu from "wu"; +import * as event from "../event"; +import * as gameStarted from "../events/game-event/game-started"; +import * as decks from "../games/cards/decks"; +import * as source from "../games/cards/source"; +import * as sources from "../games/cards/sources"; +import * as game from "../games/game"; +import * as round from "../games/game/round"; +import { Lobby } from "../lobby"; +import { Change } from "../lobby/change"; +import { GameCode } from "../lobby/game-code"; +import { ServerState } from "../server-state"; +import * as task from "../task"; + +export class StartGame extends task.TaskBase { + private readonly decks: Iterable; + + public constructor(gameCode: GameCode, decks: Iterable) { + super(gameCode); + this.decks = decks; + } + + protected async begin(server: ServerState): Promise { + return Promise.all( + wu(this.decks).map(deck => + sources.resolver(server.cache, deck).templates() + ) + ); + } + + protected resolve(lobby: Lobby, work: decks.Templates[]): Change { + if (lobby.game !== undefined) { + return {}; + } + const lobbyGame = game.start(work, lobby.users.keys(), lobby.config.rules); + const gameRound = round.censor(lobbyGame.round); + const events = wu(lobbyGame.players).map(([id, player]) => + event.targetSpecifically(gameStarted.of(gameRound, player.hand), id) + ); + lobby.game = lobbyGame; + return { lobby, events }; + } + + // This is super unlikely timing-wise, and if it happens, the user just has + // to click start again. They'll live. + public static *discover( + gameCode: GameCode, + lobby: Lobby + ): Iterable {} +} diff --git a/server/src/ts/task/tasks.ts b/server/src/ts/task/tasks.ts new file mode 100644 index 0000000..05309bb --- /dev/null +++ b/server/src/ts/task/tasks.ts @@ -0,0 +1,56 @@ +import { Lobby } from "../lobby"; +import { GameCode } from "../lobby/game-code"; +import * as logging from "../logging"; +import { ServerState } from "../server-state"; +import { Task } from "../task"; +import { LoadDeckSummary } from "./load-deck-summary"; +import { StartGame } from "./start-game"; + +interface Discoverable { + discover: (gameCode: GameCode, lobby: Lobby) => Iterable; +} + +/** + * A task queue for processing tasks in the background. + */ +export class Queue { + private static readonly tasks: Discoverable[] = [LoadDeckSummary, StartGame]; + + /** + * Discover tasks that need to be run for the given lobby state. + */ + private static *discover(gameCode: GameCode, lobby: Lobby): Iterable { + for (const task of Queue.tasks) { + yield* task.discover(gameCode, lobby); + } + } + + /** + * Search the store for lobbies and start any tasks necessary given their + * current state. + */ + public async loadFromStore(server: ServerState): Promise { + for await (const { gameCode } of server.store.lobbySummaries()) { + await server.store.read(gameCode, lobby => { + for (const task of Queue.discover(gameCode, lobby)) { + this.enqueue(server, task); + } + return { transaction: {}, result: undefined }; + }); + } + } + + // TODO: Real queuing. + // Probably not a huge deal, but we might go a bit nuts at startup in some + // cases. + /** + * Start a new background task. + */ + public enqueue(server: ServerState, task: Task): void { + logging.logger.info("Task queued:", { task }); + task + .handle(server) + .catch(error => logging.logException("Error processing task:", error)) + .then(() => logging.logger.info("Task complete:", { task })); + } +} diff --git a/server/src/ts/timeout.ts b/server/src/ts/timeout.ts new file mode 100644 index 0000000..698ce71 --- /dev/null +++ b/server/src/ts/timeout.ts @@ -0,0 +1,63 @@ +import { Lobby } from "./lobby"; +import * as change from "./lobby/change"; +import { Change } from "./lobby/change"; +import { GameCode } from "./lobby/game-code"; +import { ServerState } from "./server-state"; +import * as finishedPlaying from "./timeout/finished-playing"; +import { FinishedPlaying } from "./timeout/finished-playing"; +import * as roundStart from "./timeout/round-start"; +import { RoundStart } from "./timeout/round-start"; +import * as userDisconnect from "./timeout/user-disconnect"; +import { UserDisconnect } from "./timeout/user-disconnect"; + +/** + * A timeout represents something that must happen after a delay in-game. + */ +export type Timeout = UserDisconnect | RoundStart | FinishedPlaying; + +export type Id = string; + +export interface TimedOut { + id: Id; + timeout: Timeout; + lobby: GameCode; +} + +export interface TimeoutAfter { + timeout: Timeout; + after: number; +} + +export const handler: Handler = (server, timeout, lobby) => { + switch (timeout.timeout) { + case "UserDisconnect": + return userDisconnect.handle(server, timeout, lobby); + case "RoundStart": + return roundStart.handle(server, timeout, lobby); + case "FinishedPlaying": + return finishedPlaying.handle(server, timeout, lobby); + } +}; + +/** + * Handle all timeouts in the store that are timed out. + * Note that this will result in writing to the lobby, so it must not be called + * inside another write. + * @param server The server state. + */ +export async function handle(server: ServerState): Promise { + for await (const { id, lobby, timeout } of server.store.timedOut()) { + await change.apply( + server, + lobby, + lobby => handler(server, timeout, lobby), + id + ); + } +} + +export type Handler = ( + server: ServerState, + timeout: T, + lobby: Lobby +) => Change; diff --git a/server/src/ts/timeout/finished-playing.ts b/server/src/ts/timeout/finished-playing.ts new file mode 100644 index 0000000..358ceab --- /dev/null +++ b/server/src/ts/timeout/finished-playing.ts @@ -0,0 +1,51 @@ +import * as event from "../event"; +import * as startRevealing from "../events/game-event/start-revealing"; +import * as timeout from "../timeout"; +import * as util from "../util"; +import * as round from "../games/game/round"; +import wu from "wu"; + +/** + * Indicates that the round should start the judging phase if it is appropriate + * to do so. + */ +export interface FinishedPlaying { + timeout: "FinishedPlaying"; +} + +const isFinished = (round: round.Playing): boolean => { + const hasPlayed = new Set(wu(round.plays).map(play => play.playedBy)); + return util.setEquals(round.players, hasPlayed); +}; + +export const of = (): FinishedPlaying => ({ + timeout: "FinishedPlaying" +}); + +export const ifNeeded = (playing: round.Playing): FinishedPlaying | undefined => + isFinished(playing) ? of() : undefined; + +export const handle: timeout.Handler = ( + server, + timeout, + lobby +) => { + const game = lobby.game; + if (game === undefined) { + return {}; + } + const round = game.round; + const plays = round.plays; + if (round.stage !== "Playing" || !isFinished(round)) { + return {}; + } + game.round = { + ...round, + stage: "Revealing" + }; + const ids = Array.from(wu(plays).map(play => play.id)); + return { + lobby, + events: [event.target(startRevealing.of(ids))] + }; +}; diff --git a/server/src/ts/timeout/round-start.ts b/server/src/ts/timeout/round-start.ts new file mode 100644 index 0000000..a0e11b0 --- /dev/null +++ b/server/src/ts/timeout/round-start.ts @@ -0,0 +1,69 @@ +import wu from "wu"; +import * as event from "../event"; +import * as roundStarted from "../events/game-event/round-started"; +import * as game from "../games/game"; +import * as timeout from "../timeout"; +import * as card from "../games/cards/card"; + +/** + * Indicates that the round should start if it is still appropriate to do so. + */ +export interface RoundStart { + timeout: "RoundStart"; +} + +export const of = (): RoundStart => ({ + timeout: "RoundStart" +}); + +export const handle: timeout.Handler = (server, timeout, lobby) => { + const lobbyGame = lobby.game; + if (lobbyGame !== undefined) { + const round = lobbyGame.round; + if (round.stage === "Complete") { + const czar = game.nextCzar(lobbyGame); + const [call] = lobbyGame.decks.calls.replace(round.call); + const slotCount = card.slotCount(call); + const roundId = round.id + 1; + const playersInRound = new Set( + wu(lobbyGame.playerOrder).filter(id => id !== czar) + ); + lobbyGame.decks.responses.discard(round.plays.flatMap(play => play.play)); + lobbyGame.round = { + stage: "Playing", + id: roundId, + czar: czar, + players: playersInRound, + call: call, + plays: [] + }; + const playersArray = Array.from(playersInRound); + const basicEvent = roundStarted.of(roundId, czar, playersArray, call); + let events: event.Targeted[]; + if ( + slotCount > 2 || + (slotCount === 2 && + lobbyGame.rules.houseRules.packingHeat !== undefined) + ) { + events = [ + event.target(basicEvent, (id, user) => user.role !== "Player") + ]; + const responseDeck = lobbyGame.decks.responses; + for (const [id, player] of lobbyGame.players) { + const extra = responseDeck.draw(slotCount - 1); + player.hand.push(...extra); + events.push( + event.targetSpecifically( + roundStarted.of(roundId, czar, playersArray, call, extra), + id + ) + ); + } + } else { + events = [event.target(basicEvent)]; + } + return { lobby, events }; + } + } + return {}; +}; diff --git a/server/src/ts/timeout/user-disconnect.ts b/server/src/ts/timeout/user-disconnect.ts new file mode 100644 index 0000000..2e8420c --- /dev/null +++ b/server/src/ts/timeout/user-disconnect.ts @@ -0,0 +1,40 @@ +import * as event from "../event"; +import * as connectionChanged from "../events/lobby-event/connection-changed"; +import * as timeout from "../timeout"; +import * as user from "../user"; + +/** + * Indicates that the user should be marked as disconnected if they still are. + */ +export interface UserDisconnect { + timeout: "UserDisconnect"; + user: user.Id; +} + +export const of = (user: user.Id): UserDisconnect => ({ + timeout: "UserDisconnect", + user +}); + +export const handle: timeout.Handler = ( + server, + timeout, + lobby +) => { + const id = timeout.user; + const socket = server.socketManager.sockets.get(id); + if (socket === undefined) { + const userData = lobby.users.get(id); + if (userData === undefined) { + throw new Error("Player not in lobby."); + } + if (userData.connection !== "Disconnected") { + userData.connection = "Disconnected"; + return { + lobby, + events: [event.target(connectionChanged.disconnected(id))] + }; + } + } + return {}; +}; diff --git a/server/src/ts/user.ts b/server/src/ts/user.ts new file mode 100644 index 0000000..0537d3b --- /dev/null +++ b/server/src/ts/user.ts @@ -0,0 +1,94 @@ +import { RegisterUser } from "./action/initial/register-user"; + +/** A user in a lobby.*/ +export interface User { + name: Name; + presence: Presence; + connection: Connection; + privilege: Privilege; + role: Role; +} + +/** + * A user containing only state all users can see. + */ +export interface Public { + name: Name; + presence: Presence; + connection: Connection; + privilege: Privilege; + role: Role; +} + +/** + * A unique id for a user. + */ +export type Id = string; + +/** + * The name the user goes by. + * @maxLength 100 + * @minLength 1 + */ +export type Name = string; + +/** + * The level of privilege a user has. + */ +export type Privilege = "Privileged" | "Unprivileged"; + +/** + * If the user is currently in the lobby. + */ +export type Presence = "Joined" | "Left"; + +/** + * If the user is connected to the game at this moment. + */ +export type Connection = "Connected" | "Disconnected"; + +/** + * If the user is a spectator or a player. + */ +export type Role = "Spectator" | "Player"; + +/** + * If the user is playing. + */ +export const isPlaying: (user: User) => boolean = user => + user.role === "Player"; + +/** + * If the user is spectating. + */ +export const isSpectating: (user: User) => boolean = user => + user.role === "Spectator"; + +/** + * Create a new user. + * @param registration The details of the user to create. + * @param privilege The level of privilege the user has. + */ +export function create( + registration: RegisterUser, + privilege: Privilege = "Unprivileged" +): User { + return { + name: registration.name, + presence: "Joined", + connection: "Connected", + privilege: privilege, + role: "Player" + }; +} + +/** + * Gives a version of the user with only publicly visible properties. + */ +export const censor: (user: User) => Public = user => ({ + name: user.name, + presence: user.presence, + connection: user.connection, + privilege: user.privilege, + role: user.role +}); diff --git a/server/src/ts/user/token.ts b/server/src/ts/user/token.ts new file mode 100644 index 0000000..a3c1626 --- /dev/null +++ b/server/src/ts/user/token.ts @@ -0,0 +1,64 @@ +import * as jwt from "jsonwebtoken"; +import { + IncorrectIssuerError, + InvalidAuthenticationError +} from "../errors/authentication"; +import { GameCode } from "../lobby/game-code"; +import * as user from "../user"; + +/** + * A token that contains the encoded claims of a user. + */ +export type Token = string; + +/** + * The information needed to authorize a user. + * This is encoded into a JWT, signed so the information can be taken from the + * user and verified without storing locally. + */ +export interface Claims { + /** + * The game code for the lobby this claim is valid in. + */ + gc: GameCode; + uid: user.Id; + pvg: user.Privilege; +} + +/** + * Make a signed token from some claims. + * @param tokenClaims The claims. + * @param issuer The store these claims are valid in. + * @param secret The secret to sign the claims. + */ +export const create = ( + tokenClaims: Claims, + issuer: string, + secret: string +): Token => + jwt.sign(tokenClaims, secret, { algorithm: "HS256", issuer: issuer }); + +/** + * Verify the given token and return the claims encoded in it, if valid. + * Note this does *not* validate the game code in the claim. + * @param token The token to validate. + * @param issuer The store we are checking for. + * @param secret The secret to verify the signature on the token. + */ +export function validate(token: Token, issuer: string, secret: string): Claims { + try { + return jwt.verify(token, secret, { + algorithms: ["HS256"], + issuer: issuer + }) as Claims; + } catch (error) { + if (error.hasOwnProperty("name") && error.name === "JsonWebTokenError") { + if (error.message.startsWith("jwt issuer invalid.")) { + throw new IncorrectIssuerError(); + } else if (error.message.startsWith("invalid signature")) { + throw new InvalidAuthenticationError("invalid signature"); + } + } + throw error; + } +} diff --git a/server/src/ts/util.ts b/server/src/ts/util.ts new file mode 100644 index 0000000..ac00f17 --- /dev/null +++ b/server/src/ts/util.ts @@ -0,0 +1,134 @@ +/** + * Check if the given value is an iterable one. + */ +export const isIterable = ( + object: T | Iterable +): object is Iterable => + object !== null && + typeof (object as Iterable)[Symbol.iterator] === "function"; + +const isSingleArgument = ( + f: ((a: A, b: B) => C) | ((b: B) => C) +): f is (b: B) => C => f.length === 1; + +/** + * Take an object and apply the given function to each value, producing a new + * object. + */ +export function mapObjectValues( + obj: O, + f: ((key: string, value: V) => U) | ((value: V) => U) +): { [P in keyof O]: U } { + const newObj: { [key: string]: U } = {}; + for (const [key, value] of Object.entries(obj)) { + newObj[key] = isSingleArgument(f) ? f(value) : f(key, value); + } + return newObj as { [P in keyof O]: U }; +} + +/** + * Create an object from the given entries. + */ +export function entriesToObject( + entries: Iterable<[string, T]> +): { [key: string]: T } { + const obj: { [key: string]: T } = {}; + for (const [key, value] of entries) { + obj[key] = value; + } + return obj; +} + +/** + * Create an object from the given map. + */ +export const mapToObject = (map: Map): { [key: string]: T } => + entriesToObject(map); + +/** + * Count the number of elements in the given iterable that conform to the given + * predicates. Returns the counts in an object matching the shape of the + * predicates. + * @param iterable The iterable to count the values of. + * @param predicates The predicates to apply to each value to check if it should + * be counted. + */ +export function counts( + iterable: Iterable, + predicates: { [P in keyof U]: ((value: T) => boolean) } +): { [P in keyof U]: number } { + const keys = Object.keys(predicates) as (keyof U)[]; + const amounts = mapObjectValues(predicates, () => 0); + for (const value of iterable) { + for (const key of keys) { + if (predicates[key](value)) { + amounts[key] += 1; + } + } + } + return amounts; +} + +/** + * This will only be valid when the given outcome is impossible. Useful for + * exhaustiveness checks. + * @param value The impossible value. + */ +export function assertNever(value: never): never { + throw new Error(`Unexpected value: ${value}.`); +} + +/** + * Find the first value that satisfies the given predicate in the given iterable, + * allowing for type assertion. + * @param iterable The iterable. + * @param predicate THe predicate. + */ +export function findIs( + iterable: Iterable, + predicate: (item: T) => item is U +): U | undefined { + for (const item of iterable) { + if (predicate(item)) { + return item; + } + } + return undefined; +} + +/** + * Shuffle the given array in-place. + * @param items The array. + */ +export function shuffle(items: T[]): void { + for (let index = items.length - 1; index > 0; index -= 1) { + const random = Math.floor(Math.random() * (index + 1)); + [items[index], items[random]] = [items[random], items[index]]; + } +} + +/** + * Return a shuffled array of the given elements. + * @param items The items to shuffle. + */ +export function shuffled(items: Iterable): T[] { + const result = Array.from(items); + shuffle(result); + return result; +} + +/** + * If both sets contain the same (and only the same) values. + */ +export function setEquals(a: Set, b: Set): boolean { + if (a.size !== b.size) { + return false; + } else { + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + return true; + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..c45b2ec --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,64 @@ +{ + "include": [ + "./src/ts/**/*" + ], + "compilerOptions": { + "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "incremental": true, /* Enable incremental compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} diff --git a/test/massivedecks/lobby/ConfigSpec.scala b/test/massivedecks/lobby/ConfigSpec.scala deleted file mode 100644 index 7978e6f..0000000 --- a/test/massivedecks/lobby/ConfigSpec.scala +++ /dev/null @@ -1,63 +0,0 @@ -package massivedecks.lobby - -import org.specs2._ -import org.specs2.mock.Mockito - -import massivedecks.notifications.Notifiers -import massivedecks.models.{Game => GameModel} -import massivedecks.models.cardcast.CardcastDeck - -/** - * Specification for the configuration. - */ -class ConfigSpec extends Specification with Mockito { def is = s2""" - - The configuration should - Return added decks $addedDeck - Return added house rules $addedHouseRule - Not return removed house rules $removedHouseRule - Ignore removal of house rule that doesn't exist $nonExistantHouseRule - The model must accurately reflect the configuration $modelReflectsConfiguration - """ - - val mockHouseRule = "test" - val mockNotifiers = mock[Notifiers] - val mockDeckInfo = mock[GameModel.DeckInfo] - val mockDeck = mock[CardcastDeck] - mockDeck.info returns mockDeckInfo - - def mockConfig = new Config(mockNotifiers) - - def addedDeck = { - val config = mockConfig - config.addDeck(mockDeck) - config.decks must contain(exactly(mockDeck)) - } - - def addedHouseRule = { - val config = mockConfig - config.addHouseRule(mockHouseRule) - config.houseRules must contain(exactly(mockHouseRule)) - } - - def removedHouseRule = { - val config = mockConfig - config.addHouseRule(mockHouseRule) - config.removeHouseRule(mockHouseRule) - config.houseRules must beEmpty - } - - def nonExistantHouseRule = { - val config = mockConfig - config.removeHouseRule(mockHouseRule) - success - } - - def modelReflectsConfiguration = { - val config = mockConfig - config.addHouseRule(mockHouseRule) - config.addDeck(mockDeck) - config.config must_== GameModel.Config(List(mockDeckInfo), Set(mockHouseRule), None) - } - -} diff --git a/test/massivedecks/lobby/DeckSpec.scala b/test/massivedecks/lobby/DeckSpec.scala deleted file mode 100644 index 542c3ba..0000000 --- a/test/massivedecks/lobby/DeckSpec.scala +++ /dev/null @@ -1,47 +0,0 @@ -package massivedecks.lobby - -import massivedecks.exceptions.BadRequestException -import massivedecks.models.Game.{Call, Response} -import massivedecks.models.cardcast.CardcastDeck -import massivedecks.matchers.Matchers._ -import org.specs2._ -import org.specs2.mock.Mockito - -/** - * Specification for the deck. - */ -class DeckSpec extends Specification with Mockito { def is = s2""" - - The deck should - Throw if there are not any responses. $noResponses - Throw if there are not any calls. $noCalls - Reused responses should get unique ids. $uniqueIdsOnReusedResponses - Reused calls should get unique ids. $uniqueIdsOnReusedCalls - - """ - - def call = Call("call", List("callContent", "callContent")) - def response = Response("response", "responseContent") - - def deckNoResponses = CardcastDeck("noResponses", "", List(call), List()) - def deckNoCalls = CardcastDeck("noCalls", "", List(), List(response)) - - def decksOneOfEach = List(deckNoResponses, deckNoCalls) - - def noResponses = - new Deck(List(deckNoResponses)) must throwA[BadRequestException] - - def noCalls = - new Deck(List(deckNoCalls)) must throwA[BadRequestException] - - def uniqueIdsOnReusedResponses = { - val deck = new Deck(decksOneOfEach) - deck.drawResponses(2) must haveNoDuplicateValues - } - - def uniqueIdsOnReusedCalls = { - val deck = new Deck(decksOneOfEach) - deck.drawCall() must_!= deck.drawCall() - } - -} diff --git a/test/massivedecks/lobby/GameSpec.scala b/test/massivedecks/lobby/GameSpec.scala deleted file mode 100644 index d5f027e..0000000 --- a/test/massivedecks/lobby/GameSpec.scala +++ /dev/null @@ -1,15 +0,0 @@ -package massivedecks.lobby - -import org.specs2._ -import org.specs2.mock.Mockito - -/** - * Specification for the game. - */ -class GameSpec extends Specification with Mockito { def is = s2""" - - The game should - - """ - -} diff --git a/test/massivedecks/lobby/LobbySpec.scala b/test/massivedecks/lobby/LobbySpec.scala deleted file mode 100644 index dadf664..0000000 --- a/test/massivedecks/lobby/LobbySpec.scala +++ /dev/null @@ -1,15 +0,0 @@ -package massivedecks.lobby - -import org.specs2._ -import org.specs2.mock.Mockito - -/** - * Specification for the lobby. - */ -class LobbySpec extends Specification with Mockito { def is = s2""" - - The lobby should - - """ - -} diff --git a/test/massivedecks/lobby/PlayersSpec.scala b/test/massivedecks/lobby/PlayersSpec.scala deleted file mode 100644 index c8a6ace..0000000 --- a/test/massivedecks/lobby/PlayersSpec.scala +++ /dev/null @@ -1,83 +0,0 @@ -package massivedecks.lobby - -import massivedecks.exceptions.{BadRequestException, ForbiddenException} -import massivedecks.models.Player -import massivedecks.notifications.Notifiers -import org.specs2._ -import org.specs2.mock.Mockito - -/** - * Specification for the players. - */ -class PlayersSpec extends Specification with Mockito { def is = s2""" - - The players controller should - Not allow new players with the same name as an existing one $notAllowNewPlayerWithSameNameAsExisting - Notify all clients when a new player joins $notifyAllClientsOnNewPlayer - Validate a valid secret $validateValidSecret - Not validate a secret for a player that doesn't exist $noValidateSecretForNonExistentPlayer - Not validate a secret where the secret is wrong $noValidateSecretWhereWrong - Adding an AI should never result in a name clash $aiShouldAvoidNameClashes - Adding an AI should notify all clients $aiShouldNotifyAllClients - """ - - val playerName = "Player Name" - val playerName2 = "Player 2 Name" - - def notAllowNewPlayerWithSameNameAsExisting = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - players.addPlayer(playerName) - players.addPlayer(playerName) must throwA[BadRequestException] - } - - def notifyAllClientsOnNewPlayer = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - val secret = players.addPlayer(playerName) - there was one(mockNotifiers).playerJoin(players.getPlayer(secret.id)) - } - - def validateValidSecret = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - val secret = players.addPlayer(playerName) - players.validateSecret(secret) - success - } - - def noValidateSecretForNonExistentPlayer = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - val secret = players.addPlayer(playerName) - players.validateSecret(Player.Secret(Player.Id(-1), secret.secret)) must throwA[ForbiddenException] - } - - def noValidateSecretWhereWrong = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - val secret = players.addPlayer(playerName) - players.validateSecret(Player.Secret(secret.id, "wrong")) must throwA[ForbiddenException] - } - - def aiShouldAvoidNameClashes = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - players.addPlayer(Players.aiName) - for (name <- Players.aiNames) { - players.addPlayer(name) - } - players.addAi() - players.addAi() - success - } - - def aiShouldNotifyAllClients = { - val mockNotifiers = mock[Notifiers] - val players = new Players(mockNotifiers) - val secret = players.addAi() - there was one(mockNotifiers).playerJoin(Player(Player.Id(0), "Rando Cardrissian", Player.Neutral, 0, disconnected=false, left=false)) - there was one(mockNotifiers).playerStatus(Player.Id(0), Player.Ai) - } - -} diff --git a/test/massivedecks/matchers/Matchers.scala b/test/massivedecks/matchers/Matchers.scala deleted file mode 100644 index c529f84..0000000 --- a/test/massivedecks/matchers/Matchers.scala +++ /dev/null @@ -1,19 +0,0 @@ -package massivedecks.matchers - -import org.specs2.matcher.{Expectable, MatchResult, Matcher} - -object Matchers { - - class UniqueMatcher[E] extends Matcher[Traversable[E]] { - override def apply[S <: Traversable[E]](t: Expectable[S]): MatchResult[S] = - result( - t.value.size == t.value.toSet.size, - s"${t.description} contains no duplicate values", - s"${t.description} contains duplicate values", - t - ) - } - - def haveNoDuplicateValues[E] = new UniqueMatcher[E]() - -} diff --git a/test/massivedecks/notifications/FakeWebSocketClient.scala b/test/massivedecks/notifications/FakeWebSocketClient.scala deleted file mode 100644 index 342a408..0000000 --- a/test/massivedecks/notifications/FakeWebSocketClient.scala +++ /dev/null @@ -1,124 +0,0 @@ -package massivedecks.notifications - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Promise - -import play.api.libs.iteratee.{Enumerator, Iteratee} -import massivedecks.notifications.FakeWebSocketClient.{ReceivedBehaviour, SendingBehaviour} - -/** - * A fake web socket client that sits on the iteratee/enumerator interface and acts like a websocket. - */ -class FakeWebSocketClient(socket: (Iteratee[String, Unit], Enumerator[String])) { - private val (iteratee, enumerator) = socket - - /** - * Connect to the server, following the given behaviours for recieving and sending messages. - * - * @param receivedBehaviour The behaviour to follow when recieving messages. - * @param sendingBehaviour The behaviour to follow when sending messages initially. - * @param rest Any more behaviours to follow when sending messages after the first finishes. - */ - def connect(receivedBehaviour: ReceivedBehaviour)(sendingBehaviour: SendingBehaviour, rest: SendingBehaviour*): Unit = { - val consumer = receivedBehaviour.consumer - val producer = SendingBehaviour.sequentially(sendingBehaviour :: rest.toList) - // We bind the consumer first to try to ensure we catch responses to our messages from the producer. - enumerator(consumer) - producer(iteratee) - } - -} -object FakeWebSocketClient { - - /** - * A behaviour to take when receiving messages to the server. - */ - sealed trait ReceivedBehaviour { - def consumer: Iteratee[String, Unit] - } - - /** - * Ignore all messages from the server. - */ - case object Ignore extends ReceivedBehaviour { - val consumer = Iteratee.ignore[String] - } - - /** - * Call the given callback with each message as it comes. - * - * @param callback The callback. - */ - case class Callback(callback: (String) => Unit) extends ReceivedBehaviour { - val consumer = Iteratee.foreach(callback) - } - - /** - * A custom consumer from any iteratee, where each chunk is a message, and EOF is the socket closing. - * - * @param consumer The iteratee. - */ - case class Consumer(consumer: Iteratee[String, Unit]) extends ReceivedBehaviour - - - /** - * A behaviour to take when sending messages to the server. - */ - sealed trait SendingBehaviour { - def producer: Enumerator[String] - } - object SendingBehaviour { - /** - * Sequentially merge the given behaviours into a single enumerator. If no behaviours are given, give an eof. - * - * @param behaviours The behaviours. - * @return An enumerator. - */ - def sequentially(behaviours: List[SendingBehaviour]): Enumerator[String] = - behaviours match { - case List() => Enumerator.eof - case enumerator :: List() => enumerator.producer - case enumerator :: rest => enumerator.producer andThen sequentially(rest) - } - } - - /** - * Close the socket. Future sends will fail. - */ - case object Close extends SendingBehaviour { - val producer = Enumerator.eof[String] - } - - /** - * Do nothing, but keep the socket open. - */ - case object Nothing extends SendingBehaviour { - val producer = Enumerator.empty[String] - } - - /** - * Send the exact values, keeping the socket open when done. - * - * @param values The values to send. - */ - case class Values(values: String*) extends SendingBehaviour { - val producer = Enumerator.enumerate(values) - } - - /** - * Send the value of the promise when it succeeds. - * - * @param promise The promise. - */ - case class PromisedValue(promise: Promise[String]) extends SendingBehaviour { - val producer = Enumerator.flatten(promise.future.map(value => Enumerator(value))) - } - - /** - * A custom producer from any enumerator, where each chunk is a message, and EOF is the socket closing. - * - * @param producer Thr enumerator. - */ - case class Producer(producer: Enumerator[String]) extends SendingBehaviour - -} diff --git a/test/massivedecks/notifications/NotifierSpec.scala b/test/massivedecks/notifications/NotifierSpec.scala deleted file mode 100644 index f4fcfdd..0000000 --- a/test/massivedecks/notifications/NotifierSpec.scala +++ /dev/null @@ -1,77 +0,0 @@ -package massivedecks.notifications - -import scala.concurrent.Promise - -import massivedecks.models.Player -import massivedecks.notifications.FakeWebSocketClient._ -import Player.Formatters._ -import org.specs2._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mock.Mockito -import play.api.libs.json.Json - -/** - * Specification for the notifier. - */ -class NotifierSpec(implicit ee: ExecutionEnv) extends Specification with Mockito { def is = s2""" - - The notifier should - Run the connection callback when the websocket is connected $connectionCallback - Run the identification callback when recieving a secret on the websocket $identificationCallback - Run the closure callback when the websocket is closed $closureCallback - Notifications are sent to the websocket as expected $sendMessage - - """ - - val testSecret = Player.Secret(Player.Id(0), "secret") - val testMessage = "Test Message" - - def connectionCallback = { - val notifier = new Notifier() - val promise = Promise[Boolean]() - new FakeWebSocketClient(notifier.openedSocket( - () => { promise.success(true) }, - (secret) => {}, - () => {} - )).connect(Ignore)(Nothing) - promise.future must be_==(true).await - } - - def identificationCallback = { - val notifier = new Notifier() - val promise = Promise[Player.Secret]() - new FakeWebSocketClient(notifier.openedSocket( - () => {}, - (secret) => { promise.success(secret) }, - () => {} - )).connect(Ignore)(Values(Json.toJson(testSecret).toString())) - promise.future must be_==(testSecret).await - } - - def closureCallback = { - val notifier = new Notifier() - val promise = Promise[Boolean]() - new FakeWebSocketClient(notifier.openedSocket( - () => {}, - (secret) => {}, - () => { promise.success(true) } - )).connect(Ignore)(Close) - promise.future must be_==(true).await - } - - def sendMessage = { - val notifier = new Notifier() - val promise = Promise[String]() - new FakeWebSocketClient(notifier.openedSocket( - () => {}, - (secret) => {}, - () => {} - )).connect(Callback(message => promise.success(message)))(Nothing) - // We need to wait for the notifier to connect to the socket before sending. - // In the real world we know first messages are lossy and sync deals with anything missed. - Thread.sleep(100) - notifier.notify(testMessage) - promise.future must be_==(testMessage).await - } - -} diff --git a/test/massivedecks/notifications/NotifiersSpec.scala b/test/massivedecks/notifications/NotifiersSpec.scala deleted file mode 100644 index 6335f1e..0000000 --- a/test/massivedecks/notifications/NotifiersSpec.scala +++ /dev/null @@ -1,212 +0,0 @@ -package massivedecks.notifications - -import scala.concurrent.Promise - -import org.specs2._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mock.Mockito - -import play.api.libs.iteratee.{Enumerator, Iteratee} -import play.api.libs.json.Json - -import massivedecks.exceptions.ForbiddenException -import massivedecks.notifications.FakeWebSocketClient._ -import massivedecks.models.{Errors, Game, Lobby, Player} -import massivedecks.models.Game.Formatters._ -import massivedecks.models.Lobby.Formatters._ -import massivedecks.models.Player.Formatters._ - -/** - * Specification for the notifier manager. - */ -class NotifiersSpec(implicit ee: ExecutionEnv) extends Specification with Mockito { def is = s2""" - - The notifier manager should - Ask a new client to identify $askNewClientToIdentify - Call the identification callback when a client tries to identify $identificationCallback - Sync after identifying $syncAfterIdentifying - Call the close callback when a socket is closed $closeCallback - Send broadcast messages to all clients $broadcast - Send directed messages to the right client $unicast - Don't send directed messages to other clients $unicastWrongClient - If identification fails, don't send directed messages to the client $unicastFailedIdentification - """ - - val identifyRequest = "identify" - val lobbyAndHand = Lobby.LobbyAndHand(Lobby.Lobby("", Player.Id(0), Game.Config(List(), Set(), None), List(), Game.State.Configuring()), Game.Hand(List())) - val testSecret = Player.Secret(Player.Id(0), "secret") - - def askNewClientToIdentify = { - val notifiers = new Notifiers() - val promise = Promise[String]() - new FakeWebSocketClient(simpleOpenedSocket(notifiers)).connect(Callback(message => promise.success(message)))(Nothing) - promise.future must be_==(identifyRequest).await - } - - def identificationCallback = { - val notifiers = new Notifiers() - val promisedSecret = Promise[Player.Secret]() - identifiedClient(notifiers, testSecret, message => {}, notifiers.openedSocket( - (secret) => { - promisedSecret.success(secret) - lobbyAndHand - }, - (id) => {} - ))() - promisedSecret.future must be_==(testSecret).await - } - - def syncAfterIdentifying = { - val notifiers = new Notifiers() - - val promisedSync = Promise[String]() - identifiedClient(notifiers, testSecret, message => promisedSync.success(message), ignoreFirstSync = false)() - - val expectedMessage = Json.obj( - "event" -> "Sync", - "lobbyAndHand" -> Json.toJson(lobbyAndHand) - ).toString - - promisedSync.future must be_==(expectedMessage).await - } - - def closeCallback = { - val notifiers = new Notifiers() - val promisedId = Promise[Player.Id]() - identifiedClient(notifiers, testSecret, message => {}, notifiers.openedSocket( - (secret) => lobbyAndHand, - (id) => { promisedId.success(id) } - ))(Close) - promisedId.future must be_==(testSecret.id).await - } - - def broadcast = { - val notifiers = new Notifiers() - - val promisedIdentifiedMessage = Promise[String]() - identifiedClient(notifiers, testSecret, message => promisedIdentifiedMessage.success(message))() - - val promisedUnidentifiedMessage = Promise[String]() - new FakeWebSocketClient(simpleOpenedSocket(notifiers)).connect( - Callback(message => - if (message != identifyRequest) { - promisedUnidentifiedMessage.success(message) - }) - )( - Nothing - ) - - val call = Game.Call("call-id", List("call", "call")) - val czar = Player.Id(0) - - // We need to wait for the notifier to connect to the socket before sending. - // In the real world we know first messages are lossy and sync deals with anything missed. - Thread.sleep(100) - val expectedMessage = Json.obj( - "event" -> "GameStart", - "czar" -> Json.toJson(czar), - "call" -> Json.toJson(call) - ).toString - notifiers.gameStart(czar, call) - - promisedIdentifiedMessage.future.zip(promisedUnidentifiedMessage.future) must be_==((expectedMessage, expectedMessage)).await - } - - def unicast = { - val notifiers = new Notifiers() - - val promisedMessage = Promise[String]() - identifiedClient(notifiers, testSecret, message => promisedMessage.success(message))() - - val hand = Game.Hand(List()) - val expectedMessage = Json.obj( - "event" -> "HandChange", - "hand" -> Json.toJson(hand) - ).toString - - // We need to wait for the notifier to connect to the socket before sending. - // In the real world we know first messages are lossy and sync deals with anything missed. - Thread.sleep(100) - notifiers.handChange(testSecret.id, hand) - - promisedMessage.future must be_==(expectedMessage).await - } - - def unicastWrongClient = { - val notifiers = new Notifiers() - - val promisedMessageWrong = Promise[Nothing]() - identifiedClient(notifiers, testSecret, message => promisedMessageWrong.failure(throw new IllegalStateException()))() - - val secret = Player.Secret(Player.Id(99), "secret") - identifiedClient(notifiers, secret, message => {})() - - // We need to wait for the notifier to connect to the socket before sending. - // In the real world we know first messages are lossy and sync deals with anything missed. - Thread.sleep(100) - notifiers.handChange(testSecret.id, Game.Hand(List())) - - // Wait for us to get the message on the wrong client. - Thread.sleep(1000) - promisedMessageWrong.future.value match { - case None => success - case Some(_) => failure - } - } - - def unicastFailedIdentification = { - val notifiers = new Notifiers() - - val promisedMessageWrong = Promise[Nothing]() - identifiedClient(notifiers, testSecret, message => promisedMessageWrong.failure(throw new IllegalStateException()), - notifiers.openedSocket( - (secret) => throw ForbiddenException(Errors.SecretWrongOrNotAPlayer()), - (id) => {} - ))() - - // We need to wait for the notifier to connect to the socket before sending. - // In the real world we know first messages are lossy and sync deals with anything missed. - Thread.sleep(100) - notifiers.handChange(testSecret.id, Game.Hand(List())) - - // Wait for us to get the message despite the auth failure. - Thread.sleep(1000) - promisedMessageWrong.future.value match { - case None => success - case Some(_) => failure - } - } - - private def simpleOpenedSocket(notifiers: Notifiers) = notifiers.openedSocket( - (secret) => lobbyAndHand, - (id) => {} - ) - - private def identifiedClient(notifiers: Notifiers, secret: Player.Secret, callback: String => Unit, - socketOrNulls: (Iteratee[String, Unit], Enumerator[String]) = (null, null), - ignoreFirstSync: Boolean = true) - (sendingBehaviours: SendingBehaviour*) = { - val socket = if (socketOrNulls == (null, null)) { - simpleOpenedSocket(notifiers) - } else { - socketOrNulls - } - val promisedIdentification = Promise[String]() - var ignoredFirstSync = !ignoreFirstSync - new FakeWebSocketClient(socket).connect( - Callback(message => if (message == identifyRequest) { - promisedIdentification.success(Json.toJson(secret).toString) - } else { - if (!ignoredFirstSync && message.startsWith("{\"event\":\"Sync\"")) { - ignoredFirstSync = true - } else { - callback(message) - } - }) - )( - PromisedValue(promisedIdentification), - sendingBehaviours: _* - ) - } - -}