features implemented expect notification settings
authorebelcrom <ebelcrom@gmail.com>
Thu, 26 Dec 2019 22:23:29 +0000 (23:23 +0100)
committerebelcrom <ebelcrom@gmail.com>
Thu, 26 Dec 2019 22:23:29 +0000 (23:23 +0100)
36 files changed:
package-lock.json
package.json
public/favicon.ico
public/img/icons/android-chrome-192x192.png
public/img/icons/android-chrome-512x512.png
public/img/icons/apple-touch-icon-120x120.png
public/img/icons/apple-touch-icon-152x152.png
public/img/icons/apple-touch-icon-180x180.png
public/img/icons/apple-touch-icon-60x60.png
public/img/icons/apple-touch-icon-76x76.png
public/img/icons/apple-touch-icon.png
public/img/icons/favicon-16x16.png
public/img/icons/favicon-32x32.png
public/img/icons/msapplication-icon-144x144.png
public/img/icons/mstile-150x150.png
public/index.html
src/App.vue
src/assets/images/closed.svg [new file with mode: 0644]
src/assets/images/moving.svg [new file with mode: 0644]
src/assets/images/open.svg [new file with mode: 0644]
src/assets/images/unavailable.svg [new file with mode: 0644]
src/assets/logo.png
src/assets/logo.svg [new file with mode: 0644]
src/components/About.vue [new file with mode: 0644]
src/components/ApiKey.vue [new file with mode: 0644]
src/components/HelloWorld.vue [deleted file]
src/components/Navbar.vue [new file with mode: 0644]
src/lib/lib.js [new file with mode: 0644]
src/main.js
src/plugins/vuetify.js [new file with mode: 0644]
src/registerServiceWorker.js
src/router/index.js
src/views/About.vue [deleted file]
src/views/Home.vue
src/views/Settings.vue [new file with mode: 0644]
vue.config.js [new file with mode: 0644]

index 05f6ec878e369554bbd4929401c161404373f743..85ea57a7eca660c25474097be747b56e53de1e6d 100644 (file)
       "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
       "dev": true
     },
+    "clone-deep": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+      "dev": true,
+      "requires": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      }
+    },
     "coa": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
         }
       }
     },
+    "interpret": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
+      "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
+      "dev": true
+    },
     "invariant": {
       "version": "2.2.4",
       "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
         "readable-stream": "^2.0.2"
       }
     },
+    "rechoir": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+      "dev": true,
+      "requires": {
+        "resolve": "^1.1.6"
+      }
+    },
     "regenerate": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
+    "sass": {
+      "version": "1.23.7",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.23.7.tgz",
+      "integrity": "sha512-cYgc0fanwIpi0rXisGxl+/wadVQ/HX3RhpdRcjLdj2o2ye/sxUTpAxIhbmJy3PLQgRFbf6Pn8Jsrta2vdXcoOQ==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=2.0.0 <4.0.0"
+      }
+    },
+    "sass-loader": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz",
+      "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^4.0.1",
+        "loader-utils": "^1.2.3",
+        "neo-async": "^2.6.1",
+        "schema-utils": "^2.1.0",
+        "semver": "^6.3.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
         "safe-buffer": "^5.0.1"
       }
     },
+    "shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^6.0.2"
+      }
+    },
     "shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
       "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
       "dev": true
     },
+    "shelljs": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
+      "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "interpret": "^1.0.0",
+        "rechoir": "^0.6.2"
+      }
+    },
     "signal-exit": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
       "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
       "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
     },
+    "vue-cli-plugin-vuetify": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.2.tgz",
+      "integrity": "sha512-OJ1YUSfDlQibj111QMGv4a44atmtWdrkynk4voiEuivUqLcZGCJyF/A8ae0VojpMU2jlvZydz0nXjmZmcg+Nqw==",
+      "dev": true,
+      "requires": {
+        "semver": "^6.0.0",
+        "shelljs": "^0.8.3"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
     "vue-hot-reload-api": {
       "version": "2.3.4",
       "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
       "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
       "dev": true
     },
+    "vuetify": {
+      "version": "2.1.14",
+      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.1.14.tgz",
+      "integrity": "sha512-DZ1Jq2+PPEXYeh08FGJktB6St0ClZlcwzveCvJmkW16pYkUnimFlW6E3AQxSqxRpcGfZDsE53XkvZ2BGhy+m8Q=="
+    },
+    "vuetify-loader": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.4.3.tgz",
+      "integrity": "sha512-fS0wRil682Ebsj2as+eruBoMPKaQYDhu/fDAndnTItzSY4RK4LOEIsssVL4vD6QY8dvUgoGL84SUQ6vGr777CA==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.2.0"
+      }
+    },
     "watchpack": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
index 2dd3e225cf6833eb79e8e1ac63343892e72f7c4f..2313148273398276a51fe3af87254d8b8f7e94dd 100644 (file)
@@ -1,22 +1,29 @@
 {
-  "name": "garnod-pwa.git",
-  "version": "0.1.0",
-  "private": true,
+  "name": "garnod-pwa",
+  "version": "1.0.0",
+  "description": "A garage door monitoring application (client).",
   "scripts": {
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build"
   },
+  "author": "ebelcrom",
+  "license": "GPL-2.0-or-later",
   "dependencies": {
     "core-js": "^3.4.3",
     "register-service-worker": "^1.6.2",
     "vue": "^2.6.10",
-    "vue-router": "^3.1.3"
+    "vue-router": "^3.1.3",
+    "vuetify": "^2.1.0"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^4.1.0",
     "@vue/cli-plugin-pwa": "^4.1.0",
     "@vue/cli-plugin-router": "^4.1.0",
     "@vue/cli-service": "^4.1.0",
-    "vue-template-compiler": "^2.6.10"
+    "sass": "^1.19.0",
+    "sass-loader": "^8.0.0",
+    "vue-cli-plugin-vuetify": "^2.0.2",
+    "vue-template-compiler": "^2.6.10",
+    "vuetify-loader": "^1.3.0"
   }
 }
