Kaynağa Gözat

Rewrite frontend with Vue+Buevy and ditch React+Ant Design.

- antd+react was resulting in extremely clunky and unreadable
  spaghetti frontend code (primarily due to how antd is).
- Buefy is lighter by an order of magnitude, has excellent
  responsive views (especially tables) and usability.
- Vue's templating produces far more readable template code.
Kailash Nadh 5 yıl önce
ebeveyn
işleme
97583fe4b4
70 değiştirilmiş dosya ile 16339 ekleme ve 6243 silme
  1. 8 3
      Makefile
  2. 5 5
      campaigns.go
  3. 0 5
      frontend/.babelrc
  4. 3 0
      frontend/.browserslistrc
  5. 7 0
      frontend/.editorconfig
  6. 17 0
      frontend/.eslintrc.js
  7. 16 15
      frontend/.gitignore
  8. 16 0
      frontend/README.md
  9. 5 0
      frontend/babel.config.js
  10. 0 31
      frontend/config-overrides.js
  11. 2927 0
      frontend/fontello/config.json
  12. 30 27
      frontend/package.json
  13. 0 1
      frontend/public/gallery.svg
  14. 8 11
      frontend/public/index.html
  15. 0 15
      frontend/public/manifest.json
  16. 0 193
      frontend/src/App.js
  17. 124 0
      frontend/src/App.vue
  18. 0 879
      frontend/src/Campaign.js
  19. 0 747
      frontend/src/Campaigns.js
  20. 0 26
      frontend/src/Dashboard.css
  21. 0 190
      frontend/src/Dashboard.js
  22. 0 139
      frontend/src/Forms.js
  23. 0 469
      frontend/src/Import.js
  24. 0 275
      frontend/src/Layout.js
  25. 0 496
      frontend/src/Lists.js
  26. 0 176
      frontend/src/Media.js
  27. 0 75
      frontend/src/ModalPreview.js
  28. 0 458
      frontend/src/Subscriber.js
  29. 0 850
      frontend/src/Subscribers.js
  30. 0 443
      frontend/src/Templates.js
  31. 183 0
      frontend/src/api/index.js
  32. 43 0
      frontend/src/assets/buefy.scss
  33. BIN
      frontend/src/assets/favicon.png
  34. 72 0
      frontend/src/assets/icons/fontello.css
  35. BIN
      frontend/src/assets/icons/fontello.woff2
  36. 0 0
      frontend/src/assets/logo.svg
  37. 480 0
      frontend/src/assets/style.scss
  38. 93 0
      frontend/src/components/CampaignPreview.vue
  39. 183 0
      frontend/src/components/Editor.vue
  40. 111 0
      frontend/src/components/ListSelector.vue
  41. 22 126
      frontend/src/constants.js
  42. 0 391
      frontend/src/index.css
  43. 0 7
      frontend/src/index.js
  44. 0 2
      frontend/src/logo.svg
  45. 21 0
      frontend/src/main.js
  46. 0 117
      frontend/src/registerServiceWorker.js
  47. 95 0
      frontend/src/router/index.js
  48. 0 1
      frontend/src/static/gallery.svg
  49. 48 0
      frontend/src/store/index.js
  50. 75 65
      frontend/src/utils.js
  51. 5 0
      frontend/src/views/About.vue
  52. 366 0
      frontend/src/views/Campaign.vue
  53. 368 0
      frontend/src/views/Campaigns.vue
  54. 58 0
      frontend/src/views/Dashboard.vue
  55. 75 0
      frontend/src/views/Forms.vue
  56. 298 0
      frontend/src/views/Import.vue
  57. 118 0
      frontend/src/views/ListForm.vue
  58. 160 0
      frontend/src/views/Lists.vue
  59. 179 0
      frontend/src/views/Media.vue
  60. 75 0
      frontend/src/views/SubscriberBulkList.vue
  61. 197 0
      frontend/src/views/SubscriberForm.vue
  62. 457 0
      frontend/src/views/Subscribers.vue
  63. 139 0
      frontend/src/views/TemplateForm.vue
  64. 167 0
      frontend/src/views/Templates.vue
  65. 17 0
      frontend/vue.config.js
  66. 9060 0
      frontend/yarn.lock
  67. 1 0
      handlers.go
  68. 4 2
      init.go
  69. 2 2
      models/models.go
  70. 1 1
      queries.sql

+ 8 - 3
Makefile

@@ -4,7 +4,12 @@ VERSION := $(shell git describe)
 BUILDSTR := ${VERSION} (${LAST_COMMIT} $(shell date -u +"%Y-%m-%dT%H:%M:%S%z"))
 BUILDSTR := ${VERSION} (${LAST_COMMIT} $(shell date -u +"%Y-%m-%dT%H:%M:%S%z"))
 
 
 BIN := listmonk
 BIN := listmonk
-STATIC := config.toml.sample schema.sql queries.sql static/public:/public static/email-templates frontend/build:/frontend
+STATIC := config.toml.sample \
+	schema.sql queries.sql \
+	static/public:/public \
+	static/email-templates \
+	frontend/dist:/frontend \
+	frontend/dist/frontend:/frontend
 
 
 # Dependencies.
 # Dependencies.
 .PHONY: deps
 .PHONY: deps
@@ -19,7 +24,7 @@ build:
 
 
 .PHONY: build-frontend
 .PHONY: build-frontend
 build-frontend:
 build-frontend:
-	export REACT_APP_VERSION="${VERSION}" && cd frontend && yarn build
+	export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn build
 
 
 .PHONY: run
 .PHONY: run
 run: build
 run: build
@@ -27,7 +32,7 @@ run: build
 
 
 .PHONY: run-frontend
 .PHONY: run-frontend
 run-frontend:
 run-frontend:
-	export REACT_APP_VERSION="${VERSION}" && cd frontend && yarn start
+	export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn serve
 
 
 .PHONY: test
 .PHONY: test
 test:
 test:

+ 5 - 5
campaigns.go

@@ -301,7 +301,7 @@ func handleUpdateCampaign(c echo.Context) error {
 		o = c
 		o = c
 	}
 	}
 
 
-	res, err := app.queries.UpdateCampaign.Exec(cm.ID,
+	_, err := app.queries.UpdateCampaign.Exec(cm.ID,
 		o.Name,
 		o.Name,
 		o.Subject,
 		o.Subject,
 		o.FromEmail,
 		o.FromEmail,
@@ -318,10 +318,6 @@ func handleUpdateCampaign(c echo.Context) error {
 			fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
 	}
 	}
 
 
-	if n, _ := res.RowsAffected(); n == 0 {
-		return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
-	}
-
 	return handleGetCampaigns(c)
 	return handleGetCampaigns(c)
 }
 }
 
 
@@ -597,6 +593,10 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
 		}
 		}
 	}
 	}
 
 
+	if len(c.ListIDs) == 0 {
+		return c, errors.New("no lists selected")
+	}
+
 	camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
 	camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
 	if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
 	if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
 		return c, fmt.Errorf("Error compiling campaign body: %v", err)
 		return c, fmt.Errorf("Error compiling campaign body: %v", err)

+ 0 - 5
frontend/.babelrc

@@ -1,5 +0,0 @@
-{
-  "presets": ["env", "react"],
-  "plugins": [["transform-react-jsx", { "pragma": "h" }]]
-}
-

+ 3 - 0
frontend/.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 7 - 0
frontend/.editorconfig

@@ -0,0 +1,7 @@
+[*.{js,jsx,ts,tsx,vue}]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+max_line_length = 100

+ 17 - 0
frontend/.eslintrc.js

@@ -0,0 +1,17 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: [
+    'plugin:vue/essential',
+    '@vue/airbnb',
+  ],
+  parserOptions: {
+    parser: 'babel-eslint',
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+  },
+};

+ 16 - 15
frontend/.gitignore

@@ -1,21 +1,22 @@
-# See https://help.github.com/ignore-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-
-# testing
-/coverage
-
-# production
-/build
-
-# misc
 .DS_Store
 .DS_Store
+node_modules
+/dist
+
+# local env files
 .env.local
 .env.local
-.env.development.local
-.env.test.local
-.env.production.local
+.env.*.local
 
 
+# Log files
 npm-debug.log*
 npm-debug.log*
 yarn-debug.log*
 yarn-debug.log*
 yarn-error.log*
 yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 16 - 0
frontend/README.md

@@ -0,0 +1,16 @@
+# listmonk frontend (Vue + Buefy)
+
+It's best if the `listmonk/frontend` editor is opened in an IDE as a separate project where the frontend directory is the rool of the project.
+
+
+## Icon pack
+Buefy by default uses [Material Design Icons](https://materialdesignicons.com) (MDI) with icon classes prefixed by `mdi-`.
+
+listmonk uses only a handful of icons from the massive MDI set packed as web font, using [Fontello](https://fontello.com). To add more icons to the set using fontello:
+
+- Go to Fontello and drag and drop `frontend/fontello/config.json` (This is the full MDI set converted from TTF to SVG icons to work with Fontello).
+- Use the UI to search for icons and add them to the selection (add icons from under the `Custom` section)
+- Download the Fontello pack and from the ZIP:
+    - Copy and overwrite `config.json` to `frontend/fontello`
+    - Copy `fontello.woff2` to `frontend/src/assets/icons`.
+    - Open `css/fontello.css` and copy the individual icon definitions and overwrite the ones in `frontend/src/assets/icons/fontello.css`

+ 5 - 0
frontend/babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset',
+  ],
+};

+ 0 - 31
frontend/config-overrides.js

@@ -1,31 +0,0 @@
-const { injectBabelPlugin } = require("react-app-rewired")
-const rewireLess = require("react-app-rewire-less")
-
-module.exports = function override(config, env) {
-  config = injectBabelPlugin(
-    [
-      "import",
-      {
-        libraryName: "antd",
-        libraryDirectory: "es",
-        style: true
-      }
-    ], // change importing css to less
-    config
-  )
-  config = rewireLess.withLoaderOptions({
-    modifyVars: {
-      "@font-family":
-        '"IBM Plex Sans", "Helvetica Neueue", "Segoe UI", "sans-serif"',
-      "@font-size-base": "15px",
-      "@primary-color": "#7f2aff",
-      "@shadow-1-up": "0 -2px 3px @shadow-color",
-      "@shadow-1-down": "0 2px 3px @shadow-color",
-      "@shadow-1-left": "-2px 0 3px @shadow-color",
-      "@shadow-1-right": "2px 0 3px @shadow-color",
-      "@shadow-2": "0 2px 6px @shadow-color"
-    },
-    javascriptEnabled: true
-  })(config, env)
-  return config
-}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 2927 - 0
frontend/fontello/config.json


+ 30 - 27
frontend/package.json

@@ -2,36 +2,39 @@
   "name": "listmonk",
   "name": "listmonk",
   "version": "0.1.0",
   "version": "0.1.0",
   "private": true,
   "private": true,
-  "dependencies": {
-    "antd": "^3.6.5",
-    "axios": "^0.18.0",
-    "bizcharts": "^3.2.5-beta.4",
-    "dayjs": "^1.7.5",
-    "react": "^16.4.1",
-    "react-app-rewire-less": "^2.1.3",
-    "react-app-rewired": "^1.6.2",
-    "react-dom": "^16.4.1",
-    "react-quill": "^1.3.1",
-    "react-router": "^4.3.1",
-    "react-router-dom": "^4.3.1",
-    "react-scripts": "1.1.4"
-  },
   "scripts": {
   "scripts": {
-    "start": "react-app-rewired start",
-    "build": "GENERATE_SOURCEMAP=false PUBLIC_URL=/frontend/ react-app-rewired build",
-    "test": "react-app-rewired test --env=jsdom",
-    "eject": "react-scripts eject"
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "build-report": "vue-cli-service build --report",
+    "lint": "vue-cli-service lint"
   },
   },
-  "eslintConfig": {
-    "extends": "react-app"
+  "dependencies": {
+    "axios": "^0.19.2",
+    "buefy": "^0.8.20",
+    "core-js": "^3.6.5",
+    "dayjs": "^1.8.28",
+    "humps": "^2.0.1",
+    "node-sass": "^4.14.1",
+    "qs": "^6.9.4",
+    "quill": "^1.3.7",
+    "quill-delta": "^4.2.2",
+    "sass-loader": "^8.0.2",
+    "vue": "^2.6.11",
+    "vue-quill-editor": "^3.0.6",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "babel-plugin-import": "^1.11.0",
-    "eslint-plugin-prettier": "^3.0.1",
-    "less-plugin-npm-import": "^2.1.0",
-    "prettier": "1.15.3"
-  },
-  "prettier": {
-    "semi": false
+    "@vue/cli-plugin-babel": "~4.4.0",
+    "@vue/cli-plugin-eslint": "~4.4.0",
+    "@vue/cli-plugin-router": "~4.4.0",
+    "@vue/cli-plugin-vuex": "~4.4.0",
+    "@vue/cli-service": "~4.4.0",
+    "@vue/eslint-config-airbnb": "^5.0.2",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-import": "^2.20.2",
+    "eslint-plugin-vue": "^6.2.2",
+    "vue-template-compiler": "^2.6.11"
   }
   }
 }
 }

+ 0 - 1
frontend/public/gallery.svg

@@ -1 +0,0 @@
-<svg viewbox="0 0 18 18"><rect class="ql-stroke" height="10" width="12" x="3" y="4"></rect><circle class="ql-fill" cx="6" cy="7" r="1"></circle><polyline class="ql-even ql-fill" points="5 12 5 11 7 9 8 10 11 7 13 9 13 12 5 12"></polyline></svg>

+ 8 - 11
frontend/public/index.html

@@ -1,21 +1,18 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
   <head>
   <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="theme-color" content="#000000">
-    <script src="/api/config.js" type="text/javascript"></script>
-    <link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,600" rel="stylesheet">
-    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
-    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png">
-    <title>listmonk</title>
-    <script>VERSION = "%REACT_APP_VERSION%";</script>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
+    <link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,600" rel="stylesheet" />
+    <title><%= htmlWebpackPlugin.options.title %></title>
   </head>
   </head>
   <body>
   <body>
     <noscript>
     <noscript>
-      You need to enable JavaScript to run this app.
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
     </noscript>
     </noscript>
 
 
-    <div id="root"></div>
+    <div id="app"></div>
   </body>
   </body>
 </html>
 </html>

+ 0 - 15
frontend/public/manifest.json

@@ -1,15 +0,0 @@
-{
-  "short_name": "React App",
-  "name": "Create React App Sample",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    }
-  ],
-  "start_url": "./index.html",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}

+ 0 - 193
frontend/src/App.js

@@ -1,193 +0,0 @@
-import React from "react"
-import Utils from "./utils"
-import { BrowserRouter } from "react-router-dom"
-import { Icon, notification } from "antd"
-import axios from "axios"
-import qs from "qs"
-
-import logo from "./static/listmonk.svg"
-import Layout from "./Layout"
-import * as cs from "./constants"
-
-/*
-  App acts as a an "automagic" wrapper for all sub components. It is also the central
-  store for data required by various child components. In addition, all HTTP requests
-  are fired through App.requests(), where successful responses are set in App's state
-  for child components to access via this.props.data[type]. The structure is as follows:
-    App.state.data = {
-      "lists": [],
-      "subscribers": []
-      // etc.
-    }
-
-  A number of assumptions are made here for the "automagic" behaviour.
-  1. All responses to resources return lists
-  2. All PUT, POST, DELETE requests automatically append /:id to the API URIs.
-*/
-
-class App extends React.PureComponent {
-  models = [
-    cs.ModelUsers,
-    cs.ModelSubscribers,
-    cs.ModelLists,
-    cs.ModelCampaigns,
-    cs.ModelTemplates
-  ]
-
-  state = {
-    // Initialize empty states.
-    reqStates: this.models.reduce(
-      // eslint-disable-next-line
-      (map, obj) => ((map[obj] = cs.StatePending), map),
-      {}
-    ),
-    // eslint-disable-next-line
-    data: this.models.reduce((map, obj) => ((map[obj] = []), map), {}),
-    modStates: {}
-  }
-
-  componentDidMount = () => {
-    axios.defaults.paramsSerializer = params => {
-      return qs.stringify(params, { arrayFormat: "repeat" })
-    }
-  }
-
-  // modelRequest is an opinionated wrapper for model specific HTTP requests,
-  // including setting model states.
-  modelRequest = async (model, route, method, params) => {
-    let url = replaceParams(route, params)
-
-    this.setState({
-      reqStates: { ...this.state.reqStates, [model]: cs.StatePending }
-    })
-    try {
-      let req = {
-        method: method,
-        url: url
-      }
-
-      if (method === cs.MethodGet || method === cs.MethodDelete) {
-        req.params = params ? params : {}
-      } else {
-        req.data = params ? params : {}
-      }
-
-      let res = await axios(req)
-      this.setState({
-        reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
-      })
-
-      // If it's a GET call, set the response as the data state.
-      if (method === cs.MethodGet) {
-        this.setState({
-          data: { ...this.state.data, [model]: res.data.data }
-        })
-      }
-      return res
-    } catch (e) {
-      // If it's a GET call, throw a global notification.
-      if (method === cs.MethodGet) {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error fetching data",
-          description: Utils.HttpError(e).message
-        })
-      }
-
-      // Set states and show the error on the layout.
-      this.setState({
-        reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
-      })
-      throw Utils.HttpError(e)
-    }
-  }
-
-  // request is a wrapper for generic HTTP requests.
-  request = async (url, method, params, headers) => {
-    url = replaceParams(url, params)
-
-    this.setState({
-      reqStates: { ...this.state.reqStates, [url]: cs.StatePending }
-    })
-    try {
-      let req = {
-        method: method,
-        url: url,
-        headers: headers ? headers : {}
-      }
-
-      if (method === cs.MethodGet || method === cs.MethodDelete) {
-        req.params = params ? params : {}
-      } else {
-        req.data = params ? params : {}
-      }
-
-      let res = await axios(req)
-
-      this.setState({
-        reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
-      })
-      return res
-    } catch (e) {
-      this.setState({
-        reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
-      })
-      throw Utils.HttpError(e)
-    }
-  }
-
-  pageTitle = title => {
-    document.title = title
-  }
-
-  render() {
-    if (!window.CONFIG) {
-      return (
-        <div className="broken">
-          <p>
-            <img src={logo} alt="listmonk logo" />
-          </p>
-          <hr />
-
-          <h1>
-            <Icon type="warning" /> Something's not right
-          </h1>
-          <p>
-            The app configuration could not be loaded. Please ensure that the
-            app is running and then refresh this page.
-          </p>
-        </div>
-      )
-    }
-
-    return (
-      <BrowserRouter>
-        <Layout
-          modelRequest={this.modelRequest}
-          request={this.request}
-          reqStates={this.state.reqStates}
-          pageTitle={this.pageTitle}
-          config={window.CONFIG}
-          data={this.state.data}
-        />
-      </BrowserRouter>
-    )
-  }
-}
-
-function replaceParams(route, params) {
-  // Replace :params in the URL with params in the array.
-  let uriParams = route.match(/:([a-z0-9\-_]+)/gi)
-  if (uriParams && uriParams.length > 0) {
-    uriParams.forEach(p => {
-      let pName = p.slice(1) // Lose the ":" prefix
-      if (params && params.hasOwnProperty(pName)) {
-        route = route.replace(p, params[pName])
-      }
-    })
-  }
-
-  return route
-}
-
-export default App

+ 124 - 0
frontend/src/App.vue

@@ -0,0 +1,124 @@
+<template>
+  <div id="app">
+    <section class="sidebar">
+      <b-sidebar
+        type="is-white"
+        position="static"
+        mobile="reduce"
+        :fullheight="true"
+        :open="true"
+        :can-cancel="false"
+      >
+        <div>
+          <div class="logo">
+            <a href="/"><img class="full" src="@/assets/logo.svg"/></a>
+            <img class="favicon" src="@/assets/favicon.png"/>
+            <p class="is-size-7 has-text-grey version">{{ version }}</p>
+          </div>
+          <b-menu :accordion="false">
+            <b-menu-list>
+              <b-menu-item :to="{name: 'dashboard'}" tag="router-link"
+                :active="activeItem.dashboard"
+                icon="view-dashboard-variant-outline" label="Dashboard">
+              </b-menu-item><!-- dashboard -->
+
+              <b-menu-item :expanded="activeGroup.lists"
+                icon="format-list-bulleted-square" label="Lists">
+                <b-menu-item :to="{name: 'lists'}" tag="router-link"
+                  :active="activeItem.lists"
+                  icon="format-list-bulleted-square" label="All lists"></b-menu-item>
+
+                <b-menu-item :to="{name: 'forms'}" tag="router-link"
+                  :active="activeItem.forms"
+                  icon="newspaper-variant-outline" label="Forms"></b-menu-item>
+              </b-menu-item><!-- lists -->
+
+              <b-menu-item :expanded="activeGroup.subscribers"
+                icon="account-multiple" label="Subscribers">
+                <b-menu-item :to="{name: 'subscribers'}" tag="router-link"
+                  :active="activeItem.subscribers"
+                  icon="account-multiple" label="All subscribers"></b-menu-item>
+
+                <b-menu-item :to="{name: 'import'}" tag="router-link"
+                  :active="activeItem.import"
+                  icon="file-upload-outline" label="Import"></b-menu-item>
+              </b-menu-item><!-- subscribers -->
+
+              <b-menu-item :expanded="activeGroup.campaigns"
+                  icon="rocket-launch-outline" label="Campaigns">
+                <b-menu-item :to="{name: 'campaigns'}" tag="router-link"
+                  :active="activeItem.campaigns"
+                  icon="rocket-launch-outline" label="All campaigns"></b-menu-item>
+
+                <b-menu-item :to="{name: 'campaign', params: {id: 'new'}}" tag="router-link"
+                  :active="activeItem.campaign"
+                  icon="plus" label="Create new"></b-menu-item>
+
+                <b-menu-item :to="{name: 'media'}" tag="router-link"
+                  :active="activeItem.media"
+                  icon="image-outline" label="Media"></b-menu-item>
+
+                <b-menu-item :to="{name: 'templates'}" tag="router-link"
+                  :active="activeItem.templates"
+                  icon="file-image-outline" label="Templates"></b-menu-item>
+              </b-menu-item><!-- campaigns -->
+
+              <!-- <b-menu-item :to="{name: 'settings'}" tag="router-link"
+                :active="activeItem.settings"
+                icon="cog-outline" label="Settings"></b-menu-item> -->
+            </b-menu-list>
+          </b-menu>
+        </div>
+      </b-sidebar>
+    </section>
+    <!-- sidebar-->
+
+    <!-- body //-->
+    <div class="main">
+      <router-view :key="$route.fullPath" />
+    </div>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+
+export default Vue.extend({
+  name: 'App',
+
+  data() {
+    return {
+      activeItem: {},
+      activeGroup: {},
+    };
+  },
+
+  watch: {
+    $route(to) {
+      // Set the current route name to true for active+expanded keys in the
+      // menu to pick up.
+      this.activeItem = { [to.name]: true };
+      if (to.meta.group) {
+        this.activeGroup = { [to.meta.group]: true };
+      }
+    },
+  },
+
+  mounted() {
+    // Lists is required across different views. On app load, fetch the lists
+    // and have them in the store.
+    this.$api.getLists();
+  },
+
+  computed: {
+    version() {
+      return process.env.VUE_APP_VERSION;
+    },
+  },
+});
+</script>
+
+<style lang="scss">
+  @import "assets/style.scss";
+  @import "assets/icons/fontello.css";
+</style>

+ 0 - 879
frontend/src/Campaign.js

@@ -1,879 +0,0 @@
-import React from "react"
-import {
-  Modal,
-  Tabs,
-  Row,
-  Col,
-  Form,
-  Switch,
-  Select,
-  Radio,
-  Tag,
-  Input,
-  Button,
-  Icon,
-  Spin,
-  DatePicker,
-  Popconfirm,
-  notification
-} from "antd"
-import * as cs from "./constants"
-import Media from "./Media"
-import ModalPreview from "./ModalPreview"
-
-import moment from "moment"
-import parseUrl from "querystring"
-import ReactQuill from "react-quill"
-import Delta from "quill-delta"
-import "react-quill/dist/quill.snow.css"
-
-const formItemLayout = {
-  labelCol: { xs: { span: 16 }, sm: { span: 10 }, md: { span: 4 } },
-  wrapperCol: { xs: { span: 16 }, sm: { span: 14 }, md: { span: 10 } }
-}
-
-class Editor extends React.PureComponent {
-  state = {
-    editor: null,
-    quill: null,
-    rawInput: null,
-    selContentType: cs.CampaignContentTypeRichtext,
-    contentType: cs.CampaignContentTypeRichtext,
-    body: ""
-  }
-
-  quillModules = {
-    toolbar: {
-      container: [
-        [{ header: [1, 2, 3, false] }],
-        ["bold", "italic", "underline", "strike", "blockquote", "code"],
-        [{ color: [] }, { background: [] }, { size: [] }],
-        [
-          { list: "ordered" },
-          { list: "bullet" },
-          { indent: "-1" },
-          { indent: "+1" }
-        ],
-        [
-          { align: "" },
-          { align: "center" },
-          { align: "right" },
-          { align: "justify" }
-        ],
-        ["link", "image"],
-        ["clean", "font"]
-      ],
-      handlers: {
-        image: () => {
-          this.props.toggleMedia()
-        }
-      }
-    }
-  }
-
-  componentDidMount = () => {
-    // The editor component will only load once the individual campaign metadata
-    // has loaded, i.e., record.body is guaranteed to be available here.
-    if (this.props.record && this.props.record.id) {
-      this.setState({
-        body: this.props.record.body,
-        contentType: this.props.record.content_type,
-        selContentType: this.props.record.content_type
-      })
-    }
-  }
-
-  // Custom handler for inserting images from the media popup.
-  insertMedia = uri => {
-    const quill = this.state.quill.getEditor()
-    let range = quill.getSelection(true)
-    quill.updateContents(
-      new Delta()
-        .retain(range.index)
-        .delete(range.length)
-        .insert({ image: uri }),
-      null
-    )
-  }
-
-  handleSelContentType = (_, e) => {
-    this.setState({ selContentType: e.props.value })
-  }
-
-  handleSwitchContentType = () => {
-    this.setState({ contentType: this.state.selContentType })
-    if (!this.state.quill || !this.state.quill.editor || !this.state.rawInput) {
-      return
-    }
-
-    // Switching from richtext to html.
-    let body = ""
-    if (this.state.selContentType === cs.CampaignContentTypeHTML) {
-      body = this.state.quill.editor.container.firstChild.innerHTML
-      // eslint-disable-next-line
-      this.state.rawInput.value = body
-    } else if (this.state.selContentType === cs.CampaignContentTypeRichtext) {
-      body = this.state.rawInput.value
-      this.state.quill.editor.clipboard.dangerouslyPasteHTML(body, "raw")
-    }
-
-    this.props.setContent(this.state.selContentType, body)
-  }
-
-  render() {
-    return (
-      <div>
-        <header className="header">
-          {!this.props.formDisabled && (
-            <Row>
-              <Col span={20}>
-                <div className="content-type">
-                  <p>Content format</p>
-                  <Select
-                    name="content_type"
-                    onChange={this.handleSelContentType}
-                    style={{ minWidth: 200 }}
-                    value={this.state.selContentType}
-                  >
-                    <Select.Option value={ cs.CampaignContentTypeRichtext }>Rich Text</Select.Option>
-                    <Select.Option value={ cs.CampaignContentTypeHTML }>Raw HTML</Select.Option>
-                  </Select>
-                  {this.state.contentType !== this.state.selContentType && (
-                    <div className="actions">
-                      <Popconfirm
-                        title="The content may lose its formatting. Are you sure?"
-                        onConfirm={this.handleSwitchContentType}
-                      >
-                        <Button>
-                          <Icon type="save" /> Switch format
-                        </Button>
-                      </Popconfirm>
-                    </div>
-                  )}
-                </div>
-              </Col>
-              <Col span={4} />
-            </Row>
-          )}
-        </header>
-        <ReactQuill
-          readOnly={this.props.formDisabled}
-          style={{
-            display: this.state.contentType === cs.CampaignContentTypeRichtext ? "block" : "none"
-          }}
-          modules={this.quillModules}
-          defaultValue={this.props.record.body}
-          ref={o => {
-            if (!o) {
-              return
-            }
-
-            this.setState({ quill: o })
-            document.querySelector(".ql-editor").focus()
-          }}
-          onChange={() => {
-            if (!this.state.quill) {
-              return
-            }
-
-            this.props.setContent(
-              this.state.contentType,
-              this.state.quill.editor.root.innerHTML
-            )
-          }}
-        />
-
-        <Input.TextArea
-          readOnly={this.props.formDisabled}
-          placeholder="Your message here"
-          style={{
-            display: this.state.contentType === "html" ? "block" : "none"
-          }}
-          id="html-body"
-          rows={10}
-          autosize={{ minRows: 2, maxRows: 10 }}
-          defaultValue={this.props.record.body}
-          ref={o => {
-            if (!o) {
-              return
-            }
-
-            this.setState({ rawInput: o.textAreaRef })
-          }}
-          onChange={e => {
-            this.props.setContent(this.state.contentType, e.target.value)
-          }}
-        />
-      </div>
-    )
-  }
-}
-
-class TheFormDef extends React.PureComponent {
-  state = {
-    editorVisible: false,
-    sendLater: false,
-    loading: false
-  }
-
-  componentWillReceiveProps(nextProps) {
-    // On initial load, toggle the send_later switch if the record
-    // has a "send_at" date.
-    if (nextProps.record.send_at === this.props.record.send_at) {
-      return
-    }
-    this.setState({
-      sendLater: nextProps.isSingle && nextProps.record.send_at !== null
-    })
-  }
-
-  validateEmail = (rule, value, callback) => {
-    if (!value.match(/(.+?)\s<(.+?)@(.+?)>/)) {
-      return callback("Format should be: Your Name <email@address.com>")
-    }
-
-    callback()
-  }
-
-  handleSendLater = e => {
-    this.setState({ sendLater: e })
-  }
-
-  // Handle create / edit form submission.
-  handleSubmit = cb => {
-    if (this.state.loading) {
-      return
-    }
-
-    if (!cb) {
-      // Set a fake callback.
-      cb = () => {}
-    }
-
-    this.props.form.validateFields((err, values) => {
-      if (err) {
-        return
-      }
-
-      if (!values.tags) {
-        values.tags = []
-      }
-
-      values.type = cs.CampaignTypeRegular
-      values.body = this.props.body
-      values.content_type = this.props.contentType
-
-      if (values.send_at) {
-        values.send_later = true
-      } else {
-        values.send_later = false
-      }
-
-      // Create a new campaign.
-      this.setState({ loading: true })
-      if (!this.props.isSingle) {
-        this.props
-          .modelRequest(
-            cs.ModelCampaigns,
-            cs.Routes.CreateCampaign,
-            cs.MethodPost,
-            values
-          )
-          .then(resp => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "Campaign created",
-              description: `"${values["name"]}" created`
-            })
-
-            this.props.route.history.push({
-              pathname: cs.Routes.ViewCampaign.replace(
-                ":id",
-                resp.data.data.id
-              ),
-              hash: "content-tab"
-            })
-            cb(true)
-          })
-          .catch(e => {
-            notification["error"]({
-              placement: cs.MsgPosition,
-              message: "Error",
-              description: e.message
-            })
-            this.setState({ loading: false })
-            cb(false)
-          })
-      } else {
-        this.props
-          .modelRequest(
-            cs.ModelCampaigns,
-            cs.Routes.UpdateCampaign,
-            cs.MethodPut,
-            {
-              ...values,
-              id: this.props.record.id
-            }
-          )
-          .then(resp => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "Campaign updated",
-              description: `"${values["name"]}" updated`
-            })
-            this.setState({ loading: false })
-            this.props.setRecord(resp.data.data)
-            cb(true)
-          })
-          .catch(e => {
-            notification["error"]({
-              placement: cs.MsgPosition,
-              message: "Error",
-              description: e.message
-            })
-            this.setState({ loading: false })
-            cb(false)
-          })
-      }
-    })
-  }
-
-  handleTestCampaign = e => {
-    e.preventDefault()
-    this.props.form.validateFields((err, values) => {
-      if (err) {
-        return
-      }
-
-      if (!values.tags) {
-        values.tags = []
-      }
-
-      values.id = this.props.record.id
-      values.body = this.props.body
-      values.content_type = this.props.contentType
-
-      this.setState({ loading: true })
-      this.props
-        .request(cs.Routes.TestCampaign, cs.MethodPost, values)
-        .then(resp => {
-          this.setState({ loading: false })
-          notification["success"]({
-            placement: cs.MsgPosition,
-            message: "Test sent",
-            description: `Test messages sent`
-          })
-        })
-        .catch(e => {
-          this.setState({ loading: false })
-          notification["error"]({
-            placement: cs.MsgPosition,
-            message: "Error",
-            description: e.message
-          })
-        })
-    })
-  }
-
-  render() {
-    const { record } = this.props
-    const { getFieldDecorator } = this.props.form
-
-    let subLists = []
-    if (this.props.isSingle && record.lists) {
-      subLists = record.lists
-        .map(v => {
-          // Exclude deleted lists.
-          return v.id !== 0 ? v.id : null
-        })
-        .filter(v => v !== null)
-    } else if (this.props.route.location.search) {
-      // One or more list_id in the query params.
-      const p = parseUrl.parse(this.props.route.location.search.substring(1))
-      if (p.hasOwnProperty("list_id")) {
-        if(Array.isArray(p.list_id)) {
-          p.list_id.forEach(i => {
-            // eslint-disable-next-line radix
-            const id = parseInt(i)
-            if (id) {
-              subLists.push(id)
-            }
-          });
-        } else {
-          // eslint-disable-next-line radix
-          const id = parseInt(p.list_id)
-          if (id) {
-            subLists.push(id)
-          }
-        }
-      }
-    }
-
-    if (this.record) {
-      this.props.pageTitle(record.name + " / Campaigns")
-    } else {
-      this.props.pageTitle("New campaign")
-    }
-
-    return (
-      <div>
-        <Spin spinning={this.state.loading}>
-          <Form onSubmit={this.handleSubmit}>
-            <Form.Item {...formItemLayout} label="Campaign name">
-              {getFieldDecorator("name", {
-                extra:
-                  "This is internal and will not be visible to subscribers",
-                initialValue: record.name,
-                rules: [{ required: true }]
-              })(
-                <Input
-                  disabled={this.props.formDisabled}
-                  autoFocus
-                  maxLength={200}
-                />
-              )}
-            </Form.Item>
-            <Form.Item {...formItemLayout} label="Subject">
-              {getFieldDecorator("subject", {
-                initialValue: record.subject,
-                rules: [{ required: true }]
-              })(<Input disabled={this.props.formDisabled} maxLength={500} />)}
-            </Form.Item>
-            <Form.Item {...formItemLayout} label="From address">
-              {getFieldDecorator("from_email", {
-                initialValue: record.from_email
-                  ? record.from_email
-                  : this.props.config.fromEmail,
-                rules: [{ required: true }, { validator: this.validateEmail }]
-              })(
-                <Input
-                  disabled={this.props.formDisabled}
-                  placeholder="Company Name <email@company.com>"
-                  maxLength={200}
-                />
-              )}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              label="Lists"
-              extra="Lists to subscribe to"
-            >
-              {getFieldDecorator("lists", {
-                initialValue:
-                  subLists.length > 0
-                    ? subLists
-                    : this.props.data[cs.ModelLists].hasOwnProperty(
-                        "results"
-                      ) && this.props.data[cs.ModelLists].results.length === 1
-                    ? [this.props.data[cs.ModelLists].results[0].id]
-                    : undefined,
-                rules: [{ required: true }]
-              })(
-                <Select disabled={this.props.formDisabled} mode="multiple">
-                  {this.props.data[cs.ModelLists].hasOwnProperty("results") &&
-                    [...this.props.data[cs.ModelLists].results].map((v) => 
-                      (record.type !== cs.CampaignTypeOptin || v.optin === cs.ListOptinDouble) && (
-                      <Select.Option value={v["id"]} key={v["id"]}>
-                        {v["name"]}
-                      </Select.Option>
-                    ))}
-                </Select>
-              )}
-            </Form.Item>
-            <Form.Item {...formItemLayout} label="Template" extra="Template">
-              {getFieldDecorator("template_id", {
-                initialValue: record.template_id
-                  ? record.template_id
-                  : this.props.data[cs.ModelTemplates].length > 0
-                  ? this.props.data[cs.ModelTemplates].filter(
-                      t => t.is_default
-                    )[0].id
-                  : undefined,
-                rules: [{ required: true }]
-              })(
-                <Select disabled={this.props.formDisabled}>
-                  {this.props.data[cs.ModelTemplates].map((v, i) => (
-                    <Select.Option value={v["id"]} key={v["id"]}>
-                      {v["name"]}
-                    </Select.Option>
-                  ))}
-                </Select>
-              )}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              label="Tags"
-              extra="Hit Enter after typing a word to add multiple tags"
-            >
-              {getFieldDecorator("tags", { initialValue: record.tags })(
-                <Select disabled={this.props.formDisabled} mode="tags" />
-              )}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              label="Messenger"
-              style={{
-                display:
-                  this.props.config.messengers.length === 1 ? "none" : "block"
-              }}
-            >
-              {getFieldDecorator("messenger", {
-                initialValue: record.messenger ? record.messenger : "email"
-              })(
-                <Radio.Group className="messengers">
-                  {[...this.props.config.messengers].map((v, i) => (
-                    <Radio disabled={this.props.formDisabled} value={v} key={v}>
-                      {v}
-                    </Radio>
-                  ))}
-                </Radio.Group>
-              )}
-            </Form.Item>
-
-            <hr />
-            <Form.Item {...formItemLayout} label="Send later?">
-              <Row>
-                <Col lg={4}>
-                  {getFieldDecorator("send_later")(
-                    <Switch
-                      disabled={this.props.formDisabled}
-                      checked={this.state.sendLater}
-                      onChange={this.handleSendLater}
-                    />
-                  )}
-                </Col>
-                <Col lg={20}>
-                  {this.state.sendLater &&
-                    getFieldDecorator("send_at", {
-                      initialValue:
-                        record && typeof record.send_at === "string"
-                          ? moment(record.send_at)
-                          : moment(new Date())
-                              .add(1, "days")
-                              .startOf("day")
-                    })(
-                      <DatePicker
-                        disabled={this.props.formDisabled}
-                        showTime
-                        format="YYYY-MM-DD HH:mm:ss"
-                        placeholder="Select a date and time"
-                      />
-                    )}
-                </Col>
-              </Row>
-            </Form.Item>
-
-            {this.props.isSingle && (
-              <div>
-                <hr />
-                <Form.Item
-                  {...formItemLayout}
-                  label="Send test messages"
-                  extra="Hit Enter after typing an address to add multiple recipients. The addresses must belong to existing subscribers."
-                >
-                  {getFieldDecorator("subscribers")(
-                    <Select mode="tags" style={{ width: "100%" }} />
-                  )}
-                </Form.Item>
-                <Form.Item {...formItemLayout} label="&nbsp;" colon={false}>
-                  <Button onClick={this.handleTestCampaign}>
-                    <Icon type="mail" /> Send test
-                  </Button>
-                </Form.Item>
-              </div>
-            )}
-          </Form>
-        </Spin>
-      </div>
-    )
-  }
-}
-const TheForm = Form.create()(TheFormDef)
-
-class Campaign extends React.PureComponent {
-  state = {
-    campaignID: this.props.route.match.params
-      ? parseInt(this.props.route.match.params.campaignID, 10)
-      : 0,
-    record: {},
-    formRef: null,
-    contentType: cs.CampaignContentTypeRichtext,
-    previewRecord: null,
-    body: "",
-    currentTab: "form",
-    editor: null,
-    loading: true,
-    mediaVisible: false,
-    formDisabled: false
-  }
-
-  componentDidMount = () => {
-    // Fetch lists.
-    this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
-      per_page: "all"
-    })
-
-    // Fetch templates.
-    this.props.modelRequest(
-      cs.ModelTemplates,
-      cs.Routes.GetTemplates,
-      cs.MethodGet
-    )
-
-    // Fetch campaign.
-    if (this.state.campaignID) {
-      this.fetchRecord(this.state.campaignID)
-    } else {
-      this.setState({ loading: false })
-    }
-
-    // Content tab?
-    if (document.location.hash === "#content-tab") {
-      this.setCurrentTab("content")
-    }
-  }
-
-  setRecord = r => {
-    this.setState({ record: r })
-  }
-
-  fetchRecord = id => {
-    this.props
-      .request(cs.Routes.GetCampaign, cs.MethodGet, { id: id })
-      .then(r => {
-        const record = r.data.data
-        this.setState({ loading: false })
-        this.setRecord(record)
-
-        // The form for non draft and scheduled campaigns should be locked.
-        if (
-          record.status !== cs.CampaignStatusDraft &&
-          record.status !== cs.CampaignStatusScheduled
-        ) {
-          this.setState({ formDisabled: true })
-        }
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  setContent = (contentType, body) => {
-    this.setState({ contentType: contentType, body: body })
-  }
-
-  toggleMedia = () => {
-    this.setState({ mediaVisible: !this.state.mediaVisible })
-  }
-
-  setCurrentTab = tab => {
-    this.setState({ currentTab: tab })
-  }
-
-  handlePreview = record => {
-    this.setState({ previewRecord: record })
-  }
-
-  render() {
-    return (
-      <section className="content campaign">
-        <Row gutter={[2, 16]}>
-          <Col span={24} md={12}>
-            {!this.state.record.id && <h1>Create a campaign</h1>}
-            {this.state.record.id && (
-              <div>
-                <h1>
-                  <Tag
-                    color={cs.CampaignStatusColors[this.state.record.status]}
-                  >
-                    {this.state.record.status}
-                  </Tag>
-                  {this.state.record.type === cs.CampaignStatusOptin && (
-                    <Tag className="campaign-type" color="geekblue">
-                      {this.state.record.type}
-                    </Tag>
-                  )}
-                  {this.state.record.name}
-                </h1>
-                <span className="text-tiny text-grey">
-                  ID {this.state.record.id} &mdash; UUID{" "}
-                  {this.state.record.uuid}
-                </span>
-              </div>
-            )}
-          </Col>
-          <Col span={24} md={12} className="right header-action-break">
-            {!this.state.formDisabled && !this.state.loading && (
-              <div>
-                <Button
-                  type="primary"
-                  icon="save"
-                  onClick={() => {
-                    this.state.formRef.handleSubmit()
-                  }}
-                >
-                  {!this.state.record.id ? "Continue" : "Save changes"}
-                </Button>{" "}
-                {this.state.record.status === cs.CampaignStatusDraft &&
-                  this.state.record.send_at && (
-                    <Popconfirm
-                      title="The campaign will start automatically at the scheduled date and time. Schedule now?"
-                      onConfirm={() => {
-                        this.state.formRef.handleSubmit(() => {
-                          this.props.route.history.push({
-                            pathname: cs.Routes.ViewCampaigns,
-                            state: {
-                              campaign: this.state.record,
-                              campaignStatus: cs.CampaignStatusScheduled
-                            }
-                          })
-                        })
-                      }}
-                    >
-                      <Button icon="clock-circle" type="primary">
-                        Schedule campaign
-                      </Button>
-                    </Popconfirm>
-                  )}
-                {this.state.record.status === cs.CampaignStatusDraft &&
-                  !this.state.record.send_at && (
-                    <Popconfirm
-                      title="Campaign properties cannot be changed once it starts. Save changes and start now?"
-                      onConfirm={() => {
-                        this.state.formRef.handleSubmit(() => {
-                          this.props.route.history.push({
-                            pathname: cs.Routes.ViewCampaigns,
-                            state: {
-                              campaign: this.state.record,
-                              campaignStatus: cs.CampaignStatusRunning
-                            }
-                          })
-                        })
-                      }}
-                    >
-                      <Button icon="rocket" type="primary">
-                        Start campaign
-                      </Button>
-                    </Popconfirm>
-                  )}
-              </div>
-            )}
-          </Col>
-        </Row>
-        <br />
-
-        <Tabs
-          type="card"
-          activeKey={this.state.currentTab}
-          onTabClick={t => {
-            this.setState({ currentTab: t })
-          }}
-        >
-          <Tabs.TabPane tab="Campaign" key="form">
-            <Spin spinning={this.state.loading}>
-              <TheForm
-                {...this.props}
-                wrappedComponentRef={r => {
-                  if (!r) {
-                    return
-                  }
-                  // Take the editor's reference and save it in the state
-                  // so that it's insertMedia() function can be passed to <Media />
-                  this.setState({ formRef: r })
-                }}
-                record={this.state.record}
-                setRecord={this.setRecord}
-                isSingle={this.state.record.id ? true : false}
-                body={
-                  this.state.body ? this.state.body : this.state.record.body
-                }
-                contentType={this.state.contentType}
-                formDisabled={this.state.formDisabled}
-                fetchRecord={this.fetchRecord}
-                setCurrentTab={this.setCurrentTab}
-              />
-            </Spin>
-          </Tabs.TabPane>
-          <Tabs.TabPane
-            tab="Content"
-            disabled={this.state.record.id ? false : true}
-            key="content"
-          >
-            {this.state.record.id && (
-              <div>
-                <Editor
-                  {...this.props}
-                  ref={r => {
-                    if (!r) {
-                      return
-                    }
-                    // Take the editor's reference and save it in the state
-                    // so that it's insertMedia() function can be passed to <Media />
-                    this.setState({ editor: r })
-                  }}
-                  isSingle={this.state.record.id ? true : false}
-                  record={this.state.record}
-                  visible={this.state.editorVisible}
-                  toggleMedia={this.toggleMedia}
-                  setContent={this.setContent}
-                  formDisabled={this.state.formDisabled}
-                />
-                <div className="content-actions">
-                  <p>
-                    <Button
-                      icon="search"
-                      onClick={() => this.handlePreview(this.state.record)}
-                    >
-                      Preview
-                    </Button>
-                  </p>
-                </div>
-              </div>
-            )}
-            {!this.state.record.id && <Spin className="empty-spinner" />}
-          </Tabs.TabPane>
-        </Tabs>
-
-        <Modal
-          visible={this.state.mediaVisible}
-          width="900px"
-          title="Media"
-          okText={"Ok"}
-          onCancel={this.toggleMedia}
-          onOk={this.toggleMedia}
-        >
-          <Media
-            {...{
-              ...this.props,
-              insertMedia: this.state.editor
-                ? this.state.editor.insertMedia
-                : null,
-              onCancel: this.toggleMedia,
-              onOk: this.toggleMedia
-            }}
-          />
-        </Modal>
-
-        {this.state.previewRecord && (
-          <ModalPreview
-            title={this.state.previewRecord.name}
-            body={this.state.body}
-            previewURL={cs.Routes.PreviewCampaign.replace(
-              ":id",
-              this.state.previewRecord.id
-            )}
-            onCancel={() => {
-              this.setState({ previewRecord: null })
-            }}
-          />
-        )}
-      </section>
-    )
-  }
-}
-
-export default Campaign

+ 0 - 747
frontend/src/Campaigns.js

@@ -1,747 +0,0 @@
-import React from "react"
-import { Link } from "react-router-dom"
-import {
-  Row,
-  Col,
-  Button,
-  Table,
-  Icon,
-  Tooltip,
-  Tag,
-  Popconfirm,
-  Progress,
-  Modal,
-  notification,
-  Input
-} from "antd"
-import dayjs from "dayjs"
-import relativeTime from "dayjs/plugin/relativeTime"
-
-import ModalPreview from "./ModalPreview"
-import * as cs from "./constants"
-
-class Campaigns extends React.PureComponent {
-  defaultPerPage = 20
-
-  state = {
-    formType: null,
-    pollID: -1,
-    queryParams: {},
-    stats: {},
-    record: null,
-    previewRecord: null,
-    cloneName: "",
-    cloneModalVisible: false,
-    modalWaiting: false
-  }
-
-  // Pagination config.
-  paginationOptions = {
-    hideOnSinglePage: false,
-    showSizeChanger: true,
-    showQuickJumper: true,
-    defaultPageSize: this.defaultPerPage,
-    pageSizeOptions: ["20", "50", "70", "100"],
-    position: "both",
-    showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`
-  }
-
-  constructor(props) {
-    super(props)
-
-    this.columns = [
-      {
-        title: "Name",
-        dataIndex: "name",
-        sorter: true,
-        width: "20%",
-        vAlign: "top",
-        filterIcon: filtered => (
-          <Icon
-            type="search"
-            style={{ color: filtered ? "#1890ff" : undefined }}
-          />
-        ),
-        filterDropdown: ({
-          setSelectedKeys,
-          selectedKeys,
-          confirm,
-          clearFilters
-        }) => (
-          <div style={{ padding: 8 }}>
-            <Input
-              ref={node => {
-                this.searchInput = node
-              }}
-              placeholder={`Search`}
-              onChange={e =>
-                setSelectedKeys(e.target.value ? [e.target.value] : [])
-              }
-              onPressEnter={() => confirm()}
-              style={{ width: 188, marginBottom: 8, display: "block" }}
-            />
-            <Button
-              type="primary"
-              onClick={() => confirm()}
-              icon="search"
-              size="small"
-              style={{ width: 90, marginRight: 8 }}
-            >
-              Search
-            </Button>
-            <Button
-              onClick={() => {
-                clearFilters()
-              }}
-              size="small"
-              style={{ width: 90 }}
-            >
-              Reset
-            </Button>
-          </div>
-        ),
-        render: (text, record) => {
-          const out = []
-          out.push(
-            <div className="name" key={`name-${record.id}`}>
-              <Link to={`/campaigns/${record.id}`}>{text}</Link>{" "}
-              {record.type === cs.CampaignStatusOptin && (
-                <Tooltip title="Opt-in campaign" placement="top">
-                  <Tag className="campaign-type" color="geekblue">
-                    {record.type}
-                  </Tag>
-                </Tooltip>
-              )}
-              <br />
-              <span className="text-tiny">{record.subject}</span>
-            </div>
-          )
-
-          if (record.tags.length > 0) {
-            for (let i = 0; i < record.tags.length; i++) {
-              out.push(<Tag key={`tag-${i}`}>{record.tags[i]}</Tag>)
-            }
-          }
-
-          return out
-        }
-      },
-      {
-        title: "Status",
-        dataIndex: "status",
-        className: "status",
-        width: "10%",
-        filters: [
-          { text: "Draft", value: "draft" },
-          { text: "Running", value: "running" },
-          { text: "Scheduled", value: "scheduled" },
-          { text: "Paused", value: "paused" },
-          { text: "Cancelled", value: "cancelled" },
-          { text: "Finished", value: "finished" }
-        ],
-        render: (status, record) => {
-          let color = cs.CampaignStatusColors.hasOwnProperty(status)
-            ? cs.CampaignStatusColors[status]
-            : ""
-          return (
-            <div>
-              <Tag color={color}>{status}</Tag>
-              {record.send_at && (
-                <span className="text-tiny date">
-                  Scheduled &mdash;{" "}
-                  {dayjs(record.send_at).format(cs.DateFormat)}
-                </span>
-              )}
-            </div>
-          )
-        }
-      },
-      {
-        title: "Lists",
-        dataIndex: "lists",
-        width: "25%",
-        align: "left",
-        className: "lists",
-        render: (lists, record) => {
-          const out = []
-          lists.forEach(l => {
-            out.push(
-              <Tag className="name" key={`name-${l.id}`}>
-                <Link to={`/subscribers/lists/${l.id}`}>{l.name}</Link>
-              </Tag>
-            )
-          })
-
-          return out
-        }
-      },
-      {
-        title: "Stats",
-        className: "stats",
-        width: "30%",
-        render: (text, record) => {
-          if (
-            record.status !== cs.CampaignStatusDraft &&
-            record.status !== cs.CampaignStatusScheduled
-          ) {
-            return this.renderStats(record)
-          }
-        }
-      },
-      {
-        title: "",
-        dataIndex: "actions",
-        className: "actions",
-        width: "15%",
-        render: (text, record) => {
-          return (
-            <div className="actions">
-              {record.status === cs.CampaignStatusPaused && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() =>
-                    this.handleUpdateStatus(record, cs.CampaignStatusRunning)
-                  }
-                >
-                  <Tooltip title="Resume campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="rocket" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              {record.status === cs.CampaignStatusRunning && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() =>
-                    this.handleUpdateStatus(record, cs.CampaignStatusPaused)
-                  }
-                >
-                  <Tooltip title="Pause campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="pause-circle-o" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              {/* Draft with send_at */}
-              {record.status === cs.CampaignStatusDraft && record.send_at && (
-                <Popconfirm
-                  title="The campaign will start automatically at the scheduled date and time. Schedule now?"
-                  onConfirm={() =>
-                    this.handleUpdateStatus(record, cs.CampaignStatusScheduled)
-                  }
-                >
-                  <Tooltip title="Schedule campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="clock-circle" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              {record.status === cs.CampaignStatusDraft && !record.send_at && (
-                <Popconfirm
-                  title="Campaign properties cannot be changed once it starts. Start now?"
-                  onConfirm={() =>
-                    this.handleUpdateStatus(record, cs.CampaignStatusRunning)
-                  }
-                >
-                  <Tooltip title="Start campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="rocket" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              {(record.status === cs.CampaignStatusPaused ||
-                record.status === cs.CampaignStatusRunning) && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() =>
-                    this.handleUpdateStatus(record, cs.CampaignStatusCancelled)
-                  }
-                >
-                  <Tooltip title="Cancel campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="close-circle-o" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              <Tooltip title="Preview campaign" placement="bottom">
-                <a
-                  role="button"
-                  onClick={() => {
-                    this.handlePreview(record)
-                  }}
-                >
-                  <Icon type="search" />
-                </a>
-              </Tooltip>
-
-              <Tooltip title="Clone campaign" placement="bottom">
-                <a
-                  role="button"
-                  onClick={() => {
-                    let r = {
-                      ...record,
-                      lists: record.lists.map(i => {
-                        return i.id
-                      })
-                    }
-                    this.handleToggleCloneForm(r)
-                  }}
-                >
-                  <Icon type="copy" />
-                </a>
-              </Tooltip>
-
-              {(record.status === cs.CampaignStatusDraft ||
-                record.status === cs.CampaignStatusScheduled) && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() => this.handleDeleteRecord(record)}
-                >
-                  <Tooltip title="Delete campaign" placement="bottom">
-                    <a role="button">
-                      <Icon type="delete" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-            </div>
-          )
-        }
-      }
-    ]
-  }
-
-  progressPercent(record) {
-    return Math.round(
-      (this.getStatsField("sent", record) /
-        this.getStatsField("to_send", record)) *
-        100,
-      2
-    )
-  }
-
-  isDone(record) {
-    return (
-      this.getStatsField("status", record) === cs.CampaignStatusFinished ||
-      this.getStatsField("status", record) === cs.CampaignStatusCancelled
-    )
-  }
-
-  // getStatsField returns a stats field value of a given record if it
-  // exists in the stats state, or the value from the record itself.
-  getStatsField = (field, record) => {
-    if (this.state.stats.hasOwnProperty(record.id)) {
-      return this.state.stats[record.id][field]
-    }
-
-    return record[field]
-  }
-
-  renderStats = record => {
-    let color = cs.CampaignStatusColors.hasOwnProperty(record.status)
-      ? cs.CampaignStatusColors[record.status]
-      : ""
-    const startedAt = this.getStatsField("started_at", record)
-    const updatedAt = this.getStatsField("updated_at", record)
-    const sent = this.getStatsField("sent", record)
-    const toSend = this.getStatsField("to_send", record)
-    const isDone = this.isDone(record)
-
-    const r = this.getStatsField("rate", record)
-    const rate = r ? r : 0
-
-    return (
-      <div>
-        {!isDone && (
-          <Progress
-            strokeColor={color}
-            status="active"
-            type="line"
-            percent={this.progressPercent(record)}
-          />
-        )}
-        <Row>
-          <Col className="label" span={10}>
-            Sent
-          </Col>
-          <Col span={12}>
-            {sent >= toSend && <span>{toSend}</span>}
-            {sent < toSend && (
-              <span>
-                {sent} / {toSend}
-              </span>
-            )}
-            &nbsp;
-            {record.status === cs.CampaignStatusRunning && (
-              <Icon type="loading" style={{ fontSize: 12 }} spin />
-            )}
-          </Col>
-        </Row>
-
-        {rate > 0 && (
-          <Row>
-            <Col className="label" span={10}>
-              Rate
-            </Col>
-            <Col span={12}>{Math.round(rate, 2)} / min</Col>
-          </Row>
-        )}
-
-        <Row>
-          <Col className="label" span={10}>
-            Views
-          </Col>
-          <Col span={12}>{record.views}</Col>
-        </Row>
-        <Row>
-          <Col className="label" span={10}>
-            Clicks
-          </Col>
-          <Col span={12}>{record.clicks}</Col>
-        </Row>
-        <br />
-        <Row>
-          <Col className="label" span={10}>
-            Created
-          </Col>
-          <Col span={12}>{dayjs(record.created_at).format(cs.DateFormat)}</Col>
-        </Row>
-
-        {startedAt && (
-          <Row>
-            <Col className="label" span={10}>
-              Started
-            </Col>
-            <Col span={12}>{dayjs(startedAt).format(cs.DateFormat)}</Col>
-          </Row>
-        )}
-        {isDone && (
-          <Row>
-            <Col className="label" span={10}>
-              Ended
-            </Col>
-            <Col span={12}>{dayjs(updatedAt).format(cs.DateFormat)}</Col>
-          </Row>
-        )}
-        {startedAt && updatedAt && (
-          <Row>
-            <Col className="label" span={10}>
-              Duration
-            </Col>
-            <Col className="duration" span={12}>
-              {dayjs(updatedAt).from(dayjs(startedAt), true)}
-            </Col>
-          </Row>
-        )}
-      </div>
-    )
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Campaigns")
-    dayjs.extend(relativeTime)
-    this.fetchRecords()
-
-    // Did we land here to start a campaign?
-    let loc = this.props.route.location
-    let state = loc.state
-    if (state && state.hasOwnProperty("campaign")) {
-      this.handleUpdateStatus(state.campaign, state.campaignStatus)
-      delete state.campaign
-      delete state.campaignStatus
-      this.props.route.history.replace({ ...loc, state })
-    }
-  }
-
-  componentWillUnmount() {
-    window.clearInterval(this.state.pollID)
-  }
-
-  fetchRecords = params => {
-    if (!params) {
-      params = {}
-    }
-    let qParams = {
-      page: this.state.queryParams.page,
-      per_page: this.state.queryParams.per_page
-    }
-
-    // Avoid sending blank string where the enum check will fail.
-    if (!params.status) {
-      delete params.status
-    }
-
-    if (params) {
-      qParams = { ...qParams, ...params }
-    }
-
-    this.props
-      .modelRequest(
-        cs.ModelCampaigns,
-        cs.Routes.GetCampaigns,
-        cs.MethodGet,
-        qParams
-      )
-      .then(r => {
-        this.setState({
-          queryParams: {
-            ...this.state.queryParams,
-            total: this.props.data[cs.ModelCampaigns].total,
-            per_page: this.props.data[cs.ModelCampaigns].per_page,
-            page: this.props.data[cs.ModelCampaigns].page,
-            query: this.props.data[cs.ModelCampaigns].query,
-            status: params.status
-          }
-        })
-
-        this.startStatsPoll()
-      })
-  }
-
-  startStatsPoll = () => {
-    window.clearInterval(this.state.pollID)
-    this.setState({ stats: {} })
-
-    // If there's at least one running campaign, start polling.
-    let hasRunning = false
-    this.props.data[cs.ModelCampaigns].results.forEach(c => {
-      if (c.status === cs.CampaignStatusRunning) {
-        hasRunning = true
-        return
-      }
-    })
-
-    if (!hasRunning) {
-      return
-    }
-
-    // Poll for campaign stats.
-    let pollID = window.setInterval(() => {
-      this.props
-        .request(cs.Routes.GetRunningCampaignStats, cs.MethodGet)
-        .then(r => {
-          // No more running campaigns.
-          if (r.data.data.length === 0) {
-            window.clearInterval(this.state.pollID)
-            this.fetchRecords()
-            return
-          }
-
-          let stats = {}
-          r.data.data.forEach(s => {
-            stats[s.id] = s
-          })
-
-          this.setState({ stats: stats })
-        })
-        .catch(e => {
-          console.log(e.message)
-        })
-    }, 3000)
-
-    this.setState({ pollID: pollID })
-  }
-
-  handleUpdateStatus = (record, status) => {
-    this.props
-      .modelRequest(
-        cs.ModelCampaigns,
-        cs.Routes.UpdateCampaignStatus,
-        cs.MethodPut,
-        { id: record.id, status: status }
-      )
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: `Campaign ${status}`,
-          description: `"${record.name}" ${status}`
-        })
-
-        // Reload the table.
-        this.fetchRecords()
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  handleDeleteRecord = record => {
-    this.props
-      .modelRequest(
-        cs.ModelCampaigns,
-        cs.Routes.DeleteCampaign,
-        cs.MethodDelete,
-        { id: record.id }
-      )
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Campaign deleted",
-          description: `"${record.name}" deleted`
-        })
-
-        // Reload the table.
-        this.fetchRecords()
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  handleToggleCloneForm = record => {
-    this.setState({
-      cloneModalVisible: !this.state.cloneModalVisible,
-      record: record,
-      cloneName: record.name
-    })
-  }
-
-  handleCloneCampaign = record => {
-    this.setState({ modalWaiting: true })
-    this.props
-      .modelRequest(
-        cs.ModelCampaigns,
-        cs.Routes.CreateCampaign,
-        cs.MethodPost,
-        record
-      )
-      .then(resp => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Campaign created",
-          description: `${record.name} created`
-        })
-
-        this.setState({ record: null, modalWaiting: false })
-        this.props.route.history.push(
-          cs.Routes.ViewCampaign.replace(":id", resp.data.data.id)
-        )
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-        this.setState({ modalWaiting: false })
-      })
-  }
-
-  handlePreview = record => {
-    this.setState({ previewRecord: record })
-  }
-
-  render() {
-    const pagination = {
-      ...this.paginationOptions,
-      ...this.state.queryParams
-    }
-
-    return (
-      <section className="content campaigns">
-        <Row>
-          <Col xs={24} sm={14}>
-            <h1>Campaigns</h1>
-          </Col>
-          <Col xs={24} sm={10} className="right header-action-break">
-            <Link to="/campaigns/new">
-              <Button type="primary" icon="plus" role="link">
-                New campaign
-              </Button>
-            </Link>
-          </Col>
-        </Row>
-        <br />
-
-        <Table
-          className="campaigns"
-          columns={this.columns}
-          rowKey={record => record.uuid}
-          dataSource={(() => {
-            if (
-              !this.props.data[cs.ModelCampaigns] ||
-              !this.props.data[cs.ModelCampaigns].hasOwnProperty("results")
-            ) {
-              return []
-            }
-            return this.props.data[cs.ModelCampaigns].results
-          })()}
-          loading={this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone}
-          pagination={pagination}
-          onChange={(pagination, filters, sorter, records) => {
-            this.fetchRecords({
-              per_page: pagination.pageSize,
-              page: pagination.current,
-              status:
-                filters.status && filters.status.length > 0
-                  ? filters.status
-                  : "",
-              query:
-                filters.name && filters.name.length > 0 ? filters.name[0] : ""
-            })
-          }}
-        />
-
-        {this.state.previewRecord && (
-          <ModalPreview
-            title={this.state.previewRecord.name}
-            previewURL={cs.Routes.PreviewCampaign.replace(
-              ":id",
-              this.state.previewRecord.id
-            )}
-            onCancel={() => {
-              this.setState({ previewRecord: null })
-            }}
-          />
-        )}
-
-        {this.state.cloneModalVisible && this.state.record && (
-          <Modal
-            visible={this.state.record !== null}
-            width="500px"
-            className="clone-campaign-modal"
-            title={"Clone " + this.state.record.name}
-            okText="Clone"
-            confirmLoading={this.state.modalWaiting}
-            onCancel={this.handleToggleCloneForm}
-            onOk={() => {
-              this.handleCloneCampaign({
-                ...this.state.record,
-                name: this.state.cloneName
-              })
-            }}
-          >
-            <Input
-              autoFocus
-              defaultValue={this.state.record.name}
-              style={{ width: "100%" }}
-              onChange={e => {
-                this.setState({ cloneName: e.target.value })
-              }}
-            />
-          </Modal>
-        )}
-      </section>
-    )
-  }
-}
-
-export default Campaigns

+ 0 - 26
frontend/src/Dashboard.css

@@ -1,26 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  animation: App-logo-spin infinite 20s linear;
-  height: 80px;
-}
-
-.App-header {
-  height: 150px;
-  padding: 20px;
-}
-
-.App-title {
-  font-size: 1.5em;
-}
-
-.App-intro {
-  font-size: large;
-}
-
-@keyframes App-logo-spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}

+ 0 - 190
frontend/src/Dashboard.js

@@ -1,190 +0,0 @@
-import { Col, Row, notification, Card, Spin } from "antd"
-import React from "react"
-import { Chart, Geom, Tooltip as BizTooltip } from "bizcharts"
-
-import * as cs from "./constants"
-
-class Dashboard extends React.PureComponent {
-  state = {
-    stats: null,
-    loading: true
-  }
-
-  campaignTypes = [
-    "running",
-    "finished",
-    "paused",
-    "draft",
-    "scheduled",
-    "cancelled"
-  ]
-
-  componentDidMount = () => {
-    this.props.pageTitle("Dashboard")
-    this.props
-      .request(cs.Routes.GetDashboarcStats, cs.MethodGet)
-      .then(resp => {
-        this.setState({ stats: resp.data.data, loading: false })
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message })
-        this.setState({ loading: false })
-      })
-  }
-
-  orZero(v) {
-    return v ? v : 0
-  }
-
-  render() {
-    return (
-      <section className="dashboard">
-        <h1>Welcome</h1>
-        <hr />
-        <Spin spinning={this.state.loading}>
-          {this.state.stats && (
-            <div className="stats">
-              <Row>
-                <Col xs={24} sm={24} xl={16}>
-                  <Row gutter={24}>
-                    <Col xs={24} sm={12} md={8}>
-                      <Card title="Active subscribers" bordered={false}>
-                        <h1 className="count">
-                          {this.orZero(this.state.stats.subscribers.enabled)}
-                        </h1>
-                      </Card>
-                    </Col>
-                    <Col xs={24} sm={12} md={8}>
-                      <Card title="Blacklisted subscribers" bordered={false}>
-                        <h1 className="count">
-                          {this.orZero(
-                            this.state.stats.subscribers.blacklisted
-                          )}
-                        </h1>
-                      </Card>
-                    </Col>
-                    <Col xs={24} sm={12} md={8}>
-                      <Card title="Orphaned subscribers" bordered={false}>
-                        <h1 className="count">
-                          {this.orZero(this.state.stats.orphan_subscribers)}
-                        </h1>
-                      </Card>
-                    </Col>
-                  </Row>
-                </Col>
-                <Col xs={24} sm={24} xl={{ span: 6, offset: 2 }}>
-                  <Row gutter={24}>
-                    <Col xs={24} sm={12}>
-                      <Card title="Public lists" bordered={false}>
-                        <h1 className="count">
-                          {this.orZero(this.state.stats.lists.public)}
-                        </h1>
-                      </Card>
-                    </Col>
-                    <Col xs={24} sm={12}>
-                      <Card title="Private lists" bordered={false}>
-                        <h1 className="count">
-                          {this.orZero(this.state.stats.lists.private)}
-                        </h1>
-                      </Card>
-                    </Col>
-                  </Row>
-                </Col>
-              </Row>
-              <hr />
-              <Row>
-                <Col xs={24} sm={24} xl={16}>
-                  <Row gutter={24}>
-                    <Col xs={24} sm={12}>
-                      <Card
-                        title="Campaign views (last 3 months)"
-                        bordered={false}
-                      >
-                        <h1 className="count">
-                          {this.state.stats.campaign_views.reduce(
-                            (total, v) => total + v.count,
-                            0
-                          )}{" "}
-                          views
-                        </h1>
-                        <Chart
-                          height={220}
-                          padding={[0, 0, 0, 0]}
-                          data={this.state.stats.campaign_views}
-                          forceFit
-                        >
-                          <BizTooltip crosshairs={{ type: "y" }} />
-                          <Geom
-                            type="area"
-                            position="date*count"
-                            size={0}
-                            color="#7f2aff"
-                          />
-                          <Geom type="point" position="date*count" size={0} />
-                        </Chart>
-                      </Card>
-                    </Col>
-                    <Col xs={24} sm={12}>
-                      <Card
-                        title="Link clicks (last 3 months)"
-                        bordered={false}
-                      >
-                        <h1 className="count">
-                          {this.state.stats.link_clicks.reduce(
-                            (total, v) => total + v.count,
-                            0
-                          )}{" "}
-                          clicks
-                        </h1>
-                        <Chart
-                          height={220}
-                          padding={[0, 0, 0, 0]}
-                          data={this.state.stats.link_clicks}
-                          forceFit
-                        >
-                          <BizTooltip crosshairs={{ type: "y" }} />
-                          <Geom
-                            type="area"
-                            position="date*count"
-                            size={0}
-                            color="#7f2aff"
-                          />
-                          <Geom type="point" position="date*count" size={0} />
-                        </Chart>
-                      </Card>
-                    </Col>
-                  </Row>
-                </Col>
-
-                <Col xs={24} sm={12} xl={{ span: 6, offset: 2 }}>
-                  <Card
-                    title="Campaigns"
-                    bordered={false}
-                    className="campaign-counts"
-                  >
-                    {this.campaignTypes.map(key => (
-                      <Row key={`stats-campaigns-${key}`}>
-                        <Col span={18}>
-                          <h1 className="name">{key}</h1>
-                        </Col>
-                        <Col span={6}>
-                          <h1 className="count">
-                            {this.state.stats.campaigns.hasOwnProperty(key)
-                              ? this.state.stats.campaigns[key]
-                              : 0}
-                          </h1>
-                        </Col>
-                      </Row>
-                    ))}
-                  </Card>
-                </Col>
-              </Row>
-            </div>
-          )}
-        </Spin>
-      </section>
-    )
-  }
-}
-
-export default Dashboard

+ 0 - 139
frontend/src/Forms.js

@@ -1,139 +0,0 @@
-import React from "react"
-import {
-  Row,
-  Col,
-  Checkbox,
-} from "antd"
-
-import * as cs from "./constants"
-
-class Forms extends React.PureComponent {
-  state = {
-    lists: [],
-    selected: [],
-    selectedUUIDs: [],
-    indeterminate: false,
-    checkAll: false
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Subscription forms")
-    this.props
-      .modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
-        per_page: "all"
-      })
-      .then(() => {
-        this.setState({ lists: this.props.data[cs.ModelLists].results })
-      })
-  }
-
-  handleSelectAll = e => {
-    const uuids = this.state.lists.map(l => l.uuid)
-    this.setState({
-      selectedUUIDs: e.target.checked ? uuids : [],
-      indeterminate: false,
-      checkAll: e.target.checked
-    })
-    this.handleSelection(e.target.checked ? uuids : [])
-  }
-
-  handleSelection(sel) {
-    let out = []
-    sel.forEach(s => {
-      const item = this.state.lists.find(l => {
-        return l.uuid === s
-      })
-      if (item) {
-        out.push(item)
-      }
-    })
-
-    this.setState({
-      selected: out,
-      selectedUUIDs: sel,
-      indeterminate: sel.length > 0 && sel.length < this.state.lists.length,
-      checkAll: sel.length === this.state.lists.length
-    })
-  }
-
-  render() {
-    return (
-      <section className="content list-form">
-        <h1>Subscription forms</h1>
-        <hr />
-        <Row gutter={[16, 40]}>
-          <Col span={24} md={8}>
-            <h2>Lists</h2>
-            <Checkbox
-              indeterminate={this.state.indeterminate}
-              onChange={this.handleSelectAll}
-              checked={this.state.checkAll}
-            >
-              Select all
-            </Checkbox>
-            <hr />
-            <Checkbox.Group
-              className="lists"
-              options={this.state.lists.map(l => {
-                return { label: l.name, value: l.uuid }
-              })}
-              onChange={sel => this.handleSelection(sel)}
-              value={this.state.selectedUUIDs}
-            />
-          </Col>
-          <Col span={24} md={16}>
-            <h2>Form HTML</h2>
-            <p>
-              Use the following HTML to show a subscription form on an external
-              webpage.
-            </p>
-            <p>
-              The form should have the{" "}
-              <code>
-                <strong>email</strong>
-              </code>{" "}
-              field and one or more{" "}
-              <code>
-                <strong>l</strong>
-              </code>{" "}
-              (list UUID) fields. The{" "}
-              <code>
-                <strong>name</strong>
-              </code>{" "}
-              field is optional.
-            </p>
-            <pre className="html">
-              {`<form method="post" action="${
-                window.CONFIG.rootURL
-              }/subscription/form" class="listmonk-form">
-    <div>
-        <h3>Subscribe</h3>
-        <p><input type="text" name="email" placeholder="E-mail" /></p>
-        <p><input type="text" name="name" placeholder="Name (optional)" /></p>`}
-              {(() => {
-                let out = []
-                this.state.selected.forEach(l => {
-                  out.push(`
-        <p>
-            <input type="checkbox" name="l" value="${
-              l.uuid
-            }" id="${l.uuid.substr(0, 5)}" />
-            <label for="${l.uuid.substr(0, 5)}">${l.name}</label>
-        </p>`)
-                })
-                return out
-              })()}
-              {`
-        <p><input type="submit" value="Subscribe" /></p>
-    </div>
-</form>
-`}
-            </pre>
-          </Col>
-        </Row>
-      </section>
-    )
-  }
-}
-
-export default Forms

+ 0 - 469
frontend/src/Import.js

@@ -1,469 +0,0 @@
-import React from "react"
-import {
-  Row,
-  Col,
-  Form,
-  Select,
-  Input,
-  Upload,
-  Button,
-  Radio,
-  Icon,
-  Spin,
-  Progress,
-  Popconfirm,
-  Tag,
-  notification
-} from "antd"
-import * as cs from "./constants"
-
-const StatusNone = "none"
-const StatusImporting = "importing"
-const StatusStopping = "stopping"
-const StatusFinished = "finished"
-const StatusFailed = "failed"
-
-class TheFormDef extends React.PureComponent {
-  state = {
-    confirmDirty: false,
-    fileList: [],
-    formLoading: false,
-    mode: "subscribe"
-  }
-
-  componentDidMount() {
-    // Fetch lists.
-    this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
-      per_page: "all"
-    })
-  }
-
-  // Handle create / edit form submission.
-  handleSubmit = e => {
-    e.preventDefault()
-    var err = null,
-      values = {}
-    this.props.form.validateFields((e, v) => {
-      err = e
-      values = v
-    })
-    if (err) {
-      return
-    }
-
-    if (this.state.fileList.length < 1) {
-      notification["error"]({
-        placement: cs.MsgPosition,
-        message: "Error",
-        description: "Select a valid file to upload"
-      })
-      return
-    }
-
-    this.setState({ formLoading: true })
-    let params = new FormData()
-    params.set("params", JSON.stringify(values))
-    params.append("file", this.state.fileList[0])
-    this.props
-      .request(cs.Routes.UploadRouteImport, cs.MethodPost, params)
-      .then(() => {
-        notification["info"]({
-          placement: cs.MsgPosition,
-          message: "File uploaded",
-          description: "Please wait while the import is running"
-        })
-        this.props.fetchimportState()
-        this.setState({ formLoading: false })
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-        this.props.fetchimportState()
-        this.setState({ formLoading: false })
-      })
-  }
-
-  handleConfirmBlur = e => {
-    const value = e.target.value
-    this.setState({ confirmDirty: this.state.confirmDirty || !!value })
-  }
-
-  onFileChange = f => {
-    let fileList = [f]
-    this.setState({ fileList })
-    return false
-  }
-
-  render() {
-    const { getFieldDecorator } = this.props.form
-
-    const formItemLayout = {
-		labelCol: { sm: { span: 24 }, md: { span: 5 } },
-		wrapperCol: { sm: { span: 24 }, md: { span: 10 } }
-    }
-
-    const formItemTailLayout = {
-      wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
-    }
-
-    return (
-      <Spin spinning={this.state.formLoading}>
-        <Form onSubmit={this.handleSubmit}>
-          <Form.Item {...formItemLayout} label="Mode">
-            {getFieldDecorator("mode", {
-              rules: [{ required: true }],
-              initialValue: "subscribe"
-            })(
-              <Radio.Group
-                className="mode"
-                onChange={e => {
-                  this.setState({ mode: e.target.value })
-                }}
-              >
-                <Radio disabled={this.props.formDisabled} value="subscribe">
-                  Subscribe
-                </Radio>
-                <Radio disabled={this.props.formDisabled} value="blacklist">
-                  Blacklist
-                </Radio>
-              </Radio.Group>
-            )}
-          </Form.Item>
-          {this.state.mode === "subscribe" && (
-            <React.Fragment>
-              <Form.Item
-                {...formItemLayout}
-                label="Lists"
-                extra="Lists to subscribe to"
-              >
-                {getFieldDecorator("lists", { rules: [{ required: true }] })(
-                  <Select mode="multiple">
-                    {[...this.props.lists].map((v, i) => (
-                      <Select.Option value={v["id"]} key={v["id"]}>
-                        {v["name"]}
-                      </Select.Option>
-                    ))}
-                  </Select>
-                )}
-              </Form.Item>
-            </React.Fragment>
-          )}
-          {this.state.mode === "blacklist" && (
-            <Form.Item {...formItemTailLayout}>
-              <p className="ant-form-extra">
-                All existing subscribers found in the import will be marked as
-                'blacklisted' and will be unsubscribed from their existing
-                subscriptions. New subscribers will be imported and marked as
-                'blacklisted'.
-              </p>
-            </Form.Item>
-          )}
-          <Form.Item
-            {...formItemLayout}
-            label="CSV delimiter"
-            extra="Default delimiter is comma"
-          >
-            {getFieldDecorator("delim", {
-              initialValue: ","
-            })(<Input maxLength={1} style={{ maxWidth: 40 }} />)}
-          </Form.Item>
-          <Form.Item {...formItemLayout} label="CSV or ZIP file">
-            <div className="dropbox">
-              {getFieldDecorator("file", {
-                valuePropName: "file",
-                getValueFromEvent: this.normFile,
-                rules: [{ required: true }]
-              })(
-                <Upload.Dragger
-                  name="files"
-                  multiple={false}
-                  fileList={this.state.fileList}
-                  beforeUpload={this.onFileChange}
-                  accept=".zip,.csv"
-                >
-                  <p className="ant-upload-drag-icon">
-                    <Icon type="inbox" />
-                  </p>
-                  <p className="ant-upload-text">
-                    Click or drag a CSV or ZIP file here
-                  </p>
-                </Upload.Dragger>
-              )}
-            </div>
-          </Form.Item>
-          <Form.Item {...formItemTailLayout}>
-            <p className="ant-form-extra">
-              For existing subscribers, the names and attributes will be
-              overwritten with the values in the CSV.
-            </p>
-            <Button type="primary" htmlType="submit">
-              <Icon type="upload" /> Upload
-            </Button>
-          </Form.Item>
-        </Form>
-      </Spin>
-    )
-  }
-}
-const TheForm = Form.create()(TheFormDef)
-
-class Importing extends React.PureComponent {
-  state = {
-    pollID: -1,
-    logs: ""
-  }
-
-  stopImport = () => {
-    // Get the import status.
-    this.props
-      .request(cs.Routes.UploadRouteImport, cs.MethodDelete)
-      .then(r => {
-        this.props.fetchimportState()
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  componentDidMount() {
-    // Poll for stats until it's finished or failed.
-    let pollID = window.setInterval(() => {
-      this.props.fetchimportState()
-      this.fetchLogs()
-      if (
-        this.props.importState.status === StatusFinished ||
-        this.props.importState.status === StatusFailed
-      ) {
-        window.clearInterval(this.state.pollID)
-      }
-    }, 1000)
-
-    this.setState({ pollID: pollID })
-  }
-  componentWillUnmount() {
-    window.clearInterval(this.state.pollID)
-  }
-
-  fetchLogs() {
-    this.props
-      .request(cs.Routes.GetRouteImportLogs, cs.MethodGet)
-      .then(r => {
-        this.setState({ logs: r.data.data })
-        let t = document.querySelector("#log-textarea")
-        t.scrollTop = t.scrollHeight
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  render() {
-    let progressPercent = 0
-    if (this.props.importState.status === StatusFinished) {
-      progressPercent = 100
-    } else {
-      progressPercent = Math.floor(
-        (this.props.importState.imported / this.props.importState.total) * 100
-      )
-    }
-
-    return (
-      <section className="content import">
-        <h1>Importing &mdash; {this.props.importState.name}</h1>
-        {this.props.importState.status === StatusImporting && (
-          <p>
-            Import is in progress. It is safe to navigate away from this page.
-          </p>
-        )}
-
-        {this.props.importState.status !== StatusImporting && (
-          <p>Import has finished.</p>
-        )}
-
-        <Row className="import-container">
-          <Col span={10} offset={3}>
-            <div className="stats center">
-              <div>
-                <Progress type="line" percent={progressPercent} />
-              </div>
-
-              <div>
-                <h3>{this.props.importState.imported} records</h3>
-                <br />
-
-                {this.props.importState.status === StatusImporting && (
-                  <Popconfirm
-                    title="Are you sure?"
-                    onConfirm={() => this.stopImport()}
-                  >
-                    <p>
-                      <Icon type="loading" />
-                    </p>
-                    <Button type="primary">Stop import</Button>
-                  </Popconfirm>
-                )}
-                {this.props.importState.status === StatusStopping && (
-                  <div>
-                    <p>
-                      <Icon type="loading" />
-                    </p>
-                    <h4>Stopping</h4>
-                  </div>
-                )}
-                {this.props.importState.status !== StatusImporting &&
-                  this.props.importState.status !== StatusStopping && (
-                    <div>
-                      {this.props.importState.status !== StatusFinished && (
-                        <div>
-                          <Tag color="red">{this.props.importState.status}</Tag>
-                          <br />
-                        </div>
-                      )}
-
-                      <br />
-                      <Button type="primary" onClick={() => this.stopImport()}>
-                        Done
-                      </Button>
-                    </div>
-                  )}
-              </div>
-            </div>
-
-            <div className="logs">
-              <h3>Import log</h3>
-              <Spin spinning={this.state.logs === ""}>
-                <Input.TextArea
-                  placeholder="Import logs"
-                  id="log-textarea"
-                  rows={10}
-                  value={this.state.logs}
-                  autosize={{ minRows: 2, maxRows: 10 }}
-                />
-              </Spin>
-            </div>
-          </Col>
-        </Row>
-      </section>
-    )
-  }
-}
-
-class Import extends React.PureComponent {
-  state = {
-    importState: { status: "" }
-  }
-
-  fetchimportState = () => {
-    // Get the import status.
-    this.props
-      .request(cs.Routes.GetRouteImportStats, cs.MethodGet)
-      .then(r => {
-        this.setState({ importState: r.data.data })
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Import subscribers")
-    this.fetchimportState()
-  }
-  render() {
-    if (this.state.importState.status === "") {
-      // Fetching the status.
-      return (
-        <section className="content center">
-          <Spin />
-        </section>
-      )
-    } else if (this.state.importState.status !== StatusNone) {
-      // There's an import state
-      return (
-        <Importing
-          {...this.props}
-          importState={this.state.importState}
-          fetchimportState={this.fetchimportState}
-        />
-      )
-    }
-
-    return (
-      <section className="content import">
-        <Row>
-          <Col span={22}>
-            <h1>Import subscribers</h1>
-          </Col>
-          <Col span={2} />
-        </Row>
-
-        <TheForm
-          {...this.props}
-          fetchimportState={this.fetchimportState}
-          lists={
-            this.props.data[cs.ModelLists].hasOwnProperty("results")
-              ? this.props.data[cs.ModelLists].results
-              : []
-          }
-        />
-
-        <hr />
-        <div className="help">
-          <h2>Instructions</h2>
-          <p>
-            Upload a CSV file or a ZIP file with a single CSV file in it to bulk
-            import subscribers. The CSV file should have the following headers
-            with the exact column names. <code>attributes</code> (optional)
-            should be a valid JSON string with double escaped quotes.
-          </p>
-
-          <blockquote className="csv-example">
-            <code className="csv-headers">
-              <span>email,</span>
-              <span>name,</span>
-              <span>attributes</span>
-            </code>
-          </blockquote>
-
-          <h3>Example raw CSV</h3>
-          <blockquote className="csv-example">
-            <code className="csv-headers">
-              <span>email,</span>
-              <span>name,</span>
-              <span>attributes</span>
-            </code>
-            <code className="csv-row">
-              <span>user1@mail.com,</span>
-              <span>"User One",</span>
-              <span>{'"{""age"": 32, ""city"": ""Bangalore""}"'}</span>
-            </code>
-            <code className="csv-row">
-              <span>user2@mail.com,</span>
-              <span>"User Two",</span>
-              <span>
-                {'"{""age"": 25, ""occupation"": ""Time Traveller""}"'}
-              </span>
-            </code>
-          </blockquote>
-        </div>
-      </section>
-    )
-  }
-}
-
-export default Import

+ 0 - 275
frontend/src/Layout.js

@@ -1,275 +0,0 @@
-import React from "react"
-import { Switch, Route } from "react-router-dom"
-import { Link } from "react-router-dom"
-import { Layout, Menu, Icon } from "antd"
-
-import logo from "./static/listmonk.svg"
-
-// Views.
-import Dashboard from "./Dashboard"
-import Lists from "./Lists"
-import Forms from "./Forms"
-import Subscribers from "./Subscribers"
-import Subscriber from "./Subscriber"
-import Templates from "./Templates"
-import Import from "./Import"
-import Campaigns from "./Campaigns"
-import Campaign from "./Campaign"
-import Media from "./Media"
-
-const { Content, Footer, Sider } = Layout
-const SubMenu = Menu.SubMenu
-const year = new Date().getUTCFullYear()
-
-class Base extends React.Component {
-  state = {
-    basePath: "/" + window.location.pathname.split("/")[1],
-    error: null,
-    collapsed: false
-  }
-
-  onCollapse = collapsed => {
-    this.setState({ collapsed })
-  }
-
-  componentDidMount() {
-    // For small screen devices collapse the menu by default.
-    if (window.screen.width < 768) {
-      this.setState({ collapsed: true })
-    }
-  }
-
-  render() {
-    return (
-      <Layout style={{ minHeight: "100vh" }}>
-        <Sider
-          collapsible
-          collapsed={this.state.collapsed}
-          onCollapse={this.onCollapse}
-          theme="light"
-        >
-          <div className="logo">
-            <Link to="/">
-              <img src={logo} alt="listmonk logo" />
-            </Link>
-          </div>
-
-          <Menu
-            defaultSelectedKeys={["/"]}
-            selectedKeys={[window.location.pathname]}
-            defaultOpenKeys={[this.state.basePath]}
-            mode="inline"
-          >
-            <Menu.Item key="/">
-              <Link to="/">
-                <Icon type="dashboard" />
-                <span>Dashboard</span>
-              </Link>
-            </Menu.Item>
-            <SubMenu
-              key="/lists"
-              title={
-                <span>
-                  <Icon type="bars" />
-                  <span>Lists</span>
-                </span>
-              }
-            >
-              <Menu.Item key="/lists">
-                <Link to="/lists">
-                  <Icon type="bars" />
-                  <span>All lists</span>
-                </Link>
-              </Menu.Item>
-              <Menu.Item key="/lists/forms">
-                <Link to="/lists/forms">
-                  <Icon type="form" />
-                  <span>Forms</span>
-                </Link>
-              </Menu.Item>
-            </SubMenu>
-            <SubMenu
-              key="/subscribers"
-              title={
-                <span>
-                  <Icon type="team" />
-                  <span>Subscribers</span>
-                </span>
-              }
-            >
-              <Menu.Item key="/subscribers">
-                <Link to="/subscribers">
-                  <Icon type="team" /> All subscribers
-                </Link>
-              </Menu.Item>
-              <Menu.Item key="/subscribers/import">
-                <Link to="/subscribers/import">
-                  <Icon type="upload" /> Import
-                </Link>
-              </Menu.Item>
-            </SubMenu>
-
-            <SubMenu
-              key="/campaigns"
-              title={
-                <span>
-                  <Icon type="rocket" />
-                  <span>Campaigns</span>
-                </span>
-              }
-            >
-              <Menu.Item key="/campaigns">
-                <Link to="/campaigns">
-                  <Icon type="rocket" /> All campaigns
-                </Link>
-              </Menu.Item>
-              <Menu.Item key="/campaigns/new">
-                <Link to="/campaigns/new">
-                  <Icon type="plus" /> Create new
-                </Link>
-              </Menu.Item>
-              <Menu.Item key="/campaigns/media">
-                <Link to="/campaigns/media">
-                  <Icon type="picture" /> Media
-                </Link>
-              </Menu.Item>
-              <Menu.Item key="/campaigns/templates">
-                <Link to="/campaigns/templates">
-                  <Icon type="code-o" /> Templates
-                </Link>
-              </Menu.Item>
-            </SubMenu>
-          </Menu>
-        </Sider>
-
-        <Layout>
-          <Content style={{ margin: "0 16px" }}>
-            <div className="content-body">
-              <div id="alert-container" />
-              <Switch>
-                <Route
-                  exact
-                  key="/"
-                  path="/"
-                  render={props => (
-                    <Dashboard {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/lists"
-                  path="/lists"
-                  render={props => (
-                    <Lists {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/lists/forms"
-                  path="/lists/forms"
-                  render={props => (
-                    <Forms {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/subscribers"
-                  path="/subscribers"
-                  render={props => (
-                    <Subscribers {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/subscribers/lists/:listID"
-                  path="/subscribers/lists/:listID"
-                  render={props => (
-                    <Subscribers {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/subscribers/import"
-                  path="/subscribers/import"
-                  render={props => (
-                    <Import {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/subscribers/:subID"
-                  path="/subscribers/:subID"
-                  render={props => (
-                    <Subscriber {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/campaigns"
-                  path="/campaigns"
-                  render={props => (
-                    <Campaigns {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/campaigns/new"
-                  path="/campaigns/new"
-                  render={props => (
-                    <Campaign {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/campaigns/media"
-                  path="/campaigns/media"
-                  render={props => (
-                    <Media {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/campaigns/templates"
-                  path="/campaigns/templates"
-                  render={props => (
-                    <Templates {...{ ...this.props, route: props }} />
-                  )}
-                />
-                <Route
-                  exact
-                  key="/campaigns/:campaignID"
-                  path="/campaigns/:campaignID"
-                  render={props => (
-                    <Campaign {...{ ...this.props, route: props }} />
-                  )}
-                />
-              </Switch>
-            </div>
-          </Content>
-          <Footer>
-            <span className="text-small">
-              <a
-                href="https://listmonk.app"
-                rel="noreferrer noopener"
-                target="_blank"
-              >
-                listmonk
-              </a>{" "}
-              &copy; 2019 {year !== 2019 ? " - " + year : ""}. Version{" "}
-              {process.env.REACT_APP_VERSION} &mdash;{" "}
-              <a
-                href="https://listmonk.app/docs"
-                target="_blank"
-                rel="noopener noreferrer"
-              >
-                Docs
-              </a>
-            </span>
-          </Footer>
-        </Layout>
-      </Layout>
-    )
-  }
-}
-
-export default Base

+ 0 - 496
frontend/src/Lists.js

@@ -1,496 +0,0 @@
-import React from "react"
-import { Link } from "react-router-dom"
-import {
-  Row,
-  Col,
-  Modal,
-  Form,
-  Input,
-  Select,
-  Button,
-  Table,
-  Icon,
-  Tooltip,
-  Tag,
-  Popconfirm,
-  Spin,
-  notification
-} from "antd"
-
-import Utils from "./utils"
-import * as cs from "./constants"
-
-const tagColors = {
-  private: "orange",
-  public: "green"
-}
-
-class CreateFormDef extends React.PureComponent {
-  state = {
-    confirmDirty: false,
-    modalWaiting: false
-  }
-
-  // Handle create / edit form submission.
-  handleSubmit = e => {
-    e.preventDefault()
-    this.props.form.validateFields((err, values) => {
-      if (err) {
-        return
-      }
-
-      this.setState({ modalWaiting: true })
-      if (this.props.formType === cs.FormCreate) {
-        // Create a new list.
-        this.props
-          .modelRequest(
-            cs.ModelLists,
-            cs.Routes.CreateList,
-            cs.MethodPost,
-            values
-          )
-          .then(() => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "List created",
-              description: `"${values["name"]}" created`
-            })
-            this.props.fetchRecords()
-            this.props.onClose()
-            this.setState({ modalWaiting: false })
-          })
-          .catch(e => {
-            notification["error"]({ message: "Error", description: e.message })
-            this.setState({ modalWaiting: false })
-          })
-      } else {
-        // Edit a list.
-        this.props
-          .modelRequest(cs.ModelLists, cs.Routes.UpdateList, cs.MethodPut, {
-            ...values,
-            id: this.props.record.id
-          })
-          .then(() => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "List modified",
-              description: `"${values["name"]}" modified`
-            })
-            this.props.fetchRecords()
-            this.props.onClose()
-            this.setState({ modalWaiting: false })
-          })
-          .catch(e => {
-            notification["error"]({
-              placement: cs.MsgPosition,
-              message: "Error",
-              description: e.message
-            })
-            this.setState({ modalWaiting: false })
-          })
-      }
-    })
-  }
-
-  modalTitle(formType, record) {
-    if (formType === cs.FormCreate) {
-      return "Create a list"
-    }
-
-    return (
-      <div>
-        <Tag
-          color={
-            tagColors.hasOwnProperty(record.type) ? tagColors[record.type] : ""
-          }
-        >
-          {record.type}
-        </Tag>{" "}
-        {record.name}
-        <br />
-        <span className="text-tiny text-grey">
-          ID {record.id} / UUID {record.uuid}
-        </span>
-      </div>
-    )
-  }
-
-  render() {
-    const { formType, record, onClose } = this.props
-    const { getFieldDecorator } = this.props.form
-
-    const formItemLayout = {
-      labelCol: { xs: { span: 16 }, sm: { span: 4 } },
-      wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
-    }
-
-    if (formType === null) {
-      return null
-    }
-
-    return (
-      <Modal
-        visible={true}
-        title={this.modalTitle(formType, record)}
-        okText={this.state.form === cs.FormCreate ? "Create" : "Save"}
-        confirmLoading={this.state.modalWaiting}
-        onCancel={onClose}
-        onOk={this.handleSubmit}
-      >
-        <div id="modal-alert-container" />
-
-        <Spin
-          spinning={this.props.reqStates[cs.ModelLists] === cs.StatePending}
-        >
-          <Form onSubmit={this.handleSubmit}>
-            <Form.Item {...formItemLayout} label="Name">
-              {getFieldDecorator("name", {
-                initialValue: record.name,
-                rules: [{ required: true }]
-              })(<Input autoFocus maxLength={200} />)}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              name="type"
-              label="Type"
-              extra="Public lists are open to the world to subscribe and their
-              names may appear on public pages such as the subscription management page."
-            >
-              {getFieldDecorator("type", {
-                initialValue: record.type ? record.type : "private",
-                rules: [{ required: true }]
-              })(
-                <Select style={{ maxWidth: 120 }}>
-                  <Select.Option value="private">Private</Select.Option>
-                  <Select.Option value="public">Public</Select.Option>
-                </Select>
-              )}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              name="optin"
-              label="Opt-in"
-              extra="Double opt-in sends an e-mail to the subscriber asking for confirmation.
-              On Double opt-in lists, campaigns are only sent to confirmed subscribers."
-            >
-              {getFieldDecorator("optin", {
-                initialValue: record.optin ? record.optin : "single",
-                rules: [{ required: true }]
-              })(
-                <Select style={{ maxWidth: 120 }}>
-                  <Select.Option value="single">Single</Select.Option>
-                  <Select.Option value="double">Double</Select.Option>
-                </Select>
-              )}
-            </Form.Item>
-            <Form.Item
-              {...formItemLayout}
-              label="Tags"
-              extra="Hit Enter after typing a word to add multiple tags"
-            >
-              {getFieldDecorator("tags", { initialValue: record.tags })(
-                <Select mode="tags" />
-              )}
-            </Form.Item>
-          </Form>
-        </Spin>
-      </Modal>
-    )
-  }
-}
-
-const CreateForm = Form.create()(CreateFormDef)
-
-class Lists extends React.PureComponent {
-  defaultPerPage = 20
-  state = {
-    formType: null,
-    record: {},
-    queryParams: {}
-  }
-
-  // Pagination config.
-  paginationOptions = {
-    hideOnSinglePage: false,
-    showSizeChanger: true,
-    showQuickJumper: true,
-    defaultPageSize: this.defaultPerPage,
-    pageSizeOptions: ["20", "50", "70", "100"],
-    position: "both",
-    showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`,
-    onChange: (page, perPage) => {
-      this.fetchRecords({ page: page, per_page: perPage })
-    },
-    onShowSizeChange: (page, perPage) => {
-      this.fetchRecords({ page: page, per_page: perPage })
-    }
-  }
-
-  constructor(props) {
-    super(props)
-
-    this.columns = [
-      {
-        title: "Name",
-        dataIndex: "name",
-        sorter: true,
-        width: "40%",
-        render: (text, record) => {
-          const out = []
-          out.push(
-            <div className="name" key={`name-${record.id}`}>
-              <Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
-            </div>
-          )
-
-          if (record.tags.length > 0) {
-            for (let i = 0; i < record.tags.length; i++) {
-              out.push(<Tag key={`tag-${i}`}>{record.tags[i]}</Tag>)
-            }
-          }
-
-          return out
-        }
-      },
-      {
-        title: "Type",
-        dataIndex: "type",
-        width: "15%",
-        render: (type, record) => {
-          let color = type === "private" ? "orange" : "green"
-          return (
-            <div>
-              <p>
-                <Tag color={color}>{type}</Tag>
-                <Tag>{record.optin}</Tag>
-              </p>
-              {record.optin === cs.ListOptinDouble && (
-                <p className="text-small">
-                  <Tooltip title="Send a campaign to unconfirmed subscribers to opt-in">
-                    <Link onClick={ e => { e.preventDefault(); this.makeOptinCampaign(record)} } to={`/campaigns/new?type=optin&list_id=${record.id}`}>
-                      <Icon type="rocket" /> Send opt-in campaign
-                    </Link>
-                  </Tooltip>
-                </p>
-              )}
-            </div>
-          )
-        }
-      },
-      {
-        title: "Subscribers",
-        dataIndex: "subscriber_count",
-        width: "10%",
-        align: "center",
-        render: (text, record) => {
-          return (
-            <div className="name" key={`name-${record.id}`}>
-              <Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
-            </div>
-          )
-        }
-      },
-      {
-        title: "Created",
-        dataIndex: "created_at",
-        render: (date, _) => {
-          return Utils.DateString(date)
-        }
-      },
-      {
-        title: "Updated",
-        dataIndex: "updated_at",
-        render: (date, _) => {
-          return Utils.DateString(date)
-        }
-      },
-      {
-        title: "",
-        dataIndex: "actions",
-        width: "10%",
-        render: (text, record) => {
-          return (
-            <div className="actions">
-              <Tooltip title="Send a campaign">
-                <Link to={`/campaigns/new?list_id=${record.id}`}>
-                  <Icon type="rocket" />
-                </Link>
-              </Tooltip>
-              <Tooltip title="Edit list">
-                <a
-                  role="button"
-                  onClick={() => this.handleShowEditForm(record)}
-                >
-                  <Icon type="edit" />
-                </a>
-              </Tooltip>
-              <Popconfirm
-                title="Are you sure?"
-                onConfirm={() => this.deleteRecord(record)}
-              >
-                <Tooltip title="Delete list" placement="bottom">
-                  <a role="button">
-                    <Icon type="delete" />
-                  </a>
-                </Tooltip>
-              </Popconfirm>
-            </div>
-          )
-        }
-      }
-    ]
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Lists")
-    this.fetchRecords()
-  }
-
-  fetchRecords = params => {
-    let qParams = {
-      page: this.state.queryParams.page,
-      per_page: this.state.queryParams.per_page
-    }
-    if (params) {
-      qParams = { ...qParams, ...params }
-    }
-
-    this.props
-      .modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, qParams)
-      .then(() => {
-        this.setState({
-          queryParams: {
-            ...this.state.queryParams,
-            total: this.props.data[cs.ModelLists].total,
-            perPage: this.props.data[cs.ModelLists].per_page,
-            page: this.props.data[cs.ModelLists].page,
-            query: this.props.data[cs.ModelLists].query
-          }
-        })
-      })
-  }
-
-  deleteRecord = record => {
-    this.props
-      .modelRequest(cs.ModelLists, cs.Routes.DeleteList, cs.MethodDelete, {
-        id: record.id
-      })
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "List deleted",
-          description: `"${record.name}" deleted`
-        })
-
-        // Reload the table.
-        this.fetchRecords()
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  makeOptinCampaign = record => {
-    this.props
-    .modelRequest(
-      cs.ModelCampaigns,
-      cs.Routes.CreateCampaign,
-      cs.MethodPost,
-      {
-        type: cs.CampaignTypeOptin,
-        name: "Optin: "+ record.name,
-        subject: "Confirm your subscriptions",
-        messenger: "email",
-        content_type: cs.CampaignContentTypeRichtext,
-        lists: [record.id]
-      }
-    )
-    .then(resp => {
-      notification["success"]({
-        placement: cs.MsgPosition,
-        message: "Opt-in campaign created",
-        description: "Opt-in campaign created"
-      })
-
-      // Redirect to the newly created campaign.
-      this.props.route.history.push({
-        pathname: cs.Routes.ViewCampaign.replace(
-          ":id",
-          resp.data.data.id
-        )
-      })
-    })
-    .catch(e => {
-      notification["error"]({
-        placement: cs.MsgPosition,
-        message: "Error",
-        description: e.message
-      })
-    })
-  }
-
-  handleHideForm = () => {
-    this.setState({ formType: null })
-  }
-
-  handleShowCreateForm = () => {
-    this.setState({ formType: cs.FormCreate, record: {} })
-  }
-
-  handleShowEditForm = record => {
-    this.setState({ formType: cs.FormEdit, record: record })
-  }
-
-  render() {
-    return (
-      <section className="content">
-        <Row>
-          <Col xs={12} sm={18}>
-            <h1>Lists ({this.props.data[cs.ModelLists].total}) </h1>
-          </Col>
-          <Col xs={12} sm={6} className="right">
-            <Button
-              type="primary"
-              icon="plus"
-              onClick={this.handleShowCreateForm}
-            >
-              Create list
-            </Button>
-          </Col>
-        </Row>
-        <br />
-
-        <Table
-          className="lists"
-          columns={this.columns}
-          rowKey={record => record.uuid}
-          dataSource={(() => {
-            if (
-              !this.props.data[cs.ModelLists] ||
-              !this.props.data[cs.ModelLists].hasOwnProperty("results")
-            ) {
-              return []
-            }
-            return this.props.data[cs.ModelLists].results
-          })()}
-          loading={this.props.reqStates[cs.ModelLists] !== cs.StateDone}
-          pagination={{ ...this.paginationOptions, ...this.state.queryParams }}
-        />
-
-        <CreateForm
-          {...this.props}
-          formType={this.state.formType}
-          record={this.state.record}
-          onClose={this.handleHideForm}
-          fetchRecords={this.fetchRecords}
-        />
-      </section>
-    )
-  }
-}
-
-export default Lists

+ 0 - 176
frontend/src/Media.js

@@ -1,176 +0,0 @@
-import React from "react"
-import {
-  Row,
-  Col,
-  Form,
-  Upload,
-  Icon,
-  Spin,
-  Popconfirm,
-  Tooltip,
-  notification
-} from "antd"
-import * as cs from "./constants"
-
-class TheFormDef extends React.PureComponent {
-  state = {
-    confirmDirty: false
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Media")
-    this.fetchRecords()
-  }
-
-  fetchRecords = () => {
-    this.props.modelRequest(cs.ModelMedia, cs.Routes.GetMedia, cs.MethodGet)
-  }
-
-  handleDeleteRecord = record => {
-    this.props
-      .modelRequest(cs.ModelMedia, cs.Routes.DeleteMedia, cs.MethodDelete, {
-        id: record.id
-      })
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Image deleted",
-          description: `"${record.filename}" deleted`
-        })
-
-        // Reload the table.
-        this.fetchRecords()
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  handleInsertMedia = record => {
-    // The insertMedia callback may be passed down by the invoker (Campaign)
-    if (!this.props.insertMedia) {
-      return false
-    }
-
-    this.props.insertMedia(record.uri)
-    return false
-  }
-
-  onFileChange = f => {
-    if (
-      f.file.error &&
-      f.file.response &&
-      f.file.response.hasOwnProperty("message")
-    ) {
-      notification["error"]({
-        placement: cs.MsgPosition,
-        message: "Error uploading file",
-        description: f.file.response.message
-      })
-    } else if (f.file.status === "done") {
-      this.fetchRecords()
-    }
-
-    return false
-  }
-
-  render() {
-    const { getFieldDecorator } = this.props.form
-    const formItemLayout = {
-      labelCol: { xs: { span: 16 }, sm: { span: 4 } },
-      wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
-    }
-
-    return (
-      <Spin spinning={false}>
-        <Form>
-          <Form.Item {...formItemLayout} label="Upload images">
-            <div className="dropbox">
-              {getFieldDecorator("file", {
-                valuePropName: "file",
-                getValueFromEvent: this.normFile,
-                rules: [{ required: true }]
-              })(
-                <Upload.Dragger
-                  name="file"
-                  action="/api/media"
-                  multiple={true}
-                  listType="picture"
-                  onChange={this.onFileChange}
-                  accept=".gif, .jpg, .jpeg, .png"
-                >
-                  <p className="ant-upload-drag-icon">
-                    <Icon type="inbox" />
-                  </p>
-                  <p className="ant-upload-text">Click or drag file here</p>
-                </Upload.Dragger>
-              )}
-            </div>
-          </Form.Item>
-        </Form>
-
-        <section className="gallery">
-          {this.props.media &&
-            this.props.media.map((record, i) => (
-              <div key={i} className="image">
-                <a
-                  onClick={() => {
-                    this.handleInsertMedia(record)
-                    if (this.props.onCancel) {
-                      this.props.onCancel()
-                    }
-                  }}
-                >
-                  <img alt={record.filename} src={record.thumb_uri} />
-                </a>
-                <div className="actions">
-                  <Tooltip title="View" placement="bottom">
-                    <a role="button" href={record.uri} target="_blank">
-                      <Icon type="login" />
-                    </a>
-                  </Tooltip>
-                  <Popconfirm
-                    title="Are you sure?"
-                    onConfirm={() => this.handleDeleteRecord(record)}
-                  >
-                    <Tooltip title="Delete" placement="bottom">
-                      <a role="button">
-                        <Icon type="delete" />
-                      </a>
-                    </Tooltip>
-                  </Popconfirm>
-                </div>
-                <div className="name" title={record.filename}>
-                  {record.filename}
-                </div>
-              </div>
-            ))}
-        </section>
-      </Spin>
-    )
-  }
-}
-const TheForm = Form.create()(TheFormDef)
-
-class Media extends React.PureComponent {
-  render() {
-    return (
-      <section className="content media">
-        <Row>
-          <Col span={22}>
-            <h1>Images</h1>
-          </Col>
-          <Col span={2} />
-        </Row>
-
-        <TheForm {...this.props} media={this.props.data[cs.ModelMedia]} />
-      </section>
-    )
-  }
-}
-
-export default Media

+ 0 - 75
frontend/src/ModalPreview.js

@@ -1,75 +0,0 @@
-import React from "react"
-import { Modal } from "antd"
-import * as cs from "./constants"
-
-import { Spin } from "antd"
-
-class ModalPreview extends React.PureComponent {
-  makeForm(body) {
-    let form = document.createElement("form")
-    form.method = cs.MethodPost
-    form.action = this.props.previewURL
-    form.target = "preview-iframe"
-
-    let input = document.createElement("input")
-    input.type = "hidden"
-    input.name = "body"
-    input.value = body
-    form.appendChild(input)
-    document.body.appendChild(form)
-    form.submit()
-  }
-
-  render() {
-    return (
-      <Modal
-        visible={true}
-        title={this.props.title}
-        className="preview-modal"
-        width="90%"
-        height={900}
-        onCancel={this.props.onCancel}
-        onOk={this.props.onCancel}
-      >
-        <div className="preview-iframe-container">
-          <Spin className="preview-iframe-spinner" />
-          <iframe
-            key="preview-iframe"
-            onLoad={() => {
-              // If state is used to manage the spinner, it causes
-              // the iframe to re-render and reload everything.
-              // Hack the spinner away from the DOM directly instead.
-              let spin = document.querySelector(".preview-iframe-spinner")
-              if (spin) {
-                spin.parentNode.removeChild(spin)
-              }
-              // this.setState({ loading: false })
-            }}
-            title={this.props.title ? this.props.title : "Preview"}
-            name="preview-iframe"
-            id="preview-iframe"
-            className="preview-iframe"
-            ref={o => {
-              if (!o) {
-                return
-              }
-
-              // When the DOM reference for the iframe is ready,
-              // see if there's a body to post with the form hack.
-              if (this.props.body !== undefined && this.props.body !== null) {
-                this.makeForm(this.props.body)
-              } else {
-                if (this.props.previewURL) {
-                  o.src = this.props.previewURL
-                }
-              }
-            }}
-            src="about:blank"
-          />
-        </div>
-      </Modal>
-    )
-  }
-}
-
-export default ModalPreview

+ 0 - 458
frontend/src/Subscriber.js

@@ -1,458 +0,0 @@
-import React from "react"
-import { Link } from "react-router-dom"
-import {
-  Row,
-  Col,
-  Form,
-  Input,
-  Select,
-  Button,
-  Tag,
-  Tooltip,
-  Icon,
-  Spin,
-  Popconfirm,
-  notification
-} from "antd"
-
-import * as cs from "./constants"
-
-const tagColors = {
-  enabled: "green",
-  blacklisted: "red"
-}
-const formItemLayoutModal = {
-  labelCol: { xs: { span: 24 }, sm: { span: 4 } },
-  wrapperCol: { xs: { span: 24 }, sm: { span: 18 } }
-}
-const formItemLayout = {
-  labelCol: { xs: { span: 16 }, sm: { span: 4 } },
-  wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
-}
-const formItemTailLayout = {
-  wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
-}
-
-class CreateFormDef extends React.PureComponent {
-  state = {
-    confirmDirty: false,
-    loading: false
-  }
-
-  // Handle create / edit form submission.
-  handleSubmit = (e, cb) => {
-    e.preventDefault()
-    if (!cb) {
-      // Set a fake callback.
-      cb = () => {}
-    }
-
-    var err = null,
-      values = {}
-    this.props.form.validateFields((e, v) => {
-      err = e
-      values = v
-    })
-    if (err) {
-      return
-    }
-
-    let a = values["attribs"]
-    values["attribs"] = {}
-    if (a && a.length > 0) {
-      try {
-        values["attribs"] = JSON.parse(a)
-        if (values["attribs"] instanceof Array) {
-          notification["error"]({
-            message: "Invalid JSON type",
-            description: "Attributes should be a map {} and not an array []"
-          })
-          return
-        }
-      } catch (e) {
-        notification["error"]({
-          message: "Invalid JSON in attributes",
-          description: e.toString()
-        })
-        return
-      }
-    }
-
-    this.setState({ loading: true })
-    if (this.props.formType === cs.FormCreate) {
-      // Add a subscriber.
-      this.props
-        .modelRequest(
-          cs.ModelSubscribers,
-          cs.Routes.CreateSubscriber,
-          cs.MethodPost,
-          values
-        )
-        .then(() => {
-          notification["success"]({
-            message: "Subscriber added",
-            description: `${values["email"]} added`
-          })
-          if (!this.props.isModal) {
-            this.props.fetchRecord(this.props.record.id)
-          }
-          cb(true)
-          this.setState({ loading: false })
-        })
-        .catch(e => {
-          notification["error"]({ message: "Error", description: e.message })
-          cb(false)
-          this.setState({ loading: false })
-        })
-    } else {
-      // Edit a subscriber.
-      delete values["keys"]
-      delete values["vals"]
-      this.props
-        .modelRequest(
-          cs.ModelSubscribers,
-          cs.Routes.UpdateSubscriber,
-          cs.MethodPut,
-          { ...values, id: this.props.record.id }
-        )
-        .then(resp => {
-          notification["success"]({
-            message: "Subscriber modified",
-            description: `${values["email"]} modified`
-          })
-          if (!this.props.isModal) {
-            this.props.fetchRecord(this.props.record.id)
-          }
-          cb(true)
-          this.setState({ loading: false })
-        })
-        .catch(e => {
-          notification["error"]({ message: "Error", description: e.message })
-          cb(false)
-          this.setState({ loading: false })
-        })
-    }
-  }
-
-  handleDeleteRecord = record => {
-    this.props
-      .modelRequest(
-        cs.ModelSubscribers,
-        cs.Routes.DeleteSubscriber,
-        cs.MethodDelete,
-        { id: record.id }
-      )
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber deleted",
-          description: `${record.email} deleted`
-        })
-
-        this.props.route.history.push({
-          pathname: cs.Routes.ViewSubscribers
-        })
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message })
-      })
-  }
-
-  handleSendOptinMail = record => {
-    this.props
-      .request(cs.Routes.SendSubscriberOptinMail, cs.MethodPost, {
-        id: record.id
-      })
-      .then(r => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Sent",
-          description: `Opt-in e-mail sentto ${record.email}`
-        })
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  render() {
-    const { formType, record } = this.props
-    const { getFieldDecorator } = this.props.form
-
-    if (formType === null) {
-      return null
-    }
-
-    let subListIDs = []
-    let subStatuses = {}
-    if (this.props.record && this.props.record.lists) {
-      subListIDs = this.props.record.lists.map(v => {
-        return v["id"]
-      })
-      subStatuses = this.props.record.lists.reduce(
-        (o, item) => ({ ...o, [item.id]: item.subscription_status }),
-        {}
-      )
-    } else if (this.props.list) {
-      subListIDs = [this.props.list.id]
-    }
-
-    const layout = this.props.isModal ? formItemLayoutModal : formItemLayout
-    return (
-      <Spin spinning={this.state.loading}>
-        <Form onSubmit={this.handleSubmit}>
-          <Form.Item {...layout} label="E-mail">
-            {getFieldDecorator("email", {
-              initialValue: record.email,
-              rules: [{ required: true }]
-            })(<Input autoFocus pattern="(.+?)@(.+?)" maxLength={200} />)}
-          </Form.Item>
-          <Form.Item {...layout} label="Name">
-            {getFieldDecorator("name", {
-              initialValue: record.name,
-              rules: [{ required: true }]
-            })(<Input maxLength={200} />)}
-          </Form.Item>
-          <Form.Item
-            {...layout}
-            name="status"
-            label="Status"
-            extra="Blacklisted users will not receive any e-mails ever"
-          >
-            {getFieldDecorator("status", {
-              initialValue: record.status ? record.status : "enabled",
-              rules: [{ required: true, message: "Type is required" }]
-            })(
-              <Select style={{ maxWidth: 120 }}>
-                <Select.Option value="enabled">Enabled</Select.Option>
-                <Select.Option value="blacklisted">Blacklisted</Select.Option>
-              </Select>
-            )}
-          </Form.Item>
-          <Form.Item
-            {...layout}
-            label="Lists"
-            extra="Lists to subscribe to. Lists from which subscribers have unsubscribed themselves cannot be removed."
-          >
-            {getFieldDecorator("lists", { initialValue: subListIDs })(
-              <Select mode="multiple">
-                {[...this.props.lists].map((v, i) => (
-                  <Select.Option
-                    value={v.id}
-                    key={v.id}
-                    disabled={
-                      subStatuses[v.id] === cs.SubscriptionStatusUnsubscribed
-                    }
-                  >
-                    <span>
-                      {v.name}
-                      {subStatuses[v.id] && (
-                        <sup
-                          className={"subscription-status " + subStatuses[v.id]}
-                        >
-                          {" "}
-                          {subStatuses[v.id]}
-                        </sup>
-                      )}
-                    </span>
-                  </Select.Option>
-                ))}
-              </Select>
-            )}
-            {record.lists &&
-              record.lists.some(l => {
-                return (
-                  l.subscription_status === cs.SubscriptionStatusUnConfirmed
-                )
-              }) && (
-                <Tooltip title="Send an opt-in e-mail to the subscriber to confirm subscriptions">
-                  <Link
-                    onClick={e => {
-                      e.preventDefault()
-                      this.handleSendOptinMail(record)
-                    }}
-                    to={`/`}
-                  >
-                    <Icon type="rocket" /> Send opt-in e-mail
-                  </Link>
-                </Tooltip>
-              )}
-          </Form.Item>
-          <Form.Item {...layout} label="Attributes" colon={false}>
-            <div>
-              {getFieldDecorator("attribs", {
-                initialValue: record.attribs
-                  ? JSON.stringify(record.attribs, null, 4)
-                  : ""
-              })(
-                <Input.TextArea
-                  placeholder="{}"
-                  rows={10}
-                  readOnly={false}
-                  autosize={{ minRows: 5, maxRows: 10 }}
-                />
-              )}
-            </div>
-            <p className="ant-form-extra">
-              Attributes are defined as a JSON map, for example:
-              {' {"age": 30, "color": "red", "is_user": true}'}.{" "}
-              <a
-                href="https://listmonk.app/docs/concepts"
-                rel="noopener noreferrer"
-                target="_blank"
-              >
-                More info
-              </a>
-              .
-            </p>
-          </Form.Item>
-          {!this.props.isModal && (
-            <Form.Item {...formItemTailLayout}>
-              <Button
-                type="primary"
-                htmlType="submit"
-                icon={this.props.formType === cs.FormCreate ? "plus" : "save"}
-              >
-                {this.props.formType === cs.FormCreate ? "Add" : "Save"}
-              </Button>{" "}
-              {this.props.formType === cs.FormEdit && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() => {
-                    this.handleDeleteRecord(record)
-                  }}
-                >
-                  <Button icon="delete">Delete</Button>
-                </Popconfirm>
-              )}
-            </Form.Item>
-          )}
-        </Form>
-      </Spin>
-    )
-  }
-}
-
-const CreateForm = Form.create()(CreateFormDef)
-
-class Subscriber extends React.PureComponent {
-  state = {
-    loading: true,
-    formRef: null,
-    record: {},
-    subID: this.props.route.match.params
-      ? parseInt(this.props.route.match.params.subID, 10)
-      : 0
-  }
-
-  componentDidMount() {
-    // When this component is invoked within a modal from the subscribers list page,
-    // the necessary context is supplied and there's no need to fetch anything.
-    if (!this.props.isModal) {
-      // Fetch lists.
-      this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
-
-      // Fetch subscriber.
-      this.fetchRecord(this.state.subID)
-    } else {
-      this.setState({ record: this.props.record, loading: false })
-    }
-  }
-
-  fetchRecord = id => {
-    this.props
-      .request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id })
-      .then(r => {
-        this.setState({ record: r.data.data, loading: false })
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        })
-      })
-  }
-
-  setFormRef = r => {
-    this.setState({ formRef: r })
-  }
-
-  submitForm = (e, cb) => {
-    if (this.state.formRef) {
-      this.state.formRef.handleSubmit(e, cb)
-    }
-  }
-
-  render() {
-    return (
-      <section className="content">
-        <header className="header">
-          <Row>
-            <Col span={20}>
-              {!this.state.record.id && <h1>Add subscriber</h1>}
-              {this.state.record.id && (
-                <div>
-                  <h1>
-                    <Tag
-                      className="subscriber-status"
-                      color={
-                        tagColors.hasOwnProperty(this.state.record.status)
-                          ? tagColors[this.state.record.status]
-                          : ""
-                      }
-                    >
-                      {this.state.record.status}
-                    </Tag>{" "}
-                    <span className="subscriber-name">
-                      {this.state.record.name} ({this.state.record.email})
-                    </span>
-                  </h1>
-                  <span className="text-small text-grey">
-                    ID {this.state.record.id} / UUID {this.state.record.uuid}
-                  </span>
-                </div>
-              )}
-            </Col>
-            <Col span={4} className="right subscriber-export">
-              <Tooltip title="Export subscriber data" placement="top">
-                <a
-                  role="button"
-                  href={"/api/subscribers/" + this.state.record.id + "/export"}
-                >
-                  Export <Icon type="export" />
-                </a>
-              </Tooltip>
-            </Col>
-          </Row>
-        </header>
-        <div>
-          <Spin spinning={this.state.loading}>
-            <CreateForm
-              {...this.props}
-              formType={this.props.formType ? this.props.formType : cs.FormEdit}
-              record={this.state.record}
-              fetchRecord={this.fetchRecord}
-              lists={this.props.data[cs.ModelLists].results}
-              wrappedComponentRef={r => {
-                if (!r) {
-                  return
-                }
-
-                // Save the form's reference so that when this component
-                // is used as a modal, the invoker of the model can submit
-                // it via submitForm()
-                this.setState({ formRef: r })
-              }}
-            />
-          </Spin>
-        </div>
-      </section>
-    )
-  }
-}
-
-export default Subscriber

+ 0 - 850
frontend/src/Subscribers.js

@@ -1,850 +0,0 @@
-import React from "react";
-import { Link } from "react-router-dom";
-import {
-  Row,
-  Col,
-  Modal,
-  Form,
-  Input,
-  Select,
-  Button,
-  Table,
-  Icon,
-  Tooltip,
-  Tag,
-  Popconfirm,
-  notification,
-  Radio
-} from "antd";
-
-import Utils from "./utils";
-import Subscriber from "./Subscriber";
-import * as cs from "./constants";
-
-const tagColors = {
-  enabled: "green",
-  blacklisted: "red"
-};
-
-class ListsFormDef extends React.PureComponent {
-  state = {
-    modalWaiting: false
-  };
-
-  // Handle create / edit form submission.
-  handleSubmit = e => {
-    e.preventDefault();
-
-    var err = null,
-      values = {};
-    this.props.form.validateFields((e, v) => {
-      err = e;
-      values = v;
-    });
-    if (err) {
-      return;
-    }
-
-    if (this.props.allRowsSelected) {
-      values["list_ids"] = this.props.listIDs;
-      values["query"] = this.props.query;
-    } else {
-      values["ids"] = this.props.selectedRows.map(r => r.id);
-    }
-
-    this.setState({ modalWaiting: true });
-    this.props
-      .request(
-        !this.props.allRowsSelected
-          ? cs.Routes.AddSubscribersToLists
-          : cs.Routes.AddSubscribersToListsByQuery,
-        cs.MethodPut,
-        values
-      )
-      .then(() => {
-        notification["success"]({
-          message: "Lists changed",
-          description: `Lists changed for selected subscribers`
-        });
-        this.props.clearSelectedRows();
-        this.props.fetchRecords();
-        this.setState({ modalWaiting: false });
-        this.props.onClose();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-        this.setState({ modalWaiting: false });
-      });
-  };
-
-  render() {
-    const { getFieldDecorator } = this.props.form;
-    const formItemLayout = {
-      labelCol: { xs: { span: 16 }, sm: { span: 4 } },
-      wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
-    };
-
-    return (
-      <Modal
-        visible={true}
-        width="750px"
-        className="subscriber-lists-modal"
-        title="Manage lists"
-        okText="Ok"
-        confirmLoading={this.state.modalWaiting}
-        onCancel={this.props.onClose}
-        onOk={this.handleSubmit}
-      >
-        <Form onSubmit={this.handleSubmit}>
-          <Form.Item {...formItemLayout} label="Action">
-            {getFieldDecorator("action", {
-              initialValue: "add",
-              rules: [{ required: true }]
-            })(
-              <Radio.Group>
-                <Radio value="add">Add</Radio>
-                <Radio value="remove">Remove</Radio>
-                <Radio value="unsubscribe">Mark as unsubscribed</Radio>
-              </Radio.Group>
-            )}
-          </Form.Item>
-          <Form.Item {...formItemLayout} label="Lists">
-            {getFieldDecorator("target_list_ids", {
-              rules: [{ required: true }]
-            })(
-              <Select mode="multiple">
-                {[...this.props.lists].map((v, i) => (
-                  <Select.Option value={v.id} key={v.id}>
-                    {v.name}
-                  </Select.Option>
-                ))}
-              </Select>
-            )}
-          </Form.Item>
-        </Form>
-      </Modal>
-    );
-  }
-}
-
-const ListsForm = Form.create()(ListsFormDef);
-
-class Subscribers extends React.PureComponent {
-  defaultPerPage = 20;
-
-  state = {
-    formType: null,
-    listsFormVisible: false,
-    modalForm: null,
-    record: {},
-    queryParams: {
-      page: 1,
-      total: 0,
-      perPage: this.defaultPerPage,
-      listID: this.props.route.match.params.listID
-        ? parseInt(this.props.route.match.params.listID, 10)
-        : 0,
-      list: null,
-      query: null,
-      targetLists: []
-    },
-    listModalVisible: false,
-    allRowsSelected: false,
-    selectedRows: []
-  };
-
-  // Pagination config.
-  paginationOptions = {
-    hideOnSinglePage: true,
-    showSizeChanger: true,
-    showQuickJumper: true,
-    defaultPageSize: this.defaultPerPage,
-    pageSizeOptions: ["20", "50", "70", "100"],
-    position: "both",
-    showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`,
-    onChange: (page, perPage) => {
-      this.fetchRecords({ page: page, per_page: perPage });
-    },
-    onShowSizeChange: (page, perPage) => {
-      this.fetchRecords({ page: page, per_page: perPage });
-    }
-  };
-
-  constructor(props) {
-    super(props);
-
-    // Table layout.
-    this.columns = [
-      {
-        title: "E-mail",
-        dataIndex: "email",
-        sorter: true,
-        width: "25%",
-        render: (text, record) => {
-          const out = [];
-          out.push(
-            <div key={`sub-email-${record.id}`} className="sub-name">
-              <Link
-                to={`/subscribers/${record.id}`}
-                onClick={e => {
-                  // Open the individual subscriber page on ctrl+click
-                  // and the modal otherwise.
-                  if (!e.ctrlKey) {
-                    this.handleShowEditForm(record);
-                    e.preventDefault();
-                  }
-                }}
-              >
-                {text}
-              </Link>
-            </div>
-          );
-
-          if (record.lists.length > 0) {
-            for (let i = 0; i < record.lists.length; i++) {
-              out.push(
-                <Tag
-                  className="list"
-                  key={`sub-${record.id}-list-${record.lists[i].id}`}
-                >
-                  <Link to={`/subscribers/lists/${record.lists[i].id}`}>
-                    {record.lists[i].name}
-                  </Link>
-                  <sup
-                    className={
-                      "subscription-status " +
-                      record.lists[i].subscription_status
-                    }
-                  >
-                    {" "}
-                    {record.lists[i].subscription_status}
-                  </sup>
-                </Tag>
-              );
-            }
-          }
-
-          return out;
-        }
-      },
-      {
-        title: "Name",
-        dataIndex: "name",
-        sorter: true,
-        width: "15%",
-        render: (text, record) => {
-          return (
-            <Link
-              to={`/subscribers/${record.id}`}
-              onClick={e => {
-                // Open the individual subscriber page on ctrl+click
-                // and the modal otherwise.
-                if (!e.ctrlKey) {
-                  this.handleShowEditForm(record);
-                  e.preventDefault();
-                }
-              }}
-            >
-              {text}
-            </Link>
-          );
-        }
-      },
-      {
-        title: "Status",
-        dataIndex: "status",
-        width: "5%",
-        render: (status, _) => {
-          return (
-            <Tag
-              color={tagColors.hasOwnProperty(status) ? tagColors[status] : ""}
-            >
-              {status}
-            </Tag>
-          );
-        }
-      },
-      {
-        title: "Lists",
-        dataIndex: "lists",
-        width: "10%",
-        align: "center",
-        render: (lists, _) => {
-          return (
-            <span>
-              {lists.reduce(
-                (def, item) =>
-                  def +
-                  (item.subscription_status !==
-                  cs.SubscriptionStatusUnsubscribed
-                    ? 1
-                    : 0),
-                0
-              )}
-            </span>
-          );
-        }
-      },
-      {
-        title: "Created",
-        width: "10%",
-        dataIndex: "created_at",
-        render: (date, _) => {
-          return Utils.DateString(date);
-        }
-      },
-      {
-        title: "Updated",
-        width: "10%",
-        dataIndex: "updated_at",
-        render: (date, _) => {
-          return Utils.DateString(date);
-        }
-      },
-      {
-        title: "",
-        dataIndex: "actions",
-        width: "10%",
-        render: (text, record) => {
-          return (
-            <div className="actions">
-              {/* <Tooltip title="Send an e-mail"><a role="button"><Icon type="rocket" /></a></Tooltip> */}
-              <Tooltip title="Edit subscriber">
-                <a
-                  role="button"
-                  onClick={() => this.handleShowEditForm(record)}
-                >
-                  <Icon type="edit" />
-                </a>
-              </Tooltip>
-              <Popconfirm
-                title="Are you sure?"
-                onConfirm={() => this.handleDeleteRecord(record)}
-              >
-                <Tooltip title="Delete subscriber" placement="bottom">
-                  <a role="button">
-                    <Icon type="delete" />
-                  </a>
-                </Tooltip>
-              </Popconfirm>
-            </div>
-          );
-        }
-      }
-    ];
-  }
-
-  componentDidMount() {
-    // Load lists on boot.
-    this.props
-      .modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
-      .then(() => {
-        // If this is an individual list's view, pick up that list.
-        if (this.state.queryParams.listID) {
-          this.props.data[cs.ModelLists].results.forEach(l => {
-            if (l.id === this.state.queryParams.listID) {
-              this.setState({
-                queryParams: { ...this.state.queryParams, list: l }
-              });
-              return false;
-            }
-          });
-        }
-      });
-
-    this.fetchRecords();
-  }
-
-  fetchRecords = params => {
-    let qParams = {
-      page: this.state.queryParams.page,
-      per_page: this.state.queryParams.per_page,
-      list_id: this.state.queryParams.listID,
-      query: this.state.queryParams.query
-    };
-
-    // The records are for a specific list.
-    if (this.state.queryParams.listID) {
-      qParams.list_id = this.state.queryParams.listID;
-    }
-
-    if (params) {
-      qParams = { ...qParams, ...params };
-    }
-
-    this.props
-      .modelRequest(
-        cs.ModelSubscribers,
-        cs.Routes.GetSubscribers,
-        cs.MethodGet,
-        qParams
-      )
-      .then(() => {
-        this.setState({
-          queryParams: {
-            ...this.state.queryParams,
-            total: this.props.data[cs.ModelSubscribers].total,
-            perPage: this.props.data[cs.ModelSubscribers].per_page,
-            page: this.props.data[cs.ModelSubscribers].page,
-            query: this.props.data[cs.ModelSubscribers].query
-          }
-        });
-      });
-  };
-
-  handleDeleteRecord = record => {
-    this.props
-      .modelRequest(
-        cs.ModelSubscribers,
-        cs.Routes.DeleteSubscriber,
-        cs.MethodDelete,
-        { id: record.id }
-      )
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber deleted",
-          description: `${record.email} deleted`
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleDeleteRecords = records => {
-    this.props
-      .modelRequest(
-        cs.ModelSubscribers,
-        cs.Routes.DeleteSubscribers,
-        cs.MethodDelete,
-        { id: records.map(r => r.id) }
-      )
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber(s) deleted",
-          description: "Selected subscribers deleted"
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleBlacklistSubscribers = records => {
-    this.props
-      .request(cs.Routes.BlacklistSubscribers, cs.MethodPut, {
-        ids: records.map(r => r.id)
-      })
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber(s) blacklisted",
-          description: "Selected subscribers blacklisted"
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  // Arbitrary query based calls.
-  handleDeleteRecordsByQuery = (listIDs, query) => {
-    this.props
-      .modelRequest(
-        cs.ModelSubscribers,
-        cs.Routes.DeleteSubscribersByQuery,
-        cs.MethodPost,
-        { list_ids: listIDs, query: query }
-      )
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber(s) deleted",
-          description: "Selected subscribers have been deleted"
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleBlacklistSubscribersByQuery = (listIDs, query) => {
-    this.props
-      .request(cs.Routes.BlacklistSubscribersByQuery, cs.MethodPut, {
-        list_ids: listIDs,
-        query: query
-      })
-      .then(() => {
-        notification["success"]({
-          message: "Subscriber(s) blacklisted",
-          description: "Selected subscribers have been blacklisted"
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleQuerySubscribersIntoLists = (query, sourceList, targetLists) => {
-    let params = {
-      query: query,
-      source_list: sourceList,
-      target_lists: targetLists
-    };
-
-    this.props
-      .request(cs.Routes.QuerySubscribersIntoLists, cs.MethodPost, params)
-      .then(res => {
-        notification["success"]({
-          message: "Subscriber(s) added",
-          description: `${res.data.data.count} added`
-        });
-        this.handleToggleListModal();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleHideForm = () => {
-    this.setState({ formType: null });
-  };
-
-  handleShowCreateForm = () => {
-    this.setState({ formType: cs.FormCreate, attribs: [], record: {} });
-  };
-
-  handleShowEditForm = record => {
-    this.setState({ formType: cs.FormEdit, record: record });
-  };
-
-  handleToggleListsForm = () => {
-    this.setState({ listsFormVisible: !this.state.listsFormVisible });
-  };
-
-  handleSearch = q => {
-    q = q.trim().toLowerCase();
-    if (q === "") {
-      this.fetchRecords({ query: null });
-      return;
-    }
-
-    q = q.replace(/'/g, "''");
-    const query = `(name ~* '${q}' OR email ~* '${q}')`;
-    this.fetchRecords({ query: query });
-  };
-
-  handleSelectRow = (_, records) => {
-    this.setState({ allRowsSelected: false, selectedRows: records });
-  };
-
-  handleSelectAllRows = () => {
-    this.setState({
-      allRowsSelected: true,
-      selectedRows: this.props.data[cs.ModelSubscribers].results
-    });
-  };
-
-  clearSelectedRows = (_, records) => {
-    this.setState({ allRowsSelected: false, selectedRows: [] });
-  };
-
-  handleToggleQueryForm = () => {
-    this.setState({ queryFormVisible: !this.state.queryFormVisible });
-  };
-
-  handleToggleListModal = () => {
-    this.setState({ listModalVisible: !this.state.listModalVisible });
-  };
-
-  render() {
-    const pagination = {
-      ...this.paginationOptions,
-      ...this.state.queryParams
-    };
-
-    if (this.state.queryParams.list) {
-      this.props.pageTitle(this.state.queryParams.list.name + " / Subscribers");
-    } else {
-      this.props.pageTitle("Subscribers");
-    }
-
-    return (
-      <section className="content subscribers">
-        <header className="header">
-          <Row>
-            <Col xs={24} sm={14}>
-              <h1>
-                Subscribers
-                {this.props.data[cs.ModelSubscribers].total > 0 && (
-                  <span> ({this.props.data[cs.ModelSubscribers].total})</span>
-                )}
-                {this.state.queryParams.list && (
-                  <span> &raquo; {this.state.queryParams.list.name}</span>
-                )}
-              </h1>
-            </Col>
-            <Col xs={24} sm={10} className="right header-action-break">
-              <Button
-                type="primary"
-                icon="plus"
-                onClick={this.handleShowCreateForm}
-              >
-                Add subscriber
-              </Button>
-            </Col>
-          </Row>
-        </header>
-
-        <div className="subscriber-query">
-          <Row>
-            <Col sm={24} md={10}>
-              <Row>
-                <Row>
-                  <label>Search subscribers</label>
-                  <Input.Search
-                    name="name"
-                    placeholder="Name or e-mail"
-                    enterButton
-                    onSearch={this.handleSearch}
-                  />{" "}
-                </Row>
-                <Row style={{ marginTop: "10px" }}>
-                  <a role="button" onClick={this.handleToggleQueryForm}>
-                    <Icon type="setting" /> Advanced
-                  </a>
-                </Row>
-              </Row>
-              {this.state.queryFormVisible && (
-                <div className="advanced-query">
-                  <p>
-                    <label>Advanced query</label>
-                    <Input.TextArea
-                      placeholder="subscribers.name LIKE '%user%' or subscribers.status='blacklisted'"
-                      id="subscriber-query"
-                      rows={10}
-                      onChange={e => {
-                        this.setState({
-                          queryParams: {
-                            ...this.state.queryParams,
-                            query: e.target.value
-                          }
-                        });
-                      }}
-                      value={this.state.queryParams.query}
-                      autosize={{ minRows: 2, maxRows: 10 }}
-                    />
-                    <span className="text-tiny text-small">
-                      Partial SQL expression to query subscriber attributes.{" "}
-                      <a
-                        href="https://listmonk.app/docs/querying-and-segmentation"
-                        target="_blank"
-                        rel="noopener noreferrer"
-                      >
-                        Learn more <Icon type="link" />.
-                      </a>
-                    </span>
-                  </p>
-                  <p>
-                    <Button
-                      disabled={this.state.queryParams.query === ""}
-                      type="primary"
-                      icon="search"
-                      onClick={() => {
-                        this.fetchRecords();
-                      }}
-                    >
-                      Query
-                    </Button>{" "}
-                    <Button
-                      disabled={this.state.queryParams.query === ""}
-                      icon="refresh"
-                      onClick={() => {
-                        this.fetchRecords({ query: null });
-                      }}
-                    >
-                      Reset
-                    </Button>
-                  </p>
-                </div>
-              )}
-            </Col>
-            <Col sm={24} md={{ span: 12, offset: 2 }} className="slc-subs-section">
-              {this.state.selectedRows.length > 0 && (
-                <nav className="table-options">
-                  <p>
-                    <strong>
-                      {this.state.allRowsSelected
-                        ? this.state.queryParams.total
-                        : this.state.selectedRows.length}
-                    </strong>{" "}
-                    subscriber(s) selected
-                    {!this.state.allRowsSelected &&
-                      this.state.queryParams.total >
-                        this.state.queryParams.perPage && (
-                        <span>
-                          {" "}
-                          &mdash;{" "}
-                          <a role="button" onClick={this.handleSelectAllRows}>
-                            Select all {this.state.queryParams.total}?
-                          </a>
-                        </span>
-                      )}
-                  </p>
-                  <p class="slc-subs-actions">
-                    <a role="button" onClick={this.handleToggleListsForm}>
-                      <Icon type="bars" /> Manage lists
-                    </a>
-                    <a role="button">
-                      <Icon type="rocket" /> Send campaign
-                    </a>
-                    <Popconfirm
-                      title="Are you sure?"
-                      onConfirm={() => {
-                        if (this.state.allRowsSelected) {
-                          this.handleDeleteRecordsByQuery(
-                            this.state.queryParams.listID
-                              ? [this.state.queryParams.listID]
-                              : [],
-                            this.state.queryParams.query
-                          );
-                          this.clearSelectedRows();
-                        } else {
-                          this.handleDeleteRecords(this.state.selectedRows);
-                          this.clearSelectedRows();
-                        }
-                      }}
-                    >
-                      <a role="button">
-                        <Icon type="delete" /> Delete
-                      </a>
-                    </Popconfirm>
-                    <Popconfirm
-                      title="Are you sure?"
-                      onConfirm={() => {
-                        if (this.state.allRowsSelected) {
-                          this.handleBlacklistSubscribersByQuery(
-                            this.state.queryParams.listID
-                              ? [this.state.queryParams.listID]
-                              : [],
-                            this.state.queryParams.query
-                          );
-                          this.clearSelectedRows();
-                        } else {
-                          this.handleBlacklistSubscribers(
-                            this.state.selectedRows
-                          );
-                          this.clearSelectedRows();
-                        }
-                      }}
-                    >
-                      <a role="button">
-                        <Icon type="close" /> Blacklist
-                      </a>
-                    </Popconfirm>
-                  </p>
-                </nav>
-              )}
-            </Col>
-          </Row>
-        </div>
-
-        <Table
-          columns={this.columns}
-          rowKey={record => `sub-${record.id}`}
-          dataSource={(() => {
-            if (
-              !this.props.data[cs.ModelSubscribers] ||
-              !this.props.data[cs.ModelSubscribers].hasOwnProperty("results")
-            ) {
-              return [];
-            }
-            return this.props.data[cs.ModelSubscribers].results;
-          })()}
-          loading={this.props.reqStates[cs.ModelSubscribers] !== cs.StateDone}
-          pagination={pagination}
-          rowSelection={{
-            columnWidth: "5%",
-            onChange: this.handleSelectRow,
-            selectedRowKeys: this.state.selectedRows.map(r => `sub-${r.id}`)
-          }}
-        />
-
-        {this.state.formType !== null && (
-          <Modal
-            visible={true}
-            width="750px"
-            className="subscriber-modal"
-            okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
-            confirmLoading={this.state.modalWaiting}
-            onOk={e => {
-              if (!this.state.modalForm) {
-                return;
-              }
-
-              // This submits the form embedded in the Subscriber component.
-              this.state.modalForm.submitForm(e, ok => {
-                if (ok) {
-                  this.handleHideForm();
-                  this.fetchRecords();
-                }
-              });
-            }}
-            onCancel={this.handleHideForm}
-            okButtonProps={{
-              disabled:
-                this.props.reqStates[cs.ModelSubscribers] === cs.StatePending
-            }}
-          >
-            <Subscriber
-              {...this.props}
-              isModal={true}
-              formType={this.state.formType}
-              record={this.state.record}
-              ref={r => {
-                if (!r) {
-                  return;
-                }
-
-                this.setState({ modalForm: r });
-              }}
-            />
-          </Modal>
-        )}
-
-        {this.state.listsFormVisible && (
-          <ListsForm
-            {...this.props}
-            lists={this.props.data[cs.ModelLists].results}
-            allRowsSelected={this.state.allRowsSelected}
-            selectedRows={this.state.selectedRows}
-            selectedLists={
-              this.state.queryParams.listID
-                ? [this.state.queryParams.listID]
-                : []
-            }
-            clearSelectedRows={this.clearSelectedRows}
-            query={this.state.queryParams.query}
-            fetchRecords={this.fetchRecords}
-            onClose={this.handleToggleListsForm}
-          />
-        )}
-      </section>
-    );
-  }
-}
-
-export default Subscribers;

+ 0 - 443
frontend/src/Templates.js

@@ -1,443 +0,0 @@
-import React from "react";
-import {
-  Row,
-  Col,
-  Modal,
-  Form,
-  Input,
-  Button,
-  Table,
-  Icon,
-  Tooltip,
-  Tag,
-  Popconfirm,
-  Spin,
-  notification
-} from "antd";
-
-import ModalPreview from "./ModalPreview";
-import Utils from "./utils";
-import * as cs from "./constants";
-
-class CreateFormDef extends React.PureComponent {
-  state = {
-    confirmDirty: false,
-    modalWaiting: false,
-    previewName: "",
-    previewBody: ""
-  };
-
-  // Handle create / edit form submission.
-  handleSubmit = e => {
-    e.preventDefault();
-    this.props.form.validateFields((err, values) => {
-      if (err) {
-        return;
-      }
-
-      this.setState({ modalWaiting: true });
-      if (this.props.formType === cs.FormCreate) {
-        // Create a new list.
-        this.props
-          .modelRequest(
-            cs.ModelTemplates,
-            cs.Routes.CreateTemplate,
-            cs.MethodPost,
-            values
-          )
-          .then(() => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "Template added",
-              description: `"${values["name"]}" added`
-            });
-            this.props.fetchRecords();
-            this.props.onClose();
-            this.setState({ modalWaiting: false });
-          })
-          .catch(e => {
-            notification["error"]({
-              placement: cs.MsgPosition,
-              message: "Error",
-              description: e.message
-            });
-            this.setState({ modalWaiting: false });
-          });
-      } else {
-        // Edit a list.
-        this.props
-          .modelRequest(
-            cs.ModelTemplates,
-            cs.Routes.UpdateTemplate,
-            cs.MethodPut,
-            { ...values, id: this.props.record.id }
-          )
-          .then(() => {
-            notification["success"]({
-              placement: cs.MsgPosition,
-              message: "Template updated",
-              description: `"${values["name"]}" modified`
-            });
-            this.props.fetchRecords();
-            this.props.onClose();
-            this.setState({ modalWaiting: false });
-          })
-          .catch(e => {
-            notification["error"]({
-              placement: cs.MsgPosition,
-              message: "Error",
-              description: e.message
-            });
-            this.setState({ modalWaiting: false });
-          });
-      }
-    });
-  };
-
-  handleConfirmBlur = e => {
-    const value = e.target.value;
-    this.setState({ confirmDirty: this.state.confirmDirty || !!value });
-  };
-
-  handlePreview = (name, body) => {
-    this.setState({ previewName: name, previewBody: body });
-  };
-
-  render() {
-    const { formType, record, onClose } = this.props;
-    const { getFieldDecorator } = this.props.form;
-
-    const formItemLayout = {
-      labelCol: { xs: { span: 16 }, sm: { span: 4 } },
-      wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
-    };
-
-    if (formType === null) {
-      return null;
-    }
-
-    return (
-      <div>
-        <Modal
-          visible={true}
-          title={formType === cs.FormCreate ? "Add template" : record.name}
-          okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
-          width="90%"
-          height={900}
-          confirmLoading={this.state.modalWaiting}
-          onCancel={onClose}
-          onOk={this.handleSubmit}
-        >
-          <Spin
-            spinning={
-              this.props.reqStates[cs.ModelTemplates] === cs.StatePending
-            }
-          >
-            <Form onSubmit={this.handleSubmit}>
-              <Form.Item {...formItemLayout} label="Name">
-                {getFieldDecorator("name", {
-                  initialValue: record.name,
-                  rules: [{ required: true }]
-                })(<Input autoFocus maxLength={200} />)}
-              </Form.Item>
-              <Form.Item {...formItemLayout} name="body" label="Raw HTML">
-                {getFieldDecorator("body", {
-                  initialValue: record.body ? record.body : "",
-                  rules: [{ required: true }]
-                })(<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />)}
-              </Form.Item>
-              {this.props.form.getFieldValue("body") !== "" && (
-                <Form.Item {...formItemLayout} colon={false} label="&nbsp;">
-                  <Button
-                    icon="search"
-                    onClick={() =>
-                      this.handlePreview(
-                        this.props.form.getFieldValue("name"),
-                        this.props.form.getFieldValue("body")
-                      )
-                    }
-                  >
-                    Preview
-                  </Button>
-                </Form.Item>
-              )}
-            </Form>
-          </Spin>
-          <Row>
-            <Col span="4" />
-            <Col span="18" className="text-grey text-small">
-              The placeholder{" "}
-              <code>
-                {"{"}
-                {"{"} template "content" . {"}"}
-                {"}"}
-              </code>{" "}
-              should appear in the template.{" "}
-              <a
-                href="https://listmonk.app/docs/templating"
-                target="_blank"
-                rel="noopener noreferrer"
-              >
-                Learn more <Icon type="link" />.
-              </a>
-              .
-            </Col>
-          </Row>
-        </Modal>
-
-        {this.state.previewBody && (
-          <ModalPreview
-            title={
-              this.state.previewName
-                ? this.state.previewName
-                : "Template preview"
-            }
-            previewURL={cs.Routes.PreviewNewTemplate}
-            body={this.state.previewBody}
-            onCancel={() => {
-              this.setState({ previewBody: null, previewName: null });
-            }}
-          />
-        )}
-      </div>
-    );
-  }
-}
-
-const CreateForm = Form.create()(CreateFormDef);
-
-class Templates extends React.PureComponent {
-  state = {
-    formType: null,
-    record: {},
-    previewRecord: null
-  };
-
-  constructor(props) {
-    super(props);
-
-    this.columns = [
-      {
-        title: "Name",
-        dataIndex: "name",
-        sorter: true,
-        width: "50%",
-        render: (text, record) => {
-          return (
-            <div className="name">
-              <a role="button" onClick={() => this.handleShowEditForm(record)}>
-                {text}
-              </a>
-              {record.is_default && (
-                <div>
-                  <Tag>Default</Tag>
-                </div>
-              )}
-            </div>
-          );
-        }
-      },
-      {
-        title: "Created",
-        dataIndex: "created_at",
-        render: (date, _) => {
-          return Utils.DateString(date);
-        }
-      },
-      {
-        title: "Updated",
-        dataIndex: "updated_at",
-        render: (date, _) => {
-          return Utils.DateString(date);
-        }
-      },
-      {
-        title: "",
-        dataIndex: "actions",
-        width: "20%",
-        className: "actions",
-        render: (text, record) => {
-          return (
-            <div className="actions">
-              <Tooltip
-                title="Preview template"
-                onClick={() => this.handlePreview(record)}
-              >
-                <a role="button">
-                  <Icon type="search" />
-                </a>
-              </Tooltip>
-
-              {!record.is_default && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() => this.handleSetDefault(record)}
-                >
-                  <Tooltip title="Set as default" placement="bottom">
-                    <a role="button">
-                      <Icon type="check" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-
-              <Tooltip title="Edit template">
-                <a
-                  role="button"
-                  onClick={() => this.handleShowEditForm(record)}
-                >
-                  <Icon type="edit" />
-                </a>
-              </Tooltip>
-
-              {record.id !== 1 && (
-                <Popconfirm
-                  title="Are you sure?"
-                  onConfirm={() => this.handleDeleteRecord(record)}
-                >
-                  <Tooltip title="Delete template" placement="bottom">
-                    <a role="button">
-                      <Icon type="delete" />
-                    </a>
-                  </Tooltip>
-                </Popconfirm>
-              )}
-            </div>
-          );
-        }
-      }
-    ];
-  }
-
-  componentDidMount() {
-    this.props.pageTitle("Templates");
-    this.fetchRecords();
-  }
-
-  fetchRecords = () => {
-    this.props.modelRequest(
-      cs.ModelTemplates,
-      cs.Routes.GetTemplates,
-      cs.MethodGet
-    );
-  };
-
-  handleDeleteRecord = record => {
-    this.props
-      .modelRequest(
-        cs.ModelTemplates,
-        cs.Routes.DeleteTemplate,
-        cs.MethodDelete,
-        { id: record.id }
-      )
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Template deleted",
-          description: `"${record.name}" deleted`
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({ message: "Error", description: e.message });
-      });
-  };
-
-  handleSetDefault = record => {
-    this.props
-      .modelRequest(
-        cs.ModelTemplates,
-        cs.Routes.SetDefaultTemplate,
-        cs.MethodPut,
-        { id: record.id }
-      )
-      .then(() => {
-        notification["success"]({
-          placement: cs.MsgPosition,
-          message: "Template updated",
-          description: `"${record.name}" set as default`
-        });
-
-        // Reload the table.
-        this.fetchRecords();
-      })
-      .catch(e => {
-        notification["error"]({
-          placement: cs.MsgPosition,
-          message: "Error",
-          description: e.message
-        });
-      });
-  };
-
-  handlePreview = record => {
-    this.setState({ previewRecord: record });
-  };
-
-  hideForm = () => {
-    this.setState({ formType: null });
-  };
-
-  handleShowCreateForm = () => {
-    this.setState({ formType: cs.FormCreate, record: {} });
-  };
-
-  handleShowEditForm = record => {
-    this.setState({ formType: cs.FormEdit, record: record });
-  };
-
-  render() {
-    return (
-      <section className="content templates">
-        <Row>
-          <Col xs={24} sm={14}>
-            <h1>Templates ({this.props.data[cs.ModelTemplates].length}) </h1>
-          </Col>
-          <Col xs={24} sm={10} className="right header-action-break">
-            <Button
-              type="primary"
-              icon="plus"
-              onClick={this.handleShowCreateForm}
-            >
-              Add template
-            </Button>
-          </Col>
-        </Row>
-        <br />
-
-        <Table
-          columns={this.columns}
-          rowKey={record => record.id}
-          dataSource={this.props.data[cs.ModelTemplates]}
-          loading={this.props.reqStates[cs.ModelTemplates] !== cs.StateDone}
-          pagination={false}
-        />
-
-        <CreateForm
-          {...this.props}
-          formType={this.state.formType}
-          record={this.state.record}
-          onClose={this.hideForm}
-          fetchRecords={this.fetchRecords}
-        />
-
-        {this.state.previewRecord && (
-          <ModalPreview
-            title={this.state.previewRecord.name}
-            previewURL={cs.Routes.PreviewTemplate.replace(
-              ":id",
-              this.state.previewRecord.id
-            )}
-            onCancel={() => {
-              this.setState({ previewRecord: null });
-            }}
-          />
-        )}
-      </section>
-    );
-  }
-}
-
-export default Templates;

+ 183 - 0
frontend/src/api/index.js

@@ -0,0 +1,183 @@
+import { ToastProgrammatic as Toast } from 'buefy';
+import axios from 'axios';
+import humps from 'humps';
+import qs from 'qs';
+import store from '../store';
+import { models } from '../constants';
+
+const http = axios.create({
+  baseURL: process.env.BASE_URL,
+  withCredentials: false,
+  responseType: 'json',
+  transformResponse: [
+    // Apply the defaut transformations as well.
+    ...axios.defaults.transformResponse,
+    (resp) => {
+      if (!resp) {
+        return resp;
+      }
+
+      // There's an error message.
+      if ('message' in resp && resp.message !== '') {
+        return resp;
+      }
+
+      const data = humps.camelizeKeys(resp.data);
+      return data;
+    },
+  ],
+
+  // Override the default serializer to switch params from becoming []id=a&[]id=b ...
+  // in GET and DELETE requests to id=a&id=b.
+  paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
+});
+
+
+// Intercept requests to set the 'loading' state of a model.
+http.interceptors.request.use((config) => {
+  if ('loading' in config) {
+    store.commit('setLoading', { model: config.loading, status: true });
+  }
+  return config;
+}, (error) => Promise.reject(error));
+
+// Intercept responses to set them to store.
+http.interceptors.response.use((resp) => {
+  // Clear the loading state for a model.
+  if ('loading' in resp.config) {
+    store.commit('setLoading', { model: resp.config.loading, status: false });
+  }
+
+  // Store the API response for a model.
+  if ('store' in resp.config) {
+    store.commit('setModelResponse', { model: resp.config.store, data: resp.data });
+  }
+  return resp;
+}, (err) => {
+  // Clear the loading state for a model.
+  if ('loading' in err.config) {
+    store.commit('setLoading', { model: err.config.loading, status: false });
+  }
+
+  let msg = '';
+  if (err.response.data && err.response.data.message) {
+    msg = err.response.data.message;
+  } else {
+    msg = err.toString();
+  }
+
+  Toast.open({
+    message: msg,
+    type: 'is-danger',
+    queue: false,
+  });
+
+  return Promise.reject(err);
+});
+
+// API calls accept the following config keys.
+// loading: modelName (set's the loading status in the global store: eg: store.loading.lists = true)
+// store: modelName (set's the API response in the global store. eg: store.lists: { ... } )
+
+// Lists.
+export const getLists = () => http.get('/api/lists',
+  { loading: models.lists, store: models.lists });
+
+export const createList = (data) => http.post('/api/lists', data,
+  { loading: models.lists });
+
+export const updateList = (data) => http.put(`/api/lists/${data.id}`, data,
+  { loading: models.lists });
+
+export const deleteList = (id) => http.delete(`/api/lists/${id}`,
+  { loading: models.lists });
+
+// Subscribers.
+export const getSubscribers = async (params) => http.get('/api/subscribers',
+  { params, loading: models.subscribers, store: models.subscribers });
+
+export const createSubscriber = (data) => http.post('/api/subscribers', data,
+  { loading: models.subscribers });
+
+export const updateSubscriber = (data) => http.put(`/api/subscribers/${data.id}`, data,
+  { loading: models.subscribers });
+
+export const deleteSubscriber = (id) => http.delete(`/api/subscribers/${id}`,
+  { loading: models.subscribers });
+
+export const addSubscribersToLists = (data) => http.put('/api/subscribers/lists', data,
+  { loading: models.subscribers });
+
+export const addSubscribersToListsByQuery = (data) => http.put('/api/subscribers/query/lists',
+  data, { loading: models.subscribers });
+
+export const blacklistSubscribers = (data) => http.put('/api/subscribers/blacklist', data,
+  { loading: models.subscribers });
+
+export const blacklistSubscribersByQuery = (data) => http.put('/api/subscribers/query/blacklist', data,
+  { loading: models.subscribers });
+
+export const deleteSubscribers = (params) => http.delete('/api/subscribers',
+  { params, loading: models.subscribers });
+
+export const deleteSubscribersByQuery = (data) => http.post('/api/subscribers/query/delete', data,
+  { loading: models.subscribers });
+
+// Subscriber import.
+export const importSubscribers = (data) => http.post('/api/import/subscribers', data);
+
+export const getImportStatus = () => http.get('/api/import/subscribers');
+
+export const getImportLogs = () => http.get('/api/import/subscribers/logs');
+
+export const stopImport = () => http.delete('/api/import/subscribers');
+
+// Campaigns.
+export const getCampaigns = async (params) => http.get('/api/campaigns',
+  { params, loading: models.campaigns, store: models.campaigns });
+
+export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`,
+  { loading: models.campaigns });
+
+export const getCampaignStats = async () => http.get('/api/campaigns/running/stats', {});
+
+export const createCampaign = async (data) => http.post('/api/campaigns', data,
+  { loading: models.campaigns });
+
+export const testCampaign = async (data) => http.post(`/api/campaigns/${data.id}/test`, data,
+  { loading: models.campaigns });
+
+export const updateCampaign = async (id, data) => http.put(`/api/campaigns/${id}`, data,
+  { loading: models.campaigns });
+
+export const changeCampaignStatus = async (id, status) => http.put(`/api/campaigns/${id}/status`,
+  { status }, { loading: models.campaigns });
+
+export const deleteCampaign = async (id) => http.delete(`/api/campaigns/${id}`,
+  { loading: models.campaigns });
+
+// Media.
+export const getMedia = async () => http.get('/api/media',
+  { loading: models.media, store: models.media });
+
+export const uploadMedia = (data) => http.post('/api/media', data,
+  { loading: models.media });
+
+export const deleteMedia = (id) => http.delete(`/api/media/${id}`,
+  { loading: models.media });
+
+// Templates.
+export const createTemplate = async (data) => http.post('/api/templates', data,
+  { loading: models.templates });
+
+export const getTemplates = async () => http.get('/api/templates',
+  { loading: models.templates, store: models.templates });
+
+export const updateTemplate = async (data) => http.put(`/api/templates/${data.id}`, data,
+  { loading: models.templates });
+
+export const makeTemplateDefault = async (id) => http.put(`/api/templates/${id}/default`, {},
+  { loading: models.templates });
+
+export const deleteTemplate = async (id) => http.delete(`/api/templates/${id}`,
+  { loading: models.templates });

+ 43 - 0
frontend/src/assets/buefy.scss

@@ -0,0 +1,43 @@
+@import "~bulma/sass/base/_all";
+@import "~bulma/sass/elements/_all";
+@import "~bulma/sass/components/card";
+@import "~bulma/sass/components/dropdown";
+@import "~bulma/sass/components/level";
+@import "~bulma/sass/components/menu";
+@import "~bulma/sass/components/message";
+@import "~bulma/sass/components/modal";
+@import "~bulma/sass/components/pagination";
+@import "~bulma/sass/components/tabs";
+@import "~bulma/sass/form/_all";
+@import "~bulma/sass/grid/columns";
+@import "~bulma/sass/grid/tiles";
+@import "~bulma/sass/layout/section";
+@import "~bulma/sass/layout/footer";
+
+@import "~buefy/src/scss/utils/_all";
+@import "~buefy/src/scss/components/_autocomplete";
+@import "~buefy/src/scss/components/_carousel";
+@import "~buefy/src/scss/components/_checkbox";
+@import "~buefy/src/scss/components/_datepicker";
+@import "~buefy/src/scss/components/_dialog";
+@import "~buefy/src/scss/components/_dropdown";
+@import "~buefy/src/scss/components/_form";
+@import "~buefy/src/scss/components/_icon";
+@import "~buefy/src/scss/components/_loading";
+@import "~buefy/src/scss/components/_menu";
+@import "~buefy/src/scss/components/_message";
+@import "~buefy/src/scss/components/_modal";
+@import "~buefy/src/scss/components/_pagination";
+@import "~buefy/src/scss/components/_notices";
+@import "~buefy/src/scss/components/_progress";
+@import "~buefy/src/scss/components/_radio";
+@import "~buefy/src/scss/components/_select";
+@import "~buefy/src/scss/components/_sidebar";
+@import "~buefy/src/scss/components/_switch";
+@import "~buefy/src/scss/components/_table";
+@import "~buefy/src/scss/components/_tabs";
+@import "~buefy/src/scss/components/_tag";
+@import "~buefy/src/scss/components/_taginput";
+@import "~buefy/src/scss/components/_timepicker";
+@import "~buefy/src/scss/components/_tooltip";
+@import "~buefy/src/scss/components/_upload";

BIN
frontend/src/assets/favicon.png


+ 72 - 0
frontend/src/assets/icons/fontello.css

@@ -0,0 +1,72 @@
+@font-face {
+  font-family: 'fontello';
+  src: url('fontello.woff2') format('woff2');
+  font-weight: normal;
+  font-style: normal;
+}
+ 
+ [class^="mdi-"]:before, [class*=" mdi-"]:before {
+  font-family: "fontello";
+  font-style: normal;
+  font-weight: normal;
+  speak: never;
+ 
+  display: inline-block;
+  text-decoration: inherit;
+  width: 1em;
+  margin-right: .2em;
+  text-align: center;
+  /* opacity: .8; */
+ 
+  /* For safety - reset parent styles, that can break glyph codes*/
+  font-variant: normal;
+  text-transform: none;
+ 
+  /* fix buttons height, for twitter bootstrap */
+  line-height: 1em;
+ 
+  /* Animation center compensation - margins should be symmetric */
+  /* remove if not needed */
+  margin-left: .2em;
+ 
+  /* you can be more comfortable with increased icons size */
+  /* font-size: 120%; */
+ 
+  /* Font smoothing. That was taken from TWBS */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+ 
+  /* Uncomment for 3D effect */
+  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+ 
+.mdi-view-dashboard-variant-outline:before { content: '\e800'; } /* '' */
+.mdi-format-list-bulleted-square:before { content: '\e801'; } /* '' */
+.mdi-newspaper-variant-outline:before { content: '\e802'; } /* '' */
+.mdi-account-multiple:before { content: '\e803'; } /* '' */
+.mdi-file-upload-outline:before { content: '\e804'; } /* '' */
+.mdi-rocket-launch-outline:before { content: '\e805'; } /* '' */
+.mdi-plus:before { content: '\e806'; } /* '' */
+.mdi-image-outline:before { content: '\e807'; } /* '' */
+.mdi-file-image-outline:before { content: '\e808'; } /* '' */
+.mdi-cog-outline:before { content: '\e809'; } /* '' */
+.mdi-tag-outline:before { content: '\e80a'; } /* '' */
+.mdi-calendar-clock:before { content: '\e80b'; } /* '' */
+.mdi-email-outline:before { content: '\e80c'; } /* '' */
+.mdi-text:before { content: '\e80d'; } /* '' */
+.mdi-alarm:before { content: '\e80e'; } /* '' */
+.mdi-pause-circle-outline:before { content: '\e80f'; } /* '' */
+.mdi-file-find-outline:before { content: '\e810'; } /* '' */
+.mdi-clock-start:before { content: '\e811'; } /* '' */
+.mdi-file-multiple-outline:before { content: '\e812'; } /* '' */
+.mdi-trash-can-outline:before { content: '\e813'; } /* '' */
+.mdi-pencil-outline:before { content: '\e814'; } /* '' */
+.mdi-arrow-top-right:before { content: '\e815'; } /* '' */
+.mdi-link-variant:before { content: '\e816'; } /* '' */
+.mdi-cloud-download-outline:before { content: '\e817'; } /* '' */
+.mdi-account-search-outline:before { content: '\e818'; } /* '' */
+.mdi-check-circle-outline:before { content: '\e819'; } /* '' */
+.mdi-account-check-outline:before { content: '\e81a'; } /* '' */
+.mdi-account-off-outline:before { content: '\e81b'; } /* '' */
+.mdi-chevron-right:before { content: '\e81c'; } /* '' */
+.mdi-chevron-left:before { content: '\e81d'; } /* '' */

BIN
frontend/src/assets/icons/fontello.woff2


+ 0 - 0
frontend/src/static/listmonk.svg → frontend/src/assets/logo.svg


+ 480 - 0
frontend/src/assets/style.scss

@@ -0,0 +1,480 @@
+/* Import Bulma to set variables */
+@import "~bulma/sass/utilities/_all";
+
+$body-family: "IBM Plex Sans", "Helvetica Neue", sans-serif;
+$body-size: 15px;
+$primary: #7f2aff;
+$green: #4caf50;
+$turquoise: $green;
+$red: #ff5722;
+$link: $primary;
+$input-placeholder-color: $black-ter;
+
+$colors: map-merge($colors, (
+    "turquoise": ($green, $green-invert),
+    "green": ($green, $green-invert),
+    "success": ($green, $green-invert),
+    "danger": ($red, $green-invert),
+));
+
+$sidebar-box-shadow: none;
+$sidebar-width: 240px;
+$menu-item-active-background-color: $white-bis;
+$menu-item-active-color: $primary;
+
+/* Buefy */
+$modal-background-background-color: rgba(0, 0, 0, .30);
+$speed-slow: 25ms !default;
+$speed-slower: 50ms !default;
+
+/* Import full Bulma and Buefy to override styles. */
+// @import "~bulma";
+@import "./buefy";
+
+html, body {
+  height: 100%;
+}
+
+code {
+  color: $grey;
+}
+
+ul.no {
+  list-style-type: none;
+  padding: 0;
+  margin: 0;
+}
+
+section {
+  &.wrap {
+    max-width: 1100px;
+  }
+  &.wrap-small {
+    max-width: 900px;
+  }
+}
+.spinner.is-tiny {
+  display: inline-block;
+  height: 10px;
+  width: 10px;
+  position: relative;
+
+  .loading-overlay {
+    .loading-background {
+      background: none;
+    }
+    .loading-icon::after {
+      width: 10px;
+      height: 10px;
+      top: 0;
+      left: 0;
+      position: static;
+    }
+  }    
+}
+
+/* Two column sidebar+body layout */
+#app {
+  display: flex;
+  flex-direction: row;
+  min-height: 100%;
+  
+  > .sidebar {
+    flex-shrink: 1;
+    box-shadow: 0 0 5px #eee;
+    border-right: 1px solid #eee;
+
+    .b-sidebar {
+      position: sticky;
+      top: 0px;
+    }
+  }
+  > .main {
+    margin: 30px 30px 30px 45px;
+    flex-grow: 1;
+  }
+}
+
+.b-sidebar {
+  .logo {
+    padding: 15px;
+  }
+  .sidebar-content {
+    border-right: 1px solid #eee;
+  }
+  .menu-list {
+    .router-link-exact-active {
+      border-right: 5px solid $primary;
+      outline: 0 none;
+    }
+    li ul {
+      margin-right: 0;
+    }
+    > li {
+      margin-bottom: 5px;
+    }
+  }
+  .logo {
+    margin-bottom: 30px;
+  }
+  .favicon {
+    display: none;
+  }
+}
+
+/* Table colors and padding */
+.main table {
+  thead th {
+    background: $white-bis;
+    border-bottom: 1px solid $grey-lighter;
+  }
+  thead th, tbody td {
+    padding: 15px 10px;
+    border-color: #eaeaea;
+  }
+  .actions a {
+    margin: 0 10px;
+    display: inline-block;
+  }
+}
+
+/* Modal */
+.modal {
+  z-index: 100;
+}
+.modal-card-head {
+  display: block;
+}
+.modal .modal-card-foot {
+  justify-content: flex-end;
+}
+.modal .modal-close.is-large {
+  display: none;
+}
+
+/* Fix for button primary colour. */
+.button.is-primary {
+  background: $primary;
+  &:hover {
+    background: darken($primary, 15%);
+  }
+  &:disabled {
+    background: $grey-light;
+  }
+}
+
+.autocomplete .dropdown-content {
+  background-color: $white-bis;
+}
+
+.help {
+  color: $grey;
+}
+
+/* Tags */
+.tag {
+  min-width: 75px;
+
+  &:not(body) {
+    $color: $grey-lighter;
+    border: 1px solid $color;
+    box-shadow: 1px 1px 0 $color;
+    color: $grey;
+  }
+
+  &.private, &.scheduled, &.paused {
+    $color: #ed7b00;
+    color: $color;
+    background: #fff7e6;
+    border: 1px solid lighten($color, 37%);
+    box-shadow: 1px 1px 0 lighten($color, 37%);
+  }
+  &.public, &.running {
+    $color: #1890ff;
+    color: $color;
+    background: #e6f7ff;
+    border: 1px solid lighten($color, 37%);
+    box-shadow: 1px 1px 0 lighten($color, 25%);
+  }
+  &.finished, &.enabled {
+    $color: #50ab24;
+    color: $color;
+    background: #f6ffed;
+    border: 1px solid lighten($color, 45%);
+    box-shadow: 1px 1px 0 lighten($color, 45%);
+  }
+  &.blacklisted {
+    $color: #f5222d;
+    color: $color;
+    background: #fff1f0;
+    border: 1px solid lighten($color, 45%);
+    box-shadow: 1px 1px 0 lighten($color, 45%);
+  } 
+
+  sup {
+    font-weight: $weight-semibold;
+    letter-spacing: 0.03em;
+  }
+  &.unsubscribed sup {
+    color: #fa8c16;
+  }
+  &.confirmed sup {
+    color: #52c41a;
+  }
+  
+  &:not(body) .icon:first-child:last-child {
+    margin-right: 1px;
+  }
+}
+
+/* Dashboard */
+section.dashboard {
+  .counts {
+    .title {
+      margin-bottom: 1rem;
+    }
+    .level-item {
+      background-color: $white-bis;
+      padding: 30px;
+      margin: 10px;
+
+      &:first-child, &:last-child {
+        margin: 0;
+      }
+    }
+  }
+}
+
+/* Lists page */
+section.lists {
+  td .tag {
+    min-width: 65px;
+  }
+}
+
+/* Subscribers page */
+.subscribers-controls {
+  padding-bottom: 15px;
+}
+.subscribers-bulk {
+  .actions a {
+    display: inline-block;
+    margin-right: 30px;
+  }
+}
+
+/* Import page */
+section.import {
+  .delimiter input {
+    max-width: 100px;
+  }
+  .status {
+    padding: 60px;
+  }
+  .logs {
+    max-height: 240px;
+  }
+}
+
+/* Campaigns page */
+section.campaigns {
+  table tbody {
+    tr.running {
+      background: lighten(#1890ff, 43%);
+      td {
+        border-bottom: 1px solid lighten(#1890ff, 30%);
+      }
+
+      .spinner .loading-overlay .loading-icon::after {
+        border-bottom-color: lighten(#1890ff, 30%);
+        border-left-color: lighten(#1890ff, 30%);
+      }
+    }
+
+    td {
+      &.status .spinner {
+        margin-left: 10px;
+      }
+      .tags {
+        margin-top: 5px;
+      }
+
+      p {
+        margin: 0 !important;
+      }
+
+      &.lists ul {
+        font-size: $size-7;
+        list-style-type: circle;
+
+        a {
+          color: $grey-dark;
+          &:hover {
+            color: $primary;
+          }
+        }
+      }
+
+      .fields {
+        font-size: $size-7;
+        label {
+          font-weight: bold;
+          text-align: right;
+          min-width: 50px;
+          margin-right: 10px;
+          display: inline-block;
+        }
+      }
+
+      &.draft {
+        color: $grey-lighter;
+      }
+
+      .progress-wrapper {
+        .progress.is-small {
+          height: 0.4em;
+        }
+        display: inline-block;
+      }
+    }
+  }
+}
+
+/* Campaign / template preview popup */
+.preview {
+  padding: 0;
+  
+  /* Contain the spinner background in the content area. */
+  position: relative;
+
+  #iframe {
+    border: 0;
+    width: 100%;
+    height: 100%;
+    min-height: 500px;
+    padding: 0;
+    margin: 0 0 -5px 0;
+  }
+}
+
+/* Campaign */
+section.campaign {
+  header .buttons {
+    justify-content: flex-end;
+  }
+}
+
+/* Media gallery */
+.media-files {
+  .thumbs {
+    display: flex;
+    flex-wrap: wrap;
+    flex-direction: column;
+    flex-flow: row wrap;
+
+    .thumb {
+      margin: 10px;
+      max-height: 90px;
+      overflow: hidden;
+
+      position: relative;
+
+      .caption {
+        background-color: rgba($white, .70);
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        padding: 2px 5px;
+
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+
+      .actions {
+        background-color: rgba($white, .70);
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        padding: 2px 5px;
+        display: none;
+
+        a {
+          margin-left: 10px;
+        }
+      }
+
+      &:hover .actions {
+        display: block;
+      }
+    }
+
+    .box {
+      padding: 10px;
+    }
+  }
+}
+
+/* Template form */
+.template-modal {
+  .template-modal-content {
+    height: 95vh;
+    max-height: none;
+  }
+  .textarea {
+    max-height: none;
+    height: 55vh;
+  }
+}
+
+@media screen and (max-width: 1450px) and (min-width: 769px) {
+  section.campaigns {
+    /* Fold the stats labels until the card view */
+    table tbody td {
+      .fields {
+        label {
+          margin: 0;
+          display: block;
+          text-align: left;
+        }
+        p {
+          margin-bottom: 10px !important;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 1023px) {
+  /* Hide sidebar menu captions on mobile */
+  .b-sidebar .sidebar-content.is-mini-mobile {
+    .menu-list li {
+      margin-bottom: 30px;
+
+      span:nth-child(2) {
+        display: none;
+      }
+      .icon.is-small {
+        scale: 1.4;
+      }
+    }
+    .logo {
+      text-align: center;
+      .full {
+        display: none;
+      }
+      .favicon {
+        display: block;
+      }
+      .version {
+        display: none;
+      }
+    }
+  }
+
+  #app > .content {
+    margin: 15px;
+  }
+}

+ 93 - 0
frontend/src/components/CampaignPreview.vue

@@ -0,0 +1,93 @@
+<template>
+  <div>
+    <b-modal scroll="keep" @close="close"
+      :aria-modal="true" :active="isVisible">
+      <div>
+        <div class="modal-card" style="width: auto">
+          <header class="modal-card-head">
+            <h4>{{ title }}</h4>
+          </header>
+        </div>
+        <section expanded class="modal-card-body preview">
+          <b-loading :active="isLoading" :is-full-page="false"></b-loading>
+          <form v-if="body" method="post" :action="previewURL" target="iframe" ref="form">
+            <input type="hidden" name="body" :value="body" />
+          </form>
+
+          <iframe id="iframe" name="iframe" ref="iframe"
+            :title="title"
+            :src="body ? 'about:blank' : previewURL"
+            @load="onLoaded"
+          ></iframe>
+        </section>
+        <footer class="modal-card-foot has-text-right">
+          <b-button @click="close">Close</b-button>
+        </footer>
+      </div>
+    </b-modal>
+  </div>
+</template>
+
+<script>
+import { uris } from '../constants';
+
+export default {
+  name: 'CampaignPreview',
+
+  props: {
+    // Template or campaign ID.
+    id: Number,
+    title: String,
+
+    // campaign | template.
+    type: String,
+    body: String,
+  },
+
+  data() {
+    return {
+      isVisible: true,
+      isLoading: true,
+    };
+  },
+
+  methods: {
+    close() {
+      this.$emit('close');
+      this.isVisible = false;
+    },
+
+    // On iframe load, kill the spinner.
+    onLoaded(l) {
+      if (l.srcElement.contentWindow.location.href === 'about:blank') {
+        return;
+      }
+      this.isLoading = false;
+    },
+  },
+
+  computed: {
+    previewURL() {
+      let uri = 'about:blank';
+
+      if (this.type === 'campaign') {
+        uri = uris.previewCampaign;
+      } else if (this.type === 'template') {
+        if (this.id) {
+          uri = uris.previewTemplate;
+        } else {
+          uri = uris.previewRawTemplate;
+        }
+      }
+
+      return uri.replace(':id', this.id);
+    },
+  },
+
+  mounted() {
+    setTimeout(() => {
+      this.$refs.form.submit();
+    }, 100);
+  },
+};
+</script>

+ 183 - 0
frontend/src/components/Editor.vue

@@ -0,0 +1,183 @@
+<template>
+  <!-- Two-way Data-Binding -->
+  <section class="editor">
+    <div class="columns">
+      <div class="column is-6">
+        <b-field label="Format">
+          <div>
+            <b-radio v-model="form.radioFormat"
+              @input="onChangeFormat" :disabled="disabled" name="format"
+              native-value="richtext">Rich text</b-radio>
+            <b-radio v-model="form.radioFormat"
+              @input="onChangeFormat" :disabled="disabled" name="format"
+              native-value="html">Raw HTML</b-radio>
+          </div>
+        </b-field>
+      </div>
+      <div class="column is-6 has-text-right">
+          <b-button @click="togglePreview" type="is-primary"
+            icon-left="file-find-outline">Preview</b-button>
+      </div>
+    </div>
+
+    <!-- wsywig //-->
+    <quill-editor
+      v-if="form.format === 'richtext'"
+      v-model="form.body"
+      ref="quill"
+      :options="options"
+      :disabled="disabled"
+      placeholder="Content here"
+      @change="onEditorChange($event)"
+    />
+
+    <!-- raw html editor //-->
+    <b-input v-if="form.format === 'html'"
+      @input="onEditorChange"
+      v-model="form.body" type="textarea" />
+
+
+    <!-- campaign preview //-->
+    <campaign-preview v-if="isPreviewing"
+      @close="togglePreview"
+      type='campaign'
+      :id='id'
+      :title='title'
+      :body="form.body"></campaign-preview>
+
+    <!-- image picker -->
+    <b-modal scroll="keep" :aria-modal="true" :active.sync="isMediaVisible" :width="900">
+      <div class="modal-card content" style="width: auto">
+        <section expanded class="modal-card-body">
+          <media isModal @selected="onMediaSelect" />
+        </section>
+      </div>
+    </b-modal>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import 'quill/dist/quill.snow.css';
+import 'quill/dist/quill.core.css';
+import { quillEditor } from 'vue-quill-editor';
+// import Delta from 'quill-delta';
+import CampaignPreview from './CampaignPreview.vue';
+import Media from '../views/Media.vue';
+
+Vue.component('media', Media);
+Vue.component('campaign-preview', CampaignPreview);
+
+export default {
+  components: {
+    quillEditor,
+  },
+
+  props: {
+    id: Number,
+    title: String,
+    body: String,
+    contentType: String,
+    disabled: Boolean,
+  },
+
+  data() {
+    return {
+      isPreviewing: false,
+      isMediaVisible: false,
+      form: {
+        body: '',
+        format: this.contentType,
+
+        // Model bound to the checkboxes. This changes on click of the radio,
+        // but is reverted by the change handler if the user cancels the
+        // conversion warning. This is used to set the value of form.format
+        // that the editor uses to render content.
+        radioFormat: this.contentType,
+      },
+
+      // Quill editor options.
+      options: {
+        placeholder: 'Content here',
+        modules: {
+          toolbar: {
+            container: [
+              [{ header: [1, 2, 3, false] }],
+              ['bold', 'italic', 'underline', 'strike', 'blockquote', 'code'],
+              [{ color: [] }, { background: [] }, { size: [] }],
+              [
+                { list: 'ordered' },
+                { list: 'bullet' },
+                { indent: '-1' },
+                { indent: '+1' },
+              ],
+              [
+                { align: '' },
+                { align: 'center' },
+                { align: 'right' },
+                { align: 'justify' },
+              ],
+              ['link', 'image'],
+              ['clean', 'font'],
+            ],
+
+            handlers: {
+              image: this.toggleMedia,
+            },
+          },
+        },
+      },
+    };
+  },
+
+  methods: {
+    onChangeFormat(format) {
+      this.$utils.confirm(
+        'The content may lose some formatting. Are you sure?',
+        () => {
+          this.form.format = format;
+          this.onEditorChange();
+        },
+        () => {
+          // On cancel, undo the radio selection.
+          this.form.radioFormat = format === 'richtext' ? 'html' : 'richtext';
+        },
+      );
+    },
+
+    onEditorChange() {
+      // The parent's v-model gets { contentType, body }.
+      this.$emit('input', { contentType: this.form.format, body: this.form.body });
+    },
+
+    togglePreview() {
+      this.isPreviewing = !this.isPreviewing;
+    },
+
+    toggleMedia() {
+      this.isMediaVisible = !this.isMediaVisible;
+    },
+
+    onMediaSelect(m) {
+      this.$refs.quill.quill
+        .insertEmbed(10, 'image', m.uri);
+    },
+  },
+
+  watch: {
+    // Capture contentType and body passed from the parent as props.
+    contentType(f) {
+      this.form.format = f;
+      this.form.radioFormat = f;
+
+      // Trigger the change event so that the body and content type
+      // are propagated to the parent on first load.
+      this.onEditorChange();
+    },
+
+    body(b) {
+      this.form.body = b;
+    },
+  },
+};
+</script>

+ 111 - 0
frontend/src/components/ListSelector.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="field">
+    <b-field :label="label  + (selectedItems ? ` (${selectedItems.length})` : '')">
+      <div :class="classes">
+        <b-taglist>
+          <b-tag v-for="l in selectedItems"
+            :key="l.id"
+            :class="l.subscriptionStatus"
+            :closable="!disabled && l.subscriptionStatus !== 'unsubscribed'"
+            :data-id="l.id"
+            @close="removeList(l.id)">
+            {{ l.name }} <sup>{{ l.subscriptionStatus }}</sup>
+          </b-tag>
+        </b-taglist>
+      </div>
+    </b-field>
+
+    <b-field :message="message">
+      <b-autocomplete
+        :placeholder="placeholder"
+        clearable
+        dropdown-position="top"
+        :disabled="disabled || filteredLists.length === 0"
+        :keep-first="true"
+        :clear-on-select="true"
+        :open-on-focus="true"
+        :data="filteredLists"
+        @select="selectList"
+        field="name">
+      </b-autocomplete>
+    </b-field>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+
+export default {
+  name: 'ListSelector',
+
+  props: {
+    label: String,
+    placeholder: String,
+    message: String,
+    required: Boolean,
+    disabled: Boolean,
+    classes: {
+      type: Array,
+      default: () => [],
+    },
+    selected: {
+      type: Array,
+      default: () => [],
+    },
+    all: {
+      type: Array,
+      default: () => [],
+    },
+  },
+
+  data() {
+    return {
+      selectedItems: [],
+    };
+  },
+
+  methods: {
+    selectList(l) {
+      if (!l) {
+        return;
+      }
+      this.selectedItems.push(l);
+
+      // Propagate the items to the parent's v-model binding.
+      Vue.nextTick(() => {
+        this.$emit('input', this.selectedItems);
+      });
+    },
+
+    removeList(id) {
+      this.selectedItems = this.selectedItems.filter((l) => l.id !== id);
+
+      // Propagate the items to the parent's v-model binding.
+      Vue.nextTick(() => {
+        this.$emit('input', this.selectedItems);
+      });
+    },
+  },
+
+  computed: {
+    // Returns the list of lists to which the subscriber isn't subscribed.
+    filteredLists() {
+      // Get a map of IDs of the user subsciptions. eg: {1: true, 2: true};
+      const subIDs = this.selectedItems.reduce((obj, item) => ({ ...obj, [item.id]: true }), {});
+
+      // Filter lists from the global lists whose IDs are not in the user's
+      // subscribed ist.
+      return this.$props.all.filter((l) => !(l.id in subIDs));
+    },
+  },
+
+  watch: {
+    // This is required to update the array of lists to propagate from parent
+    // components and "react" on the selector.
+    selected() {
+      // Deep-copy.
+      this.selectedItems = JSON.parse(JSON.stringify(this.selected));
+    },
+  },
+};
+</script>

+ 22 - 126
frontend/src/constants.js

@@ -1,126 +1,22 @@
-export const DateFormat = "ddd D MMM YYYY, hh:mm A"
-
-// Data types.
-export const ModelUsers = "users"
-export const ModelSubscribers = "subscribers"
-export const ModelSubscribersByList = "subscribersByList"
-export const ModelLists = "lists"
-export const ModelMedia = "media"
-export const ModelCampaigns = "campaigns"
-export const ModelTemplates = "templates"
-
-// HTTP methods.
-export const MethodGet = "get"
-export const MethodPost = "post"
-export const MethodPut = "put"
-export const MethodDelete = "delete"
-
-// Data loading states.
-export const StatePending = "pending"
-export const StateDone = "done"
-
-// Form types.
-export const FormCreate = "create"
-export const FormEdit = "edit"
-
-// Message types.
-export const MsgSuccess = "success"
-export const MsgWarning = "warning"
-export const MsgError = "error"
-export const MsgPosition = "bottomRight"
-
-// Model specific.
-export const CampaignStatusColors = {
-  draft: "",
-  scheduled: "purple",
-  running: "blue",
-  paused: "orange",
-  finished: "green",
-  cancelled: "red"
-}
-
-export const CampaignStatusDraft = "draft"
-export const CampaignStatusScheduled = "scheduled"
-export const CampaignStatusRunning = "running"
-export const CampaignStatusPaused = "paused"
-export const CampaignStatusFinished = "finished"
-export const CampaignStatusCancelled = "cancelled"
-export const CampaignStatusRegular = "regular"
-export const CampaignStatusOptin = "optin"
-
-export const CampaignTypeRegular = "regular"
-export const CampaignTypeOptin = "optin"
-
-export const CampaignContentTypeRichtext = "richtext"
-export const CampaignContentTypeHTML = "html"
-export const CampaignContentTypePlain = "plain"
-
-export const SubscriptionStatusConfirmed = "confirmed"
-export const SubscriptionStatusUnConfirmed = "unconfirmed"
-export const SubscriptionStatusUnsubscribed = "unsubscribed"
-
-export const ListOptinSingle = "single"
-export const ListOptinDouble = "double"
-
-// API routes.
-export const Routes = {
-  GetDashboarcStats: "/api/dashboard/stats",
-  GetUsers: "/api/users",
-
-  // Lists.
-  GetLists: "/api/lists",
-  CreateList: "/api/lists",
-  UpdateList: "/api/lists/:id",
-  DeleteList: "/api/lists/:id",
-
-  // Subscribers.
-  ViewSubscribers: "/subscribers",
-  GetSubscribers: "/api/subscribers",
-  GetSubscriber: "/api/subscribers/:id",
-  GetSubscribersByList: "/api/subscribers/lists/:listID",
-  PreviewCampaign: "/api/campaigns/:id/preview",
-  CreateSubscriber: "/api/subscribers",
-  UpdateSubscriber: "/api/subscribers/:id",
-  DeleteSubscriber: "/api/subscribers/:id",
-  DeleteSubscribers: "/api/subscribers",
-  SendSubscriberOptinMail: "/api/subscribers/:id/optin",
-  BlacklistSubscriber: "/api/subscribers/:id/blacklist",
-  BlacklistSubscribers: "/api/subscribers/blacklist",
-  AddSubscriberToLists: "/api/subscribers/lists/:id",
-  AddSubscribersToLists: "/api/subscribers/lists",
-  DeleteSubscribersByQuery: "/api/subscribers/query/delete",
-  BlacklistSubscribersByQuery: "/api/subscribers/query/blacklist",
-  AddSubscribersToListsByQuery: "/api/subscribers/query/lists",
-
-  // Campaigns.
-  ViewCampaigns: "/campaigns",
-  ViewCampaign: "/campaigns/:id",
-  GetCampaignMessengers: "/api/campaigns/messengers",
-  GetCampaigns: "/api/campaigns",
-  GetCampaign: "/api/campaigns/:id",
-  GetRunningCampaignStats: "/api/campaigns/running/stats",
-  CreateCampaign: "/api/campaigns",
-  TestCampaign: "/api/campaigns/:id/test",
-  UpdateCampaign: "/api/campaigns/:id",
-  UpdateCampaignStatus: "/api/campaigns/:id/status",
-  DeleteCampaign: "/api/campaigns/:id",
-
-  // Media.
-  GetMedia: "/api/media",
-  AddMedia: "/api/media",
-  DeleteMedia: "/api/media/:id",
-
-  // Templates.
-  GetTemplates: "/api/templates",
-  PreviewTemplate: "/api/templates/:id/preview",
-  PreviewNewTemplate: "/api/templates/preview",
-  CreateTemplate: "/api/templates",
-  UpdateTemplate: "/api/templates/:id",
-  SetDefaultTemplate: "/api/templates/:id/default",
-  DeleteTemplate: "/api/templates/:id",
-
-  // Import.
-  UploadRouteImport: "/api/import/subscribers",
-  GetRouteImportStats: "/api/import/subscribers",
-  GetRouteImportLogs: "/api/import/subscribers/logs"
-}
+export const models = Object.freeze({
+  lists: 'lists',
+  subscribers: 'subscribers',
+  campaigns: 'campaigns',
+  templates: 'templates',
+  media: 'media',
+});
+
+// Ad-hoc URIs that are used outside of vuex requests.
+export const uris = Object.freeze({
+  previewCampaign: '/api/campaigns/:id/preview',
+  previewTemplate: '/api/templates/:id/preview',
+  previewRawTemplate: '/api/templates/preview',
+});
+
+// Keys used in Vuex store.
+export const storeKeys = Object.freeze({
+  models: 'models',
+  isLoading: 'isLoading',
+});
+
+export const timestamp = 'ddd D MMM YYYY, hh:mm A';

+ 0 - 391
frontend/src/index.css

@@ -1,391 +0,0 @@
-/* Disable all the ridiculous, unnecessary animations except for the spinner */
-*:not(.ant-spin-dot-spin) {
-  animation-duration: 0s;
-  transition: none !important;
-}
-
-body {
-  font-weight: 400;
-}
-
-header.header {
-  margin-bottom: 30px;
-}
-
-hr {
-  border-width: 1px 0 0 0;
-  border-style: solid;
-  border-color: #eee;
-  margin: 30px 0;
-}
-
-/* Helpers */
-.center {
-  text-align: center;
-}
-
-.right {
-  text-align: right;
-}
-
-.text-tiny {
-  font-size: 0.65em;
-}
-
-.text-small {
-  font-size: 0.85em;
-}
-
-.text-grey {
-  color: #999;
-}
-
-.hidden {
-  display: none;
-}
-
-.empty-spinner {
-  padding: 30px !important;
-}
-
-ul.no {
-  list-style-type: none;
-}
-ul.no li {
-  margin-bottom: 10px;
-}
-
-/* Layout */
-body {
-  margin: 0;
-  padding: 0;
-  font-family: sans-serif;
-}
-
-.content-body {
-  min-height: 90vh;
-}
-
-section.content {
-  padding: 24px;
-  background: #fff;
-}
-
-.logo {
-  padding: 30px;
-}
-  .logo a {
-    overflow: hidden;
-    display: inline-block;
-  }
-  .logo img {
-    width: auto;
-    height: 22px;
-  }
-
-.ant-layout-sider.ant-layout-sider-collapsed .logo a {
-  width: 20px;
-}
-
-.ant-card-head-title {
-  font-size: .85em !important;
-  color: #999 !important;
-}
-
-.broken {
-  margin: 100px;
-}
-
-.hidden {
-  display: none;
-}
-
-/* Form */
-.list-form .html {
-  background: #fafafa;
-  padding: 30px;
-  max-width: 100%;
-  overflow-y: auto;
-  max-height: 600px;
-}
-.list-form .lists label {
-  display: block;
-}
-
-
-/* Table actions */
-td .actions a {
-  display: inline-block;
-  padding: 10px;
-}
-
-td.actions {
-  text-align: right;
-}
-
-td .ant-tag {
-  margin-top: 5px;
-}
-
-/* External options */
-.table-options {
-}
-  .table-options a {
-    margin-right: 30px;
-  }
-
-/* Dashboard */
-.dashboard {
-  margin: 24px;
-}
-  .dashboard .campaign-counts .name {
-    text-transform: capitalize;
-  }
-
-/* Templates */
-.wysiwyg {
-  padding: 30px;
-}
-
-/* Subscribers */
-.subscribers table .name {
-  margin-bottom: 10px;
-}
-
-.subscriber-query {
-  margin: 0 0 15px 0;
-  padding: 30px;
-  box-shadow: 0 1px 6px #ddd;
-  min-height: 140px;
-}
-  .subscriber-query .advanced-query {
-    margin-top: 15px;
-  }
-  .subscriber-query textarea {
-    font-family: monospace;
-  }
-  .subscriber-query .actions {
-    margin-top: 15px;
-  }
-
-.subscription-status {
-  color: #999;
-}
-  .subscription-status.confirmed {
-    color: #52c41a;
-  }
-  .subscription-status.unsubscribed {
-    color: #ff7875;
-  }
-
-/* Import */
-.import .import-container {
-  margin-top: 100px;
-}
-.import .logs,
-.import .help {
-  max-width: 950px;
-  margin-top: 30px;
-}
-  .import .stats .ant-progress {
-    margin-bottom: 30px;
-  }
-.import .csv-example {
-  background: #efefef;
-  padding: 5px 10px;
-  display: inline-block;
-}
-  .import .csv-example code {
-      display: block;
-  }
-  .import .csv-example .csv-headers span {
-    font-weight: bold;
-  }
-
-/* Campaigns */
-.campaigns table tbody td {
-  vertical-align: top;
-  border-bottom-width: 3px;
-  border-bottom-color: #efefef;
-}
-  .campaigns td.status .date {
-    display: block;
-    margin-top: 5px;
-  }
-  .campaigns td.lists .name {
-    margin-right: 15px;
-  }
-  .campaigns td hr {
-    margin: 10px 0;
-  }
-
-  .campaigns td.stats .ant-row {
-    border-bottom: 1px solid #eee;
-    padding: 5px 0;
-  }
-  .campaigns td.stats .ant-row:last-child {
-    border: 0;
-  }
-  .campaigns td.stats .label {
-    font-weight: 600;
-    color: #aaa;
-  }
-  .campaigns .duration {
-    text-transform: capitalize;
-  }
-
-.campaign .messengers {
-  text-transform: capitalize;
-}
-  .campaign .content-type .actions {
-    display: inline-block;
-    margin-left: 15px;
-  }
-  .campaign .content-actions {
-    margin-top: 30px;
-  }
-
-/* gallery */
-.gallery {
-  display:flex;
-  flex-direction: row;
-  flex-flow: wrap;
-}
-  .gallery .image {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-
-    min-height: 90px;
-
-    padding: 10px;
-    border: 1px solid #eee;
-    overflow: hidden;
-    margin: 10px;
-    text-align: center;
-    position: relative;
-  }
-    .gallery .name {
-      background: rgba(255, 255, 255, 0.8);
-      position: absolute;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      padding: 3px 5px;
-      width: 100%;
-      font-size: .75em;
-
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
-    .gallery .actions {
-      position: absolute;
-      top: 10px;
-      right: 15px;
-      display: none;
-      text-align: center;
-    }
-    .gallery .actions a {
-      background: rgba(255, 255, 255, 0.9);
-      padding: 0 3px 3px 3px;
-      border-radius: 0 0 3px 3px;
-      display: inline-block;
-      margin-left: 5px;
-     }
-    .gallery .image:hover .actions {
-      display: block;
-    }
-  .gallery .image img {
-    max-width: 90px;
-    max-height: 90px;
-    display: block;
-  }
-
-/* gallery icon in the wsiwyg */
-.ql-gallery {
-  background: url('/gallery.svg');
-}
-
-/* templates */
-.templates .template-body {
-  margin-top: 30px;
-}
-  .preview-iframe-container {
-    min-height: 500px;
-  }
-  .preview-iframe-spinner {
-    position: absolute !important;
-    width: 40px;
-    height: 40px;
-    left: calc(50% - 40px);
-    top: calc(30%);
-    /* top: 15px; */
-  }
-  .preview-iframe {
-    border: 0;
-    width: 100%;
-    height: 100%;
-    min-height: 500px;
-  }
-  .preview-modal .ant-modal-footer button:first-child {
-    display: none;
-  }
-
-@media screen and (max-width: 1200px) {
-  .dashboard .ant-card {
-    margin-bottom: 20px;
-  }
-}
-
-@media screen and (max-width: 1023px) {
-	.ant-table-content {
-		overflow-x: auto;
-	}
-
-	.ant-table-thead > tr > th,
-	.ant-table-tbody > tr > td {
-		white-space: nowrap;
-	}
-}
-
-@media screen and (max-width: 768px) {
-	.ant-modal {
-		top: 0 !important;
-	}
-
-	hr {
-		margin: 20px 0;
-	}
-
-	.subscriber-query {
-		padding: 20px
-	}
-
-	.header-action-break {
-		text-align: left;
-	}
-
-	.subscribers.content .slc-subs-section .table-options {
-		margin-top: 20px;
-		padding-top: 20px;
-		border-top: 1px solid #f4f4f4;
-	}
-
-	.subscribers.content .slc-subs-actions a {
-		display: block;
-		margin-bottom: 5px;
-	}
-
-	.ant-modal.subscriber-modal .subscriber-export {
-		margin-top: 10px;
-	}
-
-	.ant-modal.subscriber-modal .subscriber-name {
-		display: block;
-	}
-
-	.dashboard {
-		margin: 24px 12px;
-	}
-}

+ 0 - 7
frontend/src/index.js

@@ -1,7 +0,0 @@
-import React from "react"
-import ReactDOM from "react-dom"
-
-import "./index.css"
-import App from "./App.js"
-
-ReactDOM.render(<App />, document.getElementById("root"))

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 2
frontend/src/logo.svg


+ 21 - 0
frontend/src/main.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import Buefy from 'buefy';
+
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import * as api from './api';
+import utils from './utils';
+
+Vue.use(Buefy, {});
+Vue.config.productionTip = false;
+
+// Custom global elements.
+Vue.prototype.$api = api;
+Vue.prototype.$utils = utils;
+
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app');

+ 0 - 117
frontend/src/registerServiceWorker.js

@@ -1,117 +0,0 @@
-// In production, we register a service worker to serve assets from local cache.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on the "N+1" visit to a page, since previously
-// cached resources are updated in the background.
-
-// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
-// This link also includes instructions on opting out of this behavior.
-
-const isLocalhost = Boolean(
-  window.location.hostname === 'localhost' ||
-    // [::1] is the IPv6 localhost address.
-    window.location.hostname === '[::1]' ||
-    // 127.0.0.1/8 is considered localhost for IPv4.
-    window.location.hostname.match(
-      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
-    )
-);
-
-export default function register() {
-  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
-    // The URL constructor is available in all browsers that support SW.
-    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
-    if (publicUrl.origin !== window.location.origin) {
-      // Our service worker won't work if PUBLIC_URL is on a different origin
-      // from what our page is served on. This might happen if a CDN is used to
-      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
-      return;
-    }
-
-    window.addEventListener('load', () => {
-      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
-      if (isLocalhost) {
-        // This is running on localhost. Lets check if a service worker still exists or not.
-        checkValidServiceWorker(swUrl);
-
-        // Add some additional logging to localhost, pointing developers to the
-        // service worker/PWA documentation.
-        navigator.serviceWorker.ready.then(() => {
-          console.log(
-            'This web app is being served cache-first by a service ' +
-              'worker. To learn more, visit https://goo.gl/SC7cgQ'
-          );
-        });
-      } else {
-        // Is not local host. Just register service worker
-        registerValidSW(swUrl);
-      }
-    });
-  }
-}
-
-function registerValidSW(swUrl) {
-  navigator.serviceWorker
-    .register(swUrl)
-    .then(registration => {
-      registration.onupdatefound = () => {
-        const installingWorker = registration.installing;
-        installingWorker.onstatechange = () => {
-          if (installingWorker.state === 'installed') {
-            if (navigator.serviceWorker.controller) {
-              // At this point, the old content will have been purged and
-              // the fresh content will have been added to the cache.
-              // It's the perfect time to display a "New content is
-              // available; please refresh." message in your web app.
-              console.log('New content is available; please refresh.');
-            } else {
-              // At this point, everything has been precached.
-              // It's the perfect time to display a
-              // "Content is cached for offline use." message.
-              console.log('Content is cached for offline use.');
-            }
-          }
-        };
-      };
-    })
-    .catch(error => {
-      console.error('Error during service worker registration:', error);
-    });
-}
-
-function checkValidServiceWorker(swUrl) {
-  // Check if the service worker can be found. If it can't reload the page.
-  fetch(swUrl)
-    .then(response => {
-      // Ensure service worker exists, and that we really are getting a JS file.
-      if (
-        response.status === 404 ||
-        response.headers.get('content-type').indexOf('javascript') === -1
-      ) {
-        // No service worker found. Probably a different app. Reload the page.
-        navigator.serviceWorker.ready.then(registration => {
-          registration.unregister().then(() => {
-            window.location.reload();
-          });
-        });
-      } else {
-        // Service worker found. Proceed as normal.
-        registerValidSW(swUrl);
-      }
-    })
-    .catch(() => {
-      console.log(
-        'No internet connection found. App is running in offline mode.'
-      );
-    });
-}
-
-export function unregister() {
-  if ('serviceWorker' in navigator) {
-    navigator.serviceWorker.ready.then(registration => {
-      registration.unregister();
-    });
-  }
-}

+ 95 - 0
frontend/src/router/index.js

@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+Vue.use(VueRouter);
+
+// The meta.group param is used in App.vue to expand menu group by name.
+const routes = [
+  {
+    path: '/',
+    name: 'dashboard',
+    meta: { title: 'Dashboard' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Dashboard.vue'),
+  },
+  {
+    path: '/lists',
+    name: 'lists',
+    meta: { title: 'Lists', group: 'lists' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Lists.vue'),
+  },
+  {
+    path: '/lists/forms',
+    name: 'forms',
+    meta: { title: 'Forms', group: 'lists' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Forms.vue'),
+  },
+  {
+    path: '/subscribers',
+    name: 'subscribers',
+    meta: { title: 'Subscribers', group: 'subscribers' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
+  },
+  {
+    path: '/subscribers/import',
+    name: 'import',
+    meta: { title: 'Import subscribers', group: 'subscribers' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Import.vue'),
+  },
+  {
+    path: '/subscribers/lists/:listID',
+    name: 'subscribers_list',
+    meta: { title: 'Subscribers', group: 'subscribers' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
+  },
+  {
+    path: '/subscribers/:id',
+    name: 'subscriber',
+    meta: { title: 'Subscribers', group: 'subscribers' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
+  },
+  {
+    path: '/campaigns',
+    name: 'campaigns',
+    meta: { title: 'Campaigns', group: 'campaigns' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Campaigns.vue'),
+  },
+  {
+    path: '/campaigns/media',
+    name: 'media',
+    meta: { title: 'Media', group: 'campaigns' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Media.vue'),
+  },
+  {
+    path: '/campaigns/templates',
+    name: 'templates',
+    meta: { title: 'Templates', group: 'campaigns' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Templates.vue'),
+  },
+  {
+    path: '/campaigns/:id',
+    name: 'campaign',
+    meta: { title: 'Campaign', group: 'campaigns' },
+    component: () => import(/* webpackChunkName: "main" */ '../views/Campaign.vue'),
+  },
+];
+
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.BASE_URL,
+  routes,
+
+  scrollBehavior(to) {
+    if (to.hash) {
+      return { selector: to.hash };
+    }
+    return { x: 0, y: 0 };
+  },
+});
+
+router.afterEach((to) => {
+  Vue.nextTick(() => {
+    document.title = to.meta.title;
+  });
+});
+
+export default router;

+ 0 - 1
frontend/src/static/gallery.svg

@@ -1 +0,0 @@
-<svg viewbox="0 0 18 18"><rect class="ql-stroke" height="10" width="12" x="3" y="4"></rect><circle class="ql-fill" cx="6" cy="7" r="1"></circle><polyline class="ql-even ql-fill" points="5 12 5 11 7 9 8 10 11 7 13 9 13 12 5 12"></polyline></svg>

+ 48 - 0
frontend/src/store/index.js

@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { models } from '../constants';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: {
+    // Data from API responses for different models, eg: lists, campaigns.
+    // The API responses are stored in this map as-is. This is invoked by
+    // API requests in `http`. This initialises lists: {}, campaigns: {}
+    // etc. on state.
+    ...Object.keys(models).reduce((obj, cur) => ({ ...obj, [cur]: [] }), {}),
+
+    // Map of loading status (true, false) indicators for different model keys
+    // like lists, campaigns etc. loading: {lists: true, campaigns: true ...}.
+    // The Axios API global request interceptor marks a model as loading=true
+    // and the response interceptor marks it as false. The model keys are being
+    // pre-initialised here to fix "reactivity" issues on first loads.
+    loading: Object.keys(models).reduce((obj, cur) => ({ ...obj, [cur]: false }), {}),
+  },
+
+  mutations: {
+    // Set data from API responses. `model` is 'lists', 'campaigns' etc.
+    setModelResponse(state, { model, data }) {
+      state[model] = data;
+    },
+
+    // Set the loading status for a model globally. When a request starts,
+    // status is set to true which is used by the UI to show loaders and block
+    // forms. When a response is received, the status is set to false. This is
+    // invoked by API requests in `http`.
+    setLoading(state, { model, status }) {
+      state.loading[model] = status;
+    },
+  },
+
+  getters: {
+    [models.lists]: (state) => state[models.lists],
+    [models.subscribers]: (state) => state[models.subscribers],
+    [models.campaigns]: (state) => state[models.campaigns],
+    [models.media]: (state) => state[models.media],
+    [models.templates]: (state) => state[models.templates],
+  },
+
+  modules: {
+  },
+});

+ 75 - 65
frontend/src/utils.js

@@ -1,82 +1,92 @@
-import React from "react"
-import ReactDOM from "react-dom"
+import {
+  ToastProgrammatic as Toast,
+  DialogProgrammatic as Dialog,
+} from 'buefy';
 
 
-import { Alert } from "antd"
+const reEmail = /(.+?)@(.+?)/ig;
 
 
-class Utils {
-  static months = [
-    "Jan",
-    "Feb",
-    "Mar",
-    "Apr",
-    "May",
-    "Jun",
-    "Jul",
-    "Aug",
-    "Sep",
-    "Oct",
-    "Nov",
-    "Dec"
-  ]
-  static days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
+export default class utils {
+  static months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
+    'Sep', 'Oct', 'Nov', 'Dec'];
 
 
-  // Converts the ISO date format to a simpler form.
-  static DateString = (stamp, showTime) => {
+  static days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+
+  // Parses an ISO timestamp to a simpler form.
+  static niceDate = (stamp, showTime) => {
     if (!stamp) {
     if (!stamp) {
-      return ""
+      return '';
     }
     }
 
 
-    let d = new Date(stamp)
-    let out =
-      Utils.days[d.getDay()] +
-      ", " +
-      d.getDate() +
-      " " +
-      Utils.months[d.getMonth()] +
-      " " +
-      d.getFullYear()
-
+    const d = new Date(stamp);
+    let out = `${utils.days[d.getDay()]}, ${d.getDate()}`;
+    out += ` ${utils.months[d.getMonth()]} ${d.getFullYear()}`;
     if (showTime) {
     if (showTime) {
-      out += " " + d.getHours() + ":" + d.getMinutes()
+      out += ` ${d.getHours()}:${d.getMinutes()}`;
     }
     }
 
 
-    return out
-  }
+    return out;
+  };
 
 
-  // HttpError takes an axios error and returns an error dict after some sanity checks.
-  static HttpError = err => {
-    if (!err.response) {
-      return err
-    }
+  // Simple, naive, e-mail address check.
+  static validateEmail = (e) => e.match(reEmail);
+
+  static niceNumber = (n) => {
+    let pfx = '';
+    let div = 1;
 
 
-    if (!err.response.data || !err.response.data.message) {
-      return {
-        message: err.message + " - " + err.response.request.responseURL,
-        data: {}
-      }
+    if (n >= 1.0e+9) {
+      pfx = 'b';
+      div = 1.0e+9;
+    } else if (n >= 1.0e+6) {
+      pfx = 'm';
+      div = 1.0e+6;
+    } else if (n >= 1.0e+4) {
+      pfx = 'k';
+      div = 1.0e+3;
+    } else {
+      return n;
     }
     }
 
 
-    return {
-      message: err.response.data.message,
-      data: err.response.data.data
+    // Whole number without decimals.
+    const out = (n / div);
+    if (Math.floor(out) === n) {
+      return out + pfx;
     }
     }
-  }
 
 
-  // Shows a flash message.
-  static Alert = (msg, msgType) => {
-    document.getElementById("alert-container").classList.add("visible")
-    ReactDOM.render(
-      <Alert message={msg} type={msgType} showIcon />,
-      document.getElementById("alert-container")
-    )
+    return out.toFixed(2) + pfx;
   }
   }
-  static ModalAlert = (msg, msgType) => {
-    document.getElementById("modal-alert-container").classList.add("visible")
-    ReactDOM.render(
-      <Alert message={msg} type={msgType} showIcon />,
-      document.getElementById("modal-alert-container")
-    )
-  }
-}
 
 
-export default Utils
+  // UI shortcuts.
+  static confirm = (msg, onConfirm, onCancel) => {
+    Dialog.confirm({
+      scroll: 'keep',
+      message: !msg ? 'Are you sure?' : msg,
+      onConfirm,
+      onCancel,
+    });
+  };
+
+  static prompt = (msg, inputAttrs, onConfirm, onCancel) => {
+    Dialog.prompt({
+      scroll: 'keep',
+      message: msg,
+      confirmText: 'OK',
+      inputAttrs: {
+        type: 'string',
+        maxlength: 200,
+        ...inputAttrs,
+      },
+      trapFocus: true,
+      onConfirm,
+      onCancel,
+    });
+  };
+
+  static toast = (msg, typ) => {
+    Toast.open({
+      message: msg,
+      type: !typ ? 'is-success' : typ,
+      queue: false,
+    });
+  };
+}

+ 5 - 0
frontend/src/views/About.vue

@@ -0,0 +1,5 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>

+ 366 - 0
frontend/src/views/Campaign.vue

@@ -0,0 +1,366 @@
+<template>
+  <section class="campaign">
+    <header class="columns">
+      <div class="column is-8">
+        <p v-if="isEditing" class="tags">
+          <b-tag v-if="isEditing" :class="data.status">{{ data.status }}</b-tag>
+          <b-tag v-if="data.type === 'optin'" :class="data.type">{{ data.type }}</b-tag>
+          <span v-if="isEditing" class="has-text-grey-light is-size-7">
+            ID: {{ data.id }} / UUID: {{ data.uuid }}
+          </span>
+        </p>
+        <h4 v-if="isEditing" class="title is-4">{{ data.name }}</h4>
+        <h4 v-else class="title is-4">New campaign</h4>
+      </div>
+
+      <div class="column">
+        <div class="buttons" v-if="isEditing && canEdit">
+          <b-button @click="onSubmit" :loading="loading.campaigns"
+            type="is-primary" icon-left="content-save-outline">Save changes</b-button>
+
+          <b-button v-if="canStart" @click="startCampaign" :loading="loading.campaigns"
+            type="is-primary" icon-left="rocket-launch-outline">
+              Start campaign
+          </b-button>
+          <b-button v-if="canSchedule" @click="startCampaign" :loading="loading.campaigns"
+            type="is-primary" icon-left="clock-start">
+              Schedule campaign
+          </b-button>
+        </div>
+      </div>
+    </header>
+
+    <b-loading :active="loading.campaigns"></b-loading>
+
+    <b-tabs type="is-boxed" :animated="false" v-model="activeTab">
+      <b-tab-item label="Campaign" icon="rocket-launch-outline">
+        <section class="wrap">
+          <div class="columns">
+            <div class="column is-7">
+              <form @submit.prevent="onSubmit">
+                <b-field label="Name">
+                  <b-input :maxlength="200" :ref="'focus'" v-model="form.name" :disabled="!canEdit"
+                    placeholder="Name" required></b-input>
+                </b-field>
+
+                <b-field label="Subject">
+                  <b-input :maxlength="200" v-model="form.subject" :disabled="!canEdit"
+                    placeholder="Subject" required></b-input>
+                </b-field>
+
+                <b-field label="From address">
+                  <b-input :maxlength="200" v-model="form.fromEmail" :disabled="!canEdit"
+                    placeholder="Your Name <noreply@yoursite.com>" required></b-input>
+                </b-field>
+
+                <list-selector
+                  v-model="form.lists"
+                  :selected="form.lists"
+                  :all="lists.results"
+                  :disabled="!canEdit"
+                  label="Lists"
+                  placeholder="Lists to send to"
+                ></list-selector>
+
+                <b-field label="Template">
+                  <b-select placeholder="Template" v-model="form.templateId"
+                    :disabled="!canEdit" required>
+                    <option v-for="t in templates" :value="t.id" :key="t.id">{{ t.name }}</option>
+                  </b-select>
+                </b-field>
+
+                <b-field label="Tags">
+                  <b-taginput v-model="form.tags" :disabled="!canEdit"
+                    ellipsis icon="tag" placeholder="Tags"></b-taginput>
+                </b-field>
+                <hr />
+
+                <b-field label="Send later?">
+                    <b-switch v-model="form.sendLater" :disabled="!canEdit"></b-switch>
+                </b-field>
+
+                <b-field v-if="form.sendLater" label="Send at">
+                  <b-datetimepicker
+                    v-model="form.sendAtDate"
+                    :disabled="!canEdit"
+                    placeholder="Date and time"
+                    icon="calendar-clock"
+                    :timepicker="{ hourFormat: '24' }"
+                    :datetime-formatter="formatDateTime"
+                    horizontal-time-picker>
+                  </b-datetimepicker>
+                </b-field>
+                <hr />
+
+                <b-field v-if="isNew">
+                  <b-button native-type="submit" type="is-primary"
+                    :loading="loading.campaigns">Continue</b-button>
+                </b-field>
+              </form>
+            </div>
+            <div class="column is-4 is-offset-1">
+              <br />
+              <div class="box">
+                <h3 class="title is-size-6">Send test message</h3>
+                  <b-field message="Hit Enter after typing an address to add multiple recipients.
+                      The addresses must belong to existing subscribers.">
+                    <b-taginput  v-model="form.testEmails"
+                      :before-adding="$utils.validateEmail" :disabled="this.isNew"
+                      ellipsis icon="email-outline" placeholder="E-mails"></b-taginput>
+                  </b-field>
+                  <b-field>
+                    <b-button @click="sendTest" :loading="loading.campaigns" :disabled="this.isNew"
+                      type="is-primary" icon-left="email-outline">Send</b-button>
+                  </b-field>
+              </div>
+            </div>
+          </div>
+        </section>
+      </b-tab-item><!-- campaign -->
+
+      <b-tab-item label="Content" icon="text" :disabled="isNew">
+        <section class="wrap">
+          <editor
+            v-model="form.content"
+            :id="data.id"
+            :title="data.name"
+            :contentType="data.contentType"
+            :body="data.body"
+            :disabled="!canEdit"
+          />
+        </section>
+      </b-tab-item><!-- content -->
+    </b-tabs>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import dayjs from 'dayjs';
+import ListSelector from '../components/ListSelector.vue';
+import Editor from '../components/Editor.vue';
+
+Vue.component('list-selector', ListSelector);
+Vue.component('editor', Editor);
+
+export default Vue.extend({
+  name: 'Campaign',
+
+  data() {
+    return {
+      isNew: false,
+      isEditing: false,
+      activeTab: 0,
+
+      data: {},
+
+      // Binds form input values.
+      form: {
+        name: '',
+        subject: '',
+        fromEmail: '',
+        templateId: 0,
+        lists: [],
+        tags: [],
+        sendAt: null,
+        content: { contentType: 'richtext', body: '' },
+
+        // Parsed Date() version of send_at from the API.
+        sendAtDate: null,
+        sendLater: false,
+
+        testEmails: [],
+      },
+    };
+  },
+
+  methods: {
+    formatDateTime(s) {
+      return dayjs(s).format('YYYY-MM-DD HH:mm');
+    },
+
+    getCampaign(id) {
+      this.$api.getCampaign(id).then((r) => {
+        this.data = r.data;
+        this.form = { ...this.form, ...r.data };
+
+        if (r.data.sendAt !== null) {
+          this.form.sendLater = true;
+          this.form.sendAtDate = dayjs(r.data.sendAt).toDate();
+        }
+      });
+    },
+
+    sendTest() {
+      const data = {
+        id: this.data.id,
+        name: this.form.name,
+        subject: this.form.subject,
+        lists: this.form.lists.map((l) => l.id),
+        from_email: this.form.fromEmail,
+        content_type: 'richtext',
+        messenger: 'email',
+        type: 'regular',
+        tags: this.form.tags,
+        template_id: this.form.templateId,
+        body: this.form.body,
+        subscribers: this.form.testEmails,
+      };
+
+      this.$api.testCampaign(data).then(() => {
+        this.$utils.toast('Test message sent');
+      });
+      return false;
+    },
+
+    onSubmit() {
+      if (this.isNew) {
+        this.createCampaign();
+      } else {
+        this.updateCampaign();
+      }
+    },
+
+    createCampaign() {
+      const data = {
+        name: this.form.name,
+        subject: this.form.subject,
+        lists: this.form.lists.map((l) => l.id),
+        from_email: this.form.fromEmail,
+        content_type: 'richtext',
+        messenger: 'email',
+        type: 'regular',
+        tags: this.form.tags,
+        template_id: this.form.templateId,
+        // body: this.form.body,
+      };
+
+      this.$api.createCampaign(data).then((r) => {
+        this.$router.push({ name: 'campaign', params: { id: r.data.id } });
+
+        this.data = r.data;
+        this.isEditing = true;
+        this.isNew = false;
+        this.activeTab = 1;
+      });
+      return false;
+    },
+
+    async updateCampaign(typ) {
+      const data = {
+        name: this.form.name,
+        subject: this.form.subject,
+        lists: this.form.lists.map((l) => l.id),
+        from_email: this.form.fromEmail,
+        messenger: 'email',
+        type: 'regular',
+        tags: this.form.tags,
+        send_later: this.form.sendLater,
+        send_at: this.form.sendLater ? this.form.sendAtDate : null,
+        template_id: this.form.templateId,
+        content_type: this.form.content.contentType,
+        body: this.form.content.body,
+      };
+
+      let typMsg = 'updated';
+      if (typ === 'start') {
+        typMsg = 'started';
+      }
+
+      // This promise is used by startCampaign to first save before starting.
+      return new Promise((resolve) => {
+        this.$api.updateCampaign(this.data.id, data).then((resp) => {
+          this.data = resp.data;
+          this.$buefy.toast.open({
+            message: `'${resp.data.name}' ${typMsg}`,
+            type: 'is-success',
+            queue: false,
+          });
+          resolve();
+        });
+      });
+    },
+
+    // Starts or schedule a campaign.
+    startCampaign() {
+      let status = '';
+      if (this.canStart) {
+        status = 'running';
+      } else if (this.canSchedule) {
+        status = 'scheduled';
+      } else {
+        return;
+      }
+
+      this.$utils.confirm(null,
+        () => {
+          // First save the campaign.
+          this.updateCampaign().then(() => {
+            // Then start/schedule it.
+            this.$api.changeCampaignStatus(this.data.id, status).then(() => {
+              this.$router.push({ name: 'campaigns' });
+            });
+          });
+        });
+    },
+  },
+
+  computed: {
+    ...mapState(['lists', 'templates', 'loading']),
+
+    canEdit() {
+      return this.isNew
+        || this.data.status === 'draft' || this.data.status === 'scheduled';
+    },
+
+    canSchedule() {
+      return this.data.status === 'draft' && this.data.sendAt;
+    },
+
+    canStart() {
+      return this.data.status === 'draft' && !this.data.sendAt;
+    },
+  },
+
+  mounted() {
+    const { id } = this.$route.params;
+
+
+    // New campaign.
+    if (id === 'new') {
+      this.isNew = true;
+    } else {
+      const intID = parseInt(id, 10);
+      if (intID <= 0 || Number.isNaN(intID)) {
+        this.$buefy.toast.open({
+          message: 'Invalid campaign',
+          type: 'is-danger',
+          queue: false,
+        });
+        return;
+      }
+
+      this.isEditing = true;
+    }
+
+    // Get templates list.
+    this.$api.getTemplates().then((r) => {
+      if (r.data.length > 0) {
+        if (!this.form.templateId) {
+          this.form.templateId = r.data.find((i) => i.isDefault === true).id;
+        }
+      }
+    });
+
+    // Fetch campaign.
+    if (this.isEditing) {
+      this.getCampaign(id);
+    }
+
+    this.$nextTick(() => {
+      this.$refs.focus.focus();
+    });
+  },
+});
+</script>

+ 368 - 0
frontend/src/views/Campaigns.vue

@@ -0,0 +1,368 @@
+<template>
+  <section class="campaigns">
+    <header class="columns">
+      <div class="column is-two-thirds">
+        <h1 class="title is-4">Campaigns
+          <span v-if="campaigns.total > 0">({{ campaigns.total }})</span>
+        </h1>
+      </div>
+      <div class="column has-text-right">
+        <b-button :to="{name: 'campaign', params:{id: 'new'}}" tag="router-link"
+          type="is-primary" icon-left="plus">New</b-button>
+      </div>
+    </header>
+
+    <b-table
+      :data="campaigns.results"
+      :loading="loading.campaigns"
+      :row-class="highlightedRow"
+      paginated backend-pagination pagination-position="both" @page-change="onPageChange"
+      :current-page="queryParams.page" :per-page="campaigns.perPage" :total="campaigns.total"
+      hoverable>
+        <template slot-scope="props">
+            <b-table-column class="status" field="status" label="Status"
+              width="10%" :id="props.row.id">
+              <div>
+                <p>
+                  <router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
+                    <b-tag :class="props.row.status">{{ props.row.status }}</b-tag>
+                    <span class="spinner is-tiny" v-if="isRunning(props.row.id)">
+                      <b-loading :is-full-page="false" active />
+                    </span>
+                  </router-link>
+                </p>
+                <p v-if="isSheduled(props.row)">
+                  <b-tooltip label="Scheduled" type="is-dark">
+                    <span class="is-size-7 has-text-grey scheduled">
+                      <b-icon icon="alarm" size="is-small" />
+                      {{ $utils.niceDate(props.row.sendAt, true) }}
+                    </span>
+                  </b-tooltip>
+                </p>
+              </div>
+            </b-table-column>
+            <b-table-column field="name" label="Name" sortable width="25%">
+              <div>
+                <p>
+                  <router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
+                    {{ props.row.name }}</router-link>
+                </p>
+                <p class="is-size-7 has-text-grey">{{ props.row.subject }}</p>
+                <b-taglist>
+                    <b-tag v-for="t in props.row.tags" :key="t">{{ t }}</b-tag>
+                </b-taglist>
+              </div>
+            </b-table-column>
+            <b-table-column class="lists" field="lists" label="Lists" width="15%">
+              <ul class="no">
+                <li v-for="l in props.row.lists" :key="l.id">
+                  <router-link :to="{name: 'subscribers_list', params: { listID: l.id }}">
+                    {{ l.name }}
+                  </router-link>
+                </li>
+              </ul>
+            </b-table-column>
+            <b-table-column field="updatedAt" label="Timestamps" width="19%" sortable>
+              <div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
+                <p>
+                  <label>Created</label>
+                  {{ $utils.niceDate(props.row.createdAt, true) }}
+                </p>
+                <p v-if="stats.startedAt">
+                  <label>Started</label>
+                  {{ $utils.niceDate(stats.startedAt, true) }}
+                </p>
+                <p v-if="isDone(props.row)">
+                  <label>Ended</label>
+                  {{ $utils.niceDate(stats.updatedAt, true) }}
+                </p>
+                <p v-if="stats.startedAt && stats.updatedAt"
+                  class="is-capitalized" title="Duration">
+                  <label><b-icon icon="alarm" size="is-small" /></label>
+                  {{ duration(stats.startedAt, stats.updatedAt) }}
+                </p>
+              </div>
+            </b-table-column>
+
+            <b-table-column :class="props.row.status" label="Stats" width="18%">
+              <div class="fields stats" :set="stats = getCampaignStats(props.row)">
+                <p>
+                  <label>Views</label>
+                  {{ props.row.views }}
+                </p>
+                <p>
+                  <label>Clicks</label>
+                  {{ props.row.clicks }}
+                </p>
+                <p>
+                  <label>Sent</label>
+                  {{ stats.sent }} / {{ stats.toSend }}
+                </p>
+                <p title="Speed" v-if="stats.rate">
+                  <label><b-icon icon="speedometer" size="is-small"></b-icon></label>
+                  <span class="send-rate">
+                    {{ stats.rate }} / min
+                  </span>
+                </p>
+                <p v-if="isRunning(props.row.id)">
+                  <label>Progress
+                    <span class="spinner is-tiny">
+                      <b-loading :is-full-page="false" active />
+                    </span>
+                  </label>
+                  <b-progress :value="stats.sent / stats.toSend * 100" size="is-small" />
+                </p>
+              </div>
+            </b-table-column>
+
+            <b-table-column class="actions" width="13%" align="right">
+              <a href="" v-if="canStart(props.row)"
+                @click.prevent="$utils.confirm(null,
+                  () => changeCampaignStatus(props.row, 'running'))">
+                <b-tooltip label="Start" type="is-dark">
+                  <b-icon icon="rocket-launch-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" v-if="canPause(props.row)"
+                @click.prevent="$utils.confirm(null,
+                  () => changeCampaignStatus(props.row, 'paused'))">
+                <b-tooltip label="Pause" type="is-dark">
+                  <b-icon icon="pause-circle-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" v-if="canResume(props.row)"
+                @click.prevent="$utils.confirm(null,
+                  () => changeCampaignStatus(props.row, 'running'))">
+                <b-tooltip label="Send" type="is-dark">
+                  <b-icon icon="rocket-launch-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" v-if="canSchedule(props.row)"
+                @click.prevent="$utils.confirm(`This campaign will start automatically at the
+                    scheduled date and time. Schedule now?`,
+                      () => changeCampaignStatus(props.row, 'scheduled'))">
+                <b-tooltip label="Schedule" type="is-dark">
+                  <b-icon icon="clock-start" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" @click.prevent="previewCampaign(props.row)">
+                <b-tooltip label="Preview" type="is-dark">
+                  <b-icon icon="file-find-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" @click.prevent="$utils.prompt(`Clone campaign`,
+                      { placeholder: 'Campaign name', value: `Copy of ${props.row.name}`},
+                      (name) => cloneCampaign(name, props.row))">
+                <b-tooltip label="Clone" type="is-dark">
+                  <b-icon icon="file-multiple-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" v-if="canCancel(props.row)"
+                @click.prevent="$utils.confirm(null,
+                  () => changeCampaignStatus(props.row, 'cancelled'))">
+                <b-tooltip label="Cancel" type="is-dark">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" v-if="canDelete(props.row)"
+                @click.prevent="$utils.confirm(`Delete '${props.row.name}'?`,
+                  () => deleteCampaign(props.row))">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+              </a>
+            </b-table-column>
+        </template>
+        <template slot="empty" v-if="!loading.lists">
+            <section class="section">
+                <div class="content has-text-grey has-text-centered">
+                    <p>
+                        <b-icon icon="plus" size="is-large" />
+                    </p>
+                    <p>Nothing here.</p>
+                </div>
+            </section>
+        </template>
+    </b-table>
+
+    <campaign-preview v-if="previewItem"
+      type='campaign'
+      :id="previewItem.id"
+      :title="previewItem.name"
+      @close="closePreview"></campaign-preview>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import CampaignPreview from '../components/CampaignPreview.vue';
+
+Vue.component('campaign-preview', CampaignPreview);
+
+dayjs.extend(relativeTime);
+
+export default Vue.extend({
+  data() {
+    return {
+      previewItem: null,
+      queryParams: {
+        page: 1,
+      },
+      pollID: null,
+      campaignStatsData: {},
+    };
+  },
+
+  methods: {
+    // Campaign statuses.
+    canStart(c) {
+      return c.status === 'draft' && !c.sendAt;
+    },
+    canSchedule(c) {
+      return c.status === 'draft' && c.sendAt;
+    },
+    canPause(c) {
+      return c.status === 'running';
+    },
+    canCancel(c) {
+      return c.status === 'running' || c.status === 'paused';
+    },
+    canResume(c) {
+      return c.status === 'paused';
+    },
+    canDelete(c) {
+      return c.status === 'draft' || c.status === 'scheduled';
+    },
+    isSheduled(c) {
+      return c.status === 'scheduled' || c.sendAt !== null;
+    },
+    isDone(c) {
+      return c.status === 'finished' || c.status === 'cancelled';
+    },
+
+    isRunning(id) {
+      if (id in this.campaignStatsData) {
+        return true;
+      }
+      return false;
+    },
+
+    highlightedRow(data) {
+      if (data.status === 'running') {
+        return ['running'];
+      }
+      return '';
+    },
+
+    duration(start, end) {
+      return dayjs(end).from(dayjs(start), true);
+    },
+
+    onPageChange(p) {
+      this.queryParams.page = p;
+      this.getCampaigns();
+    },
+
+    // Campaign actions.
+    previewCampaign(c) {
+      this.previewItem = c;
+    },
+
+    closePreview() {
+      this.previewItem = null;
+    },
+
+    getCampaigns() {
+      this.$api.getCampaigns({
+        page: this.queryParams.page,
+      });
+    },
+
+    // Stats returns the campaign object with stats (sent, toSend etc.)
+    // if there's live stats availabe for running campaigns. Otherwise,
+    // it returns the incoming campaign object that has the static stats
+    // values.
+    getCampaignStats(c) {
+      if (c.id in this.campaignStatsData) {
+        return this.campaignStatsData[c.id];
+      }
+      return c;
+    },
+
+    pollStats() {
+      // Clear any running status polls.
+      clearInterval(this.pollID);
+
+      // Poll for the status as long as the import is running.
+      this.pollID = setInterval(() => {
+        this.$api.getCampaignStats().then((r) => {
+          // Stop polling. No running campaigns.
+          if (r.data.length === 0) {
+            clearInterval(this.pollID);
+
+            // There were running campaigns and stats earlier. Clear them
+            // and refetch the campaigns list with up-to-date fields.
+            if (Object.keys(this.campaignStatsData).length > 0) {
+              this.getCampaigns();
+              this.campaignStatsData = {};
+            }
+          } else {
+            // Turn the list of campaigns [{id: 1, ...}, {id: 2, ...}] into
+            // a map indexed by the id: {1: {}, 2: {}}.
+            this.campaignStatsData = r.data.reduce((obj, cur) => ({ ...obj, [cur.id]: cur }), {});
+          }
+        }, () => {
+          clearInterval(this.pollID);
+        });
+      }, 1000);
+    },
+
+    changeCampaignStatus(c, status) {
+      this.$api.changeCampaignStatus(c.id, status).then(() => {
+        this.$utils.toast(`'${c.name}' is ${status}`);
+        this.getCampaigns();
+        this.pollStats();
+      });
+    },
+
+    cloneCampaign(name, c) {
+      const data = {
+        name,
+        subject: c.subject,
+        lists: c.lists.map((l) => l.id),
+        type: c.type,
+        from_email: c.fromEmail,
+        content_type: c.contentType,
+        messenger: c.messenger,
+        tags: c.tags,
+        template_id: c.templateId,
+        body: c.body,
+      };
+      this.$api.createCampaign(data).then((r) => {
+        this.$router.push({ name: 'campaign', params: { id: r.data.id } });
+      });
+    },
+
+    deleteCampaign(c) {
+      this.$api.deleteCampaign(c.id).then(() => {
+        this.getCampaigns();
+        this.$utils.toast(`'${c.name}' deleted`);
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['campaigns', 'loading']),
+  },
+
+  mounted() {
+    this.getCampaigns();
+    this.pollStats();
+  },
+
+  destroyed() {
+    clearInterval(this.pollID);
+  },
+});
+</script>

+ 58 - 0
frontend/src/views/Dashboard.vue

@@ -0,0 +1,58 @@
+<template>
+  <section class="dashboard content">
+    <header class="columns">
+      <div class="column is-two-thirds">
+        <h1 class="title is-5">{{ dayjs().format("ddd, DD MMM") }}</h1>
+      </div>
+      <div class="column has-text-right">
+        <b-button type="is-primary" icon-left="plus" @click="showNewForm">New</b-button>
+      </div>
+    </header>
+
+    <div class="columns counts">
+      <div class="column is-half">
+        <div class="level">
+          <div class="level-item has-text-centered">
+            <div>
+              <p class="title">0</p>
+              <p class="heading">Subscribers</p>
+            </div>
+          </div>
+          <div class="level-item has-text-centered">
+            <div>
+              <p class="title">0</p>
+              <p class="heading">Lists</p>
+            </div>
+          </div>
+          <div class="level-item has-text-centered">
+            <div>
+              <p class="title">0</p>
+              <p class="heading">Campaigns</p>
+            </div>
+          </div>
+          <div class="level-item has-text-centered">
+            <div>
+              <p class="title">0</p>
+              <p class="heading">Messages sent</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import dayjs from 'dayjs';
+
+export default Vue.extend({
+  name: 'Home',
+
+  computed: {
+    dayjs() {
+      return dayjs;
+    },
+  },
+});
+</script>

+ 75 - 0
frontend/src/views/Forms.vue

@@ -0,0 +1,75 @@
+<template>
+  <section class="forms content">
+    <h1 class="title is-4">Forms</h1>
+    <hr />
+    <div class="columns">
+      <div class="column is-4">
+        <h4>Public lists</h4>
+        <p>Select lists to add to the form.</p>
+        <ul class="no">
+          <li v-for="l in lists" :key="l.id">
+            <b-checkbox v-model="checked" :native-value="l.uuid">{{ l.name }}</b-checkbox>
+          </li>
+        </ul>
+      </div>
+      <div class="column">
+        <h4>Form HTML</h4>
+        <p>
+          Use the following HTML to show a subscription form on an external webpage.
+        </p>
+        <p>
+          The form should have the <code>email</code> field and one or more <code>l</code>
+          (list UUID) fields. The <code>name</code> field is optional.
+        </p>
+
+        <pre><!-- eslint-disable max-len -->&lt;form method=&quot;post&quot; action=&quot;http://localhost:9000/subscription/form&quot; class=&quot;listmonk-form&quot;&gt;
+    &lt;div&gt;
+        &lt;h3&gt;Subscribe&lt;/h3&gt;
+        &lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;email&quot; placeholder=&quot;E-mail&quot; /&gt;&lt;/p&gt;
+        &lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;Name (optional)&quot; /&gt;&lt;/p&gt;
+      <template v-for="l in lists"><span v-if="l.uuid in selected" :key="l.id" :set="id = l.uuid.substr(0, 5)">
+        &lt;p&gt;
+          &lt;input id=&quot;{{ id }}&quot; type=&quot;checkbox&quot; name=&quot;l&quot; value=&quot;{{ uuid }}&quot; /&gt;
+          &lt;label for=&quot;{{ id }}&quot;&gt;{{ l.name }}&lt;/label&gt;
+        &lt;/p&gt;</span></template>
+        &lt;p&gt;&lt;input type=&quot;submit&quot; value=&quot;Subscribe&quot; /&gt;&lt;/p&gt;
+    &lt;/div&gt;
+&lt;/form&gt;</pre>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+
+export default Vue.extend({
+  name: 'ListForm',
+
+  data() {
+    return {
+      checked: [],
+    };
+  },
+
+  computed: {
+    lists() {
+      if (!this.$store.state.lists.results) {
+        return [];
+      }
+      return this.$store.state.lists.results.filter((l) => l.type === 'public');
+    },
+    selected() {
+      const sel = [];
+      this.checked.forEach((uuid) => {
+        sel[uuid] = true;
+      });
+      return sel;
+    },
+  },
+
+  mounted() {
+    this.$api.getLists();
+  },
+});
+</script>

+ 298 - 0
frontend/src/views/Import.vue

@@ -0,0 +1,298 @@
+<template>
+  <section class="import">
+    <h1 class="title is-4">Import subscribers</h1>
+
+    <b-loading :active="isLoading"></b-loading>
+
+    <section v-if="isFree()" class="wrap-small">
+      <form @submit.prevent="onSubmit" class="box">
+        <div>
+          <b-field label="Mode">
+            <div>
+              <b-radio v-model="form.mode" name="mode"
+                native-value="subscribe">Subscribe</b-radio>
+              <b-radio v-model="form.mode" name="mode"
+                native-value="blacklist">Blacklist</b-radio>
+            </div>
+          </b-field>
+
+          <list-selector
+            label="Lists"
+            placeholder="Lists to subscribe to"
+            message="Lists to subscribe to."
+            v-model="form.lists"
+            :selected="form.lists"
+            :all="lists.results"
+          ></list-selector>
+          <hr />
+          <b-field label="CSV delimiter" message="Default delimiter is comma."
+            class="delimiter">
+            <b-input v-model="form.delim" name="delim"
+              placeholder="," maxlength="1" required />
+          </b-field>
+
+          <b-field label="CSV or ZIP file"
+            message="For existing subscribers, the names and attributes
+            will be overwritten with the values in the CSV.">
+            <b-upload v-model="form.file" drag-drop expanded required>
+              <div class="has-text-centered section">
+                <p>
+                  <b-icon icon="file-upload-outline" size="is-large"></b-icon>
+                </p>
+                <p>Click or drag a CSV or ZIP file here</p>
+              </div>
+            </b-upload>
+          </b-field>
+          <div class="tags" v-if="form.file">
+            <b-tag size="is-medium" closable @close="clearFile">
+              {{ form.file.name }}
+            </b-tag>
+          </div>
+          <div class="buttons">
+            <b-button native-type="submit" type="is-primary"
+              :disabled="form.lists.length === 0"
+              :loading="isProcessing">Upload</b-button>
+          </div>
+        </div>
+      </form>
+      <hr />
+
+      <div class="import-help">
+        <h5 class="title is-size-6">Instructions</h5>
+        <p>
+          Upload a CSV file or a ZIP file with a single CSV file in it to bulk
+          import subscribers. The CSV file should have the following headers
+          with the exact column names. <code>attributes</code> (optional)
+          should be a valid JSON string with double escaped quotes.
+        </p>
+        <br />
+        <blockquote className="csv-example">
+          <code className="csv-headers">
+            <span>email,</span>
+            <span>name,</span>
+            <span>attributes</span>
+          </code>
+        </blockquote>
+
+        <hr />
+
+        <h5 class="title is-size-6">Example raw CSV</h5>
+        <blockquote className="csv-example">
+          <code className="csv-headers">
+            <span>email,</span>
+            <span>name,</span>
+            <span>attributes</span>
+          </code><br />
+          <code className="csv-row">
+            <span>user1@mail.com,</span>
+            <span>"User One",</span>
+            <span>{'"{""age"": 42, ""planet"": ""Mars""}"'}</span>
+          </code><br />
+          <code className="csv-row">
+            <span>user2@mail.com,</span>
+            <span>"User Two",</span>
+            <span>
+              {'"{""age"": 24, ""job"": ""Time Traveller""}"'}
+            </span>
+          </code>
+        </blockquote>
+      </div>
+    </section><!-- upload //-->
+
+    <section v-if="isRunning() || isDone()" class="wrap status box has-text-centered">
+      <b-progress :value="progress" show-value type="is-success"></b-progress>
+      <br />
+      <p :class="['is-size-5', 'is-capitalized',
+          {'has-text-success': status.status === 'finished'},
+          {'has-text-danger': (status.status === 'failed' || status.status === 'stopped')}]">
+        {{ status.status }}</p>
+
+      <p>{{ status.imported }} / {{ status.total }} records</p>
+      <br />
+
+      <p>
+        <b-button @click="stopImport" :loading="isProcessing" icon-left="file-upload-outline"
+          type="is-primary">{{ isDone() ? 'Done' : 'Stop import' }}</b-button>
+      </p>
+      <br />
+
+      <p>
+        <b-input v-model="logs" id="import-log" class="logs"
+          type="textarea" readonly placeholder="Import log" />
+      </p>
+    </section>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import ListSelector from '../components/ListSelector.vue';
+
+Vue.component('list-selector', ListSelector);
+
+export default Vue.extend({
+  name: 'Import',
+
+  props: {
+    data: {},
+    isEditing: null,
+  },
+
+  data() {
+    return {
+      form: {
+        mode: 'subscribe',
+        delim: ',',
+        lists: [],
+        file: null,
+      },
+
+      // Initial page load still has to wait for the status API to return
+      // to either show the form or the status box.
+      isLoading: true,
+
+      isProcessing: false,
+      status: { status: '' },
+      logs: '',
+      pollID: null,
+    };
+  },
+
+  methods: {
+    clearFile() {
+      this.form.file = null;
+    },
+
+    // Returns true if we're free to do an upload.
+    isFree() {
+      if (this.status.status === 'none') {
+        return true;
+      }
+      return false;
+    },
+
+    // Returns true if an import is running.
+    isRunning() {
+      if (this.status.status === 'importing'
+        || this.status.status === 'stopping') {
+        return true;
+      }
+      return false;
+    },
+
+    isSuccessful() {
+      return this.status.status === 'finished';
+    },
+
+    isFailed() {
+      return (
+        this.status.status === 'stopped'
+        || this.status.status === 'failed'
+      );
+    },
+
+    // Returns true if an import has finished (failed or sucessful).
+    isDone() {
+      if (this.status.status === 'finished'
+        || this.status.status === 'stopped'
+        || this.status.status === 'failed'
+      ) {
+        return true;
+      }
+      return false;
+    },
+
+    pollStatus() {
+      // Clear any running status polls.
+      clearInterval(this.pollID);
+
+      // Poll for the status as long as the import is running.
+      this.pollID = setInterval(() => {
+        this.$api.getImportStatus().then((r) => {
+          this.isProcessing = false;
+          this.isLoading = false;
+          this.status = r.data;
+          this.getLogs();
+
+          if (!this.isRunning()) {
+            clearInterval(this.pollID);
+          }
+        }, () => {
+          this.isProcessing = false;
+          this.isLoading = false;
+          this.status = { status: 'none' };
+          clearInterval(this.pollID);
+        });
+        return true;
+      }, 250);
+    },
+
+    getLogs() {
+      this.$api.getImportLogs().then((r) => {
+        this.logs = r.data;
+
+        Vue.nextTick(() => {
+          // vue.$refs doesn't work as the logs textarea is rendered dynamiaclly.
+          const ref = document.getElementById('import-log');
+          if (ref) {
+            ref.scrollTop = ref.scrollHeight;
+          }
+        });
+      });
+    },
+
+    // Cancel a running import or clears a finished import.
+    stopImport() {
+      this.isProcessing = true;
+      this.$api.stopImport().then(() => {
+        this.pollStatus();
+      });
+    },
+
+    onSubmit() {
+      this.isProcessing = true;
+
+      // Prepare the upload payload.
+      const params = new FormData();
+      params.set('params', JSON.stringify({
+        mode: this.form.mode,
+        delim: this.form.delim,
+        lists: this.form.lists.map((l) => l.id),
+      }));
+      params.set('file', this.form.file);
+
+      // Make the API request.
+      this.$api.importSubscribers(params).then(() => {
+        // On file upload, show a confirmation.
+        this.$buefy.toast.open({
+          message: 'Import started',
+          type: 'is-success',
+          queue: false,
+        });
+
+        // Start polling status.
+        this.pollStatus();
+      }, () => {
+        this.isProcessing = false;
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['lists']),
+
+    // Import progress bar value.
+    progress() {
+      if (!this.status) {
+        return 0;
+      }
+      return Math.ceil((this.status.imported / this.status.total) * 100);
+    },
+  },
+
+  mounted() {
+    this.pollStatus();
+  },
+});
+</script>

+ 118 - 0
frontend/src/views/ListForm.vue

@@ -0,0 +1,118 @@
+<template>
+  <form @submit.prevent="onSubmit">
+    <div class="modal-card content" style="width: auto">
+      <header class="modal-card-head">
+        <b-tag v-if="isEditing" :class="[data.type, 'is-pulled-right']">{{ data.type }}</b-tag>
+        <h4 v-if="isEditing">{{ data.name }}</h4>
+        <h4 v-else>New list</h4>
+
+        <p v-if="isEditing" class="has-text-grey is-size-7">
+          ID: {{ data.id }} / UUID: {{ data.uuid }}
+        </p>
+      </header>
+      <section expanded class="modal-card-body">
+        <b-field label="Name">
+          <b-input :maxlength="200" :ref="'focus'" v-model="form.name"
+            placeholder="Name" required></b-input>
+        </b-field>
+
+        <b-field label="Type"
+          message="Public lists are open to the world to subscribe
+                   and their names may appear on public pages such as the subscription
+                   management page.">
+          <b-select v-model="form.type" placeholder="Type" required>
+            <option value="private">Private</option>
+            <option value="public">Public</option>
+          </b-select>
+        </b-field>
+
+        <b-field label="Opt-in"
+          message="Double opt-in sends an e-mail to the subscriber asking for
+                   confirmation. On Double opt-in lists, campaigns are only sent to
+                   confirmed subscribers.">
+          <b-select v-model="form.optin" placeholder="Opt-in type" required>
+            <option value="single">Single</option>
+            <option value="double">Double</option>
+          </b-select>
+        </b-field>
+      </section>
+      <footer class="modal-card-foot has-text-right">
+        <b-button @click="$parent.close()">Close</b-button>
+        <b-button native-type="submit" type="is-primary"
+          :loading="loading.lists">Save</b-button>
+      </footer>
+    </div>
+  </form>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+
+export default Vue.extend({
+  name: 'ListForm',
+
+  props: {
+    data: {},
+    isEditing: null,
+  },
+
+  data() {
+    return {
+      // Binds form input values.
+      form: {
+        name: '',
+        type: '',
+        optin: '',
+      },
+    };
+  },
+
+  methods: {
+    onSubmit() {
+      if (this.isEditing) {
+        this.updateList();
+        return;
+      }
+
+      this.createList();
+    },
+
+    createList() {
+      this.$api.createList(this.form).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' created`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+
+    updateList() {
+      this.$api.updateList({ id: this.data.id, ...this.form }).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' updated`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['loading']),
+  },
+
+  mounted() {
+    this.form = { ...this.$props.data };
+
+    this.$nextTick(() => {
+      this.$refs.focus.focus();
+    });
+  },
+});
+</script>

+ 160 - 0
frontend/src/views/Lists.vue

@@ -0,0 +1,160 @@
+<template>
+  <section class="lists">
+    <header class="columns">
+      <div class="column is-two-thirds">
+        <h1 class="title is-4">Lists <span v-if="lists.total > 0">({{ lists.total }})</span></h1>
+      </div>
+      <div class="column has-text-right">
+        <b-button type="is-primary" icon-left="plus" @click="showNewForm">New</b-button>
+      </div>
+    </header>
+
+    <b-table
+      :data="lists.results"
+      :loading="loading.lists"
+      hoverable
+      default-sort="createdAt">
+        <template slot-scope="props">
+            <b-table-column field="name" label="Name" sortable>
+              <router-link :to="{name: 'subscribers_list', params: { listID: props.row.id }}">
+                {{ props.row.name }}
+              </router-link>
+            </b-table-column>
+
+            <b-table-column field="type" label="Type" sortable>
+                <b-tag :class="props.row.type">{{ props.row.type }}</b-tag>
+                {{ ' ' }}
+                <b-tag>
+                  <b-icon :icon="props.row.optin === 'double' ?
+                    'account-check-outline' : 'account-off-outline'" size="is-small" />
+                  {{ ' ' }}
+                  {{ props.row.optin }}
+                </b-tag>{{ ' ' }}
+                <router-link :to="{name: 'campaign', params: {id: 'new'},
+                  query: {type: 'optin', 'list_id': props.row.id}}"
+                  v-if="props.row.optin === 'double'" class="is-size-7 send-optin">
+                  <b-tooltip label="Send opt-in campaign" type="is-dark">
+                    <b-icon icon="rocket-launch-outline" size="is-small" />
+                    Send opt-in campaign
+                  </b-tooltip>
+                </router-link>
+            </b-table-column>
+
+            <b-table-column field="subscribers" label="Subscribers" numeric sortable centered>
+                <router-link :to="`/subscribers/lists/${props.row.id}`">
+                  {{ props.row.subscriberCount }}
+                </router-link>
+            </b-table-column>
+
+            <b-table-column field="createdAt" label="Created" sortable>
+                {{ $utils.niceDate(props.row.createdAt) }}
+            </b-table-column>
+            <b-table-column field="updatedAt" label="Updated" sortable>
+                {{ $utils.niceDate(props.row.updatedAt) }}
+            </b-table-column>
+
+            <b-table-column class="actions" align="right">
+              <router-link :to="`/campaign/new?list_id=${props.row.id}`">
+                <b-tooltip label="Send campaign" type="is-dark">
+                  <b-icon icon="rocket-launch-outline" size="is-small" />
+                </b-tooltip>
+              </router-link>
+              <a href="" @click.prevent="showEditForm(props.row)">
+                <b-tooltip label="Edit" type="is-dark">
+                  <b-icon icon="pencil-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="" @click.prevent="deleteList(props.row)">
+                <b-tooltip label="Delete" type="is-dark">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+            </b-table-column>
+        </template>
+
+        <template slot="empty" v-if="!loading.lists">
+            <section class="section">
+                <div class="content has-text-grey has-text-centered">
+                    <p>
+                        <b-icon icon="plus" size="is-large" />
+                    </p>
+                    <p>Nothing here.</p>
+                </div>
+            </section>
+        </template>
+    </b-table>
+
+    <!-- Add / edit form modal -->
+    <b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="450">
+      <list-form :data="curItem" :isEditing="isEditing" @finished="formFinished"></list-form>
+    </b-modal>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import ListForm from './ListForm.vue';
+
+Vue.component('list-form', ListForm);
+
+export default Vue.extend({
+  components: {
+    ListForm,
+  },
+
+  data() {
+    return {
+      // Current list item being edited.
+      curItem: null,
+      isEditing: false,
+      isFormVisible: false,
+    };
+  },
+
+  methods: {
+    // Show the edit list form.
+    showEditForm(list) {
+      this.curItem = list;
+      this.isFormVisible = true;
+      this.isEditing = true;
+    },
+
+    // Show the new list form.
+    showNewForm() {
+      this.curItem = {};
+      this.isFormVisible = true;
+      this.isEditing = false;
+    },
+
+    formFinished() {
+      this.$api.getLists();
+    },
+
+    deleteList(list) {
+      this.$utils.confirm(
+        'Are you sure? This does not delete subscribers.',
+        () => {
+          this.$api.deleteList(list.id).then(() => {
+            this.$api.getLists();
+
+            this.$buefy.toast.open({
+              message: `'${list.name}' deleted`,
+              type: 'is-success',
+              queue: false,
+            });
+          });
+        },
+      );
+    },
+  },
+
+  computed: {
+    ...mapState(['lists', 'loading']),
+  },
+
+  mounted() {
+    this.$api.getLists();
+  },
+});
+</script>

+ 179 - 0
frontend/src/views/Media.vue

@@ -0,0 +1,179 @@
+<template>
+  <section class="media-files">
+    <h1 class="title is-4">Media
+      <span v-if="media.length > 0">({{ media.length }})</span>
+    </h1>
+
+    <b-loading :active="isProcessing || loading.media"></b-loading>
+
+    <section class="wrap-small">
+      <form @submit.prevent="onSubmit" class="box">
+        <div>
+          <b-field label="Upload image">
+            <b-upload
+              v-model="form.files"
+              drag-drop
+              multiple
+              accept=".png,.jpg,.jpeg,.gif"
+              expanded required>
+              <div class="has-text-centered section">
+                <p>
+                  <b-icon icon="file-upload-outline" size="is-large"></b-icon>
+                </p>
+                <p>Click or drag one or more images here</p>
+              </div>
+            </b-upload>
+          </b-field>
+          <div class="tags" v-if="form.files.length > 0">
+            <b-tag v-for="(f, i) in form.files" :key="i" size="is-medium"
+              closable @close="removeUploadFile(i)">
+              {{ f.name }}
+            </b-tag>
+          </div>
+          <div class="buttons">
+            <b-button native-type="submit" type="is-primary" icon-left="file-upload-outline"
+              :disabled="form.files.length === 0"
+              :loading="isProcessing">Upload</b-button>
+          </div>
+        </div>
+      </form>
+    </section>
+
+    <section class="section gallery">
+      <div v-for="group in items" :key="group.title">
+        <h3 class="title is-5">{{ group.title }}</h3>
+
+        <div class="thumbs">
+          <div v-for="m in group.items" :key="m.id" class="box thumb">
+            <a @click="(e) => onMediaSelect(m, e)" :href="m.uri" target="_blank">
+              <img :src="m.thumbUri" :title="m.filename" />
+            </a>
+            <span class="caption is-size-7" :title="m.filename">{{ m.filename }}</span>
+
+            <div class="actions has-text-right">
+              <a :href="m.uri" target="_blank">
+                  <b-icon icon="arrow-top-right" size="is-small" />
+              </a>
+              <a href="#" @click.prevent="deleteMedia(m.id)">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+              </a>
+            </div>
+          </div>
+        </div>
+        <hr />
+      </div>
+    </section>
+
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import dayjs from 'dayjs';
+
+export default Vue.extend({
+  name: 'Media',
+
+  props: {
+    isModal: Boolean,
+  },
+
+  data() {
+    return {
+      form: {
+        files: [],
+      },
+      toUpload: 0,
+      uploaded: 0,
+    };
+  },
+
+  methods: {
+    removeUploadFile(i) {
+      this.form.files.splice(i, 1);
+    },
+
+    onMediaSelect(m, e) {
+      // If the component is open in the modal mode, close the modal and
+      // fire the selection event.
+      // Otherwise, do nothing and let the image open like a normal link.
+      if (this.isModal) {
+        e.preventDefault();
+        this.$emit('selected', m);
+        this.$parent.close();
+      }
+    },
+
+    onSubmit() {
+      this.toUpload = this.form.files.length;
+
+      // Upload N files with N requests.
+      for (let i = 0; i < this.toUpload; i += 1) {
+        const params = new FormData();
+        params.set('file', this.form.files[i]);
+        this.$api.uploadMedia(params).then(() => {
+          this.onUploaded();
+        }, () => {
+          this.onUploaded();
+        });
+      }
+    },
+
+    deleteMedia(id) {
+      this.$api.deleteMedia(id).then(() => {
+        this.$api.getMedia();
+      });
+    },
+
+    onUploaded() {
+      this.uploaded += 1;
+      if (this.uploaded >= this.toUpload) {
+        this.toUpload = 0;
+        this.uploaded = 0;
+        this.form.files = [];
+
+        this.$api.getMedia();
+      }
+    },
+  },
+
+  computed: {
+    ...mapState(['media', 'loading']),
+
+    isProcessing() {
+      if (this.toUpload > 0 && this.uploaded < this.toUpload) {
+        return true;
+      }
+      return false;
+    },
+
+    // Filters the list of media items by months into:
+    // [{"title": "Jan 2020", items: [...]}, ...]
+    items() {
+      const out = [];
+      if (!this.media || !(this.media instanceof Array)) {
+        return out;
+      }
+
+      let lastStamp = '';
+      let lastIndex = 0;
+      this.media.forEach((m) => {
+        const stamp = dayjs(m.createdAt).format('MMM YYYY');
+        if (stamp !== lastStamp) {
+          out.push({ title: stamp, items: [] });
+          lastStamp = stamp;
+          lastIndex = out.length;
+        }
+
+        out[lastIndex - 1].items.push(m);
+      });
+      return out;
+    },
+  },
+
+  mounted() {
+    this.$api.getMedia();
+  },
+});
+</script>

+ 75 - 0
frontend/src/views/SubscriberBulkList.vue

@@ -0,0 +1,75 @@
+<template>
+  <form @submit.prevent="onSubmit">
+    <div class="modal-card" style="width: auto">
+      <header class="modal-card-head">
+        <h4>Manage lists</h4>
+        <p>{{ numSubscribers }} subscriber(s) selected</p>
+      </header>
+
+      <section expanded class="modal-card-body">
+        <b-field label="Action">
+          <div>
+            <b-radio v-model="form.action" name="action" native-value="add">Add</b-radio>
+            <b-radio v-model="form.action" name="action" native-value="remove">Remove</b-radio>
+            <b-radio
+              v-model="form.action"
+              name="action"
+              native-value="unsubscribe"
+            >Mark as unsubscribed</b-radio>
+          </div>
+        </b-field>
+
+        <list-selector
+          label="Target lists"
+          placeholder="Lists to apply to"
+          v-model="form.lists"
+          :selected="form.lists"
+          :all="lists.results"
+        ></list-selector>
+      </section>
+
+      <footer class="modal-card-foot has-text-right">
+        <b-button @click="$parent.close()">Close</b-button>
+        <b-button native-type="submit" type="is-primary"
+          :disabled="form.lists.length === 0">Save</b-button>
+      </footer>
+    </div>
+  </form>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import ListSelector from '../components/ListSelector.vue';
+
+Vue.component('list-selector', ListSelector);
+
+export default Vue.extend({
+  name: 'SubscriberBulkList',
+
+  props: {
+    numSubscribers: Number,
+  },
+
+  data() {
+    return {
+      // Binds form input values.
+      form: {
+        action: 'add',
+        lists: [],
+      },
+    };
+  },
+
+  methods: {
+    onSubmit() {
+      this.$emit('finished', this.form.action, this.form.lists);
+      this.$parent.close();
+    },
+  },
+
+  computed: {
+    ...mapState(['lists', 'loading']),
+  },
+});
+</script>

+ 197 - 0
frontend/src/views/SubscriberForm.vue

@@ -0,0 +1,197 @@
+<template>
+  <form @submit.prevent="onSubmit">
+    <div class="modal-card content" style="width: auto">
+      <header class="modal-card-head">
+
+        <b-tag v-if="isEditing" :class="[data.status, 'is-pulled-right']">{{ data.status }}</b-tag>
+        <h4 v-if="isEditing">{{ data.name }}</h4>
+        <h4 v-else>New subscriber</h4>
+
+        <p v-if="isEditing" class="has-text-grey is-size-7">
+          ID: {{ data.id }} / UUID: {{ data.uuid }}
+        </p>
+      </header>
+      <section expanded class="modal-card-body">
+        <b-field label="E-mail">
+          <b-input :maxlength="200" v-model="form.email" :ref="'focus'"
+            placeholder="E-mail" required></b-input>
+        </b-field>
+
+        <b-field label="Name">
+          <b-input :maxlength="200" v-model="form.name" placeholder="Name"></b-input>
+        </b-field>
+
+        <b-field label="Status" message="Blacklisted subscribers will never receive any e-mails.">
+          <b-select v-model="form.status" placeholder="Status" required>
+            <option value="enabled">Enabled</option>
+            <option value="blacklisted">Blacklisted</option>
+          </b-select>
+        </b-field>
+
+        <list-selector
+          label="Lists"
+          placeholder="Lists to subscribe to"
+          message="Lists from which subscribers have unsubscribed themselves cannot be removed."
+          v-model="form.lists"
+          :selected="form.lists"
+          :all="lists.results"
+        ></list-selector>
+
+        <b-field label="Attributes"
+          message='Attributes are defined as a JSON map, for example:
+            {"job": "developer", "location": "Mars", "has_rocket": true}.'>
+          <b-input v-model="form.strAttribs" type="textarea" />
+        </b-field>
+        <a href="https://listmonk.app/docs/concepts"
+          target="_blank" rel="noopener noreferrer" class="is-size-7">
+          Learn more <b-icon icon="link" size="is-small" />.
+        </a>
+      </section>
+      <footer class="modal-card-foot has-text-right">
+        <b-button @click="$parent.close()">Close</b-button>
+        <b-button native-type="submit" type="is-primary"
+          :loading="loading.subscribers">Save</b-button>
+      </footer>
+    </div>
+  </form>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import ListSelector from '../components/ListSelector.vue';
+
+Vue.component('list-selector', ListSelector);
+
+export default Vue.extend({
+  name: 'SubscriberForm',
+
+  props: {
+    data: {
+      type: Object,
+      default: () => {},
+    },
+    isEditing: Boolean,
+  },
+
+  data() {
+    return {
+      // Binds form input values. This is populated by subscriber props passed
+      // from the parent component in mounted().
+      form: { lists: [], strAttribs: '{}' },
+    };
+  },
+
+  methods: {
+    onSubmit() {
+      if (this.isEditing) {
+        this.updateSubscriber();
+        return;
+      }
+
+      this.createSubscriber();
+    },
+
+    createSubscriber() {
+      const attribs = this.validateAttribs(this.form.strAttribs);
+      if (!attribs) {
+        return;
+      }
+
+      const data = {
+        email: this.form.email,
+        name: this.form.name,
+        status: this.form.status,
+        attribs,
+
+        // List IDs.
+        lists: this.form.lists.map((l) => l.id),
+      };
+
+      this.$api.createSubscriber(data).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' created`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+
+    updateSubscriber() {
+      const attribs = this.validateAttribs(this.form.strAttribs);
+      if (!attribs) {
+        return;
+      }
+
+      const data = {
+        id: this.form.id,
+        email: this.form.email,
+        name: this.form.name,
+        status: this.form.status,
+        attribs,
+
+        // List IDs.
+        lists: this.form.lists.map((l) => l.id),
+      };
+
+      this.$api.updateSubscriber(data).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' updated`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+
+    validateAttribs(str) {
+      // Parse and validate attributes JSON.
+      let attribs = {};
+      try {
+        attribs = JSON.parse(str);
+      } catch (e) {
+        this.$buefy.toast.open({
+          message: `Invalid JSON in attributes: ${e.toString()}`,
+          type: 'is-danger',
+          duration: 3000,
+          queue: false,
+        });
+        return null;
+      }
+      if (attribs instanceof Array) {
+        this.$buefy.toast.open({
+          message: 'Attributes should be a map {} and not an array []',
+          type: 'is-danger',
+          duration: 3000,
+          queue: false,
+        });
+        return null;
+      }
+
+      return attribs;
+    },
+  },
+
+  computed: {
+    ...mapState(['lists', 'loading']),
+  },
+
+  mounted() {
+    if (this.$props.isEditing) {
+      this.form = {
+        ...this.$props.data,
+
+        // Deep-copy the lists array on to the form.
+        strAttribs: JSON.stringify(this.$props.data.attribs, null, 4),
+      };
+    }
+
+    this.$nextTick(() => {
+      this.$refs.focus.focus();
+    });
+  },
+});
+</script>

+ 457 - 0
frontend/src/views/Subscribers.vue

@@ -0,0 +1,457 @@
+<template>
+  <section class="subscribers">
+    <header class="columns">
+      <div class="column is-half">
+        <h1 class="title is-4">Subscribers
+          <span v-if="subscribers.total > 0">({{ subscribers.total }})</span>
+
+          <span v-if="currentList">
+            &raquo; {{ currentList.name }}
+          </span>
+        </h1>
+      </div>
+      <div class="column has-text-right">
+        <b-button type="is-primary" icon-left="plus" @click="showNewForm">New</b-button>
+      </div>
+    </header>
+
+    <section class="subscribers-controls columns">
+      <div class="column is-4">
+        <form @submit.prevent="querySubscribers">
+          <div>
+            <b-field grouped>
+              <b-input v-model="queryParams.query"
+                placeholder="E-mail or name" icon="account-search-outline" ref="query"
+                :disabled="isSearchAdvanced"></b-input>
+              <b-button native-type="submit" type="is-primary" icon-left="account-search-outline"
+                :disabled="isSearchAdvanced"></b-button>
+            </b-field>
+
+            <p>
+              <a href="#" @click.prevent="toggleAdvancedSearch">
+                <b-icon icon="cog-outline" size="is-small" /> Advanced</a>
+            </p>
+
+            <div v-if="isSearchAdvanced">
+              <b-field>
+                <b-input v-model="queryParams.fullQuery"
+                  type="textarea" ref="fullQuery"
+                  placeholder="subscribers.name LIKE '%user%' or subscribers.status='blacklisted'">
+                </b-input>
+              </b-field>
+              <b-field>
+                <span class="is-size-6 has-text-grey">
+                  Partial SQL expression to query subscriber attributes.{{ ' ' }}
+                  <a href="https://listmonk.app/docs/querying-and-segmentation"
+                    target="_blank" rel="noopener noreferrer">
+                    Learn more <b-icon icon="link" size="is-small" />.
+                  </a>
+                </span>
+              </b-field>
+
+              <div class="buttons">
+                <b-button native-type="submit" type="is-primary"
+                  icon-left="account-search-outline">Query</b-button>
+                <b-button @click.prevent="toggleAdvancedSearch" icon-left="close">Reset</b-button>
+              </div>
+            </div><!-- advanced query -->
+          </div>
+        </form>
+      </div><!-- search -->
+
+      <div class="column is-4 subscribers-bulk" v-if="bulk.checked.length > 0">
+        <div>
+          <p>
+            <span class="is-size-5 has-text-weight-semibold">
+              {{ numSelectedSubscribers }} subscriber(s) selected
+            </span>
+            <span v-if="!bulk.all && subscribers.total > subscribers.perPage">
+              &mdash; <a href="" @click.prevent="selectAllSubscribers">
+                Select all {{ subscribers.total }}</a>
+            </span>
+          </p>
+
+          <p class="actions">
+            <a href='' @click.prevent="showBulkListForm">
+              <b-icon icon="format-list-bulleted-square" size="is-small" /> Manage lists
+            </a>
+
+            <a href='' @click.prevent="deleteSubscribers">
+              <b-icon icon="trash-can-outline" size="is-small" /> Delete
+            </a>
+
+            <a href='' @click.prevent="blacklistSubscribers">
+              <b-icon icon="account-off-outline" size="is-small" /> Blacklist
+            </a>
+          </p><!-- selection actions //-->
+        </div>
+      </div>
+    </section><!-- control -->
+
+    <b-table
+      :data="subscribers.results"
+      :loading="loading.subscribers"
+      @check-all="onTableCheck" @check="onTableCheck"
+      :checked-rows.sync="bulk.checked"
+      paginated backend-pagination pagination-position="both" @page-change="onPageChange"
+      :current-page="queryParams.page" :per-page="subscribers.perPage" :total="subscribers.total"
+      hoverable
+      checkable>
+        <template slot-scope="props">
+            <b-table-column field="status" label="Status">
+              <a :href="`/subscribers/${props.row.id}`"
+                @click.prevent="showEditForm(props.row)">
+                <b-tag :class="props.row.status">{{ props.row.status }}</b-tag>
+              </a>
+            </b-table-column>
+
+            <b-table-column field="email" label="E-mail">
+              <a :href="`/subscribers/${props.row.id}`"
+                @click.prevent="showEditForm(props.row)">
+                {{ props.row.email }}
+              </a>
+              <b-taglist>
+                  <router-link :to="`/subscribers/lists/${props.row.id}`">
+                    <b-tag :class="l.subscriptionStatus" v-for="l in props.row.lists"
+                      size="is-small" :key="l.id">
+                        {{ l.name }} <sup>{{ l.subscriptionStatus }}</sup>
+                    </b-tag>
+                  </router-link>
+              </b-taglist>
+            </b-table-column>
+
+            <b-table-column field="name" label="Name">
+              <a :href="`/subscribers/${props.row.id}`"
+                @click.prevent="showEditForm(props.row)">
+                {{ props.row.name }}
+              </a>
+            </b-table-column>
+
+            <b-table-column field="lists" label="Lists" numeric centered>
+              {{ listCount(props.row.lists) }}
+            </b-table-column>
+
+            <b-table-column field="createdAt" label="Created">
+                {{ $utils.niceDate(props.row.createdAt) }}
+            </b-table-column>
+
+            <b-table-column field="updatedAt" label="Updated">
+                {{ $utils.niceDate(props.row.updatedAt) }}
+            </b-table-column>
+
+            <b-table-column class="actions" align="right">
+              <a :href="`/api/subscribers/${props.row.id}/export`">
+                <b-tooltip label="Download data" type="is-dark">
+                  <b-icon icon="cloud-download-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a :href="`/subscribers/${props.row.id}`"
+                @click.prevent="showEditForm(props.row)">
+                <b-tooltip label="Edit" type="is-dark">
+                  <b-icon icon="pencil-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href='' @click.prevent="deleteSubscriber(props.row)">
+                <b-tooltip label="Delete" type="is-dark">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+            </b-table-column>
+        </template>
+        <template slot="empty" v-if="!loading.subscribers">
+            <section class="section">
+                <div class="content has-text-grey has-text-centered">
+                    <p>
+                        <b-icon icon="plus" size="is-large" />
+                    </p>
+                    <p>No subscribers.</p>
+                </div>
+            </section>
+        </template>
+    </b-table>
+
+    <!-- Manage list modal -->
+    <b-modal scroll="keep" :aria-modal="true" :active.sync="isBulkListFormVisible" :width="450">
+      <subscriber-bulk-list :numSubscribers="this.numSelectedSubscribers"
+        @finished="bulkChangeLists" />
+    </b-modal>
+
+    <!-- Add / edit form modal -->
+    <b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="750">
+      <subscriber-form :data="curItem" :isEditing="isEditing"
+        @finished="querySubscribers"></subscriber-form>
+    </b-modal>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import SubscriberForm from './SubscriberForm.vue';
+import SubscriberBulkList from './SubscriberBulkList.vue';
+
+Vue.component('subscriber-form', SubscriberForm);
+Vue.component('subscriber-bulk-list', SubscriberBulkList);
+
+export default Vue.extend({
+  components: {
+    SubscriberForm,
+  },
+
+  data() {
+    return {
+      // Current subscriber item being edited.
+      curItem: null,
+      isSearchAdvanced: false,
+      isEditing: false,
+      isFormVisible: false,
+      isBulkListFormVisible: false,
+
+      // Table bulk row selection states.
+      bulk: {
+        checked: [],
+        all: false,
+      },
+
+      // Query params to filter the getSubscribers() API call.
+      queryParams: {
+        // Simple query field.
+        query: '',
+
+        // Advanced query filled. This value should be accessed via fullQueryExp().
+        fullQuery: '',
+
+        // ID of the list the current subscriber view is filtered by.
+        listID: null,
+        page: 1,
+      },
+    };
+  },
+
+  methods: {
+    // Count the lists from which a subscriber has not unsubscribed.
+    listCount(lists) {
+      return lists.reduce((defVal, item) => (defVal + item.status !== 'unsubscribed' ? 1 : 0), 0);
+    },
+
+    toggleAdvancedSearch() {
+      this.isSearchAdvanced = !this.isSearchAdvanced;
+
+      // Toggling to simple search.
+      if (!this.isSearchAdvanced) {
+        this.$nextTick(() => {
+          this.$refs.query.focus();
+        });
+        return;
+      }
+
+      // Toggling to advanced search.
+      this.$nextTick(() => {
+        // Turn the string in the simple query input into an SQL exprssion and
+        // show in the full query input.
+        if (this.queryParams.query !== '') {
+          this.queryParams.fullQuery = this.fullQueryExp;
+        }
+        this.$refs.fullQuery.focus();
+      });
+    },
+
+    // Mark all subscribers in the query as selected.
+    selectAllSubscribers() {
+      this.bulk.all = true;
+    },
+
+    onTableCheck() {
+      // Disable bulk.all selection if there are no rows checked in the table.
+      if (this.bulk.checked.length !== this.subscribers.total) {
+        this.bulk.all = false;
+      }
+    },
+
+    // Show the edit list form.
+    showEditForm(sub) {
+      this.curItem = sub;
+      this.isFormVisible = true;
+      this.isEditing = true;
+    },
+
+    // Show the new list form.
+    showNewForm() {
+      this.curItem = {};
+      this.isFormVisible = true;
+      this.isEditing = false;
+    },
+
+    showBulkListForm() {
+      this.isBulkListFormVisible = true;
+    },
+
+    sortSubscribers(field, order, event) {
+      console.log(field, order, event);
+    },
+
+    onPageChange(p) {
+      this.queryParams.page = p;
+      this.querySubscribers();
+    },
+
+    // Search / query subscribers.
+    querySubscribers() {
+      this.$api.getSubscribers({
+        list_id: this.queryParams.listID,
+        query: this.fullQueryExp,
+        page: this.queryParams.page,
+      }).then(() => {
+        this.bulk.checked = [];
+      });
+    },
+
+    deleteSubscriber(sub) {
+      this.$utils.confirm(
+        'Are you sure?',
+        () => {
+          this.$api.deleteSubscriber(sub.id).then(() => {
+            this.querySubscribers();
+
+            this.$buefy.toast.open({
+              message: `'${sub.name}' deleted.`,
+              type: 'is-success',
+              queue: false,
+            });
+          });
+        },
+      );
+    },
+
+    blacklistSubscribers() {
+      let fn = null;
+      if (!this.bulk.all && this.bulk.checked.length > 0) {
+        // If 'all' is not selected, blacklist subscribers by IDs.
+        fn = () => {
+          const ids = this.bulk.checked.map((s) => s.id);
+          this.$api.blacklistSubscribers({ ids })
+            .then(() => this.querySubscribers());
+        };
+      } else {
+        // 'All' is selected, blacklist by query.
+        fn = () => {
+          this.$api.blacklistSubscribersByQuery({
+            query: this.fullQueryExp,
+            list_ids: [],
+          }).then(() => this.querySubscribers());
+        };
+      }
+
+      this.$utils.confirm(
+        `Blacklist ${this.numSelectedSubscribers} subscriber(s)?`,
+        fn,
+      );
+    },
+
+    deleteSubscribers() {
+      let fn = null;
+      if (!this.bulk.all && this.bulk.checked.length > 0) {
+        // If 'all' is not selected, delete subscribers by IDs.
+        fn = () => {
+          const ids = this.bulk.checked.map((s) => s.id);
+          this.$api.deleteSubscribers({ id: ids })
+            .then(() => {
+              this.querySubscribers();
+
+              this.$buefy.toast.open({
+                message: `${this.numSelectedSubscribers} subscriber(s) deleted`,
+                type: 'is-success',
+                queue: false,
+              });
+            });
+        };
+      } else {
+        // 'All' is selected, delete by query.
+        fn = () => {
+          this.$api.deleteSubscribersByQuery({
+            query: this.fullQueryExp,
+            list_ids: [],
+          }).then(() => {
+            this.querySubscribers();
+
+            this.$buefy.toast.open({
+              message: `${this.numSelectedSubscribers} subscriber(s) deleted`,
+              type: 'is-success',
+              queue: false,
+            });
+          });
+        };
+      }
+
+      this.$utils.confirm(
+        `Delete ${this.numSelectedSubscribers} subscriber(s)?`,
+        fn,
+      );
+    },
+
+    bulkChangeLists(action, lists) {
+      const data = {
+        action,
+        target_list_ids: lists.map((l) => l.id),
+      };
+
+      let fn = null;
+      if (!this.bulk.all && this.bulk.checked.length > 0) {
+        // If 'all' is not selected, perform by IDs.
+        fn = this.$api.addSubscribersToLists;
+        data.ids = this.bulk.checked.map((s) => s.id);
+      } else {
+        // 'All' is selected, perform by query.
+        fn = this.$api.addSubscribersToListsByQuery;
+      }
+
+      fn(data).then(() => {
+        this.querySubscribers();
+        this.$buefy.toast.open({
+          message: 'List change applied',
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['subscribers', 'lists', 'loading']),
+
+    // Turns the value into the simple input field into an SQL query expression.
+    fullQueryExp() {
+      const q = this.queryParams.query.replace(/'/g, "''").trim();
+      if (!q) {
+        return '';
+      }
+      return `(name ~* '${q}' OR email ~* '${q}')`;
+    },
+
+    numSelectedSubscribers() {
+      if (this.bulk.all) {
+        return this.subscribers.total;
+      }
+      return this.bulk.checked.length;
+    },
+
+    // Returns the list that the subscribers are being filtered by in.
+    currentList() {
+      if (!this.queryParams.listID || !this.lists.results) {
+        return null;
+      }
+
+      return this.lists.results.find((l) => l.id === this.queryParams.listID);
+    },
+  },
+
+  mounted() {
+    if (this.$route.params.listID) {
+      this.queryParams.listID = parseInt(this.$route.params.listID, 10);
+    }
+
+    // Get subscribers on load.
+    this.querySubscribers();
+  },
+});
+</script>

+ 139 - 0
frontend/src/views/TemplateForm.vue

@@ -0,0 +1,139 @@
+<template>
+  <section>
+    <form @submit.prevent="onSubmit">
+      <div class="modal-card content template-modal-content" style="width: auto">
+        <header class="modal-card-head">
+            <b-button @click="previewTemplate"
+              class="is-pulled-right" type="is-primary"
+              icon-left="file-find-outline">Preview</b-button>
+
+            <h4 v-if="isEditing">{{ data.name }}</h4>
+            <h4 v-else>New template</h4>
+        </header>
+        <section expanded class="modal-card-body">
+            <b-field label="Name">
+            <b-input :maxlength="200" :ref="'focus'" v-model="form.name"
+                placeholder="Name" required></b-input>
+            </b-field>
+
+            <b-field label="Raw HTML">
+            <b-input v-model="form.body" type="textarea" required />
+            </b-field>
+
+            <p class="is-size-7">
+                The placeholder <code>{{ egPlaceholder }}</code>
+                should appear in the template.
+                <a target="_blank" href="https://listmonk.app/docs/templating">Learn more.</a>
+            </p>
+        </section>
+        <footer class="modal-card-foot has-text-right">
+            <b-button @click="$parent.close()">Close</b-button>
+            <b-button native-type="submit" type="is-primary"
+            :loading="loading.templates">Save</b-button>
+        </footer>
+      </div>
+    </form>
+    <campaign-preview v-if="previewItem"
+      type='template'
+      :title="previewItem.name"
+      :body="form.body"
+      @close="closePreview"></campaign-preview>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import CampaignPreview from '../components/CampaignPreview.vue';
+
+Vue.component('campaign-preview', CampaignPreview);
+
+export default Vue.extend({
+  name: 'TemplateForm',
+
+  props: {
+    data: Object,
+    isEditing: null,
+  },
+
+  data() {
+    return {
+      // Binds form input values.
+      form: {
+        name: '',
+        type: '',
+        optin: '',
+      },
+      previewItem: null,
+      egPlaceholder: '{{ template "content" . }}',
+    };
+  },
+
+  methods: {
+    previewTemplate() {
+      this.previewItem = this.data;
+    },
+
+    closePreview() {
+      this.previewItem = null;
+    },
+
+    onSubmit() {
+      if (this.isEditing) {
+        this.updateTemplate();
+        return;
+      }
+
+      this.createTemplate();
+    },
+
+    createTemplate() {
+      const data = {
+        id: this.data.id,
+        name: this.form.name,
+        body: this.form.body,
+      };
+
+      this.$api.createTemplate(data).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' created`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+
+    updateTemplate() {
+      const data = {
+        id: this.data.id,
+        name: this.form.name,
+        body: this.form.body,
+      };
+
+      this.$api.updateTemplate(data).then((resp) => {
+        this.$emit('finished');
+        this.$parent.close();
+        this.$buefy.toast.open({
+          message: `'${resp.data.name}' updated`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['loading']),
+  },
+
+  mounted() {
+    this.form = { ...this.$props.data };
+
+    this.$nextTick(() => {
+      this.$refs.focus.focus();
+    });
+  },
+});
+</script>

+ 167 - 0
frontend/src/views/Templates.vue

@@ -0,0 +1,167 @@
+<template>
+  <section class="templates">
+    <header class="columns">
+      <div class="column is-two-thirds">
+        <h1 class="title is-4">Templates
+          <span v-if="templates.length > 0">({{ templates.length }})</span></h1>
+      </div>
+      <div class="column has-text-right">
+        <b-button type="is-primary" icon-left="plus" @click="showNewForm">New</b-button>
+      </div>
+    </header>
+
+    <b-table :data="templates" :hoverable="true" :loading="loading.templates"
+      default-sort="createdAt">
+        <template slot-scope="props">
+            <b-table-column field="name" label="Name" sortable>
+                <a :href="props.row.id" @click.prevent="showEditForm(props.row)">
+                  {{ props.row.name }}
+                </a>
+                <b-tag v-if="props.row.isDefault">default</b-tag>
+            </b-table-column>
+
+            <b-table-column field="createdAt" label="Created" sortable>
+                {{ $utils.niceDate(props.row.createdAt) }}
+            </b-table-column>
+
+            <b-table-column field="updatedAt" label="Updated" sortable>
+                {{ $utils.niceDate(props.row.updatedAt) }}
+            </b-table-column>
+
+            <b-table-column class="actions" align="right">
+              <a href="#" @click.prevent="previewTemplate(props.row)">
+                <b-tooltip label="Preview" type="is-dark">
+                  <b-icon icon="file-find-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a href="#" @click.prevent="showEditForm(props.row)">
+                <b-tooltip label="Edit" type="is-dark">
+                  <b-icon icon="pencil-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+             <a v-if="!props.row.isDefault" href="#"
+                @click.prevent="$utils.confirm(null, () => makeTemplateDefault(props.row))">
+                <b-tooltip label="Make default" type="is-dark">
+                  <b-icon icon="check-circle-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+              <a v-if="!props.row.isDefault"
+                href="#" @click.prevent="$utils.confirm(null, () => deleteTemplate(props.row))">
+                <b-tooltip label="Delete" type="is-dark">
+                  <b-icon icon="trash-can-outline" size="is-small" />
+                </b-tooltip>
+              </a>
+            </b-table-column>
+        </template>
+
+        <template slot="empty" v-if="!loading.templates">
+            <section class="section">
+                <div class="content has-text-grey has-text-centered">
+                    <p>
+                        <b-icon icon="plus" size="is-large" />
+                    </p>
+                    <p>Nothing here.</p>
+                </div>
+            </section>
+        </template>
+    </b-table>
+
+    <!-- Add / edit form modal -->
+    <b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible"
+      :width="1200" :can-cancel="false" class="template-modal">
+      <template-form :data="curItem" :isEditing="isEditing"
+        @finished="formFinished"></template-form>
+    </b-modal>
+
+    <campaign-preview v-if="previewItem"
+      type='template'
+      :id="previewItem.id"
+      :title="previewItem.name"
+      @close="closePreview"></campaign-preview>
+  </section>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import TemplateForm from './TemplateForm.vue';
+import CampaignPreview from '../components/CampaignPreview.vue';
+
+Vue.component('campaign-preview', CampaignPreview);
+Vue.component('template-form', TemplateForm);
+
+export default Vue.extend({
+  components: {
+    TemplateForm,
+  },
+
+  data() {
+    return {
+      curItem: null,
+      isEditing: false,
+      isFormVisible: false,
+      previewItem: null,
+    };
+  },
+
+  methods: {
+    // Show the edit form.
+    showEditForm(data) {
+      this.curItem = data;
+      this.isFormVisible = true;
+      this.isEditing = true;
+    },
+
+    // Show the new form.
+    showNewForm() {
+      this.curItem = {};
+      this.isFormVisible = true;
+      this.isEditing = false;
+    },
+
+    formFinished() {
+      this.$api.getTemplates();
+    },
+
+    previewTemplate(c) {
+      this.previewItem = c;
+    },
+
+    closePreview() {
+      this.previewItem = null;
+    },
+
+    makeTemplateDefault(tpl) {
+      this.$api.makeTemplateDefault(tpl.id).then(() => {
+        this.$api.getTemplates();
+
+        this.$buefy.toast.open({
+          message: `'${tpl.name}' made default`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+
+    deleteTemplate(tpl) {
+      this.$api.deleteTemplate(tpl.id).then(() => {
+        this.$api.getTemplates();
+
+        this.$buefy.toast.open({
+          message: `'${tpl.name}' deleted`,
+          type: 'is-success',
+          queue: false,
+        });
+      });
+    },
+  },
+
+  computed: {
+    ...mapState(['templates', 'loading']),
+  },
+
+  mounted() {
+    this.$api.getTemplates();
+  },
+});
+</script>

+ 17 - 0
frontend/vue.config.js

@@ -0,0 +1,17 @@
+module.exports = {
+  publicPath: '/',
+  outputDir: 'dist',
+
+  // This is to make all static file requests generated by Vue to go to
+  // /frontend/*. However, this also ends up creating a `dist/frontend`
+  // directory and moves all the static files in it. The physical directory
+  // and the URI for assets are tightly coupled. This is handled in the Go app
+  // by using stuffbin aliases.
+  assetsDir: 'frontend',
+  
+  // Move the index.html file from dist/index.html to dist/frontend/index.html
+  indexPath: './frontend/index.html',
+
+  productionSourceMap: false,
+  filenameHashing: false,
+};

+ 9060 - 0
frontend/yarn.lock

@@ -0,0 +1,9060 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff"
+  integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==
+  dependencies:
+    "@babel/highlight" "^7.10.1"
+
+"@babel/compat-data@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.1.tgz#b1085ffe72cd17bf2c0ee790fc09f9626011b2db"
+  integrity sha512-CHvCj7So7iCkGKPRFUfryXIkU2gSBw7VSZFYLsqVhrS47269VK2Hfi9S/YcublPMW8k1u2bQBlbDruoQEm4fgw==
+  dependencies:
+    browserslist "^4.12.0"
+    invariant "^2.2.4"
+    semver "^5.5.0"
+
+"@babel/core@^7.9.6":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a"
+  integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.1"
+    "@babel/generator" "^7.10.2"
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helpers" "^7.10.1"
+    "@babel/parser" "^7.10.2"
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.2"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
+"@babel/generator@^7.10.1", "@babel/generator@^7.10.2":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9"
+  integrity sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==
+  dependencies:
+    "@babel/types" "^7.10.2"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
+"@babel/helper-annotate-as-pure@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268"
+  integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.1.tgz#0ec7d9be8174934532661f87783eb18d72290059"
+  integrity sha512-cQpVq48EkYxUU0xozpGCLla3wlkdRRqLWu1ksFMXA9CM5KQmyyRpSEsYXbao7JUkOw/tAaYKCaYyZq6HOFYtyw==
+  dependencies:
+    "@babel/helper-explode-assignable-expression" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-compilation-targets@^7.10.2", "@babel/helper-compilation-targets@^7.9.6":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz#a17d9723b6e2c750299d2a14d4637c76936d8285"
+  integrity sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA==
+  dependencies:
+    "@babel/compat-data" "^7.10.1"
+    browserslist "^4.12.0"
+    invariant "^2.2.4"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/helper-create-class-features-plugin@^7.10.1":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz#7474295770f217dbcf288bf7572eb213db46ee67"
+  integrity sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/helper-member-expression-to-functions" "^7.10.1"
+    "@babel/helper-optimise-call-expression" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-replace-supers" "^7.10.1"
+    "@babel/helper-split-export-declaration" "^7.10.1"
+
+"@babel/helper-create-regexp-features-plugin@^7.10.1", "@babel/helper-create-regexp-features-plugin@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd"
+  integrity sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.1"
+    "@babel/helper-regex" "^7.10.1"
+    regexpu-core "^4.7.0"
+
+"@babel/helper-define-map@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.1.tgz#5e69ee8308648470dd7900d159c044c10285221d"
+  integrity sha512-+5odWpX+OnvkD0Zmq7panrMuAGQBu6aPUgvMzuMGo4R+jUOvealEj2hiqI6WhxgKrTpFoFj0+VdsuA8KDxHBDg==
+  dependencies:
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/types" "^7.10.1"
+    lodash "^4.17.13"
+
+"@babel/helper-explode-assignable-expression@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.1.tgz#e9d76305ee1162ca467357ae25df94f179af2b7e"
+  integrity sha512-vcUJ3cDjLjvkKzt6rHrl767FeE7pMEYfPanq5L16GRtrXIoznc0HykNW2aEYkcnP76P0isoqJ34dDMFZwzEpJg==
+  dependencies:
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-function-name@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz#92bd63829bfc9215aca9d9defa85f56b539454f4"
+  integrity sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.10.1"
+    "@babel/template" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-get-function-arity@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz#7303390a81ba7cb59613895a192b93850e373f7d"
+  integrity sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-hoist-variables@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.1.tgz#7e77c82e5dcae1ebf123174c385aaadbf787d077"
+  integrity sha512-vLm5srkU8rI6X3+aQ1rQJyfjvCBLXP8cAGeuw04zeAM2ItKb1e7pmVmLyHb4sDaAYnLL13RHOZPLEtcGZ5xvjg==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-member-expression-to-functions@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz#432967fd7e12a4afef66c4687d4ca22bc0456f15"
+  integrity sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.1", "@babel/helper-module-imports@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz#dd331bd45bccc566ce77004e9d05fe17add13876"
+  integrity sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-module-transforms@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622"
+  integrity sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-replace-supers" "^7.10.1"
+    "@babel/helper-simple-access" "^7.10.1"
+    "@babel/helper-split-export-declaration" "^7.10.1"
+    "@babel/template" "^7.10.1"
+    "@babel/types" "^7.10.1"
+    lodash "^4.17.13"
+
+"@babel/helper-optimise-call-expression@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz#b4a1f2561870ce1247ceddb02a3860fa96d72543"
+  integrity sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.8.0":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127"
+  integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==
+
+"@babel/helper-regex@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96"
+  integrity sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g==
+  dependencies:
+    lodash "^4.17.13"
+
+"@babel/helper-remap-async-to-generator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.1.tgz#bad6aaa4ff39ce8d4b82ccaae0bfe0f7dbb5f432"
+  integrity sha512-RfX1P8HqsfgmJ6CwaXGKMAqbYdlleqglvVtht0HGPMSsy2V6MqLlOJVF/0Qyb/m2ZCi2z3q3+s6Pv7R/dQuZ6A==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.1"
+    "@babel/helper-wrap-function" "^7.10.1"
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-replace-supers@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d"
+  integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.10.1"
+    "@babel/helper-optimise-call-expression" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-simple-access@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e"
+  integrity sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==
+  dependencies:
+    "@babel/template" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-split-export-declaration@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f"
+  integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==
+  dependencies:
+    "@babel/types" "^7.10.1"
+
+"@babel/helper-validator-identifier@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5"
+  integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==
+
+"@babel/helper-wrap-function@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9"
+  integrity sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/helpers@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973"
+  integrity sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==
+  dependencies:
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/highlight@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0"
+  integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.1"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.10.1", "@babel/parser@^7.10.2", "@babel/parser@^7.7.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0"
+  integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==
+
+"@babel/plugin-proposal-async-generator-functions@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.1.tgz#6911af5ba2e615c4ff3c497fe2f47b35bf6d7e55"
+  integrity sha512-vzZE12ZTdB336POZjmpblWfNNRpMSua45EYnRigE2XsZxcXcIyly2ixnTJasJE4Zq3U7t2d8rRF7XRUuzHxbOw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-remap-async-to-generator" "^7.10.1"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+
+"@babel/plugin-proposal-class-properties@^7.10.1", "@babel/plugin-proposal-class-properties@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01"
+  integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-proposal-decorators@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.1.tgz#9373c2d8db45345c6e30452ad77b469758e5c8f7"
+  integrity sha512-xBfteh352MTke2U1NpclzMDmAmCdQ2fBZjhZQQfGTjXw6qcRYMkt528sA1U8o0ThDCSeuETXIj5bOGdxN+5gkw==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-decorators" "^7.10.1"
+
+"@babel/plugin-proposal-dynamic-import@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0"
+  integrity sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+
+"@babel/plugin-proposal-json-strings@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09"
+  integrity sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78"
+  integrity sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123"
+  integrity sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.1"
+
+"@babel/plugin-proposal-object-rest-spread@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.1.tgz#cba44908ac9f142650b4a65b8aa06bf3478d5fb6"
+  integrity sha512-Z+Qri55KiQkHh7Fc4BW6o+QBuTagbOp9txE+4U1i79u9oWlf2npkiDx+Rf3iK3lbcHBuNy9UOkwuR5wOMH3LIQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-transform-parameters" "^7.10.1"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz#c9f86d99305f9fa531b568ff5ab8c964b8b223d2"
+  integrity sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.1.tgz#15f5d6d22708629451a91be28f8facc55b0e818c"
+  integrity sha512-dqQj475q8+/avvok72CF3AOSV/SGEcH29zT5hhohqqvvZ2+boQoOr7iGldBG5YXTO2qgCgc2B3WvVLUdbeMlGA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-private-methods@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598"
+  integrity sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.10.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f"
+  integrity sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5"
+  integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-syntax-decorators@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.1.tgz#16b869c4beafc9a442565147bda7ce0967bd4f13"
+  integrity sha512-a9OAbQhKOwSle1Vr0NJu/ISg1sPfdEkfRKWpgPuzhnWWzForou2gIeUIIwjAMHRekhhpJ7eulZlYs0H14Cbi+g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
+  integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-json-strings@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-jsx@^7.2.0", "@babel/plugin-syntax-jsx@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz#0ae371134a42b91d5418feb3c8c8d43e1565d2da"
+  integrity sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99"
+  integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz#8b8733f8c57397b3eaa47ddba8841586dcaef362"
+  integrity sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-arrow-functions@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b"
+  integrity sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-async-to-generator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz#e5153eb1a3e028f79194ed8a7a4bf55f862b2062"
+  integrity sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-remap-async-to-generator" "^7.10.1"
+
+"@babel/plugin-transform-block-scoped-functions@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz#146856e756d54b20fff14b819456b3e01820b85d"
+  integrity sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-block-scoping@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz#47092d89ca345811451cd0dc5d91605982705d5e"
+  integrity sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    lodash "^4.17.13"
+
+"@babel/plugin-transform-classes@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.1.tgz#6e11dd6c4dfae70f540480a4702477ed766d733f"
+  integrity sha512-P9V0YIh+ln/B3RStPoXpEQ/CoAxQIhRSUn7aXqQ+FZJ2u8+oCtjIXR3+X0vsSD8zv+mb56K7wZW1XiDTDGiDRQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.1"
+    "@babel/helper-define-map" "^7.10.1"
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/helper-optimise-call-expression" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-replace-supers" "^7.10.1"
+    "@babel/helper-split-export-declaration" "^7.10.1"
+    globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.1.tgz#59aa399064429d64dce5cf76ef9b90b7245ebd07"
+  integrity sha512-mqSrGjp3IefMsXIenBfGcPXxJxweQe2hEIwMQvjtiDQ9b1IBvDUjkAtV/HMXX47/vXf14qDNedXsIiNd1FmkaQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-destructuring@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz#abd58e51337815ca3a22a336b85f62b998e71907"
+  integrity sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-dotall-regex@^7.10.1", "@babel/plugin-transform-dotall-regex@^7.4.4":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz#920b9fec2d78bb57ebb64a644d5c2ba67cc104ee"
+  integrity sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-duplicate-keys@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz#c900a793beb096bc9d4d0a9d0cde19518ffc83b9"
+  integrity sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-exponentiation-operator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz#279c3116756a60dd6e6f5e488ba7957db9c59eb3"
+  integrity sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA==
+  dependencies:
+    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-for-of@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz#ff01119784eb0ee32258e8646157ba2501fcfda5"
+  integrity sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-function-name@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz#4ed46fd6e1d8fde2a2ec7b03c66d853d2c92427d"
+  integrity sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw==
+  dependencies:
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-literals@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz#5794f8da82846b22e4e6631ea1658bce708eb46a"
+  integrity sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-member-expression-literals@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz#90347cba31bca6f394b3f7bd95d2bbfd9fce2f39"
+  integrity sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-modules-amd@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz#65950e8e05797ebd2fe532b96e19fc5482a1d52a"
+  integrity sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    babel-plugin-dynamic-import-node "^2.3.3"
+
+"@babel/plugin-transform-modules-commonjs@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301"
+  integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-simple-access" "^7.10.1"
+    babel-plugin-dynamic-import-node "^2.3.3"
+
+"@babel/plugin-transform-modules-systemjs@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.1.tgz#9962e4b0ac6aaf2e20431ada3d8ec72082cbffb6"
+  integrity sha512-ewNKcj1TQZDL3YnO85qh9zo1YF1CHgmSTlRQgHqe63oTrMI85cthKtZjAiZSsSNjPQ5NCaYo5QkbYqEw1ZBgZA==
+  dependencies:
+    "@babel/helper-hoist-variables" "^7.10.1"
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    babel-plugin-dynamic-import-node "^2.3.3"
+
+"@babel/plugin-transform-modules-umd@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz#ea080911ffc6eb21840a5197a39ede4ee67b1595"
+  integrity sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c"
+  integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+
+"@babel/plugin-transform-new-target@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz#6ee41a5e648da7632e22b6fb54012e87f612f324"
+  integrity sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-object-super@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz#2e3016b0adbf262983bf0d5121d676a5ed9c4fde"
+  integrity sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-replace-supers" "^7.10.1"
+
+"@babel/plugin-transform-parameters@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd"
+  integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-property-literals@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz#cffc7315219230ed81dc53e4625bf86815b6050d"
+  integrity sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-regenerator@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.1.tgz#10e175cbe7bdb63cc9b39f9b3f823c5c7c5c5490"
+  integrity sha512-B3+Y2prScgJ2Bh/2l9LJxKbb8C8kRfsG4AdPT+n7ixBHIxJaIG8bi8tgjxUMege1+WqSJ+7gu1YeoMVO3gPWzw==
+  dependencies:
+    regenerator-transform "^0.14.2"
+
+"@babel/plugin-transform-reserved-words@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz#0fc1027312b4d1c3276a57890c8ae3bcc0b64a86"
+  integrity sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-runtime@^7.9.6":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1"
+  integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    resolve "^1.8.1"
+    semver "^5.5.1"
+
+"@babel/plugin-transform-shorthand-properties@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3"
+  integrity sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-spread@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz#0c6d618a0c4461a274418460a28c9ccf5239a7c8"
+  integrity sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-sticky-regex@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz#90fc89b7526228bed9842cff3588270a7a393b00"
+  integrity sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/helper-regex" "^7.10.1"
+
+"@babel/plugin-transform-template-literals@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.1.tgz#914c7b7f4752c570ea00553b4284dad8070e8628"
+  integrity sha512-t7B/3MQf5M1T9hPCRG28DNGZUuxAuDqLYS03rJrIk2prj/UV7Z6FOneijhQhnv/Xa039vidXeVbvjK2SK5f7Gg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-typeof-symbol@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz#60c0239b69965d166b80a84de7315c1bc7e0bb0e"
+  integrity sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-unicode-escapes@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940"
+  integrity sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/plugin-transform-unicode-regex@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f"
+  integrity sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+
+"@babel/preset-env@^7.9.6":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.2.tgz#715930f2cf8573b0928005ee562bed52fb65fdfb"
+  integrity sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA==
+  dependencies:
+    "@babel/compat-data" "^7.10.1"
+    "@babel/helper-compilation-targets" "^7.10.2"
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-proposal-async-generator-functions" "^7.10.1"
+    "@babel/plugin-proposal-class-properties" "^7.10.1"
+    "@babel/plugin-proposal-dynamic-import" "^7.10.1"
+    "@babel/plugin-proposal-json-strings" "^7.10.1"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.1"
+    "@babel/plugin-proposal-numeric-separator" "^7.10.1"
+    "@babel/plugin-proposal-object-rest-spread" "^7.10.1"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.10.1"
+    "@babel/plugin-proposal-optional-chaining" "^7.10.1"
+    "@babel/plugin-proposal-private-methods" "^7.10.1"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.10.1"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-class-properties" "^7.10.1"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.1"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.10.1"
+    "@babel/plugin-transform-arrow-functions" "^7.10.1"
+    "@babel/plugin-transform-async-to-generator" "^7.10.1"
+    "@babel/plugin-transform-block-scoped-functions" "^7.10.1"
+    "@babel/plugin-transform-block-scoping" "^7.10.1"
+    "@babel/plugin-transform-classes" "^7.10.1"
+    "@babel/plugin-transform-computed-properties" "^7.10.1"
+    "@babel/plugin-transform-destructuring" "^7.10.1"
+    "@babel/plugin-transform-dotall-regex" "^7.10.1"
+    "@babel/plugin-transform-duplicate-keys" "^7.10.1"
+    "@babel/plugin-transform-exponentiation-operator" "^7.10.1"
+    "@babel/plugin-transform-for-of" "^7.10.1"
+    "@babel/plugin-transform-function-name" "^7.10.1"
+    "@babel/plugin-transform-literals" "^7.10.1"
+    "@babel/plugin-transform-member-expression-literals" "^7.10.1"
+    "@babel/plugin-transform-modules-amd" "^7.10.1"
+    "@babel/plugin-transform-modules-commonjs" "^7.10.1"
+    "@babel/plugin-transform-modules-systemjs" "^7.10.1"
+    "@babel/plugin-transform-modules-umd" "^7.10.1"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3"
+    "@babel/plugin-transform-new-target" "^7.10.1"
+    "@babel/plugin-transform-object-super" "^7.10.1"
+    "@babel/plugin-transform-parameters" "^7.10.1"
+    "@babel/plugin-transform-property-literals" "^7.10.1"
+    "@babel/plugin-transform-regenerator" "^7.10.1"
+    "@babel/plugin-transform-reserved-words" "^7.10.1"
+    "@babel/plugin-transform-shorthand-properties" "^7.10.1"
+    "@babel/plugin-transform-spread" "^7.10.1"
+    "@babel/plugin-transform-sticky-regex" "^7.10.1"
+    "@babel/plugin-transform-template-literals" "^7.10.1"
+    "@babel/plugin-transform-typeof-symbol" "^7.10.1"
+    "@babel/plugin-transform-unicode-escapes" "^7.10.1"
+    "@babel/plugin-transform-unicode-regex" "^7.10.1"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.10.2"
+    browserslist "^4.12.0"
+    core-js-compat "^3.6.2"
+    invariant "^2.2.2"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/preset-modules@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
+  integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
+
+"@babel/runtime@^7.8.4", "@babel/runtime@^7.9.6":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
+  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/template@^7.10.1":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
+  integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==
+  dependencies:
+    "@babel/code-frame" "^7.10.1"
+    "@babel/parser" "^7.10.1"
+    "@babel/types" "^7.10.1"
+
+"@babel/traverse@^7.10.1", "@babel/traverse@^7.7.0":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27"
+  integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.1"
+    "@babel/generator" "^7.10.1"
+    "@babel/helper-function-name" "^7.10.1"
+    "@babel/helper-split-export-declaration" "^7.10.1"
+    "@babel/parser" "^7.10.1"
+    "@babel/types" "^7.10.1"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
+"@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d"
+  integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.1"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@hapi/address@2.x.x":
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
+  integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
+
+"@hapi/bourne@1.x.x":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a"
+  integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==
+
+"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0":
+  version "8.5.1"
+  resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06"
+  integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
+
+"@hapi/joi@^15.0.1":
+  version "15.1.1"
+  resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
+  integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==
+  dependencies:
+    "@hapi/address" "2.x.x"
+    "@hapi/bourne" "1.x.x"
+    "@hapi/hoek" "8.x.x"
+    "@hapi/topo" "3.x.x"
+
+"@hapi/topo@3.x.x":
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29"
+  integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==
+  dependencies:
+    "@hapi/hoek" "^8.3.0"
+
+"@intervolga/optimize-cssnano-plugin@^1.0.5":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz#be7c7846128b88f6a9b1d1261a0ad06eb5c0fdf8"
+  integrity sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA==
+  dependencies:
+    cssnano "^4.0.0"
+    cssnano-preset-default "^4.0.0"
+    postcss "^7.0.0"
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+  integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+  dependencies:
+    call-me-maybe "^1.0.1"
+    glob-to-regexp "^0.3.0"
+
+"@nodelib/fs.stat@^1.1.2":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+  integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+
+"@soda/friendly-errors-webpack-plugin@^1.7.1":
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz#706f64bcb4a8b9642b48ae3ace444c70334d615d"
+  integrity sha512-cWKrGaFX+rfbMrAxVv56DzhPNqOJPZuNIS2HGMELtgGzb+vsMzyig9mml5gZ/hr2BGtSLV+dP2LUEuAL8aG2mQ==
+  dependencies:
+    chalk "^1.1.3"
+    error-stack-parser "^2.0.0"
+    string-width "^2.0.0"
+
+"@soda/get-current-script@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@soda/get-current-script/-/get-current-script-1.0.1.tgz#f4afffcb36e069a801d5339c90499601c47a2516"
+  integrity sha512-zeOomWIE52M9JpYXlsR3iOf7TXTTmNQHnSbqjMsQZ5phzfAenHzL/1+vQ0ZoJfagocK11LNf8vnn2JG0ufRMUQ==
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/glob@^7.1.1":
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987"
+  integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/json-schema@^7.0.4":
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
+  integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+
+"@types/minimatch@*":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+  integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+
+"@types/node@*":
+  version "14.0.11"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3"
+  integrity sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg==
+
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
+  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+
+"@types/q@^1.5.1":
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
+  integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
+
+"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
+  integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==
+
+"@vue/babel-plugin-transform-vue-jsx@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0"
+  integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
+    html-tags "^2.0.0"
+    lodash.kebabcase "^4.1.1"
+    svg-tags "^1.0.0"
+
+"@vue/babel-preset-app@^4.4.1":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.4.1.tgz#97c6796183cd0abf96a17297dc335c4c702fd8c4"
+  integrity sha512-VHVROEBBiW0dnuNuzlFElkncXo+zxh5Px0MZ51Th5da8UPbQodf43mnpotMnFtmCPTXAFL58tzDttu1FgrgfpQ==
+  dependencies:
+    "@babel/core" "^7.9.6"
+    "@babel/helper-compilation-targets" "^7.9.6"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/plugin-proposal-class-properties" "^7.8.3"
+    "@babel/plugin-proposal-decorators" "^7.8.3"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+    "@babel/plugin-syntax-jsx" "^7.8.3"
+    "@babel/plugin-transform-runtime" "^7.9.6"
+    "@babel/preset-env" "^7.9.6"
+    "@babel/runtime" "^7.9.6"
+    "@vue/babel-preset-jsx" "^1.1.2"
+    babel-plugin-dynamic-import-node "^2.3.3"
+    core-js "^3.6.5"
+    core-js-compat "^3.6.5"
+    semver "^6.1.0"
+
+"@vue/babel-preset-jsx@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.2.tgz#2e169eb4c204ea37ca66c2ea85a880bfc99d4f20"
+  integrity sha512-zDpVnFpeC9YXmvGIDSsKNdL7qCG2rA3gjywLYHPCKDT10erjxF4U+6ay9X6TW5fl4GsDlJp9bVfAVQAAVzxxvQ==
+  dependencies:
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.1.2"
+    "@vue/babel-sugar-functional-vue" "^1.1.2"
+    "@vue/babel-sugar-inject-h" "^1.1.2"
+    "@vue/babel-sugar-v-model" "^1.1.2"
+    "@vue/babel-sugar-v-on" "^1.1.2"
+
+"@vue/babel-sugar-functional-vue@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.1.2.tgz#f7e24fba09e6f1ee70104560a8808057555f1a9a"
+  integrity sha512-YhmdJQSVEFF5ETJXzrMpj0nkCXEa39TvVxJTuVjzvP2rgKhdMmQzlJuMv/HpadhZaRVMCCF3AEjjJcK5q/cYzQ==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-inject-h@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.1.2.tgz#8a5276b6d8e2ed16ffc8078aad94236274e6edf0"
+  integrity sha512-VRSENdTvD5htpnVp7i7DNuChR5rVMcORdXjvv5HVvpdKHzDZAYiLSD+GhnhxLm3/dMuk8pSzV+k28ECkiN5m8w==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-v-model@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.1.2.tgz#1ff6fd1b800223fc9cb1e84dceb5e52d737a8192"
+  integrity sha512-vLXPvNq8vDtt0u9LqFdpGM9W9IWDmCmCyJXuozlq4F4UYVleXJ2Fa+3JsnTZNJcG+pLjjfnEGHci2339Kj5sGg==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.1.2"
+    camelcase "^5.0.0"
+    html-tags "^2.0.0"
+    svg-tags "^1.0.0"
+
+"@vue/babel-sugar-v-on@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.1.2.tgz#b2ef99b8f2fab09fbead25aad70ef42e1cf5b13b"
+  integrity sha512-T8ZCwC8Jp2uRtcZ88YwZtZXe7eQrJcfRq0uTFy6ShbwYJyz5qWskRFoVsdTi9o0WEhmQXxhQUewodOSCUPVmsQ==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.1.2"
+    camelcase "^5.0.0"
+
+"@vue/cli-overlay@^4.4.1":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-4.4.1.tgz#f1f51f31f7a00d371e9a5b5a941525184475bf8b"
+  integrity sha512-EQqAVy7O/qqGOfSYIGL073FWlr/s6QFA0wA1wY8pHnTS5WPwAiHT+D+xe+fgXKZ3KeL7v7u/le7YFIEVXFVXOg==
+
+"@vue/cli-plugin-babel@~4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.4.1.tgz#2c6e969fe51f1b4b211bea84afb7cad18240f70f"
+  integrity sha512-dmhymfm2UnZDw13k/zKT6YIj7je53mE37Y+jEJxpRUlCKFmZUDuYkJ8i5HmO0SnaCnEGqNELaBkoIFnY3aE2Gw==
+  dependencies:
+    "@babel/core" "^7.9.6"
+    "@vue/babel-preset-app" "^4.4.1"
+    "@vue/cli-shared-utils" "^4.4.1"
+    babel-loader "^8.1.0"
+    cache-loader "^4.1.0"
+    thread-loader "^2.1.3"
+    webpack "^4.0.0"
+
+"@vue/cli-plugin-eslint@~4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.4.1.tgz#e39d6517da6de231195d227f995f495e2958a74a"
+  integrity sha512-T+9+q44iajQEbe59z6Io3otFOsWnPOEVU+/hrDyC6aOToJbQo6P4VacByDDcuGYENAjAd8ENLSt18TaPNSIyRw==
+  dependencies:
+    "@vue/cli-shared-utils" "^4.4.1"
+    eslint-loader "^2.2.1"
+    globby "^9.2.0"
+    inquirer "^7.1.0"
+    webpack "^4.0.0"
+    yorkie "^2.0.0"
+
+"@vue/cli-plugin-router@^4.4.1", "@vue/cli-plugin-router@~4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.4.1.tgz#07d09df0f4bea816e637da814f578b808f1f93b2"
+  integrity sha512-kCSsJG7pjDvCJDjGtcCI5l0UjmqwNigOR41RkeGSjSUvzV4ArSniXjFqrOmtpMp36S5xCtwtt9MFm/K4fCubkQ==
+  dependencies:
+    "@vue/cli-shared-utils" "^4.4.1"
+
+"@vue/cli-plugin-vuex@^4.4.1", "@vue/cli-plugin-vuex@~4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.4.1.tgz#98d18fc5d36fa6e0d1fc2ecaeea37aa965564f19"
+  integrity sha512-FtOFsDP0qznwVaCz0BZmTzUm5vhHSJzX2/XD3L5dLTkrNxyDEbZmbKoX0n1OzBcQwZC7dkJZP2tdoCQx0mX//g==
+
+"@vue/cli-service@~4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-4.4.1.tgz#b26a435d8c953bc7efaf7b784c9835c1415bcf1c"
+  integrity sha512-DVV0zr5Sw7pzdm3z3PRrfqihLgoJP/d9AgNFcSSQF/J9Gtvjf1t0PTJJFeLANHSL3kDWte+3kjc22sXayu0BJQ==
+  dependencies:
+    "@intervolga/optimize-cssnano-plugin" "^1.0.5"
+    "@soda/friendly-errors-webpack-plugin" "^1.7.1"
+    "@soda/get-current-script" "^1.0.0"
+    "@vue/cli-overlay" "^4.4.1"
+    "@vue/cli-plugin-router" "^4.4.1"
+    "@vue/cli-plugin-vuex" "^4.4.1"
+    "@vue/cli-shared-utils" "^4.4.1"
+    "@vue/component-compiler-utils" "^3.1.2"
+    "@vue/preload-webpack-plugin" "^1.1.0"
+    "@vue/web-component-wrapper" "^1.2.0"
+    acorn "^7.2.0"
+    acorn-walk "^7.1.1"
+    address "^1.1.2"
+    autoprefixer "^9.8.0"
+    browserslist "^4.12.0"
+    cache-loader "^4.1.0"
+    case-sensitive-paths-webpack-plugin "^2.3.0"
+    cli-highlight "^2.1.4"
+    clipboardy "^2.3.0"
+    cliui "^6.0.0"
+    copy-webpack-plugin "^5.1.1"
+    css-loader "^3.5.3"
+    cssnano "^4.1.10"
+    debug "^4.1.1"
+    default-gateway "^5.0.5"
+    dotenv "^8.2.0"
+    dotenv-expand "^5.1.0"
+    file-loader "^4.2.0"
+    fs-extra "^7.0.1"
+    globby "^9.2.0"
+    hash-sum "^2.0.0"
+    html-webpack-plugin "^3.2.0"
+    launch-editor-middleware "^2.2.1"
+    lodash.defaultsdeep "^4.6.1"
+    lodash.mapvalues "^4.6.0"
+    lodash.transform "^4.6.0"
+    mini-css-extract-plugin "^0.9.0"
+    minimist "^1.2.5"
+    pnp-webpack-plugin "^1.6.4"
+    portfinder "^1.0.26"
+    postcss-loader "^3.0.0"
+    ssri "^7.1.0"
+    terser-webpack-plugin "^2.3.6"
+    thread-loader "^2.1.3"
+    url-loader "^2.2.0"
+    vue-loader "^15.9.2"
+    vue-style-loader "^4.1.2"
+    webpack "^4.0.0"
+    webpack-bundle-analyzer "^3.8.0"
+    webpack-chain "^6.4.0"
+    webpack-dev-server "^3.11.0"
+    webpack-merge "^4.2.2"
+
+"@vue/cli-shared-utils@^4.4.1":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.4.1.tgz#930304ade5a9f9bf0d2fd67d9305cad83d04aae1"
+  integrity sha512-teevHgI7XUsKVMOncx3M+6iLjO28woGfRwgUG4hR83moVBHQe5x2OCr2i5t/58bwpv269RD5RYXBQCGtIXuxZw==
+  dependencies:
+    "@hapi/joi" "^15.0.1"
+    chalk "^2.4.2"
+    execa "^1.0.0"
+    launch-editor "^2.2.1"
+    lru-cache "^5.1.1"
+    node-ipc "^9.1.1"
+    open "^6.3.0"
+    ora "^3.4.0"
+    read-pkg "^5.1.1"
+    request "^2.88.2"
+    request-promise-native "^1.0.8"
+    semver "^6.1.0"
+    strip-ansi "^6.0.0"
+
+"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz#8213a5ff3202f9f2137fe55370f9e8b9656081c3"
+  integrity sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug==
+  dependencies:
+    consolidate "^0.15.1"
+    hash-sum "^1.0.2"
+    lru-cache "^4.1.2"
+    merge-source-map "^1.1.0"
+    postcss "^7.0.14"
+    postcss-selector-parser "^6.0.2"
+    source-map "~0.6.1"
+    vue-template-es2015-compiler "^1.9.0"
+  optionalDependencies:
+    prettier "^1.18.2"
+
+"@vue/eslint-config-airbnb@^5.0.2":
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/@vue/eslint-config-airbnb/-/eslint-config-airbnb-5.0.2.tgz#4e3ba49e8d7a7c0bf6b244b4d7a2fe488bfb9371"
+  integrity sha512-9wD5OfdkQ0TDYLRynP46AxOHck866zkvZoT8MgjyJNBPegTtrsIQ3cu10ZF4Nl/aU5qKeSOaY58D4YXPqF0NZg==
+  dependencies:
+    eslint-config-airbnb-base "^14.0.0"
+    eslint-import-resolver-node "^0.3.3"
+    eslint-import-resolver-webpack "^0.11.1"
+    eslint-plugin-import "^2.18.2"
+
+"@vue/preload-webpack-plugin@^1.1.0":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.1.tgz#18723530d304f443021da2292d6ec9502826104a"
+  integrity sha512-8VCoJeeH8tCkzhkpfOkt+abALQkS11OIHhte5MBzYaKMTqK0A3ZAKEUVAffsOklhEv7t0yrQt696Opnu9oAx+w==
+
+"@vue/web-component-wrapper@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@vue/web-component-wrapper/-/web-component-wrapper-1.2.0.tgz#bb0e46f1585a7e289b4ee6067dcc5a6ae62f1dd1"
+  integrity sha512-Xn/+vdm9CjuC9p3Ae+lTClNutrVhsXpzxvoTXXtoys6kVRX9FkueSUAqSWAyZntmVLlR4DosBV4pH8y5Z/HbUw==
+
+"@webassemblyjs/ast@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
+  integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==
+  dependencies:
+    "@webassemblyjs/helper-module-context" "1.9.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+    "@webassemblyjs/wast-parser" "1.9.0"
+
+"@webassemblyjs/floating-point-hex-parser@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4"
+  integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==
+
+"@webassemblyjs/helper-api-error@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2"
+  integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==
+
+"@webassemblyjs/helper-buffer@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00"
+  integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==
+
+"@webassemblyjs/helper-code-frame@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27"
+  integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==
+  dependencies:
+    "@webassemblyjs/wast-printer" "1.9.0"
+
+"@webassemblyjs/helper-fsm@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8"
+  integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==
+
+"@webassemblyjs/helper-module-context@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07"
+  integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+
+"@webassemblyjs/helper-wasm-bytecode@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790"
+  integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==
+
+"@webassemblyjs/helper-wasm-section@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346"
+  integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-buffer" "1.9.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+    "@webassemblyjs/wasm-gen" "1.9.0"
+
+"@webassemblyjs/ieee754@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4"
+  integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95"
+  integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab"
+  integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==
+
+"@webassemblyjs/wasm-edit@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf"
+  integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-buffer" "1.9.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+    "@webassemblyjs/helper-wasm-section" "1.9.0"
+    "@webassemblyjs/wasm-gen" "1.9.0"
+    "@webassemblyjs/wasm-opt" "1.9.0"
+    "@webassemblyjs/wasm-parser" "1.9.0"
+    "@webassemblyjs/wast-printer" "1.9.0"
+
+"@webassemblyjs/wasm-gen@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c"
+  integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+    "@webassemblyjs/ieee754" "1.9.0"
+    "@webassemblyjs/leb128" "1.9.0"
+    "@webassemblyjs/utf8" "1.9.0"
+
+"@webassemblyjs/wasm-opt@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61"
+  integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-buffer" "1.9.0"
+    "@webassemblyjs/wasm-gen" "1.9.0"
+    "@webassemblyjs/wasm-parser" "1.9.0"
+
+"@webassemblyjs/wasm-parser@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e"
+  integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-api-error" "1.9.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+    "@webassemblyjs/ieee754" "1.9.0"
+    "@webassemblyjs/leb128" "1.9.0"
+    "@webassemblyjs/utf8" "1.9.0"
+
+"@webassemblyjs/wast-parser@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914"
+  integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/floating-point-hex-parser" "1.9.0"
+    "@webassemblyjs/helper-api-error" "1.9.0"
+    "@webassemblyjs/helper-code-frame" "1.9.0"
+    "@webassemblyjs/helper-fsm" "1.9.0"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.9.0":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899"
+  integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/wast-parser" "1.9.0"
+    "@xtuc/long" "4.2.2"
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abbrev@1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
+  integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+  dependencies:
+    mime-types "~2.1.24"
+    negotiator "0.6.2"
+
+acorn-jsx@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
+  integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
+
+acorn-walk@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
+acorn@^6.4.1:
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
+  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+
+acorn@^7.1.1, acorn@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
+  integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==
+
+address@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
+  integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
+
+aggregate-error@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0"
+  integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==
+  dependencies:
+    clean-stack "^2.0.0"
+    indent-string "^4.0.0"
+
+ajv-errors@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
+  integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
+
+ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
+  integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
+
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5:
+  version "6.12.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
+  integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+amdefine@>=0.0.4:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+  integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
+
+ansi-colors@^3.0.0:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
+  integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+
+ansi-escapes@^4.2.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
+  integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
+  dependencies:
+    type-fest "^0.11.0"
+
+ansi-html@0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+  integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+
+ansi-styles@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+any-promise@^1.0.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+  integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
+
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+anymatch@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+aproba@^1.0.3, aproba@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+  integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+arch@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
+  integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
+
+are-we-there-yet@~1.1.2:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+  integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-find-index@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+  integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
+
+array-find@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8"
+  integrity sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=
+
+array-flatten@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+  integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+array-flatten@^2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
+  integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
+
+array-includes@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
+  integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0"
+    is-string "^1.0.5"
+
+array-union@^1.0.1, array-union@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+  integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+  integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+array.prototype.flat@^1.2.1:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
+  integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+asn1.js@^4.0.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+  integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert@^1.1.1:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
+  integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+  dependencies:
+    object-assign "^4.1.1"
+    util "0.10.3"
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+astral-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+  integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+async-each@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
+  integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+
+async-foreach@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+  integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
+
+async-limiter@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+async@^2.6.2:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
+  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+  dependencies:
+    lodash "^4.17.14"
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+autoprefixer@^9.8.0:
+  version "9.8.0"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.0.tgz#68e2d2bef7ba4c3a65436f662d0a56a741e56511"
+  integrity sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==
+  dependencies:
+    browserslist "^4.12.0"
+    caniuse-lite "^1.0.30001061"
+    chalk "^2.4.2"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.30"
+    postcss-value-parser "^4.1.0"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
+  integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
+
+axios@^0.19.2:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
+babel-eslint@^10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
+  integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/parser" "^7.7.0"
+    "@babel/traverse" "^7.7.0"
+    "@babel/types" "^7.7.0"
+    eslint-visitor-keys "^1.0.0"
+    resolve "^1.12.0"
+
+babel-loader@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
+  integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
+  dependencies:
+    find-cache-dir "^2.1.0"
+    loader-utils "^1.4.0"
+    mkdirp "^0.5.3"
+    pify "^4.0.1"
+    schema-utils "^2.6.5"
+
+babel-plugin-dynamic-import-node@^2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
+  integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==
+  dependencies:
+    object.assign "^4.1.0"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-js@^1.0.2:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+  dependencies:
+    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"
+
+batch@0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+  integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+bfj@^6.1.1:
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.2.tgz#325c861a822bcb358a41c78a33b8e6e2086dde7f"
+  integrity sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==
+  dependencies:
+    bluebird "^3.5.5"
+    check-types "^8.0.3"
+    hoopy "^0.1.4"
+    tryer "^1.0.1"
+
+big.js@^3.1.3:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+  integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
+
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
+  integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
+block-stream@*:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+  integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+  dependencies:
+    inherits "~2.0.0"
+
+bluebird@^3.1.1, bluebird@^3.5.5:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
+  version "4.11.9"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
+  integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
+
+bn.js@^5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
+  integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
+
+body-parser@1.19.0:
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
+  integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+  dependencies:
+    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"
+
+bonjour@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
+  integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU=
+  dependencies:
+    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@^1.0.0, boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+  dependencies:
+    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"
+
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+brorand@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+  integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+  integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+  dependencies:
+    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@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+  integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+  dependencies:
+    browserify-aes "^1.0.4"
+    browserify-des "^1.0.0"
+    evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+  integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+  dependencies:
+    cipher-base "^1.0.1"
+    des.js "^1.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+  integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+  dependencies:
+    bn.js "^4.1.0"
+    randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11"
+  integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==
+  dependencies:
+    bn.js "^5.1.1"
+    browserify-rsa "^4.0.1"
+    create-hash "^1.2.0"
+    create-hmac "^1.1.7"
+    elliptic "^6.5.2"
+    inherits "^2.0.4"
+    parse-asn1 "^5.1.5"
+    readable-stream "^3.6.0"
+    safe-buffer "^5.2.0"
+
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+  integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+  dependencies:
+    pako "~1.0.5"
+
+browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5:
+  version "4.12.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d"
+  integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==
+  dependencies:
+    caniuse-lite "^1.0.30001043"
+    electron-to-chromium "^1.3.413"
+    node-releases "^1.1.53"
+    pkg-up "^2.0.0"
+
+buefy@^0.8.20:
+  version "0.8.20"
+  resolved "https://registry.yarnpkg.com/buefy/-/buefy-0.8.20.tgz#75708800548220654575903d031a81fc8575b7f3"
+  integrity sha512-pg8Cn0m9cjqp2/vaKT4VIfU8KIumuX/gAT1GtearXRs56+kKqAPx3j9O8cm9W6P4jPUCHajKX6H8AqD0ram2Bg==
+  dependencies:
+    bulma "0.7.5"
+
+buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-indexof@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
+  integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==
+
+buffer-json@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-json/-/buffer-json-2.0.0.tgz#f73e13b1e42f196fe2fd67d001c7d7107edd7c23"
+  integrity sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==
+
+buffer-xor@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+  integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
+builtin-status-codes@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+  integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bulma@0.7.5:
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.5.tgz#35066c37f82c088b68f94450be758fc00a967208"
+  integrity sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==
+
+bytes@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+  integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+bytes@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cacache@^12.0.2, cacache@^12.0.3:
+  version "12.0.4"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c"
+  integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==
+  dependencies:
+    bluebird "^3.5.5"
+    chownr "^1.1.1"
+    figgy-pudding "^3.5.1"
+    glob "^7.1.4"
+    graceful-fs "^4.1.15"
+    infer-owner "^1.0.3"
+    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"
+
+cacache@^13.0.1:
+  version "13.0.1"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-13.0.1.tgz#a8000c21697089082f85287a1aec6e382024a71c"
+  integrity sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==
+  dependencies:
+    chownr "^1.1.2"
+    figgy-pudding "^3.5.1"
+    fs-minipass "^2.0.0"
+    glob "^7.1.4"
+    graceful-fs "^4.2.2"
+    infer-owner "^1.0.4"
+    lru-cache "^5.1.1"
+    minipass "^3.0.0"
+    minipass-collect "^1.0.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.2"
+    mkdirp "^0.5.1"
+    move-concurrently "^1.0.1"
+    p-map "^3.0.0"
+    promise-inflight "^1.0.1"
+    rimraf "^2.7.1"
+    ssri "^7.0.0"
+    unique-filename "^1.1.1"
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+  dependencies:
+    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"
+
+cache-loader@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-4.1.0.tgz#9948cae353aec0a1fcb1eafda2300816ec85387e"
+  integrity sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==
+  dependencies:
+    buffer-json "^2.0.0"
+    find-cache-dir "^3.0.0"
+    loader-utils "^1.2.3"
+    mkdirp "^0.5.1"
+    neo-async "^2.6.1"
+    schema-utils "^2.0.0"
+
+call-me-maybe@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
+caller-callsite@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+  integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+  dependencies:
+    callsites "^2.0.0"
+
+caller-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+  integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+  dependencies:
+    caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+  integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camel-case@3.0.x:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
+  integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
+  dependencies:
+    no-case "^2.2.0"
+    upper-case "^1.1.1"
+
+camelcase-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+  integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc=
+  dependencies:
+    camelcase "^2.0.0"
+    map-obj "^1.0.0"
+
+camelcase@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+  integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061:
+  version "1.0.30001078"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001078.tgz#e1b6e2ae327b6a1ec11f65ec7a0dde1e7093074c"
+  integrity sha512-sF12qXe9VMm32IEf/+NDvmTpwJaaU7N1igpiH2FdI4DyABJSsOqG3ZAcFvszLkoLoo1y6VJLMYivukUAxaMASw==
+
+case-sensitive-paths-webpack-plugin@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7"
+  integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.1, chalk@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+  dependencies:
+    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"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chardet@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+  integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
+check-types@^8.0.3:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
+  integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
+
+chokidar@^2.1.8:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
+  integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
+  dependencies:
+    anymatch "^2.0.0"
+    async-each "^1.0.1"
+    braces "^2.3.2"
+    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"
+  optionalDependencies:
+    fsevents "^1.2.7"
+
+chokidar@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8"
+  integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.4.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
+chownr@^1.1.1, chownr@^1.1.2:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chrome-trace-event@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
+  integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
+  dependencies:
+    tslib "^1.9.0"
+
+ci-info@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+  integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+  integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+clean-css@4.2.x:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
+  integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
+  dependencies:
+    source-map "~0.6.0"
+
+clean-stack@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+  integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cli-cursor@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+  integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+  dependencies:
+    restore-cursor "^2.0.0"
+
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+  dependencies:
+    restore-cursor "^3.1.0"
+
+cli-highlight@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b"
+  integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==
+  dependencies:
+    chalk "^3.0.0"
+    highlight.js "^9.6.0"
+    mz "^2.4.0"
+    parse5 "^5.1.1"
+    parse5-htmlparser2-tree-adapter "^5.1.1"
+    yargs "^15.0.0"
+
+cli-spinners@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5"
+  integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w==
+
+cli-width@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
+  integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
+
+clipboardy@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
+  integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==
+  dependencies:
+    arch "^2.1.1"
+    execa "^1.0.0"
+    is-wsl "^2.1.1"
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
+
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+clone@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
+coa@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+  integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+  dependencies:
+    "@types/q" "^1.5.1"
+    chalk "^2.4.1"
+    q "^1.1.2"
+
+code-point-at@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+  integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
+color-convert@^1.9.0, color-convert@^1.9.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.5.2:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
+  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
+  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  dependencies:
+    color-convert "^1.9.1"
+    color-string "^1.5.2"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@2.17.x:
+  version "2.17.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+  integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
+commander@^2.18.0, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@~2.19.0:
+  version "2.19.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+  integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
+
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+  integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-emitter@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+compressible@~2.0.16:
+  version "2.0.18"
+  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+  integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+  dependencies:
+    mime-db ">= 1.43.0 < 2"
+
+compression@^1.7.4:
+  version "1.7.4"
+  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+  integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+  dependencies:
+    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@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^1.5.0:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+confusing-browser-globals@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd"
+  integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==
+
+connect-history-api-fallback@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
+  integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+
+console-browserify@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+  integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+  integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+consolidate@^0.15.1:
+  version "0.15.1"
+  resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
+  integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
+  dependencies:
+    bluebird "^3.1.1"
+
+constants-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+  integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+contains-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+  integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+
+content-disposition@0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
+  integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+  dependencies:
+    safe-buffer "5.1.2"
+
+content-type@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+convert-source-map@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
+cookie-signature@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+  integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
+  integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+
+copy-concurrently@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+  integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+  dependencies:
+    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@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+copy-webpack-plugin@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz#5481a03dea1123d88a988c6ff8b78247214f0b88"
+  integrity sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==
+  dependencies:
+    cacache "^12.0.3"
+    find-cache-dir "^2.1.0"
+    glob-parent "^3.1.0"
+    globby "^7.1.1"
+    is-glob "^4.0.1"
+    loader-utils "^1.2.3"
+    minimatch "^3.0.4"
+    normalize-path "^3.0.0"
+    p-limit "^2.2.1"
+    schema-utils "^1.0.0"
+    serialize-javascript "^2.1.2"
+    webpack-log "^2.0.0"
+
+core-js-compat@^3.6.2, core-js-compat@^3.6.5:
+  version "3.6.5"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c"
+  integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==
+  dependencies:
+    browserslist "^4.8.5"
+    semver "7.0.0"
+
+core-js@^3.6.5:
+  version "3.6.5"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
+  integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^5.0.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
+  integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
+  dependencies:
+    import-fresh "^2.0.0"
+    is-directory "^0.3.1"
+    js-yaml "^3.13.1"
+    parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
+  integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==
+  dependencies:
+    bn.js "^4.1.0"
+    elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+  integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+  dependencies:
+    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@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+  integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+  dependencies:
+    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@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+  integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI=
+  dependencies:
+    lru-cache "^4.0.1"
+    which "^1.2.9"
+
+cross-spawn@^5.0.1:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+  integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+  dependencies:
+    lru-cache "^4.0.1"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cross-spawn@^7.0.0:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+crypto-browserify@^3.11.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+  integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+  dependencies:
+    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-color-names@0.0.4, css-color-names@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+  integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+
+css-declaration-sorter@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22"
+  integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+  dependencies:
+    postcss "^7.0.1"
+    timsort "^0.3.0"
+
+css-loader@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf"
+  integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==
+  dependencies:
+    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.27"
+    postcss-modules-extract-imports "^2.0.0"
+    postcss-modules-local-by-default "^3.0.2"
+    postcss-modules-scope "^2.2.0"
+    postcss-modules-values "^3.0.0"
+    postcss-value-parser "^4.0.3"
+    schema-utils "^2.6.6"
+    semver "^6.3.0"
+
+css-select-base-adapter@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+  integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+  dependencies:
+    boolbase "~1.0.0"
+    css-what "2.1"
+    domutils "1.5.1"
+    nth-check "~1.0.1"
+
+css-select@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^3.2.1"
+    domutils "^1.7.0"
+    nth-check "^1.0.2"
+
+css-tree@1.0.0-alpha.37:
+  version "1.0.0-alpha.37"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
+  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
+  dependencies:
+    mdn-data "2.0.4"
+    source-map "^0.6.1"
+
+css-tree@1.0.0-alpha.39:
+  version "1.0.0-alpha.39"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
+  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+  dependencies:
+    mdn-data "2.0.6"
+    source-map "^0.6.1"
+
+css-what@2.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+  integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+css-what@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
+  integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
+  integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+  dependencies:
+    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@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f"
+  integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+
+cssnano-util-get-match@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d"
+  integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+
+cssnano-util-raw-cache@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282"
+  integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+  dependencies:
+    postcss "^7.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
+  integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+
+cssnano@^4.0.0, cssnano@^4.1.10:
+  version "4.1.10"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
+  integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+  dependencies:
+    cosmiconfig "^5.0.0"
+    cssnano-preset-default "^4.0.7"
+    is-resolvable "^1.0.0"
+    postcss "^7.0.0"
+
+csso@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
+  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+  dependencies:
+    css-tree "1.0.0-alpha.39"
+
+currently-unhandled@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+  integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
+  dependencies:
+    array-find-index "^1.0.1"
+
+cyclist@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
+  integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+dayjs@^1.8.28:
+  version "1.8.28"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.28.tgz#37aa6201df483d089645cb6c8f6cef6f0c4dbc07"
+  integrity sha512-ccnYgKC0/hPSGXxj7Ju6AV/BP4HUkXC2u15mikXT5mX9YorEaoi1bEKOmAqdkJHN4EEkmAf97SpH66Try5Mbeg==
+
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
+
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@=3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  dependencies:
+    ms "^2.1.1"
+
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+  dependencies:
+    ms "^2.1.1"
+
+decamelize@^1.1.2, decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-equal@^1.0.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+  integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+  dependencies:
+    is-arguments "^1.0.4"
+    is-date-object "^1.0.1"
+    is-regex "^1.0.4"
+    object-is "^1.0.1"
+    object-keys "^1.1.1"
+    regexp.prototype.flags "^1.2.0"
+
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+deepmerge@^1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
+  integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
+
+default-gateway@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
+  integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==
+  dependencies:
+    execa "^1.0.0"
+    ip-regex "^2.1.0"
+
+default-gateway@^5.0.5:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-5.0.5.tgz#4fd6bd5d2855d39b34cc5a59505486e9aafc9b10"
+  integrity sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA==
+  dependencies:
+    execa "^3.3.0"
+
+defaults@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+  integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
+  dependencies:
+    clone "^1.0.2"
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
+del@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
+  integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==
+  dependencies:
+    "@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@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+  integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+  dependencies:
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detect-node@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
+  integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+
+diffie-hellman@^5.0.0:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+  integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+  dependencies:
+    bn.js "^4.1.0"
+    miller-rabin "^4.0.0"
+    randombytes "^2.0.0"
+
+dir-glob@^2.0.0, dir-glob@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+  integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
+  dependencies:
+    path-type "^3.0.0"
+
+dns-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
+  integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
+
+dns-packet@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
+  integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
+  dependencies:
+    ip "^1.1.0"
+    safe-buffer "^5.0.1"
+
+dns-txt@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
+  integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=
+  dependencies:
+    buffer-indexof "^1.0.0"
+
+doctrine@1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+  integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
+  dependencies:
+    esutils "^2.0.2"
+    isarray "^1.0.0"
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+dom-converter@^0.2:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
+  integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
+  dependencies:
+    utila "~0.4"
+
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+domain-browser@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+  integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+domelementtype@1, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
+domutils@1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^1.5.1, domutils@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+dot-prop@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
+  integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+  dependencies:
+    is-obj "^2.0.0"
+
+dotenv-expand@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
+  integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+
+dotenv@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
+  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+
+duplexer@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+  integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
+  integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
+  dependencies:
+    end-of-stream "^1.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+    stream-shift "^1.0.0"
+
+easy-stack@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
+  integrity sha1-EskbMIWjfwuqM26UhurEv5Tj54g=
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+ejs@^2.6.1:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
+  integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
+
+electron-to-chromium@^1.3.413:
+  version "1.3.462"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.462.tgz#82087404c20ed664963ececab23337ac7a150e21"
+  integrity sha512-HST/xWLOeA0LGUhxBqvcPDDUGHjB6rn99VBgPWmaHv+zqwXgOaZO5RnRcd5owjRE7nh+z1c0SwcK8qP8o7sofg==
+
+elliptic@^6.0.0, elliptic@^6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
+  integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
+  dependencies:
+    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"
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emojis-list@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+  integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+enhanced-resolve@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66"
+  integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==
+  dependencies:
+    graceful-fs "^4.1.2"
+    memory-fs "^0.5.0"
+    tapable "^1.0.0"
+
+enhanced-resolve@~0.9.0:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e"
+  integrity sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=
+  dependencies:
+    graceful-fs "^4.1.2"
+    memory-fs "^0.2.0"
+    tapable "^0.1.8"
+
+entities@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
+  integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
+
+errno@^0.1.3, errno@~0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+  integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+  dependencies:
+    prr "~1.0.1"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+error-stack-parser@^2.0.0:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8"
+  integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==
+  dependencies:
+    stackframe "^1.1.1"
+
+es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
+  version "1.17.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+eslint-config-airbnb-base@^14.0.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.1.0.tgz#2ba4592dd6843258221d9bff2b6831bd77c874e4"
+  integrity sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw==
+  dependencies:
+    confusing-browser-globals "^1.0.9"
+    object.assign "^4.1.0"
+    object.entries "^1.1.1"
+
+eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404"
+  integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
+  dependencies:
+    debug "^2.6.9"
+    resolve "^1.13.1"
+
+eslint-import-resolver-webpack@^0.11.1:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.11.1.tgz#fcf1fd57a775f51e18f442915f85dd6ba45d2f26"
+  integrity sha512-eK3zR7xVQR/MaoBWwGuD+CULYVuqe5QFlDukman71aI6IboCGzggDUohHNfu1ZeBnbHcUHJc0ywWoXUBNB6qdg==
+  dependencies:
+    array-find "^1.0.0"
+    debug "^2.6.8"
+    enhanced-resolve "~0.9.0"
+    find-root "^1.1.0"
+    has "^1.0.1"
+    interpret "^1.0.0"
+    lodash "^4.17.4"
+    node-libs-browser "^1.0.0 || ^2.0.0"
+    resolve "^1.10.0"
+    semver "^5.3.0"
+
+eslint-loader@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337"
+  integrity sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==
+  dependencies:
+    loader-fs-cache "^1.0.0"
+    loader-utils "^1.0.2"
+    object-assign "^4.0.1"
+    object-hash "^1.1.4"
+    rimraf "^2.6.1"
+
+eslint-module-utils@^2.4.1:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
+  integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
+  dependencies:
+    debug "^2.6.9"
+    pkg-dir "^2.0.0"
+
+eslint-plugin-import@^2.18.2, eslint-plugin-import@^2.20.2:
+  version "2.20.2"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d"
+  integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg==
+  dependencies:
+    array-includes "^3.0.3"
+    array.prototype.flat "^1.2.1"
+    contains-path "^0.1.0"
+    debug "^2.6.9"
+    doctrine "1.5.0"
+    eslint-import-resolver-node "^0.3.2"
+    eslint-module-utils "^2.4.1"
+    has "^1.0.3"
+    minimatch "^3.0.4"
+    object.values "^1.1.0"
+    read-pkg-up "^2.0.0"
+    resolve "^1.12.0"
+
+eslint-plugin-vue@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz#27fecd9a3a24789b0f111ecdd540a9e56198e0fe"
+  integrity sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==
+  dependencies:
+    natural-compare "^1.4.0"
+    semver "^5.6.0"
+    vue-eslint-parser "^7.0.0"
+
+eslint-scope@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
+  integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-scope@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5"
+  integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-utils@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
+  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa"
+  integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==
+
+eslint@^6.7.2:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
+  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    ajv "^6.10.0"
+    chalk "^2.1.0"
+    cross-spawn "^6.0.5"
+    debug "^4.0.1"
+    doctrine "^3.0.0"
+    eslint-scope "^5.0.0"
+    eslint-utils "^1.4.3"
+    eslint-visitor-keys "^1.1.0"
+    espree "^6.1.2"
+    esquery "^1.0.1"
+    esutils "^2.0.2"
+    file-entry-cache "^5.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^5.0.0"
+    globals "^12.1.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    inquirer "^7.0.0"
+    is-glob "^4.0.0"
+    js-yaml "^3.13.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.3.0"
+    lodash "^4.17.14"
+    minimatch "^3.0.4"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    optionator "^0.8.3"
+    progress "^2.0.0"
+    regexpp "^2.0.1"
+    semver "^6.1.2"
+    strip-ansi "^5.2.0"
+    strip-json-comments "^3.0.1"
+    table "^5.2.3"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+espree@^6.1.2, espree@^6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
+  integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-jsx "^5.2.0"
+    eslint-visitor-keys "^1.1.0"
+
+esprima@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.0.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
+  integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+  integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
+  dependencies:
+    estraverse "^4.1.0"
+
+estraverse@^4.1.0, estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
+  integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+event-pubsub@4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e"
+  integrity sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==
+
+eventemitter3@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
+  integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
+
+eventemitter3@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
+  integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
+
+events@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
+  integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
+
+eventsource@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
+  integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+  dependencies:
+    original "^1.0.0"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+  integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+  dependencies:
+    md5.js "^1.3.4"
+    safe-buffer "^5.1.1"
+
+execa@^0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
+  integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=
+  dependencies:
+    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"
+
+execa@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+  integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+  dependencies:
+    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"
+
+execa@^3.3.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
+  integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    p-finally "^2.0.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+  dependencies:
+    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"
+
+express@^4.16.3, express@^4.17.1:
+  version "4.17.1"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
+  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+  dependencies:
+    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"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
+extend@^3.0.2, extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+external-editor@^3.0.3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+  integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+  dependencies:
+    chardet "^0.7.0"
+    iconv-lite "^0.4.24"
+    tmp "^0.0.33"
+
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+  dependencies:
+    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"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-diff@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
+  integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==
+
+fast-diff@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
+fast-glob@^2.2.6:
+  version "2.2.7"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+  integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+  dependencies:
+    "@mrmlnc/readdir-enhanced" "^2.2.1"
+    "@nodelib/fs.stat" "^1.1.2"
+    glob-parent "^3.1.0"
+    is-glob "^4.0.0"
+    merge2 "^1.2.3"
+    micromatch "^3.1.10"
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+faye-websocket@^0.10.0:
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+  integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
+  dependencies:
+    websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.1:
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
+  integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==
+  dependencies:
+    websocket-driver ">=0.5.1"
+
+figgy-pudding@^3.5.1:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
+  integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
+
+figures@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+  dependencies:
+    escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+  integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+  dependencies:
+    flat-cache "^2.0.1"
+
+file-loader@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.3.0.tgz#780f040f729b3d18019f20605f723e844b8a58af"
+  integrity sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==
+  dependencies:
+    loader-utils "^1.2.3"
+    schema-utils "^2.5.0"
+
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+filesize@^3.6.1:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+  integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+finalhandler@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+  integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+  dependencies:
+    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@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+  integrity sha1-yN765XyKUqinhPnjHFfHQumToLk=
+  dependencies:
+    commondir "^1.0.1"
+    mkdirp "^0.5.1"
+    pkg-dir "^1.0.0"
+
+find-cache-dir@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
+  integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^2.0.0"
+    pkg-dir "^3.0.0"
+
+find-cache-dir@^3.0.0, find-cache-dir@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880"
+  integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^3.0.2"
+    pkg-dir "^4.1.0"
+
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
+find-up@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+  integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+  dependencies:
+    path-exists "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+  dependencies:
+    locate-path "^2.0.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+find-up@^4.0.0, find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+flat-cache@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+  integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+  dependencies:
+    flatted "^2.0.0"
+    rimraf "2.6.3"
+    write "1.0.3"
+
+flatted@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+
+flush-write-stream@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
+  integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
+  dependencies:
+    inherits "^2.0.3"
+    readable-stream "^2.3.6"
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+follow-redirects@^1.0.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb"
+  integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==
+  dependencies:
+    debug "^3.0.0"
+
+for-in@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+forwarded@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+  integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+  dependencies:
+    map-cache "^0.2.2"
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+from2@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+  integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+  dependencies:
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+
+fs-extra@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+  integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs-minipass@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+  dependencies:
+    minipass "^3.0.0"
+
+fs-write-stream-atomic@^1.0.8:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+  integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+  dependencies:
+    graceful-fs "^4.1.2"
+    iferr "^0.1.5"
+    imurmurhash "^0.1.4"
+    readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
+  dependencies:
+    bindings "^1.5.0"
+    nan "^2.12.1"
+
+fsevents@~2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
+fstream@^1.0.0, fstream@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+  integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+gauge@~2.7.3:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+  integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+  dependencies:
+    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@^1.0.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
+  integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
+  dependencies:
+    globule "^1.0.0"
+
+gensync@^1.0.0-beta.1:
+  version "1.0.0-beta.1"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+
+get-caller-file@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-stdin@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+  integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
+
+get-stream@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+  integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
+
+get-stream@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+  dependencies:
+    pump "^3.0.0"
+
+get-stream@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
+  integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
+  dependencies:
+    pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
+glob-parent@^5.0.0, glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+  integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    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"
+
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+globals@^12.1.0:
+  version "12.4.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
+  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+  dependencies:
+    type-fest "^0.8.1"
+
+globby@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+  integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+  dependencies:
+    array-union "^1.0.1"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+globby@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
+  integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA=
+  dependencies:
+    array-union "^1.0.1"
+    dir-glob "^2.0.0"
+    glob "^7.1.2"
+    ignore "^3.3.5"
+    pify "^3.0.0"
+    slash "^1.0.0"
+
+globby@^9.2.0:
+  version "9.2.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
+  integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==
+  dependencies:
+    "@types/glob" "^7.1.1"
+    array-union "^1.0.2"
+    dir-glob "^2.2.2"
+    fast-glob "^2.2.6"
+    glob "^7.1.3"
+    ignore "^4.0.3"
+    pify "^4.0.1"
+    slash "^2.0.0"
+
+globule@^1.0.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.1.tgz#90a25338f22b7fbeb527cee63c629aea754d33b9"
+  integrity sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==
+  dependencies:
+    glob "~7.1.1"
+    lodash "~4.17.12"
+    minimatch "~3.0.2"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
+gzip-size@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
+  integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==
+  dependencies:
+    duplexer "^0.1.1"
+    pify "^4.0.1"
+
+handle-thing@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
+  integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-unicode@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+  integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+  dependencies:
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
+
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+  dependencies:
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
+
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.1, has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+hash-base@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
+  integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
+  dependencies:
+    inherits "^2.0.4"
+    readable-stream "^3.6.0"
+    safe-buffer "^5.2.0"
+
+hash-sum@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+  integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
+
+hash-sum@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a"
+  integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+  integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+  dependencies:
+    inherits "^2.0.3"
+    minimalistic-assert "^1.0.1"
+
+he@1.2.x, he@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hex-color-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
+  integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+
+highlight.js@^9.6.0:
+  version "9.18.1"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
+  integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==
+
+hmac-drbg@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+  integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+  dependencies:
+    hash.js "^1.0.3"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.1"
+
+hoopy@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
+  integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==
+
+hosted-git-info@^2.1.4:
+  version "2.8.8"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
+  integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+
+hpack.js@^2.1.6:
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+  integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
+  dependencies:
+    inherits "^2.0.1"
+    obuf "^1.0.0"
+    readable-stream "^2.0.1"
+    wbuf "^1.1.0"
+
+hsl-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
+  integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+
+hsla-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
+  integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+
+html-comment-regex@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
+  integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+
+html-entities@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44"
+  integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==
+
+html-minifier@^3.2.3:
+  version "3.5.21"
+  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
+  integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
+  dependencies:
+    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"
+
+html-tags@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b"
+  integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=
+
+html-webpack-plugin@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
+  integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s=
+  dependencies:
+    html-minifier "^3.2.3"
+    loader-utils "^0.2.16"
+    lodash "^4.17.3"
+    pretty-error "^2.0.2"
+    tapable "^1.0.0"
+    toposort "^1.0.0"
+    util.promisify "1.0.0"
+
+htmlparser2@^3.3.0:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    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@^1.2.7:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+  integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
+
+http-errors@1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
+  integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-errors@~1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.0"
+    statuses ">= 1.4.0 < 2"
+
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-parser-js@>=0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77"
+  integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==
+
+http-proxy-middleware@0.19.1:
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a"
+  integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==
+  dependencies:
+    http-proxy "^1.17.0"
+    is-glob "^4.0.0"
+    lodash "^4.17.11"
+    micromatch "^3.1.10"
+
+http-proxy@^1.17.0:
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+  dependencies:
+    eventemitter3 "^4.0.0"
+    follow-redirects "^1.0.0"
+    requires-port "^1.0.0"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+  integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+
+humps@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
+  integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=
+
+iconv-lite@0.4.24, iconv-lite@^0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+icss-utils@^4.0.0, icss-utils@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
+  integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
+  dependencies:
+    postcss "^7.0.14"
+
+ieee754@^1.1.4:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+iferr@^0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+  integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore@^3.3.5:
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
+  integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
+
+ignore@^4.0.3, ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+import-cwd@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
+  integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=
+  dependencies:
+    import-from "^2.1.0"
+
+import-fresh@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+  integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+  dependencies:
+    caller-path "^2.0.0"
+    resolve-from "^3.0.0"
+
+import-fresh@^3.0.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
+  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+import-from@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
+  integrity sha1-M1238qev/VOqpHHUuAId7ja387E=
+  dependencies:
+    resolve-from "^3.0.0"
+
+import-local@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+  integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+  dependencies:
+    pkg-dir "^3.0.0"
+    resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+in-publish@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c"
+  integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==
+
+indent-string@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+  integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
+  dependencies:
+    repeating "^2.0.0"
+
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+infer-owner@^1.0.3, infer-owner@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+  integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+  integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inquirer@^7.0.0, inquirer@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
+  integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    chalk "^3.0.0"
+    cli-cursor "^3.1.0"
+    cli-width "^2.0.0"
+    external-editor "^3.0.3"
+    figures "^3.0.0"
+    lodash "^4.17.15"
+    mute-stream "0.0.8"
+    run-async "^2.4.0"
+    rxjs "^6.5.3"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+    through "^2.3.6"
+
+internal-ip@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
+  integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==
+  dependencies:
+    default-gateway "^4.2.0"
+    ipaddr.js "^1.9.0"
+
+interpret@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+
+invariant@^2.2.2, invariant@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+  dependencies:
+    loose-envify "^1.0.0"
+
+ip-regex@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+  integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+
+ip@^1.1.0, ip@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+  integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@1.9.1, ipaddr.js@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-absolute-url@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+  integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+
+is-absolute-url@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-arguments@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+  integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-binary-path@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+  integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+  dependencies:
+    binary-extensions "^1.0.0"
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
+  integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+
+is-ci@^1.0.10:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+  integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+  dependencies:
+    ci-info "^1.5.0"
+
+is-color-stop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
+  integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+  dependencies:
+    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@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+  integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+  integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-docker@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
+  integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+  dependencies:
+    is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-finite@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
+  integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
+
+is-fullwidth-code-point@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+  integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-obj@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
+  integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+
+is-path-cwd@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
+  integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
+
+is-path-in-cwd@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb"
+  integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==
+  dependencies:
+    is-path-inside "^2.1.0"
+
+is-path-inside@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2"
+  integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==
+  dependencies:
+    path-is-inside "^1.0.2"
+
+is-plain-obj@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+  integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-regex@^1.0.4, is-regex@^1.0.5:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
+  integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-resolvable@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+  integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
+is-string@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+  integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
+is-svg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
+  integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+  dependencies:
+    html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-utf8@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+  integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+
+is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+is-wsl@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+  dependencies:
+    is-docker "^2.0.0"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+javascript-stringify@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.0.1.tgz#6ef358035310e35d667c675ed63d3eb7c1aa19e5"
+  integrity sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow==
+
+jest-worker@^25.4.0:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.5.0.tgz#2611d071b79cea0f43ee57a3d118593ac1547db1"
+  integrity sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==
+  dependencies:
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+js-base64@^2.1.8:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209"
+  integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==
+
+js-message@1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.5.tgz#2300d24b1af08e89dd095bc1a4c9c9cfcb892d15"
+  integrity sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=
+
+js-queue@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/js-queue/-/js-queue-2.0.0.tgz#362213cf860f468f0125fc6c96abc1742531f948"
+  integrity sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=
+  dependencies:
+    easy-stack "^1.0.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.1:
+  version "3.14.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
+  integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsesc@^2.5.1:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+  integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json3@^3.3.2:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
+  integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
+
+json5@^0.5.0:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+  integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+killable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
+  integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+  integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+launch-editor-middleware@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/launch-editor-middleware/-/launch-editor-middleware-2.2.1.tgz#e14b07e6c7154b0a4b86a0fd345784e45804c157"
+  integrity sha512-s0UO2/gEGiCgei3/2UN3SMuUj1phjQN8lcpnvgLSz26fAzNWPQ6Nf/kF5IFClnfU2ehp6LrmKdMU/beveO+2jg==
+  dependencies:
+    launch-editor "^2.2.1"
+
+launch-editor@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.2.1.tgz#871b5a3ee39d6680fcc26d37930b6eeda89db0ca"
+  integrity sha512-On+V7K2uZK6wK7x691ycSUbLD/FyKKelArkbaAMSSJU8JmqmhwN2+mnJDNINuJWSrh2L0kDk+ZQtbC/gOWUwLw==
+  dependencies:
+    chalk "^2.3.0"
+    shell-quote "^1.6.1"
+
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levenary@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
+  integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+  dependencies:
+    leven "^3.1.0"
+
+levn@^0.3.0, levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
+load-json-file@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+  integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+    strip-bom "^2.0.0"
+
+load-json-file@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+  integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    strip-bom "^3.0.0"
+
+loader-fs-cache@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz#f08657646d607078be2f0a032f8bd69dd6f277d9"
+  integrity sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==
+  dependencies:
+    find-cache-dir "^0.1.1"
+    mkdirp "^0.5.1"
+
+loader-runner@^2.3.1, loader-runner@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
+  integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
+
+loader-utils@^0.2.16:
+  version "0.2.17"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
+  integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=
+  dependencies:
+    big.js "^3.1.3"
+    emojis-list "^2.0.0"
+    json5 "^0.5.0"
+    object-assign "^4.0.1"
+
+loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^1.0.1"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+  integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+  dependencies:
+    p-locate "^2.0.0"
+    path-exists "^3.0.0"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.defaultsdeep@^4.6.1:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
+  integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.kebabcase@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
+  integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
+
+lodash.mapvalues@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
+  integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.transform@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"
+  integrity sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@~4.17.12:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+log-symbols@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+  integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
+  dependencies:
+    chalk "^2.0.1"
+
+loglevel@^1.6.8:
+  version "1.6.8"
+  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
+  integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
+
+loose-envify@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+loud-rejection@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+  integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
+  dependencies:
+    currently-unhandled "^0.4.1"
+    signal-exit "^3.0.0"
+
+lower-case@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
+  integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
+
+lru-cache@^4.0.1, lru-cache@^4.1.2:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+  dependencies:
+    pseudomap "^1.0.2"
+    yallist "^2.1.2"
+
+lru-cache@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
+make-dir@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+  integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+  dependencies:
+    pify "^4.0.1"
+    semver "^5.6.0"
+
+make-dir@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+  integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+  dependencies:
+    semver "^6.0.0"
+
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+  dependencies:
+    object-visit "^1.0.0"
+
+md5.js@^1.3.4:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+  integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+mdn-data@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
+  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+
+mdn-data@2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
+  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
+
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+  integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+memory-fs@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
+  integrity sha1-8rslNovBIeORwlIN6Slpyu4KApA=
+
+memory-fs@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+  integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+  dependencies:
+    errno "^0.1.3"
+    readable-stream "^2.0.1"
+
+memory-fs@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
+  integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
+  dependencies:
+    errno "^0.1.3"
+    readable-stream "^2.0.1"
+
+meow@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+  integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
+  dependencies:
+    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"
+
+merge-descriptors@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+  integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+merge-source-map@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
+  integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==
+  dependencies:
+    source-map "^0.6.1"
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.2.3:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+methods@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+  dependencies:
+    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"
+
+miller-rabin@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+  integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+  dependencies:
+    bn.js "^4.0.0"
+    brorand "^1.0.1"
+
+mime-db@1.44.0, "mime-db@>= 1.43.0 < 2":
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
+  version "2.1.27"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+  dependencies:
+    mime-db "1.44.0"
+
+mime@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mime@^2.4.4:
+  version "2.4.6"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
+  integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
+
+mimic-fn@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+  integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mini-css-extract-plugin@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
+  integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==
+  dependencies:
+    loader-utils "^1.1.0"
+    normalize-url "1.9.1"
+    schema-utils "^1.0.0"
+    webpack-sources "^1.1.0"
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+  integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+minimatch@^3.0.4, minimatch@~3.0.2:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+minipass-collect@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
+  integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-flush@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
+  integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-pipeline@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34"
+  integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass@^3.0.0, minipass@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
+  integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
+  dependencies:
+    yallist "^4.0.0"
+
+mississippi@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+  integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+  dependencies:
+    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@^1.2.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+  dependencies:
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
+
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
+  dependencies:
+    minimist "^1.2.5"
+
+move-concurrently@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+  integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+  dependencies:
+    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@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+  integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+multicast-dns-service-types@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
+  integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=
+
+multicast-dns@^6.0.1:
+  version "6.2.3"
+  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
+  integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
+  dependencies:
+    dns-packet "^1.3.1"
+    thunky "^1.0.2"
+
+mute-stream@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+  integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+
+mz@^2.4.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+  dependencies:
+    any-promise "^1.0.0"
+    object-assign "^4.0.1"
+    thenify-all "^1.0.0"
+
+nan@^2.12.1, nan@^2.13.2:
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
+  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+
+nanomatch@^1.2.9:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+  dependencies:
+    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@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+negotiator@0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
+  integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+
+neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
+  integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+no-case@^2.2.0:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
+  integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
+  dependencies:
+    lower-case "^1.1.1"
+
+node-forge@0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
+  integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
+
+node-gyp@^3.8.0:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+  integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+  dependencies:
+    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"
+
+node-ipc@^9.1.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.1.1.tgz#4e245ed6938e65100e595ebc5dc34b16e8dd5d69"
+  integrity sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==
+  dependencies:
+    event-pubsub "4.3.0"
+    js-message "1.0.5"
+    js-queue "2.0.0"
+
+"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
+  integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+  dependencies:
+    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"
+
+node-releases@^1.1.53:
+  version "1.1.58"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935"
+  integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==
+
+node-sass@^4.14.1:
+  version "4.14.1"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5"
+  integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==
+  dependencies:
+    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.15"
+    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.5"
+    stdout-stream "^1.4.0"
+    "true-case-path" "^1.0.2"
+
+"nopt@2 || 3":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+  integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+  dependencies:
+    abbrev "1"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+  dependencies:
+    hosted-git-info "^2.1.4"
+    resolve "^1.10.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
+normalize-path@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
+  integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=
+
+normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize-url@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
+  integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
+  dependencies:
+    object-assign "^4.0.1"
+    prepend-http "^1.0.0"
+    query-string "^4.1.0"
+    sort-keys "^1.0.0"
+
+normalize-url@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
+  integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+  dependencies:
+    path-key "^2.0.0"
+
+npm-run-path@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+  integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+  dependencies:
+    are-we-there-yet "~1.1.2"
+    console-control-strings "~1.1.0"
+    gauge "~2.7.3"
+    set-blocking "~2.0.0"
+
+nth-check@^1.0.2, nth-check@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+number-is-nan@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-hash@^1.1.4:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
+  integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-is@^1.0.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
+  integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+  dependencies:
+    isobject "^3.0.0"
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.entries@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
+  integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    has "^1.0.3"
+
+object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+  dependencies:
+    isobject "^3.0.1"
+
+object.values@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+obuf@^1.0.0, obuf@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+  integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+
+on-finished@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  dependencies:
+    ee-first "1.1.1"
+
+on-headers@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+  integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+  integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+  dependencies:
+    mimic-fn "^1.0.0"
+
+onetime@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
+  integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+open@^6.3.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9"
+  integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==
+  dependencies:
+    is-wsl "^1.1.0"
+
+opener@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
+  integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
+
+opn@^5.5.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+  integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+  dependencies:
+    is-wsl "^1.1.0"
+
+optionator@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+ora@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
+  integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==
+  dependencies:
+    chalk "^2.4.2"
+    cli-cursor "^2.1.0"
+    cli-spinners "^2.0.0"
+    log-symbols "^2.2.0"
+    strip-ansi "^5.2.0"
+    wcwidth "^1.0.1"
+
+original@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
+  integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==
+  dependencies:
+    url-parse "^1.4.3"
+
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+  integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+os-homedir@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@0:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+  dependencies:
+    os-homedir "^1.0.0"
+    os-tmpdir "^1.0.0"
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-finally@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
+  integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==
+
+p-limit@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+  integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+  dependencies:
+    p-try "^1.0.0"
+
+p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1, p-limit@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+  integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+  dependencies:
+    p-limit "^1.1.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-map@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+p-map@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
+  integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
+  dependencies:
+    aggregate-error "^3.0.0"
+
+p-retry@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328"
+  integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==
+  dependencies:
+    retry "^0.12.0"
+
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+pako@~1.0.5:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parallel-transform@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
+  integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
+  dependencies:
+    cyclist "^1.0.1"
+    inherits "^2.0.3"
+    readable-stream "^2.1.5"
+
+param-case@2.1.x:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
+  integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
+  dependencies:
+    no-case "^2.2.0"
+
+parchment@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"
+  integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-asn1@^5.0.0, parse-asn1@^5.1.5:
+  version "5.1.5"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e"
+  integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==
+  dependencies:
+    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@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+  integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+  dependencies:
+    error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
+parse-json@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
+  integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+    lines-and-columns "^1.1.6"
+
+parse5-htmlparser2-tree-adapter@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz#e8c743d4e92194d5293ecde2b08be31e67461cbc"
+  integrity sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==
+  dependencies:
+    parse5 "^5.1.1"
+
+parse5@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+
+parseurl@~1.3.2, parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
+  integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+  integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+  dependencies:
+    pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+  integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^2.0.0, path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+  integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+path-type@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+  integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
+  dependencies:
+    graceful-fs "^4.1.2"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+path-type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+  integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
+  dependencies:
+    pify "^2.0.0"
+
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+  dependencies:
+    pify "^3.0.0"
+
+pbkdf2@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
+  integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
+  dependencies:
+    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@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+  integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinkie-promise@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+  integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+  dependencies:
+    pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+  integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+  integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
+  dependencies:
+    find-up "^1.0.0"
+
+pkg-dir@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+  integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
+  dependencies:
+    find-up "^2.1.0"
+
+pkg-dir@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+  integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+  dependencies:
+    find-up "^3.0.0"
+
+pkg-dir@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
+  integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
+  dependencies:
+    find-up "^2.1.0"
+
+pnp-webpack-plugin@^1.6.4:
+  version "1.6.4"
+  resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
+  integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==
+  dependencies:
+    ts-pnp "^1.1.6"
+
+portfinder@^1.0.26:
+  version "1.0.26"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70"
+  integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==
+  dependencies:
+    async "^2.6.2"
+    debug "^3.1.1"
+    mkdirp "^0.5.1"
+
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-calc@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1"
+  integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==
+  dependencies:
+    postcss "^7.0.27"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381"
+  integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+  dependencies:
+    browserslist "^4.0.0"
+    color "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
+  integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-discard-comments@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033"
+  integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-duplicates@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb"
+  integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-empty@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765"
+  integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-overridden@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57"
+  integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-load-config@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003"
+  integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==
+  dependencies:
+    cosmiconfig "^5.0.0"
+    import-cwd "^2.0.0"
+
+postcss-loader@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d"
+  integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==
+  dependencies:
+    loader-utils "^1.1.0"
+    postcss "^7.0.0"
+    postcss-load-config "^2.0.0"
+    schema-utils "^1.0.0"
+
+postcss-merge-longhand@^4.0.11:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
+  integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+  dependencies:
+    css-color-names "0.0.4"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650"
+  integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+  dependencies:
+    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"
+
+postcss-minify-font-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6"
+  integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471"
+  integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    is-color-stop "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874"
+  integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+  dependencies:
+    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"
+
+postcss-minify-selectors@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8"
+  integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+  integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+  dependencies:
+    postcss "^7.0.5"
+
+postcss-modules-local-by-default@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
+  integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
+  dependencies:
+    icss-utils "^4.1.1"
+    postcss "^7.0.16"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.0"
+
+postcss-modules-scope@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
+  integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
+  dependencies:
+    postcss "^7.0.6"
+    postcss-selector-parser "^6.0.0"
+
+postcss-modules-values@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
+  integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
+  dependencies:
+    icss-utils "^4.0.0"
+    postcss "^7.0.6"
+
+postcss-normalize-charset@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4"
+  integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-normalize-display-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a"
+  integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f"
+  integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c"
+  integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c"
+  integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+  dependencies:
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9"
+  integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb"
+  integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1"
+  integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+  dependencies:
+    is-absolute-url "^2.0.0"
+    normalize-url "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
+  integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee"
+  integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-reduce-initial@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df"
+  integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-api "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+
+postcss-reduce-transforms@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29"
+  integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-selector-parser@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270"
+  integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==
+  dependencies:
+    dot-prop "^5.2.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-svgo@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
+  integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+  dependencies:
+    is-svg "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
+  integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    postcss "^7.0.0"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+  integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.5, postcss@^7.0.6:
+  version "7.0.32"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
+  integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+prepend-http@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+  integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
+
+prettier@^1.18.2:
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
+  integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
+
+pretty-error@^2.0.2:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
+  integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=
+  dependencies:
+    renderkid "^2.0.1"
+    utila "~0.4"
+
+private@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+promise-inflight@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+proxy-addr@~2.0.5:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
+  integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
+  dependencies:
+    forwarded "~0.1.2"
+    ipaddr.js "1.9.1"
+
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+  integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+public-encrypt@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+  integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+  dependencies:
+    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"
+
+pump@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+  integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+pumpify@^1.3.3:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+  integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+  dependencies:
+    duplexify "^3.6.0"
+    inherits "^2.0.3"
+    pump "^2.0.0"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+q@^1.1.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qs@6.7.0:
+  version "6.7.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
+  integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+
+qs@^6.9.4:
+  version "6.9.4"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
+  integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+query-string@^4.1.0:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
+  integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  dependencies:
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
+querystring-es3@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+  integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+querystringify@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
+  integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
+
+quill-delta@^3.6.2:
+  version "3.6.3"
+  resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
+  integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==
+  dependencies:
+    deep-equal "^1.0.1"
+    extend "^3.0.2"
+    fast-diff "1.1.2"
+
+quill-delta@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.2.2.tgz#015397d046e0a3bed087cd8a51f98c11a1b8f351"
+  integrity sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==
+  dependencies:
+    fast-diff "1.2.0"
+    lodash.clonedeep "^4.5.0"
+    lodash.isequal "^4.5.0"
+
+quill@^1.3.4, quill@^1.3.7:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
+  integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==
+  dependencies:
+    clone "^2.1.1"
+    deep-equal "^1.0.1"
+    eventemitter3 "^2.0.3"
+    extend "^3.0.2"
+    parchment "^1.1.4"
+    quill-delta "^3.6.2"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+  integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+  dependencies:
+    randombytes "^2.0.5"
+    safe-buffer "^5.1.0"
+
+range-parser@^1.2.1, range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
+  integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+  dependencies:
+    bytes "3.1.0"
+    http-errors "1.7.2"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
+read-pkg-up@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+  integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
+  dependencies:
+    find-up "^1.0.0"
+    read-pkg "^1.0.0"
+
+read-pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+  integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^2.0.0"
+
+read-pkg@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+  integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
+  dependencies:
+    load-json-file "^1.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^1.0.0"
+
+read-pkg@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+  integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
+  dependencies:
+    load-json-file "^2.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^2.0.0"
+
+read-pkg@^5.1.1:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    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"
+
+readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readdirp@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+  integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+    micromatch "^3.1.10"
+    readable-stream "^2.0.2"
+
+readdirp@~3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
+  integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
+  dependencies:
+    picomatch "^2.2.1"
+
+redent@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+  integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
+  dependencies:
+    indent-string "^2.1.0"
+    strip-indent "^1.0.1"
+
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+  dependencies:
+    regenerate "^1.4.0"
+
+regenerate@^1.4.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
+  integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+
+regenerator-transform@^0.14.2:
+  version "0.14.4"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
+  integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+  dependencies:
+    "@babel/runtime" "^7.8.4"
+    private "^0.1.8"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
+regexp.prototype.flags@^1.2.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
+  integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+regexpp@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+
+regexpu-core@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+  dependencies:
+    regenerate "^1.4.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
+    unicode-match-property-ecmascript "^1.0.4"
+    unicode-match-property-value-ecmascript "^1.2.0"
+
+regjsgen@^0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
+  integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
+
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+  dependencies:
+    jsesc "~0.5.0"
+
+relateurl@0.2.x:
+  version "0.2.7"
+  resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+  integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
+
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+renderkid@^2.0.1:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
+  integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==
+  dependencies:
+    css-select "^1.1.0"
+    dom-converter "^0.2"
+    htmlparser2 "^3.3.0"
+    strip-ansi "^3.0.0"
+    utila "^0.4.0"
+
+repeat-element@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+  integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+repeating@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+  integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+  dependencies:
+    is-finite "^1.0.0"
+
+request-promise-core@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+  integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+  dependencies:
+    lodash "^4.17.15"
+
+request-promise-native@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+  integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+  dependencies:
+    request-promise-core "1.1.3"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request@^2.87.0, request@^2.88.0, request@^2.88.2:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    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.3"
+    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.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+requires-port@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+  integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+resolve-cwd@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+  integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+  dependencies:
+    resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+  integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.8.1:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+  dependencies:
+    path-parse "^1.0.6"
+
+restore-cursor@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+  integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+  dependencies:
+    onetime "^2.0.0"
+    signal-exit "^3.0.2"
+
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+  dependencies:
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+retry@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+  integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
+rgb-regex@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
+  integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+
+rgba-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
+  integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+
+rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+rimraf@2.6.3:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+  dependencies:
+    glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+  integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+
+run-async@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+  integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+  dependencies:
+    aproba "^1.1.1"
+
+rxjs@^6.5.3:
+  version "6.5.5"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
+  integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
+  dependencies:
+    tslib "^1.9.0"
+
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+  dependencies:
+    ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sass-graph@2.2.5:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8"
+  integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==
+  dependencies:
+    glob "^7.0.0"
+    lodash "^4.0.0"
+    scss-tokenizer "^0.2.3"
+    yargs "^13.3.2"
+
+sass-loader@^8.0.2:
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d"
+  integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==
+  dependencies:
+    clone-deep "^4.0.1"
+    loader-utils "^1.2.3"
+    neo-async "^2.6.1"
+    schema-utils "^2.6.1"
+    semver "^6.3.0"
+
+sax@~1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+schema-utils@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+  integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+  dependencies:
+    ajv "^6.1.0"
+    ajv-errors "^1.0.0"
+    ajv-keywords "^3.1.0"
+
+schema-utils@^2.0.0, schema-utils@^2.5.0, schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
+  integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
+  dependencies:
+    "@types/json-schema" "^7.0.4"
+    ajv "^6.12.2"
+    ajv-keywords "^3.4.1"
+
+scss-tokenizer@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+  integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
+  dependencies:
+    js-base64 "^2.1.8"
+    source-map "^0.4.2"
+
+select-hose@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+  integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
+
+selfsigned@^1.10.7:
+  version "1.10.7"
+  resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
+  integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
+  dependencies:
+    node-forge "0.9.0"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
+  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+
+semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@~5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+  integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
+
+send@0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
+  integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+  dependencies:
+    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"
+
+serialize-javascript@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
+  integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
+
+serialize-javascript@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea"
+  integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==
+  dependencies:
+    randombytes "^2.1.0"
+
+serve-index@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+  integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=
+  dependencies:
+    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"
+
+serve-static@1.14.1:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
+  integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.17.1"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+  integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+setprototypeof@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+  integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+shell-quote@^1.6.1:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
+  integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+  dependencies:
+    is-arrayish "^0.3.1"
+
+slash@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+  integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
+
+slash@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+  integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+slice-ansi@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+  integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+  dependencies:
+    ansi-styles "^3.2.0"
+    astral-regex "^1.0.0"
+    is-fullwidth-code-point "^2.0.0"
+
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+  dependencies:
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+  dependencies:
+    kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+  dependencies:
+    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"
+
+sockjs-client@1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
+  integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==
+  dependencies:
+    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"
+
+sockjs@0.3.20:
+  version "0.3.20"
+  resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855"
+  integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==
+  dependencies:
+    faye-websocket "^0.10.0"
+    uuid "^3.4.0"
+    websocket-driver "0.6.5"
+
+sort-keys@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
+  integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
+  dependencies:
+    is-plain-obj "^1.0.0"
+
+source-list-map@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-resolve@^0.5.0:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+  dependencies:
+    atob "^2.1.2"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
+source-map-support@~0.5.12:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@^0.4.2:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+  integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
+  dependencies:
+    amdefine ">=0.0.4"
+
+source-map@^0.5.0, source-map@^0.5.6:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+  integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+  integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
+  dependencies:
+    spdx-expression-parse "^3.0.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+
+spdx-expression-parse@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
+  integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+
+spdy-transport@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+  integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+  dependencies:
+    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"
+
+spdy@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b"
+  integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
+  dependencies:
+    debug "^4.1.0"
+    handle-thing "^2.0.0"
+    http-deceiver "^1.2.7"
+    select-hose "^2.0.0"
+    spdy-transport "^3.0.0"
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+  dependencies:
+    extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+  dependencies:
+    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@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+  integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+  dependencies:
+    figgy-pudding "^3.5.1"
+
+ssri@^7.0.0, ssri@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.0.tgz#92c241bf6de82365b5c7fb4bd76e975522e1294d"
+  integrity sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==
+  dependencies:
+    figgy-pudding "^3.5.1"
+    minipass "^3.1.1"
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+stackframe@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303"
+  integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==
+
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+stdout-stream@^1.4.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
+  integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
+  dependencies:
+    readable-stream "^2.0.1"
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+stream-browserify@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+  integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
+  integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+  dependencies:
+    end-of-stream "^1.1.0"
+    stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+  integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.3.6"
+    to-arraybuffer "^1.0.0"
+    xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+
+strict-uri-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+  integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+
+string-width@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+  integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
+string.prototype.trimend@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
+  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimstart "^1.0.0"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
+  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimend "^1.0.0"
+
+string.prototype.trimstart@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+string_decoder@^1.0.0, string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+  dependencies:
+    ansi-regex "^5.0.0"
+
+strip-bom@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+  integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+  dependencies:
+    is-utf8 "^0.2.0"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-indent@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+  integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
+  dependencies:
+    get-stdin "^4.0.1"
+
+strip-indent@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
+  integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
+
+strip-json-comments@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
+  integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
+
+stylehacks@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
+  integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+supports-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
+svg-tags@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+  integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
+
+svgo@^1.0.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
+  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
+  dependencies:
+    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.37"
+    csso "^4.0.2"
+    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"
+
+table@^5.2.3:
+  version "5.4.6"
+  resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
+  integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+  dependencies:
+    ajv "^6.10.2"
+    lodash "^4.17.14"
+    slice-ansi "^2.1.0"
+    string-width "^3.0.0"
+
+tapable@^0.1.8:
+  version "0.1.10"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
+  integrity sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=
+
+tapable@^1.0.0, tapable@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
+  integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
+
+tar@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
+  integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==
+  dependencies:
+    block-stream "*"
+    fstream "^1.0.12"
+    inherits "2"
+
+terser-webpack-plugin@^1.4.3:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f"
+  integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==
+  dependencies:
+    cacache "^12.0.2"
+    find-cache-dir "^2.1.0"
+    is-wsl "^1.1.0"
+    schema-utils "^1.0.0"
+    serialize-javascript "^3.1.0"
+    source-map "^0.6.1"
+    terser "^4.1.2"
+    webpack-sources "^1.4.0"
+    worker-farm "^1.7.0"
+
+terser-webpack-plugin@^2.3.6:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.7.tgz#4910ff5d1a872168cc7fa6cd3749e2b0d60a8a0b"
+  integrity sha512-xzYyaHUNhzgaAdBsXxk2Yvo/x1NJdslUaussK3fdpBbvttm1iIwU+c26dj9UxJcwk2c5UWt5F55MUTIA8BE7Dg==
+  dependencies:
+    cacache "^13.0.1"
+    find-cache-dir "^3.3.1"
+    jest-worker "^25.4.0"
+    p-limit "^2.3.0"
+    schema-utils "^2.6.6"
+    serialize-javascript "^3.1.0"
+    source-map "^0.6.1"
+    terser "^4.6.12"
+    webpack-sources "^1.4.3"
+
+terser@^4.1.2, terser@^4.6.12:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006"
+  integrity sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+thenify-all@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+  integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
+  dependencies:
+    thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
+  integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
+  dependencies:
+    any-promise "^1.0.0"
+
+thread-loader@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/thread-loader/-/thread-loader-2.1.3.tgz#cbd2c139fc2b2de6e9d28f62286ab770c1acbdda"
+  integrity sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg==
+  dependencies:
+    loader-runner "^2.3.1"
+    loader-utils "^1.1.0"
+    neo-async "^2.6.0"
+
+through2@^2.0.0:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+  dependencies:
+    readable-stream "~2.3.6"
+    xtend "~4.0.1"
+
+through@^2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+thunky@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
+  integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+
+timers-browserify@^2.0.4:
+  version "2.0.11"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
+  integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+  dependencies:
+    setimmediate "^1.0.4"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+tmp@^0.0.33:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+to-arraybuffer@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+  integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
+toidentifier@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+toposort@^1.0.0:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
+  integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
+
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+trim-newlines@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+  integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
+
+"true-case-path@^1.0.2":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
+  integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
+  dependencies:
+    glob "^7.1.2"
+
+tryer@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
+  integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
+
+ts-pnp@^1.1.6:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
+  integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
+
+tslib@^1.9.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
+tty-browserify@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+  integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-fest@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
+  integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
+
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+type-is@~1.6.17, type-is@~1.6.18:
+  version "1.6.18"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.24"
+
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+uglify-js@3.4.x:
+  version "3.4.10"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
+  integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
+  dependencies:
+    commander "~2.19.0"
+    source-map "~0.6.1"
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+  integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+  integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+  dependencies:
+    unicode-canonical-property-names-ecmascript "^1.0.4"
+    unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+
+unicode-property-aliases-ecmascript@^1.0.4:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
+  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
+
+union-value@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^2.0.1"
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+unique-filename@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+  integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+  dependencies:
+    unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+  integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+  dependencies:
+    imurmurhash "^0.1.4"
+
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unquote@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+  integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
+
+upath@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
+  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+
+upper-case@^1.1.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
+  integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-loader@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b"
+  integrity sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==
+  dependencies:
+    loader-utils "^1.2.3"
+    mime "^2.4.4"
+    schema-utils "^2.5.0"
+
+url-parse@^1.4.3:
+  version "1.4.7"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
+  integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
+  dependencies:
+    querystringify "^2.1.1"
+    requires-port "^1.0.0"
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+use@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+  integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+  dependencies:
+    define-properties "^1.1.2"
+    object.getownpropertydescriptors "^2.0.3"
+
+util.promisify@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
+
+util@0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+  integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+  dependencies:
+    inherits "2.0.1"
+
+util@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+  integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+  dependencies:
+    inherits "2.0.3"
+
+utila@^0.4.0, utila@~0.4:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
+  integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
+
+utils-merge@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+  integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@^3.3.2, uuid@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+v8-compile-cache@^2.0.3:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
+  integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
+
+validate-npm-package-license@^3.0.1:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+  integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+  dependencies:
+    spdx-correct "^3.0.0"
+    spdx-expression-parse "^3.0.0"
+
+vary@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+  integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+vendors@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+vm-browserify@^1.0.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
+vue-eslint-parser@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.1.0.tgz#9cdbcc823e656b087507a1911732b867ac101e83"
+  integrity sha512-Kr21uPfthDc63nDl27AGQEhtt9VrZ9nkYk/NTftJ2ws9XiJwzJJCnCr3AITQ2jpRMA0XPGDECxYH8E027qMK9Q==
+  dependencies:
+    debug "^4.1.1"
+    eslint-scope "^5.0.0"
+    eslint-visitor-keys "^1.1.0"
+    espree "^6.2.1"
+    esquery "^1.0.1"
+    lodash "^4.17.15"
+
+vue-hot-reload-api@^2.3.0:
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
+  integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
+
+vue-loader@^15.9.2:
+  version "15.9.2"
+  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.2.tgz#ae01f5f4c9c6a04bff4483912e72ef91a402c1ae"
+  integrity sha512-oXBubaY//CYEISBlHX+c2YPJbmOH68xXPXjFv4MAgPqQvUsnjrBAjCJi8HXZ/r/yfn0tPL5VZj1Zcp8mJPI8VA==
+  dependencies:
+    "@vue/component-compiler-utils" "^3.1.0"
+    hash-sum "^1.0.2"
+    loader-utils "^1.1.0"
+    vue-hot-reload-api "^2.3.0"
+    vue-style-loader "^4.1.0"
+
+vue-quill-editor@^3.0.6:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/vue-quill-editor/-/vue-quill-editor-3.0.6.tgz#1f85646211d68a31a80a72cb7f45bb2f119bc8fb"
+  integrity sha512-g20oSZNWg8Hbu41Kinjd55e235qVWPLfg4NvsLW6d+DhgBTFbEuMpcWlUdrD6qT3+Noim6DRu18VLM9lVShXOQ==
+  dependencies:
+    object-assign "^4.1.1"
+    quill "^1.3.4"
+
+vue-router@^3.2.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.3.2.tgz#0099de402edb2fe92f9711053ab5a2156f239cad"
+  integrity sha512-5sEbcfb7MW8mY8lbUVbF4kgcipGXsagkM/X+pb6n0MhjP+RorWIUTPAPSqgPaiPOxVCXgAItBl8Vwz8vq78faA==
+
+vue-style-loader@^4.1.0, vue-style-loader@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
+  integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==
+  dependencies:
+    hash-sum "^1.0.2"
+    loader-utils "^1.0.2"
+
+vue-template-compiler@^2.6.11:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
+  integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.1.0"
+
+vue-template-es2015-compiler@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
+  integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
+
+vue@^2.6.11:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
+  integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
+
+vuex@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb34e7fceeaebde45"
+  integrity sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg==
+
+watchpack-chokidar2@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
+  integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
+  dependencies:
+    chokidar "^2.1.8"
+
+watchpack@^1.6.1:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa"
+  integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==
+  dependencies:
+    graceful-fs "^4.1.2"
+    neo-async "^2.5.0"
+  optionalDependencies:
+    chokidar "^3.4.0"
+    watchpack-chokidar2 "^2.0.0"
+
+wbuf@^1.1.0, wbuf@^1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+  integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+  dependencies:
+    minimalistic-assert "^1.0.0"
+
+wcwidth@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+  integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+  dependencies:
+    defaults "^1.0.3"
+
+webpack-bundle-analyzer@^3.8.0:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz#ce6b3f908daf069fd1f7266f692cbb3bded9ba16"
+  integrity sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
+    bfj "^6.1.1"
+    chalk "^2.4.1"
+    commander "^2.18.0"
+    ejs "^2.6.1"
+    express "^4.16.3"
+    filesize "^3.6.1"
+    gzip-size "^5.0.0"
+    lodash "^4.17.15"
+    mkdirp "^0.5.1"
+    opener "^1.5.1"
+    ws "^6.0.0"
+
+webpack-chain@^6.4.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.4.0.tgz#22f0b27b6a9bc9ee3cba4f9e6513cf66394034e2"
+  integrity sha512-f97PYqxU+9/u0IUqp/ekAHRhBD1IQwhBv3wlJo2nvyELpr2vNnUqO3XQEk+qneg0uWGP54iciotszpjfnEExFA==
+  dependencies:
+    deepmerge "^1.5.2"
+    javascript-stringify "^2.0.1"
+
+webpack-dev-middleware@^3.7.2:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
+  integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
+  dependencies:
+    memory-fs "^0.4.1"
+    mime "^2.4.4"
+    mkdirp "^0.5.1"
+    range-parser "^1.2.1"
+    webpack-log "^2.0.0"
+
+webpack-dev-server@^3.11.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c"
+  integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==
+  dependencies:
+    ansi-html "0.0.7"
+    bonjour "^3.5.0"
+    chokidar "^2.1.8"
+    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.3.1"
+    http-proxy-middleware "0.19.1"
+    import-local "^2.0.0"
+    internal-ip "^4.3.0"
+    ip "^1.1.5"
+    is-absolute-url "^3.0.3"
+    killable "^1.0.1"
+    loglevel "^1.6.8"
+    opn "^5.5.0"
+    p-retry "^3.0.1"
+    portfinder "^1.0.26"
+    schema-utils "^1.0.0"
+    selfsigned "^1.10.7"
+    semver "^6.3.0"
+    serve-index "^1.9.1"
+    sockjs "0.3.20"
+    sockjs-client "1.4.0"
+    spdy "^4.0.2"
+    strip-ansi "^3.0.1"
+    supports-color "^6.1.0"
+    url "^0.11.0"
+    webpack-dev-middleware "^3.7.2"
+    webpack-log "^2.0.0"
+    ws "^6.2.1"
+    yargs "^13.3.2"
+
+webpack-log@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
+  integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==
+  dependencies:
+    ansi-colors "^3.0.0"
+    uuid "^3.3.2"
+
+webpack-merge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d"
+  integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==
+  dependencies:
+    lodash "^4.17.15"
+
+webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
+  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+  dependencies:
+    source-list-map "^2.0.0"
+    source-map "~0.6.1"
+
+webpack@^4.0.0:
+  version "4.43.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6"
+  integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==
+  dependencies:
+    "@webassemblyjs/ast" "1.9.0"
+    "@webassemblyjs/helper-module-context" "1.9.0"
+    "@webassemblyjs/wasm-edit" "1.9.0"
+    "@webassemblyjs/wasm-parser" "1.9.0"
+    acorn "^6.4.1"
+    ajv "^6.10.2"
+    ajv-keywords "^3.4.1"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^4.1.0"
+    eslint-scope "^4.0.3"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^2.4.0"
+    loader-utils "^1.2.3"
+    memory-fs "^0.4.1"
+    micromatch "^3.1.10"
+    mkdirp "^0.5.3"
+    neo-async "^2.6.1"
+    node-libs-browser "^2.2.1"
+    schema-utils "^1.0.0"
+    tapable "^1.1.3"
+    terser-webpack-plugin "^1.4.3"
+    watchpack "^1.6.1"
+    webpack-sources "^1.4.1"
+
+websocket-driver@0.6.5:
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
+  integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=
+  dependencies:
+    websocket-extensions ">=0.1.1"
+
+websocket-driver@>=0.5.1:
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
+  integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
+  dependencies:
+    http-parser-js ">=0.5.1"
+    safe-buffer ">=5.1.0"
+    websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+  integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@1, which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wide-align@^1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+  dependencies:
+    string-width "^1.0.2 || 2"
+
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+worker-farm@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
+  integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
+  dependencies:
+    errno "~0.1.7"
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
+
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+  integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
+  dependencies:
+    mkdirp "^0.5.1"
+
+ws@^6.0.0, ws@^6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
+  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+  dependencies:
+    async-limiter "~1.0.0"
+
+xtend@^4.0.0, xtend@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+  integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+  integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.2:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@^13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@^18.1.1:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^13.3.2:
+  version "13.3.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+  dependencies:
+    cliui "^5.0.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    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.2"
+
+yargs@^15.0.0:
+  version "15.3.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b"
+  integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.1"
+
+yorkie@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yorkie/-/yorkie-2.0.0.tgz#92411912d435214e12c51c2ae1093e54b6bb83d9"
+  integrity sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==
+  dependencies:
+    execa "^0.8.0"
+    is-ci "^1.0.10"
+    normalize-path "^1.0.0"
+    strip-indent "^2.0.0"

+ 1 - 0
handlers.go

@@ -115,6 +115,7 @@ func registerHTTPHandlers(e *echo.Echo) {
 
 
 	// Static views.
 	// Static views.
 	e.GET("/lists", handleIndexPage)
 	e.GET("/lists", handleIndexPage)
+	e.GET("/lists/forms", handleIndexPage)
 	e.GET("/subscribers", handleIndexPage)
 	e.GET("/subscribers", handleIndexPage)
 	e.GET("/subscribers/lists/:listID", handleIndexPage)
 	e.GET("/subscribers/lists/:listID", handleIndexPage)
 	e.GET("/subscribers/import", handleIndexPage)
 	e.GET("/subscribers/import", handleIndexPage)

+ 4 - 2
init.go

@@ -52,8 +52,10 @@ func initFS(staticDir string) stuffbin.FileSystem {
 			"static/public:/public",
 			"static/public:/public",
 
 
 			// The frontend app's static assets are aliased to /frontend
 			// The frontend app's static assets are aliased to /frontend
-			// so that they are accessible at localhost:port/frontend/static/ ...
-			"frontend/build:/frontend",
+			// so that they are accessible at /frontend/js/* etc.
+			// Alias all files inside dist/ and dist/frontend to frontend/*.
+			"frontend/dist/:/frontend",
+			"frontend/dist/frontend:/frontend",
 		}
 		}
 
 
 		fs, err = stuffbin.NewLocalFS("/", files...)
 		fs, err = stuffbin.NewLocalFS("/", files...)

+ 2 - 2
models/models.go

@@ -158,7 +158,7 @@ type Campaign struct {
 	Name        string         `db:"name" json:"name"`
 	Name        string         `db:"name" json:"name"`
 	Subject     string         `db:"subject" json:"subject"`
 	Subject     string         `db:"subject" json:"subject"`
 	FromEmail   string         `db:"from_email" json:"from_email"`
 	FromEmail   string         `db:"from_email" json:"from_email"`
-	Body        string         `db:"body" json:"body,omitempty"`
+	Body        string         `db:"body" json:"body"`
 	SendAt      null.Time      `db:"send_at" json:"send_at"`
 	SendAt      null.Time      `db:"send_at" json:"send_at"`
 	Status      string         `db:"status" json:"status"`
 	Status      string         `db:"status" json:"status"`
 	ContentType string         `db:"content_type" json:"content_type"`
 	ContentType string         `db:"content_type" json:"content_type"`
@@ -177,7 +177,7 @@ type Campaign struct {
 
 
 // CampaignMeta contains fields tracking a campaign's progress.
 // CampaignMeta contains fields tracking a campaign's progress.
 type CampaignMeta struct {
 type CampaignMeta struct {
-	CampaignID int `db:"campaign_id" json:""`
+	CampaignID int `db:"campaign_id" json:"-"`
 	Views      int `db:"views" json:"views"`
 	Views      int `db:"views" json:"views"`
 	Clicks     int `db:"clicks" json:"clicks"`
 	Clicks     int `db:"clicks" json:"clicks"`
 
 

+ 1 - 1
queries.sql

@@ -379,7 +379,7 @@ FROM campaigns
 WHERE ($1 = 0 OR id = $1)
 WHERE ($1 = 0 OR id = $1)
     AND status=ANY(CASE WHEN ARRAY_LENGTH($2::campaign_status[], 1) != 0 THEN $2::campaign_status[] ELSE ARRAY[status] END)
     AND status=ANY(CASE WHEN ARRAY_LENGTH($2::campaign_status[], 1) != 0 THEN $2::campaign_status[] ELSE ARRAY[status] END)
     AND ($3 = '' OR (to_tsvector(name || subject) @@ to_tsquery($3)))
     AND ($3 = '' OR (to_tsvector(name || subject) @@ to_tsquery($3)))
-ORDER BY created_at DESC OFFSET $4 LIMIT $5;
+ORDER BY campaigns.updated_at DESC OFFSET $4 LIMIT $5;
 
 
 -- name: get-campaign
 -- name: get-campaign
 SELECT campaigns.*,
 SELECT campaigns.*,

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor