summaryrefslogtreecommitdiffstats
path: root/webapp/src
diff options
context:
space:
mode:
authorJannik Schönartz2018-07-02 22:39:23 +0200
committerJannik Schönartz2018-07-02 22:39:23 +0200
commit718f9b58331f4a9bee5eba3296329cc58b4364a6 (patch)
tree63e0048923f1a2a4f4fc44ef9aec0e7c51a967e1 /webapp/src
parent[server] Initial commit to add the node server stuff. (diff)
downloadbas-718f9b58331f4a9bee5eba3296329cc58b4364a6.tar.gz
bas-718f9b58331f4a9bee5eba3296329cc58b4364a6.tar.xz
bas-718f9b58331f4a9bee5eba3296329cc58b4364a6.zip
[webapp] Initial commit to add the node webapp stuff.
Diffstat (limited to 'webapp/src')
-rw-r--r--webapp/src/assets/logo.svg18
-rw-r--r--webapp/src/assets/styles.css27
-rw-r--r--webapp/src/components/AccountModule.vue32
-rw-r--r--webapp/src/components/BackendModule.vue67
-rw-r--r--webapp/src/components/ComponentTemplate.vue30
-rw-r--r--webapp/src/components/DashboardPage.vue237
-rw-r--r--webapp/src/components/LoginPage.vue127
-rw-r--r--webapp/src/components/MyModule.vue50
-rw-r--r--webapp/src/components/PermissionModule.vue51
-rw-r--r--webapp/src/components/SettingsModule.vue86
-rw-r--r--webapp/src/components/YourModule.vue51
-rw-r--r--webapp/src/main.js42
-rw-r--r--webapp/src/router.js44
-rw-r--r--webapp/src/store.js31
14 files changed, 893 insertions, 0 deletions
diff --git a/webapp/src/assets/logo.svg b/webapp/src/assets/logo.svg
new file mode 100644
index 0000000..cadb999
--- /dev/null
+++ b/webapp/src/assets/logo.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 655 768" style="enable-background:new 0 0 655 768;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill:#0095FF;}
+ .st2{fill:#004373;}
+ .st3{fill:#0076CA;}
+ .st4{fill:#005998;}
+</style>
+<polygon id="XMLID_5_" class="st0" points="618.8,360.3 3.5,715.5 3.5,753.6 651.8,379.4 "/>
+<polygon id="XMLID_13_" class="st1" points="586,342.5 585.4,342.9 171.5,582.7 171.5,491.3 428.4,343 428.5,342.9 427.8,342.5
+ 333.5,288.1 333.5,196.3 "/>
+<polygon id="XMLID_14_" class="st2" points="249.5,147.6 249.5,239.6 87.5,146.1 87.5,631.3 3.5,680 3.5,5.1 5.5,6.2 "/>
+<polygon id="XMLID_8_" class="st3" points="586,342.5 585.4,342.9 428.4,343 428.5,342.9 427.8,342.5 333.5,288.1 333.5,196.3 "/>
+<polygon id="XMLID_11_" class="st4" points="249.5,147.6 249.5,239.6 87.5,146.1 5.5,6.2 "/>
+</svg>
diff --git a/webapp/src/assets/styles.css b/webapp/src/assets/styles.css
new file mode 100644
index 0000000..3a7da9a
--- /dev/null
+++ b/webapp/src/assets/styles.css
@@ -0,0 +1,27 @@
+html {
+ overflow-y: auto;
+}
+
+.non-selectable {
+ -webkit-user-select: none; /* Safari 3.1+ */
+ -moz-user-select: none; /* Firefox 2+ */
+ -ms-user-select: none; /* IE 10+ */
+ user-select: none; /* Standard syntax */
+}
+
+.non-draggable {
+ -webkit-user-drag: none;
+ user-drag: none;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #666666;
+}
+
+::-webkit-scrollbar-button {
+ display: none;
+}
diff --git a/webapp/src/components/AccountModule.vue b/webapp/src/components/AccountModule.vue
new file mode 100644
index 0000000..8d93dbe
--- /dev/null
+++ b/webapp/src/components/AccountModule.vue
@@ -0,0 +1,32 @@
+<i18n>
+{
+ "en": {
+ },
+ "de": {
+ }
+}
+</i18n>
+
+<template>
+ <div class="account-page">
+
+ </div>
+</template>
+
+<script>
+
+export default {
+ name: 'AccountPage',
+ data () {
+ return {
+ }
+ },
+ methods: {
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/components/BackendModule.vue b/webapp/src/components/BackendModule.vue
new file mode 100644
index 0000000..8774db4
--- /dev/null
+++ b/webapp/src/components/BackendModule.vue
@@ -0,0 +1,67 @@
+<i18n>
+{
+ "en": {
+ },
+ "de": {
+ }
+}
+</i18n>
+
+<template>
+ <v-container>
+ <v-layout>
+ <v-flex md8 offset-md2 sm10 offset-sm1>
+ <v-stepper v-model="step" vertical>
+ <v-stepper-step :complete="stepCompleted >= 1" step="1" :editable="stepCompleted >= 1" edit-icon="check">
+ Select an app
+ <small>Summarize if needed</small>
+ </v-stepper-step>
+ <v-stepper-content step="1">
+ <v-card color="grey lighten-1" class="mb-5" height="200px"></v-card>
+ <v-btn color="primary" @click.native="step = 2; stepCompleted = Math.max(1, stepCompleted)">Continue</v-btn>
+ <v-btn flat>Cancel</v-btn>
+ </v-stepper-content>
+ <v-stepper-step :complete="stepCompleted >= 2" step="2" :editable="stepCompleted >= 2" edit-icon="check">Configure analytics for this app</v-stepper-step>
+ <v-stepper-content step="2">
+ <v-card color="grey lighten-1" class="mb-5" height="200px"></v-card>
+ <v-btn color="primary" @click.native="step = 3; stepCompleted = Math.max(2, stepCompleted)">Continue</v-btn>
+ <v-btn flat>Cancel</v-btn>
+ </v-stepper-content>
+ <v-stepper-step :complete="stepCompleted >= 3" step="3" :editable="stepCompleted >= 3" edit-icon="check">Select an ad format and name ad unit</v-stepper-step>
+ <v-stepper-content step="3">
+ <v-card color="grey lighten-1" class="mb-5" height="200px"></v-card>
+ <v-btn color="primary" @click.native="step = 4; stepCompleted = Math.max(3, stepCompleted)">Continue</v-btn>
+ <v-btn flat>Cancel</v-btn>
+ </v-stepper-content>
+ <v-stepper-step step="4" edit-icon="check">View setup instructions</v-stepper-step>
+ <v-stepper-content step="4" :editable="step > 3">
+ <v-card color="grey lighten-1" class="mb-5" height="200px"></v-card>
+ <v-btn color="primary" @click.native="step = 1">Continue</v-btn>
+ <v-btn flat>Cancel</v-btn>
+ </v-stepper-content>
+ </v-stepper>
+ </v-flex>
+ </v-layout>
+ </v-container>
+</template>
+
+<script>
+
+export default {
+ name: 'BackendModule',
+ data () {
+ return {
+ step: 1,
+ stepCompleted: 0
+ }
+ },
+ methods: {
+ }
+}
+
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/components/ComponentTemplate.vue b/webapp/src/components/ComponentTemplate.vue
new file mode 100644
index 0000000..3de9b36
--- /dev/null
+++ b/webapp/src/components/ComponentTemplate.vue
@@ -0,0 +1,30 @@
+<i18n>
+{
+ "en": {
+ },
+ "de": {
+ }
+}
+</i18n>
+
+<template>
+
+</template>
+
+<script>
+
+export default {
+ name: 'ComponentTemplate',
+ data () {
+ return {
+ }
+ },
+ methods: {
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/components/DashboardPage.vue b/webapp/src/components/DashboardPage.vue
new file mode 100644
index 0000000..4004147
--- /dev/null
+++ b/webapp/src/components/DashboardPage.vue
@@ -0,0 +1,237 @@
+<i18n>
+{
+ "en": {
+ "mymodule": "My Module",
+ "yourmodule": "Your Module",
+ "backend": "External Backends",
+ "permission": "Permissions",
+ "title": "Boot Selection Server",
+ "account": "Account",
+ "settings": "Settings",
+ "logout": "Logout"
+ },
+ "de": {
+ "mymodule": "Mein Modul",
+ "yourmodule": "Dein Module",
+ "backend": "Externe Backends",
+ "permission": "Rechteverwaltung",
+ "title": "Boot Auswahl Server",
+ "account": "Konto",
+ "settings": "Einstellungen",
+ "logout": "Abmelden"
+ }
+}
+</i18n>
+
+<template>
+ <v-app :dark="dark" class="non-selectable">
+ <v-touch
+ @panstart="drawerDragStart"
+ @panmove="drawerDragMove"
+ @panend="drawerDragEnd"
+ @pancancel="drawerDragEnd"
+ :pan-options="{ direction: 'horizontal', threshold: 0 }"
+ class="drawer-handle-closed"
+ ></v-touch>
+ <v-navigation-drawer
+ class="drawer"
+ :style="dragging && { transform: 'translateX(' + drawerTranslateX + 'px)', transition: 'none' }"
+ persistent
+ :mini-variant="desktop && drawerMini"
+ :clipped="clipped && desktop"
+ v-model="drawerOpen"
+ fixed
+ width="250"
+ app
+ touchless
+ > <v-touch
+ @panstart="drawerDragStart"
+ @panmove="drawerDragMove"
+ @panend="drawerDragEnd"
+ :pan-options="{ direction: 'horizontal', threshold: 0 }"
+ class="drawer-handle-open"
+ >
+ <div class="drawer-header grey darken-4 hidden-lg-and-up"><img class="logo" src="@/assets/logo.svg" /></div>
+ <v-list>
+ <v-list-tile ripple v-for="child in $options.dashboardModules" :key="child.path" :to="child.path">
+ <v-list-tile-action>
+ <v-icon v-html="child.dashboardIcon"></v-icon>
+ </v-list-tile-action>
+ <v-list-tile-content>
+ <v-list-tile-title v-text="$t(child.path)"></v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </v-list>
+ </v-touch>
+ </v-navigation-drawer>
+
+ <v-toolbar dark class="topbar" app :clipped-left="clipped">
+ <v-toolbar-side-icon class="drawer-icon" @click.stop="toggleDrawer"></v-toolbar-side-icon>
+ <img class="logo non-draggable hidden-md-and-down" src="@/assets/logo.svg" />
+ <v-spacer></v-spacer>
+ <v-toolbar-items>
+ <v-menu offset-y>
+ <v-btn class="user-button" flat slot="activator">
+ <v-icon left>account_circle</v-icon>{{ userFullName }}
+ </v-btn>
+ <v-list>
+ <v-list-tile to="account" active-class="">
+ <v-icon class="user-menu-icon">perm_identity</v-icon>
+ <v-list-tile-title>{{ $t('account') }}</v-list-tile-title>
+ </v-list-tile>
+ <v-list-tile to="settings" active-class="">
+ <v-icon class="user-menu-icon">settings</v-icon>
+ <v-list-tile-title>{{ $t('settings') }}</v-list-tile-title>
+ </v-list-tile>
+ <v-list-tile @click="logout">
+ <v-icon class="user-menu-icon">power_settings_new</v-icon>
+ <v-list-tile-title>{{ $t('logout') }}</v-list-tile-title>
+ </v-list-tile>
+ </v-list>
+ </v-menu>
+ </v-toolbar-items>
+ </v-toolbar>
+
+ <v-content>
+ <router-view />
+ </v-content>
+ </v-app>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import AccountModule from '@/components/AccountModule'
+import SettingsModule from '@/components/SettingsModule'
+import BackendModule from '@/components/BackendModule'
+import PermissionModule from '@/components/PermissionModule'
+import MyModule from '@/components/MyModule'
+import YourModule from '@/components/YourModule'
+
+export default {
+ name: 'DashboardPage',
+ dashboardModules: [
+ { path: 'yourmodule', component: YourModule, dashboardIcon: 'build' },
+ { path: 'mymodule', component: MyModule, dashboardIcon: 'build' },
+ { path: 'backend', component: BackendModule, dashboardIcon: 'cloud' },
+ { path: 'permission', component: PermissionModule, dashboardIcon: 'lock_open' }
+ ],
+ hiddenModules: [
+ { path: 'account', component: AccountModule },
+ { path: 'settings', component: SettingsModule }
+ ],
+ data () {
+ return {
+ dragging: false,
+ drawerTranslateX: 0,
+ drawerStartX: 0,
+ drawerMini: localStorage.getItem('drawerMini') === 'true',
+ drawerOpen: localStorage.getItem('drawerOpen') !== 'false',
+ userFullName: ''
+ }
+ },
+ computed: {
+ ...mapState(['dark', 'clipped', 'mini']),
+ desktop () { return this.$vuetify.breakpoint.lgAndUp }
+ },
+ watch: {
+ mini (value) {
+ if (value) {
+ this.drawerMini = !this.drawerOpen
+ this.drawerOpen = true
+ } else {
+ this.drawerOpen = !this.drawerMini
+ this.drawerMini = false
+ }
+ },
+ drawerMini (value) { localStorage.setItem('drawerMini', value) },
+ drawerOpen (value) { localStorage.setItem('drawerOpen', value) }
+ },
+ methods: {
+ toggleDrawer () {
+ if (this.mini && this.desktop) this.drawerMini = !this.drawerMini
+ else this.drawerOpen = !this.drawerOpen
+ },
+ drawerDragStart (event) {
+ this.drawerTranslateStartX = this.drawerTranslateX = this.drawerOpen ? 0 : -250
+ this.dragging = true
+ },
+ drawerDragMove (event) {
+ var newX = this.drawerTranslateStartX + event.deltaX
+ if (newX <= 0 && newX >= -250) this.drawerTranslateX = newX
+ },
+ drawerDragEnd (event) {
+ this.dragging = false
+ if ((this.drawerTranslateX < -100 && event.velocityX <= 0) || event.velocityX < -0.4) this.drawerOpen = false
+ else this.drawerOpen = true
+ },
+ logout () {
+ this.$http.post('/api/logout').then(response => {
+ this.$router.push('/login')
+ })
+ }
+ },
+ created () {
+ if (this.mini && this.desktop) this.drawerOpen = true
+ this.$http('/api/user/info').then(response => {
+ this.userFullName = response.data.name
+ })
+ }
+}
+</script>
+
+<style scoped>
+
+.topbar {
+ overflow: hidden;
+}
+
+.logo {
+ margin-left: 20px;
+ height: 400px;
+}
+
+.drawer {
+ height: 100vh !important;
+}
+
+.drawer-handle-closed {
+ position: fixed;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ width: 15px;
+ z-index: 1000;
+ /* border: 2px red solid; */
+}
+
+.drawer-handle-open {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ /* border: 2px red solid; */
+}
+
+.drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 140px;
+}
+
+.drawer-header > img {
+ height: 60%;
+}
+
+.user-button {
+ min-width: 160px;
+}
+
+.user-menu-icon {
+ margin-right: 8px;
+}
+
+.drawer-icon {
+ margin-left: 16px !important;
+}
+
+</style>
diff --git a/webapp/src/components/LoginPage.vue b/webapp/src/components/LoginPage.vue
new file mode 100644
index 0000000..36a7de5
--- /dev/null
+++ b/webapp/src/components/LoginPage.vue
@@ -0,0 +1,127 @@
+<i18n>
+{
+ "en": {
+ "username": "Username",
+ "password": "Password",
+ "login": "Login",
+ "usernameError": "User not found.",
+ "passwordError": "Wrong password.",
+ "usernameEmptyError": "Username can not be empty.",
+ "passwordEmptyError": "Passwort can not be empty."
+ },
+ "de": {
+ "username": "Benutzername",
+ "password": "Passwort",
+ "login": "Anmelden",
+ "usernameError": "Benutzer nicht gefunden.",
+ "passwordError": "Passwort falsch.",
+ "usernameEmptyError": "Benutzername kann nicht leer sein.",
+ "passwordEmptyError": "Passwort kann nicht leer sein."
+ }
+}
+</i18n>
+
+<template>
+ <v-app dark class="grey darken-4 non-selectable">
+ <div class="login-page">
+ <img class="logo non-draggable" src="@/assets/logo.svg" />
+ <v-form class="login-form" ref="form" v-model="valid" lazy-validation @submit.prevent="login">
+ <v-text-field
+ v-model="username"
+ :rules="usernameRules"
+ :label="$t('username')"
+ autocomplete="off"
+ @input="clearErrors"
+ ></v-text-field>
+ <v-text-field
+ type="password"
+ v-model="password"
+ :rules="passwordRules"
+ :label="$t('password')"
+ autocomplete="off"
+ @input="passwordError = false"
+ ></v-text-field>
+ <v-btn type="submit" class="login-button primary" raised>{{ $t('login') }}</v-btn>
+ </v-form>
+ </div>
+ </v-app>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+
+export default {
+ name: 'LoginPage',
+ data () {
+ return {
+ valid: true,
+ username: '',
+ usernameError: false,
+ usernameRules: [
+ v => !!v || this.$t('usernameEmptyError'),
+ v => !this.usernameError || this.$t('usernameError')
+ ],
+ password: '',
+ passwordError: false,
+ passwordRules: [
+ v => !!v || this.$t('passwordEmptyError'),
+ v => !this.passwordError || this.$t('passwordError')
+ ]
+ }
+ },
+ computed: mapState(['dark']),
+ methods: {
+ clearErrors () {
+ if (this.usernameError) this.usernameError = false
+ if (this.passwordError) {
+ this.passwordError = false
+ this.$refs.form.validate()
+ }
+ },
+ login () {
+ if (this.$refs.form.validate()) {
+ this.$http.post('/api/login', { username: this.username, password: this.password })
+ .then(response => {
+ this.$router.push('/dashboard')
+ })
+ .catch(error => {
+ if (error.response.data.status === 'USER_NOTFOUND') {
+ this.usernameError = true
+ } else if (error.response.data.status === 'PASSWORD_INVALID') {
+ this.passwordError = true
+ }
+ this.$refs.form.validate()
+ })
+ }
+ }
+ }
+}
+</script>
+
+<style scoped>
+
+.login-page {
+ height: 100%;
+ width: 100%;
+ min-height: 500px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.logo {
+ height: 180px;
+ margin-bottom: 60px;
+}
+
+.login-form {
+ width: 300px;
+}
+
+.login-button {
+ margin-top: 20px;
+ float: right;
+}
+
+</style>
diff --git a/webapp/src/components/MyModule.vue b/webapp/src/components/MyModule.vue
new file mode 100644
index 0000000..67aa859
--- /dev/null
+++ b/webapp/src/components/MyModule.vue
@@ -0,0 +1,50 @@
+<i18n>
+{
+ "en": {
+ "hello": "hello world!",
+ "test": {
+ "abc": "abc"
+ }
+ },
+ "de": {
+ "hello": "Hallo Welt!",
+ "test": {
+ "abc": "abc"
+ }
+ }
+}
+</i18n>
+
+<template>
+ <div class="my-module">
+ </div>
+</template>
+
+<script>
+
+export default {
+ name: 'MyModule',
+ data () {
+ return {
+ msg: 'Welcome to Your Vue.js App'
+ }
+ },
+ methods: {
+ logout () {
+ localStorage.removeItem('apiToken')
+ this.$router.push('/login')
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+.my-module {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+</style>
diff --git a/webapp/src/components/PermissionModule.vue b/webapp/src/components/PermissionModule.vue
new file mode 100644
index 0000000..c63f962
--- /dev/null
+++ b/webapp/src/components/PermissionModule.vue
@@ -0,0 +1,51 @@
+<i18n>
+{
+ "en": {
+ "roles": "Roles",
+ "users": "Users",
+ "createRole": "Create Role",
+ "assignRole": "Assign Role",
+ "revokeRole": "Revoke Role"
+ },
+ "de": {
+ "roles": "Rollen",
+ "users": "Nutzer",
+ "createRole": "Rolle erstellen",
+ "assignRole": "Rolle zuweisen",
+ "revokeROle": "Rolleentziehen"
+ }
+}
+</i18n>
+
+<template>
+ <v-container>
+ <v-layout>
+ <v-flex md10 offset-md1 sm10 offset-sm1>
+ <v-btn color="primary" @click="createRole">{{ $t('createRole') }}</v-btn>
+ <v-btn class="right" color="success">{{ $t('assignRole') }}</v-btn>
+ <v-btn class="right" color="warning">{{ $t('revokeRole') }}</v-btn>
+ </v-flex>
+ </v-layout>
+ </v-container>
+</template>
+
+<script>
+
+export default {
+ name: 'AccountPage',
+ data () {
+ return {
+ }
+ },
+ methods: {
+ createRole: function (event) {
+ window.open('https://bas.stfu-kthx.net:8000/#/dashboard/permission/createRole', '_self')
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/components/SettingsModule.vue b/webapp/src/components/SettingsModule.vue
new file mode 100644
index 0000000..b7625ef
--- /dev/null
+++ b/webapp/src/components/SettingsModule.vue
@@ -0,0 +1,86 @@
+<i18n>
+{
+ "en": {
+ "darkTheme": "Dark Theme",
+ "clipped": "Show drawer under the top bar",
+ "mini": "Show icons in collapsed drawer"
+ },
+ "de": {
+ "darkTheme": "Dark Theme",
+ "clipped": "Zeige den Drawer unter der Hauptleiste",
+ "mini": "Zeige die Symbole im eingeklappten Drawer"
+ }
+}
+</i18n>
+
+<template>
+ <v-container>
+ <v-layout>
+ <v-flex md4 offset-md4 sm10 offset-sm1>
+ <v-select
+ class="lang-select"
+ :items="langChoices"
+ v-model="locale"
+ solo
+ ></v-select>
+ <v-switch
+ :label="$t('darkTheme')"
+ v-model="dark"
+ color="primary"
+ ></v-switch>
+ <v-switch
+ :label="$t('clipped')"
+ v-model="clipped"
+ color="primary"
+ class="hidden-md-and-down"
+ ></v-switch>
+ <v-switch
+ :label="$t('mini')"
+ v-model="mini"
+ color="primary"
+ class="hidden-md-and-down"
+ ></v-switch>
+ </v-flex>
+ </v-layout>
+ </v-container>
+</template>
+
+<script>
+
+export default {
+ name: 'SettingsPage',
+ data () {
+ return {
+ langChoices: [ { text: 'Deutsch', value: 'de' }, { text: 'English', value: 'en' } ]
+ }
+ },
+ computed: {
+ locale: {
+ get () { return this.$store.state.locale },
+ set (value) { this.$store.commit('setLocale', value) }
+ },
+ dark: {
+ get () { return this.$store.state.dark },
+ set (value) { this.$store.commit('setDark', value) }
+ },
+ clipped: {
+ get () { return this.$store.state.clipped },
+ set (value) { this.$store.commit('setClipped', value) }
+ },
+ mini: {
+ get () { return this.$store.state.mini },
+ set (value) { this.$store.commit('setMini', value) }
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+.lang-select {
+ margin-top: 40px;
+ margin-bottom: 40px;
+}
+
+</style>
diff --git a/webapp/src/components/YourModule.vue b/webapp/src/components/YourModule.vue
new file mode 100644
index 0000000..8215c8d
--- /dev/null
+++ b/webapp/src/components/YourModule.vue
@@ -0,0 +1,51 @@
+<i18n>
+{
+ "en": {
+ "hello": "hello world!",
+ "test": {
+ "abc": "abc"
+ }
+ },
+ "de": {
+ "hello": "Hallo Welt!",
+ "test": {
+ "abc": "abc"
+ }
+ }
+}
+</i18n>
+
+<template>
+ <div class="your-module">
+
+ </div>
+</template>
+
+<script>
+
+export default {
+ name: 'YourModule',
+ data () {
+ return {
+ msg: 'Welcome to Your Vue.js App'
+ }
+ },
+ methods: {
+ logout () {
+ localStorage.removeItem('apiToken')
+ this.$router.push('/login')
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+.your-module {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+</style>
diff --git a/webapp/src/main.js b/webapp/src/main.js
new file mode 100644
index 0000000..4a553aa
--- /dev/null
+++ b/webapp/src/main.js
@@ -0,0 +1,42 @@
+import Vue from 'vue'
+import Vuetify from 'vuetify'
+import 'vuetify/dist/vuetify.min.css'
+import VueTouch from 'vue-touch'
+import store from './store'
+import axios from 'axios'
+import router from './router'
+import VueI18n from 'vue-i18n'
+import '@/assets/styles.css'
+
+Vue.config.productionTip = false
+
+Vue.use(Vuetify, {
+ theme: {
+ primary: '#0195ff'
+ }
+})
+
+Vue.use(VueTouch)
+
+Vue.use(VueI18n)
+
+const i18n = new VueI18n({
+ locale: store.state.locale
+})
+
+axios.interceptors.response.use(null, error => {
+ if (error.response.data.status === 'TOKEN_INVALID' || error.response.data.status === 'TOKEN_MISSING') {
+ axios.post('/api/logout').then(response => { router.push('/login') })
+ }
+ return Promise.reject(error)
+})
+Vue.prototype.$http = axios
+
+new Vue({
+ store,
+ router,
+ i18n,
+ computed: { locale: () => store.state.locale },
+ watch: { locale: v => { i18n.locale = v } },
+ template: '<router-view/>'
+}).$mount('#app')
diff --git a/webapp/src/router.js b/webapp/src/router.js
new file mode 100644
index 0000000..862198b
--- /dev/null
+++ b/webapp/src/router.js
@@ -0,0 +1,44 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import LoginPage from '@/components/LoginPage'
+import DashboardPage from '@/components/DashboardPage'
+
+Vue.use(Router)
+
+var router = new Router({
+ routes: [
+ {
+ path: '/login',
+ name: 'LoginPage',
+ component: LoginPage
+ },
+ {
+ path: '/dashboard',
+ redirect: '/dashboard/mymodule'
+ },
+ {
+ path: '/dashboard',
+ name: 'Dashboard',
+ component: DashboardPage,
+ children: DashboardPage.dashboardModules.concat(DashboardPage.hiddenModules)
+ },
+ {
+ path: '*',
+ redirect: '/dashboard'
+ }
+ ]
+})
+
+router.beforeEach((to, from, next) => {
+ var loggedIn = document.cookie.indexOf('jwt_hp') >= 0
+ if (to.path === '/login') {
+ if (loggedIn) next('/dashboard')
+ else next()
+ } else if (!loggedIn) {
+ next('/login')
+ } else {
+ next()
+ }
+})
+
+export default router
diff --git a/webapp/src/store.js b/webapp/src/store.js
new file mode 100644
index 0000000..a9cb637
--- /dev/null
+++ b/webapp/src/store.js
@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+ state: {
+ locale: localStorage.getItem('locale') || 'en',
+ dark: localStorage.getItem('dark') !== 'false',
+ clipped: localStorage.getItem('clipped') !== 'false',
+ mini: localStorage.getItem('mini') !== 'false'
+ },
+ mutations: {
+ setLocale (state, value) {
+ state.locale = value
+ localStorage.setItem('locale', value)
+ },
+ setDark (state, value) {
+ state.dark = value
+ localStorage.setItem('dark', value)
+ },
+ setClipped (state, value) {
+ state.clipped = value
+ localStorage.setItem('clipped', value)
+ },
+ setMini (state, value) {
+ state.mini = value
+ localStorage.setItem('mini', value)
+ }
+ }
+})