index df36fcfb72584e00488330b560ebcf34a41c64c2..55ef748fcc5ba44818708c6c080f667548083c9f 100644 (file)
Binary files a/public/favicon.ico and b/public/favicon.ico differ
index b02aa64d97167ad649e496908b35f14c603d9249..2a78f13ec0f7e40909c48da9ec4602d4ae951da2 100644 (file)
Binary files a/public/img/icons/android-chrome-192x192.png and b/public/img/icons/android-chrome-192x192.png differ
index 06088b011eccebb820b6e8de0cd244aa443208ba..ffbd457fecdf7fea5b505992d65633df78cd2e48 100644 (file)
Binary files a/public/img/icons/android-chrome-512x512.png and b/public/img/icons/android-chrome-512x512.png differ
index 1427cf62752646ad7217df0a61aa01fdef7475d1..2704c845bd4dd8b13779c686113919d1e244f371 100644 (file)
Binary files a/public/img/icons/apple-touch-icon-120x120.png and b/public/img/icons/apple-touch-icon-120x120.png differ
index f24d454a2ecb8851bb893192b64ee09386d30e24..6057af4082ea9962e7a275e5305cbefa9c6772a6 100644 (file)
Binary files a/public/img/icons/apple-touch-icon-152x152.png and b/public/img/icons/apple-touch-icon-152x152.png differ
index 404e192a95ccccbede087203c42b1f25f6bc6e67..4ba873b4d162f4e80b8d8d8ab4a9573b8cc73c44 100644 (file)
Binary files a/public/img/icons/apple-touch-icon-180x180.png and b/public/img/icons/apple-touch-icon-180x180.png differ
index cf10a5602e653bb126332934e2b7f34081c19a01..7304ff29b82794814644ae19bd521ae102ca98b7 100644 (file)
Binary files a/public/img/icons/apple-touch-icon-60x60.png and b/public/img/icons/apple-touch-icon-60x60.png differ
index c500769e3df9d6a6f1977ace8be4e63a8095e36a..b727b14856d0b57d15f75c0960964db18e4a3583 100644 (file)
Binary files a/public/img/icons/apple-touch-icon-76x76.png and b/public/img/icons/apple-touch-icon-76x76.png differ
index 03c0c5d5ec302ed7b0ee2c401df9427fb9d3c117..4ba873b4d162f4e80b8d8d8ab4a9573b8cc73c44 100644 (file)
Binary files a/public/img/icons/apple-touch-icon.png and b/public/img/icons/apple-touch-icon.png differ
index 42af00963d81b8e39a30435c60ac482d1f8756e0..85f8bcb9b5ea42d9f4de219a5b185a8ac393c2bd 100644 (file)
Binary files a/public/img/icons/favicon-16x16.png and b/public/img/icons/favicon-16x16.png differ
index 46ca04dee251a4fa85a2891a145fbe20cc619d96..bb65e6022c8b6e8d7e1451c9cb1182c8500484f3 100644 (file)
Binary files a/public/img/icons/favicon-32x32.png and b/public/img/icons/favicon-32x32.png differ
index 7808237a18d4009501f950044f8388d13c5e1044..58e3dd6e6377062a576d87aefe4c091d0488c05b 100644 (file)
Binary files a/public/img/icons/msapplication-icon-144x144.png and b/public/img/icons/msapplication-icon-144x144.png differ
index 3b37a43ae2fdef53050291d95da2e49f78cf398e..6037e484c35e91365bd36a2a3b9ccdee6c4cb98b 100644 (file)
Binary files a/public/img/icons/mstile-150x150.png and b/public/img/icons/mstile-150x150.png differ
index 6eb6e4023c890986866e2eacd4a29da555313ef8..9cc8098c6310b580f5792bda64225f89f00611d4 100644 (file)
@@ -5,11 +5,13 @@
     <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 %>favicon.ico">
-    <title>garnod-pwa.git</title>
+    <title>Garage Node</title>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
   </head>
   <body>
     <noscript>
-      <strong>We're sorry but garnod-pwa.git doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+      <strong>We're sorry but Garage Node doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
     </noscript>
     <div id="app"></div>
     <!-- built files will be auto injected -->
index 85345149879fa538f72201238543eceda9e1cb81..5dae0f460e9027b1169a575c5297a30c9505242a 100644 (file)
@@ -1,32 +1,36 @@
 <template>
-  <div id="app">
-    <div id="nav">
-      <router-link to="/">Home</router-link> |
-      <router-link to="/about">About</router-link>
-    </div>
-    <router-view/>
-  </div>
-</template>
+  <v-app>
+
+    <Navbar v-on:openAboutEvent="openAboutDialog" />
 
-<style>
-#app {
-  font-family: 'Avenir', Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-}
+    <v-content>
+      <router-view></router-view>
+    </v-content>
 
-#nav {
-  padding: 30px;
-}
+    <AboutDialog :showAboutDialog="showAboutDialog"/>
+
+  </v-app>
+</template>
 
-#nav a {
-  font-weight: bold;
-  color: #2c3e50;
-}
+<script>
+import Navbar from '@/components/Navbar';
+import AboutDialog from '@/components/About';
 
