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 [](https://travis-ci.org/Lattyware/massivedecks)
+# Massive Decks [](https://travis-ci.org/Lattyware/massivedecks)
-[](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.)
-
-[][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)
-
-
-
-
-
-
@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 0000000..ff40f0b
Binary files /dev/null and b/client/assets/fonts/gothic-a1-v4-latin-700.woff2 differ
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 0000000..1686b35
Binary files /dev/null and b/client/assets/fonts/gothic-a1-v4-latin-regular.woff2 differ
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 0000000..d5acffb
Binary files /dev/null and b/client/assets/fonts/nimbus-sans-l-700.woff2 differ
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 0000000..55a50a4
Binary files /dev/null and b/client/assets/fonts/nimbus-sans-l-regular.woff2 differ
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 0000000..e327dc9
Binary files /dev/null and b/client/assets/fonts/roboto-v18-latin-700.woff2 differ
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 0000000..d3239a3
Binary files /dev/null and b/client/assets/fonts/roboto-v18-latin-700italic.woff2 differ
diff --git a/client/assets/fonts/roboto-v18-latin-italic.woff2 b/client/assets/fonts/roboto-v18-latin-italic.woff2
new file mode 100644
index 0000000..3791c88
Binary files /dev/null and b/client/assets/fonts/roboto-v18-latin-italic.woff2 differ
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 0000000..7e854e6
Binary files /dev/null and b/client/assets/fonts/roboto-v18-latin-regular.woff2 differ
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 0000000..acf1f05
Binary files /dev/null and b/client/assets/fonts/source-code-pro-v8-latin-regular.woff2 differ
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 0000000..49eedc3
Binary files /dev/null and b/client/assets/images/cardcast.png differ
diff --git a/client/assets/images/certificate-thumbs-up.svg b/client/assets/images/certificate-thumbs-up.svg
new file mode 100644
index 0000000..be639f2
--- /dev/null
+++ b/client/assets/images/certificate-thumbs-up.svg
@@ -0,0 +1,3 @@
+
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 0000000..4856514
Binary files /dev/null and b/client/assets/images/icon.png differ
diff --git a/client/assets/images/icon.svg b/client/assets/images/icon.svg
new file mode 100644
index 0000000..5753a21
--- /dev/null
+++ b/client/assets/images/icon.svg
@@ -0,0 +1,4 @@
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 827f4d2..0000000
Binary files a/public/apple-touch-icon-precomposed.png and /dev/null differ
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
deleted file mode 100644
index ff066f8..0000000
Binary files a/public/apple-touch-icon.png and /dev/null differ
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index c60ba7b..0000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/icon.png b/public/icon.png
deleted file mode 100644
index d40a566..0000000
Binary files a/public/icon.png and /dev/null differ
diff --git a/public/images/bgnoise_lg.png b/public/images/bgnoise_lg.png
deleted file mode 100644
index caacc6f..0000000
Binary files a/public/images/bgnoise_lg.png and /dev/null differ
diff --git a/public/images/icon.svg b/public/images/icon.svg
deleted file mode 100644
index ddfd25c..0000000
--- a/public/images/icon.svg
+++ /dev/null
@@ -1,112 +0,0 @@
-
-
-
-
diff --git a/public/images/icons/fast-forward.png b/public/images/icons/fast-forward.png
deleted file mode 100644
index ef972b6..0000000
Binary files a/public/images/icons/fast-forward.png and /dev/null differ
diff --git a/public/images/icons/gavel.png b/public/images/icons/gavel.png
deleted file mode 100644
index e60c318..0000000
Binary files a/public/images/icons/gavel.png and /dev/null differ
diff --git a/public/images/icons/hourglass.png b/public/images/icons/hourglass.png
deleted file mode 100644
index 2ae4dd0..0000000
Binary files a/public/images/icons/hourglass.png and /dev/null differ
diff --git a/public/javascripts/mui.min.js b/public/javascripts/mui.min.js
deleted file mode 100644
index de60160..0000000
--- a/public/javascripts/mui.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function e(t,n,i){function o(s,l){if(!n[s]){if(!t[s]){var a="function"==typeof require&&require;if(!l&&a)return a(s,!0);if(r)return r(s,!0);throw new Error("Cannot find module '"+s+"'")}var c=n[s]={exports:{}};t[s][0].call(c.exports,function(e){var n=t[s][1][e];return o(n?n:e)},c,c.exports,e,t,n,i)}return n[s].exports}for(var r="function"==typeof require&&require,s=0;sd&&(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 cdbe609..0000000
Binary files a/public/mstile.png and /dev/null differ
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 @@
-
-
-
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