-#nav a.router-link-exact-active {
-  color: #42b983;
-}
-</style>
+export default {
+  name: 'App',
+  components: {
+    Navbar,
+    AboutDialog,
+  },
+  data() {
+    return {
+      showAboutDialog: null,
+    };
+  },
+  methods: {
+    openAboutDialog() {
+      this.showAboutDialog = ['showDialog'];
+    },
+  },
+};
+</script>
diff --git a/src/assets/images/closed.svg b/src/assets/images/closed.svg
new file mode 100644 (file)
index 0000000..aa94fa0
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+        viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,8L4,17.9V42h40V17.9L24,8z"/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M4.9,21.8L24,12.2l19.1,9.6l0.9,0.5V22v-2L24,10L4,20v2v0.3L4.9,21.8z"/>\r
+<rect x="9" y="24" fill="#607D8B" width="30" height="18"/>\r
+<polygon fill="#546E7A" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 "/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<path fill="#90A4AE" d="M39,32H9v-2h30V32z M39,34H9v2h30V34z M39,38H9v2h30V38z M39,26H9v2h30V26z"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M39,30h-2v2h2V30z M39,34h-2v2h2V34z M39,38h-2v2h2V38z M11,26H9v2h2V26z M11,30H9v2h2\r
+       V30z M11,34H9v2h2V34z M11,38H9v2h2V38z"/>\r
+</svg>\r
diff --git a/src/assets/images/moving.svg b/src/assets/images/moving.svg
new file mode 100644 (file)
index 0000000..0c5b607
--- /dev/null
@@ -0,0 +1,19 @@
+<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg">
+
+ <g>
+  <title>background</title>
+  <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+  <title>Layer 1</title>
+  <path id="svg_1" d="m24,8l-20,9.9l0,24.1l40,0l0,-24.1l-20,-9.9z" fill="#E8EAF6"/>
+  <path id="svg_2" d="m44,42l-40,0l0,-5l40,0l0,5zm-39.1,-20.2l19.1,-9.6l19.1,9.6l0.9,0.5l0,-0.3l0,-2l-20,-10l-20,10l0,2l0,0.3l0.9,-0.5z" fill="#C5CAE9"/>
+  <rect id="svg_3" height="18" width="30" fill="#607D8B" y="24" x="9"/>
+  <polygon id="svg_4" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 " fill="#546E7A"/>
+  <polygon id="svg_5" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 " fill="#D32F2F"/>
+  <rect id="svg_6" height="4" width="6" fill="#01579B" y="17" x="21"/>
+  <path id="svg_7" d="m39,32l-30,0l0,-2l30,0l0,2zm0,2l-30,0l0,2l30,0l0,-2zm0,4l-30,0l0,2l30,0l0,-2zm0,-12l-30,0l0,2l30,0l0,-2z" fill="#90A4AE"/>
+  <path id="svg_8" d="m39,28l-2,0l0,-2l2,0l0,2zm0,2l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm-28,-12l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2z" fill="#78909C"/>
+  <path stroke="#000" transform="rotate(-90 24.00000000000001,32.98158264160157) " id="svg_10" d="m16.312502,32.981592l4.457476,-3.687508l0,1.843749l6.460048,0l0,-1.843749l4.457474,3.687508l-4.457474,3.687489l0,-1.843745l-6.460048,0l0,1.843745l-4.457476,-3.687489z" stroke-opacity="null" fill="#FFF093"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/images/open.svg b/src/assets/images/open.svg
new file mode 100644 (file)
index 0000000..0a5dd8c
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+        viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,7.4L4,17.9V42h40V17.9L24,7.4z"/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M24,12.2l20,9.9V22v-2L24,10L4,20v2v0.2L24,12.2z"/>\r
+<rect x="9" y="24" fill="#546E7A" width="30" height="2"/>\r
+<rect x="9" y="26" fill="#455A64" width="30" height="16"/>\r
+<rect x="9" y="26" fill="#90A4AE" width="30" height="2"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M11,26H9v2h2V26z"/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<polygon fill="#263238" points="9,28 9,37 9,42 14,37 34,37 39,42 39,37 39,28 "/>\r
+</svg>\r
diff --git a/src/assets/images/unavailable.svg b/src/assets/images/unavailable.svg
new file mode 100644 (file)
index 0000000..24d2644
--- /dev/null
@@ -0,0 +1,11 @@
+<svg width="40" height="30" xmlns="http://www.w3.org/2000/svg">
+ <g>
+  <title>background</title>
+  <rect x="-1" y="-1" width="42" height="32" id="canvas_background" fill="none"/>
+ </g>
+
+ <g>
+  <title>Layer 1</title>
+  <path opacity="0.6" stroke="#C74343" d="m20,1.311108c-7.594386,0 -13.751391,6.129021 -13.751391,13.688892s6.157005,13.688892 13.751391,13.688892s13.751391,-6.129021 13.751391,-13.688892s-6.157005,-13.688892 -13.751391,-13.688892zm-10.132604,13.688892c0,-5.570659 4.536512,-10.086552 10.132604,-10.086552c2.144493,0 4.12976,0.667154 5.766899,1.798288l-14.093005,14.028952c-1.136299,-1.629699 -1.806499,-3.605942 -1.806499,-5.740689l0.000001,0.000001zm10.132604,10.086552c-2.144493,0 -4.12976,-0.667154 -5.766899,-1.798288l14.093005,-14.028952c1.136299,1.629699 1.806499,3.605942 1.806499,5.740689c0,5.570659 -4.536512,10.086552 -10.132604,10.086552l-0.000001,-0.000001z" stroke-miterlimit="10" fill="#F78F8F" id="svg_1"/>
+ </g>
+</svg>
\ No newline at end of file
index f3d2503fc2a44b5053b0837ebea6e87a2d339a43..5e977f91f254dc7eadd259dbdb08fa30662ced4b 100644 (file)
Binary files a/src/assets/logo.png and b/src/assets/logo.png differ
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
new file mode 100644 (file)
index 0000000..aa94fa0
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+        viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,8L4,17.9V42h40V17.9L24,8z"/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M4.9,21.8L24,12.2l19.1,9.6l0.9,0.5V22v-2L24,10L4,20v2v0.3L4.9,21.8z"/>\r
+<rect x="9" y="24" fill="#607D8B" width="30" height="18"/>\r
+<polygon fill="#546E7A" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 "/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<path fill="#90A4AE" d="M39,32H9v-2h30V32z M39,34H9v2h30V34z M39,38H9v2h30V38z M39,26H9v2h30V26z"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M39,30h-2v2h2V30z M39,34h-2v2h2V34z M39,38h-2v2h2V38z M11,26H9v2h2V26z M11,30H9v2h2\r
+       V30z M11,34H9v2h2V34z M11,38H9v2h2V38z"/>\r
+</svg>\r
diff --git a/src/components/About.vue b/src/components/About.vue
new file mode 100644 (file)
index 0000000..95346bd
--- /dev/null
@@ -0,0 +1,36 @@
+<template>
+  <v-row justify="center">
+    <v-dialog v-model="aboutDialog" persistent>
+      <v-card>
+        <v-card-title>Garage Node</v-card-title>
+        <v-card-subtitle>
+          &copy;2019 ebelcrom
+        </v-card-subtitle>
+        <v-card-text>
+          App for push notifications about or checking the garage
+          door status and performing a move action.
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer />
+          <v-btn color="primary" @click="aboutDialog = false">Close</v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </v-row>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      aboutDialog: false,
+    };
+  },
+  props: ['showAboutDialog'],
+  watch: {
+    'showAboutDialog': function(args) {
+      this.aboutDialog = true;
+    }
+  },
+};
+</script>
diff --git a/src/components/ApiKey.vue b/src/components/ApiKey.vue
new file mode 100644 (file)
index 0000000..e7a16de
--- /dev/null
@@ -0,0 +1,63 @@
+<template>
+  <v-row justify="center">
+    <v-dialog v-model="apiKeyDialog" persistent>
+      <v-card>
+        <v-card-title>Production API-Key</v-card-title>
+        <v-card-text>
+          Set your API-Key below to get access to production functions.
+          <v-input>
+            <v-text-field
+              v-model="apiKey"
+              label="API-Key"
+              :rules="[validation]"
+            />
+          </v-input>
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer />
+          <v-btn @click="cancelAction">Cancel</v-btn>
+          <v-btn :disabled="disabled" color="primary" @click="okAction">Ok</v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </v-row>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      apiKeyDialog: false,
+      disabled: true,
+      apiKey: '',
+    };
+  },
+  props: ['showApiKeyDialog'],
+  watch: {
+    'showApiKeyDialog': function(args) {
+      this.apiKeyDialog = true;
+    }
+  },
+  methods: {
+    validation(value) {
+      if (!!value) {
+        this.disabled = false;
+        return !!value;
+      } else {
+        this.disabled = true;
+        return 'Input required';
+      }
+    },
+    okAction() {
+      if (this.apiKey !== '') {
+        this.apiKeyDialog = false;
+        this.$emit('apiKeySetEvent', this.apiKey);
+      }
+    },
+    cancelAction() {
+      this.apiKeyDialog = false;
+      this.$emit('apiKeySetEvent', this.apiKey);
+    },
+  },
+};
+</script>
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
deleted file mode 100644 (file)
index 49fc75f..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
-      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'HelloWorld',
-  props: {
-    msg: String
-  }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>
diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue
new file mode 100644 (file)
index 0000000..64f4819
--- /dev/null
@@ -0,0 +1,96 @@
+<template>
+  <nav>
+    <div v-if="showSettingsView">
+      <v-app-bar dense />
+
+      <v-app-bar fixed dark color="primary">
+        <v-btn icon v-on:click="goHome()">
+          <v-icon>mdi-arrow-left</v-icon>
+        </v-btn>
+        <v-toolbar-title>Settings</v-toolbar-title>
+      </v-app-bar>
+    </div>
+    <div v-else>
+      <v-app-bar dense />
+
+      <v-app-bar fixed dark color="primary">
+        <v-app-bar-nav-icon @click.stop="navbarDrawer = !navbarDrawer" />
+        <v-toolbar-title>Garage Node</v-toolbar-title>
+      </v-app-bar>
+
+      <v-navigation-drawer v-model="navbarDrawer" app temporary>
+        <v-list-item>
+          <v-list-item-content>
+            <v-list-item-title class="title">
+              DemoApp
+            </v-list-item-title>
+          </v-list-item-content>
+        </v-list-item>
+
+        <v-divider />
+
+        <v-list>
+          <v-list-item
+            v-for="item in navbarItems"
+            :key="item.title"
+            :to="item.link"
+            v-on:click="wrapToolbar(item.title)"
+            link
+          >
+            <v-list-item-icon>
+              <v-icon>{{ item.icon }}</v-icon>
+            </v-list-item-icon>
+            <v-list-item-content>
+              <v-list-item-title>{{ item.title }}</v-list-item-title>
+            </v-list-item-content>
+          </v-list-item>
+          <v-list-item v-on:click="openAboutDialog">
+            <v-list-item-icon>
+              <v-icon>{{ navbarItem.icon }}</v-icon>
+            </v-list-item-icon>
+            <v-list-item-content>
+              <v-list-item-title>{{ navbarItem.title }}</v-list-item-title>
+            </v-list-item-content>
+          </v-list-item>
+        </v-list>
+      </v-navigation-drawer>
+    </div>
+  </nav>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      navbarItems: [
+        { title: 'Home', icon: 'mdi-home', link: '/' },
+        { title: 'Settings', icon: 'mdi-settings', link: '/settings' },
+      ],
+      navbarItem: { title: 'About', icon: 'mdi-information', },
+      navbarDrawer: false,
+      showSettingsView: false,
+    };
+  },
+  methods: {
+    wrapToolbar(item) {
+      switch (item) {
+        case 'Home':
+          this.navbarDrawer = false;
+          this.showSettingsView = false;
+          break;
+        case 'Settings':
+          this.showSettingsView = true;
+          break;
+      };
+    },
+    goHome() {
+      this.showSettingsView = false;
+      this.$router.push('/');
+    },
+    openAboutDialog() {
+      this.navbarDrawer = false;
+      this.$emit('openAboutEvent');
+    },
+  },
+};
+</script>
diff --git a/src/lib/lib.js b/src/lib/lib.js
new file mode 100644 (file)
index 0000000..9c213c1
--- /dev/null
@@ -0,0 +1,210 @@
+function setItem(key, value) {
+  if (key === null) {
+    return;
+  } else {
+    localStorage.setItem(key, JSON.stringify(value));
+  }
+}
+
+function getItem(key) {
+  if (key === null) {
+    return null;
+  } else {
+    if (localStorage.getItem(key) === '') {
+      return '';
+    } else {
+      return JSON.parse(localStorage.getItem(key));
+    }
+  }
+}
+
+function hasItem(key) {
+  if (key === null) {
+    return false;
+  } else {
+    return localStorage.getItem(key) !== null;
+  }
+}
+
+export const storage = {
+  setItem: setItem,
+  getItem: getItem,
+  hasItem: hasItem,
+};
+
+export const notification = {
+};
+
+const url = 'https://binomiant.duckdns.org/mVk7Yr3k/v1';
+const prodStatus = '/status';
+const prodEvents = '/events';
+const prodControl = '/control';
+const testStatus = '/test/status';
+const testEvents = '/test/events';
+const testControl = '/test/control';
+
+async function sendRequest(data) {
+  var init = {
+    method: data.method,
+    headers: {},
+  }
+  if (data.requestContentType != null) {
+    init.headers['Content-Type'] = 'application/json';
+  }
+  if (data.apiKey == null) {
+    init.headers['X-API-Key-Test'] = '2TTqCD4mNNny';
+  } else {
+    init.headers['X-API-Key'] = data.apiKey;
+  }
+
+  const response = await fetch(url + data.path + data.query, init);
+  if (response.status >= 200 && response.status <= 399) {
+    if (response.headers.get('Content-Type') != null) {
+      const result = await response.json();
+      return result;
+    } else {
+      return {};
+    }
+  } else {
+    return null;
+  }
+}
+
+function getStatus() {
+  const testMode = getItem('testMode');
+  var path = '',
+      apiKey = null;
+  var query = new URLSearchParams('');
+
+  if (testMode) {
+    path = testStatus + '?';
+  } else {
+    path = prodStatus + '?';
+    apiKey = getItem('apiKey');
+  }
+  query.append('image', getItem('imageTransmission'));
+
+  var data = {
+    path: path,
+    query: query.toString(),
+    method: 'GET',
+    apiKey: apiKey,
+    requestContentType: null,
+    responseContentType: 'json',
+  }
+
+  return sendRequest(data);
+}
+
+function getEvents() {
+  const testMode = getItem('testMode');
+  var path = '',
+      apiKey = null;
+  var query = new URLSearchParams('');
+
+  if (testMode) {
+    path = testEvents + '?';
+  } else {
+    path = prodEvents + '?';
+    apiKey = getItem('apiKey');
+  }
+  query.append('image', getItem('imageTransmission'));
+  query.append('timeout', 30);
+
+  var data = {
+    path: path,
+    query: query.toString(),
+    method: 'GET',
+    apiKey: apiKey,
+    requestContentType: null,
+    responseContentType: 'json',
+  }
+
+  return new Promise((resolve, reject) => {
+    sendRequest(data)
+    .then(response => {
+      if (response !== null) {
+        if (Object.entries(response).length === 0) {
+          // 304, request again
+          sendRequest(data)
+          .then(response => {
+            if (response !== null) {
+              if (Object.entries(response).length === 0) {
+                // 304, request again
+                sendRequest(data)
+                .then(response => {
+                  if (response !== null) {
+                    if (Object.entries(response).length === 0) {
+                      // 304, abort
+                      reject(null);
+                      return;
+                    } else {
+                      resolve(response);
+                      return;
+                    }
+                  } else {
+                    throw new Error('server response not 2xx nor 3xx');
+                  }
+                })
+                .catch(err => {
+                  reject(null);
+                  return;
+                });
+              } else {
+                resolve(response);
+                return;
+              }
+            } else {
+              throw new Error('server response not 2xx nor 3xx');
+            }
+          })
+          .catch(err => {
+            reject(null);
+            return;
+          });
+        } else {
+          resolve(response);
+          return;
+        }
+      } else {
+        throw new Error('server response not 2xx nor 3xx');
+      }
+    })
+    .catch(err => {
+      reject(null);
+      return;
+    });
+  });
+}
+
+function postControl() {
+  const testMode = getItem('testMode');
+  var path = '',
+      apiKey = null;
+  var query = new URLSearchParams('');
+
+  if (testMode) {
+    path = testControl + '?';
+  } else {
+    path = prodControl + '?';
+    apiKey = getItem('apiKey');
+  }
+  query.append('command', 'move');
+
+  var data = {
+    path: path,
+    query: query.toString(),
+    method: 'POST',
+    apiKey: apiKey,
+    requestContentType: null,
+    responseContentType: null,
+  }
+
+  return sendRequest(data);
+}
+
+export const server = {
+  getStatus: getStatus,
+  getEvents: getEvents,
+  postControl: postControl,
+}
index 9f15cebd2840f0ad4e39357d2dedc60fdd4db578..7219cd9f44caecfa82c8792e0daa6cd2ce44fef2 100644 (file)
@@ -2,10 +2,12 @@ import Vue from 'vue'
 import App from './App.vue'
 import './registerServiceWorker'
 import router from './router'
+import vuetify from './plugins/vuetify';
 
 Vue.config.productionTip = false
 
 new Vue({
   router,
+  vuetify,
   render: h => h(App)
 }).$mount('#app')
diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js
new file mode 100644 (file)
index 0000000..ec46adb
--- /dev/null
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify/lib';
+
+Vue.use(Vuetify);
+
+export default new Vuetify({
+});
index 76cede074d8a8393586f6567de3020e2e506591d..942c5302dae960c5af2c083484c089c9298ad57f 100644 (file)
@@ -2,7 +2,7 @@
 
 import { register } from 'register-service-worker'
 
-if (process.env.NODE_ENV === 'production') {
+//if (process.env.NODE_ENV === 'production') {
   register(`${process.env.BASE_URL}service-worker.js`, {
     ready () {
       console.log(
@@ -29,4 +29,4 @@ if (process.env.NODE_ENV === 'production') {
       console.error('Error during service worker registration:', error)
     }
   })
-}
+//}
index 9d86d9d030ce9bb13bb9397cf502ea14d9a9fcf4..5c06e42456c3fa145afce5d3843145a81f7e2073 100644 (file)
@@ -1,8 +1,9 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-import Home from '../views/Home.vue'
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import Home from '@/views/Home';
+import Settings from '@/views/Settings';
 
-Vue.use(VueRouter)
+Vue.use(VueRouter);
 
 const routes = [
   {
@@ -11,17 +12,14 @@ const routes = [
     component: Home
   },
   {
-    path: '/about',
-    name: 'about',
-    // route level code-splitting
-    // this generates a separate chunk (about.[hash].js) for this route
-    // which is lazy-loaded when the route is visited.
-    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
+    path: '/settings',
+    name: 'settings',
+    component: Settings
   }
 ]
 
 const router = new VueRouter({
   routes
-})
+});
 
-export default router
+export default router;
diff --git a/src/views/About.vue b/src/views/About.vue
deleted file mode 100644 (file)
index 3fa2807..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>
index fc2e9402a0fbec85a9dd7f273084bfd2196aaa22..789cb70e03dab7e85020d7d5d7e140b8fbc2daa1 100644 (file)
 <template>
-  <div class="home">
-    <img alt="Vue logo" src="../assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
-  </div>
+  <v-container>
+    <v-row dense>
+      <v-col>
+        <v-card>
+          <v-list-item three-line>
+            <v-list-item-content>
+              <div class="overline mb-4">DOOR STATUS</div>
+              <v-list-item-title class="headline mb-1">
+                {{status.state}}
+              </v-list-item-title>
+              <v-list-item-subtitle>
+                {{status.text}}
+              </v-list-item-subtitle>
+            </v-list-item-content>
+            <v-list-item-avatar tile size="80">
+              <v-img
+                :src="icon"
+              />
+            </v-list-item-avatar>
+          </v-list-item>
+          <v-card-actions>
+            <v-btn
+              color="primary"
+              :disabled="buttons.action.disabled"
+              :loading="buttons.action.loading"
+              @click="action"
+            >
+              {{buttons.action.name}}
+            </v-btn>
+            <v-spacer />
+            <v-btn
+              color="secondary"
+              :disabled="buttons.refresh.disabled"
+              :loading="buttons.refresh.loading"
+              @click="refresh"
+            >
+              {{buttons.refresh.name}}
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-col>
+
+      <v-col>
+        <v-card>
+          <v-list-item>
+            <v-list-item-content>
+              <div class="overline mb-4">LIVE IMAGE</div>
+            </v-list-item-content>
+          </v-list-item>
+          <v-list-item>
+            <v-list-item-content>
+            <v-img
+              :src="image"
+              :aspect-ratio="4/3"
+              min-width="150"
+            />
+            </v-list-item-content>
+          </v-list-item>
+        </v-card>
+      </v-col>
+    </v-row>
+
+    <v-snackbar
+      v-model="snackbar.show"
+      :timeout="snackbar.timeout"
+      :color="snackbar.color"
+      bottom
+    >
+      {{snackbar.text}}
+    </v-snackbar>
+  </v-container>
 </template>
 
 <script>
-// @ is an alias to /src
-import HelloWorld from '@/components/HelloWorld.vue'
+import { storage, server } from '@/lib/lib.js'
+
+const status = {
+  unknown: {
+    state: 'Unknown',
+    text: 'Door state is unknown.',
+  },
+  closed: {
+    state: 'Closed',
+    text: 'The garage door is closed.',
+  },
+  open: {
+    state: 'Open',
+    text: 'The garage door is open.',
+  },
+  moving: {
+    state: 'Moving',
+    text: 'The garage door is moving.',
+  },
+};
+
+const messages = {
+  checkSettings: {
+    text: 'Check settings',
+    color: 'warning',
+  },
+  refreshFailed: {
+    text: 'Refresh failed',
+    color: 'error',
+  },
+  closeFailed: {
+    text: 'Close failed',
+    color: 'error',
+  },
+};
 
 export default {
-  name: 'home',
-  components: {
-    HelloWorld
-  }
-}
+  data() {
+    return {
+      buttons: {
+        action: {
+          name: 'Close door',
+          disabled: true,
+          loading: false,
+        },
+        refresh: {
+          name: 'Refresh',
+          disabled: true,
+          loading: false,
+        },
+      },
+      status: {
+        state: status.unknown.state,
+        text: status.unknown.text,
+      },
+      icon: require('@/assets/images/unavailable.svg'),
+      image: require('@/assets/images/unavailable.svg'),
+      snackbar: {
+        show: false,
+        timeout: 5000,
+        color: messages.checkSettings.color,
+        text: messages.checkSettings.text,
+      },
+    };
+  },
+  created() {
+    if (storage.hasItem('testMode')) {
+      if (storage.getItem('testMode') || storage.getItem('apiKey') !== '') {
+        this.buttons.refresh.disabled = false;
+      } else {
+        this.buttons.refresh.disabled = true;
+        this.snackbar.show = true;
+      }
+    } else {
+      this.buttons.refresh.disabled = true;
+      this.snackbar.show = true;
+    }
+  },
+  methods: {
+    action() {
+      this.buttons.action.loading = true;
+      this.buttons.refresh.loading = true;
+      server.postControl()
+      .then(response => {
+        if (response === null) {
+          this.stateError(err);
+        } else {
+          this.stateMoving(response);
+        }
+      })
+      .catch(err => {
+        this.stateError(err);
+      })
+      .then(response => {
+        server.getEvents()
+        .then(response => {;
+          if (response === null) {
+            this.stateError(err);
+          } else {
+            switch (response.state) {
+              case 'closed':
+                this.stateClosed(response);
+                break;
+              default:
+                break;
+            }
+          }
+        })
+        .catch(err => {
+          this.stateError(err);
+        });
+      });
+    },
+    refresh() {
+      this.buttons.refresh.loading = true;
+      server.getStatus()
+      .then(response => {
+        if (response === null) {
+          this.stateError(err);
+        } else {
+          switch (response.state) {
+            case 'closed':
+              this.stateClosed(response);
+              break;
+            case 'open':
+              this.stateOpen(response);
+              break;
+            case 'moving':
+              this.stateMoving(response);
+              break;
+            default:
+              break;
+          }
+        }
+      })
+      .catch(err => {
+        this.stateError(err);
+      });
+    },
+
+    stateClosed(response) {
+      var image;
+
+      if (typeof response.image != 'undefined') {
+        image = true;
+      } else {
+        image = false;
+      }
+
+      this.buttons.action.loading = false;
+      this.buttons.action.disabled = true;
+      this.buttons.refresh.loading = false;
+      this.buttons.refresh.disabled = false;
+
+      this.status.state = status.closed.state;
+      this.status.text = status.closed.text;
+      this.icon = require('@/assets/images/closed.svg');
+
+      if (image) {
+        this.image = 'data:image/jpeg;base64,' + response.image;
+      } else {
+        this.image = require('@/assets/images/closed.svg');
+      }
+    },
+    stateOpen(response) {
+      var image;
+
+      if (typeof response.image != 'undefined') {
+        image = true;
+      } else {
+        image = false;
+      }
+
+      this.buttons.action.loading = false;
+      this.buttons.action.disabled = false;
+      this.buttons.refresh.loading = false;
+      this.buttons.refresh.disabled = false;
+
+      this.status.state = status.open.state;
+      this.status.text = status.open.text;
+      this.icon = require('@/assets/images/open.svg');
+
+      if (image) {
+        this.image = 'data:image/jpeg;base64,' + response.image;
+      } else {
+        this.image = require('@/assets/images/open.svg');
+      }
+    },
+    stateMoving(response) {
+      var image;
+
+      if (typeof response.image != 'undefined') {
+        image = true;
+      } else {
+        image = false;
+      }
+
+      this.buttons.action.loading = true;
+      this.buttons.action.disabled = true;
+      this.buttons.refresh.loading = true;
+      this.buttons.refresh.disabled = true;
+
+      this.status.state = status.moving.state;
+      this.status.text = status.moving.text;
+      this.icon = require('@/assets/images/moving.svg');
+
+      if (image) {
+        this.image = 'data:image/jpeg;base64,' + response.image;
+      } else {
+        this.image = require('@/assets/images/moving.svg');
+      }
+    },
+    stateError(err) {
+      this.buttons.action.loading = false;
+      this.buttons.refresh.loading = false;
+      this.snackbar.text = messages.refreshFailed.text;
+      this.snackbar.color = messages.refreshFailed.color;
+      this.snackbar.show = true;
+    },
+  },
+};
 </script>
diff --git a/src/views/Settings.vue b/src/views/Settings.vue
new file mode 100644 (file)
index 0000000..1a59720
--- /dev/null
@@ -0,0 +1,178 @@
+<template>
+  <v-content>
+    <v-card flat>
+      <v-list-item three-line>
+        <v-list-item-content>
+          <v-list-item-subtitle>
+            Either Production API-Key or Test mode must be activated.
+          </v-list-item-subtitle>
+        </v-list-item-content>
+      </v-list-item>
+    </v-card>
+
+    <v-list
+      three-line
+      flat
+    >
+      <v-list-item-group
+        v-model="settings"
+        multiple
+      >
+        <v-divider />
+        <template v-for="item in listItems">
+          <v-divider v-if="item.title === null" />
+          <v-list-item v-else v-on:change="item.itemAction">
+            <template>
+              <v-list-item-action>
+                <v-switch
+                  v-model="item.value"
+                  color="primary"
+                  v-on:change="item.switchAction"
+                />
+              </v-list-item-action>
+              <v-list-item-content>
+                <v-list-item-title>
+                  {{item.title}}
+                </v-list-item-title>
+                <v-list-item-subtitle>
+                  {{item.subtitle}}
+                </v-list-item-subtitle>
+              </v-list-item-content>
+            </template>
+          </v-list-item>
+        </template>
+      </v-list-item-group>
+    </v-list>
+
+    <ApiKeyDialog
+      :showApiKeyDialog="showApiKeyDialog"
+      v-on:apiKeySetEvent="setApiKey"
+    />
+  </v-content>
+</template>
+
+<script>
+import ApiKeyDialog from '@/components/ApiKey'
+import { storage } from '@/lib/lib.js'
+
+export default {
+  components: {
+    ApiKeyDialog,
+  },
+  data() {
+    return {
+      listItems: [
+        {
+          value: false,
+          itemAction: this.setApiKeyInItem,
+          switchAction: this.setApiKeyInSwitch,
+          title: 'Production API-Key',
+          subtitle: 'Allows access on pruduction features',
+        },
+        {
+          value: false,
+          itemAction: this.setPushNotificationInItem,
+          switchAction: this.setPushNotificationInSwitch,
+          title: 'Push notification',
+          subtitle: 'Allows access on production features',
+        },
+        {
+          value: false,
+          itemAction: this.setImageTransmissionInItem,
+          switchAction: this.setImageTransmissionInSwitch,
+          title: 'Transmit live image',
+          subtitle: 'Gets web cam image of garage door every time it\'s refreshed',
+        },
+        {
+          title: null,
+        },
+        {
+          value: false,
+          itemAction: this.setTestModeInItem,
+          switchAction: this.setTestModeInSwitch,
+          title: 'Test mode',
+          subtitle: 'Enables simulation of production features',
+        },
+      ],
+      settings: [
+        {
+          key: 'apiKey',
+          value: '',
+        },
+        {
+          key: 'pushNotification',
+          value: false,
+        },
+        {
+          key: 'imageTransmission',
+          value: false,
+        },
+        {
+        },
+        {
+          key: 'testMode',
+          value: false,
+        },
+      ],
+      showApiKeyDialog: false,
+    };
+  },
+  created() {
+    if (storage.hasItem('testMode')) {
+      this.listItems[0].value = storage.getItem('apiKey');
+      this.listItems[1].value = storage.getItem('pushNotification');
+      this.listItems[2].value = storage.getItem('imageTransmission');
+      this.listItems[4].value = storage.getItem('testMode');
+    } else {
+      storage.setItem('apiKey', '');
+      storage.setItem('pushNotification', false);
+      storage.setItem('imageTransmission', false);
+      storage.setItem('testMode', false);
+    }
+  },
+  methods: {
+    setApiKeyInItem() {
+      this.listItems[0].value = !this.listItems[0].value;
+      if (this.listItems[0].value) {
+        this.showApiKeyDialog = ['showDialog'];
+      } else {
+        this.setApiKey('');
+      }
+    },
+    setApiKeyInSwitch() {
+      this.listItems[0].value = !this.listItems[0].value;
+    },
+    setApiKey(value) {
+      value === ''
+        ? this.listItems[0].value = false
+        : this.listItems[0].value = true;
+      this.settings[0].value = value;
+      storage.setItem('apiKey', this.settings[0].value);
+    },
+    setPushNotificationInItem() {
+      this.listItems[1].value = !this.listItems[1].value;
+      this.settings[1].value = this.listItems[1].value;
+      storage.setItem('pushNotification', this.settings[1].value);
+    },
+    setPushNotificationInSwitch() {
+      this.listItems[1].value = !this.listItems[1].value;
+    },
+    setImageTransmissionInItem() {
+      this.listItems[2].value = !this.listItems[2].value;
+      this.settings[2].value = this.listItems[2].value;
+      storage.setItem('imageTransmission', this.settings[2].value);
+    },
+    setImageTransmissionInSwitch() {
+      this.listItems[2].value = !this.listItems[2].value;
+    },
+    setTestModeInItem() {
+      this.listItems[4].value = !this.listItems[4].value;
+      this.settings[4].value = this.listItems[4].value;
+      storage.setItem('testMode', this.settings[4].value);
+    },
+    setTestModeInSwitch() {
+      this.listItems[4].value = !this.listItems[4].value;
+    },
+  },
+};
+</script>
diff --git a/vue.config.js b/vue.config.js
new file mode 100644 (file)
index 0000000..9405fcc
--- /dev/null
@@ -0,0 +1,13 @@
+module.exports = {
+  "transpileDependencies": [
+    "vuetify"
+  ],
+
+  pwa: {
+    name: 'Garage Node',
+    themeColor: '#1976D2',
+    msTileColor: '#424242'
+  },
+
+  publicPath: ''
+}