summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--webapp/src/components/DashboardPage.vue68
-rw-r--r--webapp/src/components/NotificationsAlerts.vue132
-rw-r--r--webapp/src/components/NotificationsSnackbars.vue93
-rw-r--r--webapp/src/config/store.js2
-rw-r--r--webapp/src/main.js7
-rw-r--r--webapp/src/store/global.js15
-rw-r--r--webapp/src/store/notifications.js36
7 files changed, 290 insertions, 63 deletions
diff --git a/webapp/src/components/DashboardPage.vue b/webapp/src/components/DashboardPage.vue
index 87c630a..89c8c0c 100644
--- a/webapp/src/components/DashboardPage.vue
+++ b/webapp/src/components/DashboardPage.vue
@@ -45,7 +45,7 @@
: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>
+ <div class="drawer-header grey darken-4 hidden-lg-and-up"><img src="@/assets/logo.svg" /></div>
<v-list>
<v-list-tile ripple v-for="module in modules" :key="module.name" :to="{ name: module.name }">
<v-list-tile-action>
@@ -59,12 +59,13 @@
</v-touch>
</v-navigation-drawer>
- <v-toolbar dark class="topbar" app :clipped-left="settings.clipped">
+ <v-toolbar dark app :clipped-left="settings.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" />
+ <div class="logo"><img class="non-draggable hidden-md-and-down" src="@/assets/logo.svg" /></div>
<v-spacer></v-spacer>
<v-btn v-if="devMode" icon @click="$store.commit('saveSetting', { name: 'locale', value: settings.locale === 'en' ? 'de' : 'en' })"><v-icon>language</v-icon></v-btn>
<v-btn v-if="devMode" icon @click="$store.commit('saveSetting', { name: 'dark', value: !settings.dark })"><v-icon>invert_colors</v-icon></v-btn>
+ <notifications-alerts></notifications-alerts>
<v-toolbar-items>
<v-menu offset-y>
<v-btn class="user-button" flat slot="activator">
@@ -92,27 +93,14 @@
<router-view />
</v-content>
- <v-snackbar
- color="primary"
- v-model="showSnackbar"
- bottom
- right
- :timeout="2000"
- >
- {{ snackbarText }}
- <v-btn
- flat
- @click="showSnackbar = false"
- >
- Close
- </v-btn>
- </v-snackbar>
-
+ <notifications-snackbars></notifications-snackbars>
</v-app>
</template>
<script>
-import { mapState, mapGetters, mapMutations } from 'vuex'
+import { mapState, mapMutations } from 'vuex'
+import NotificationsAlerts from '@/components/NotificationsAlerts'
+import NotificationsSnackbars from '@/components/NotificationsSnackbars'
import AccountModule from '@/components/AccountModule'
import SettingsModule from '@/components/SettingsModule'
import dashboardModules from '@/config/dashboard'
@@ -125,12 +113,13 @@ export default {
{ path: 'settings', component: SettingsModule }
]
},
+ components: {
+ NotificationsAlerts,
+ NotificationsSnackbars
+ },
data () {
return {
modules: dashboardModules,
- snackbarsInQueue: false,
- showSnackbar: false,
- snackbarText: 'MESSAGE',
dragging: false,
drawerWidth: 250,
drawerTranslateX: 0,
@@ -140,9 +129,8 @@ export default {
}
},
computed: {
- ...mapState(['settings', 'snackbars']),
+ ...mapState(['settings']),
mini () { return this.settings.mini },
- ...mapGetters(['nextSnackbar']),
desktop () { return this.$vuetify.breakpoint.lgAndUp },
drawerDraggingStyle () {
var style = {}
@@ -156,28 +144,6 @@ export default {
devMode () { return localStorage.getItem('dev') === 'true' }
},
watch: {
- showSnackbar (value) {
- if (!value) {
- var text = this.nextSnackbar
- if (text) {
- this.shiftSnackbars()
- setTimeout(() => {
- this.snackbarText = text
- this.showSnackbar = true
- }, 500)
- } else {
- this.snackbarsInQueue = false
- }
- }
- },
- snackbars (value) {
- if (!this.snackbarsInQueue) {
- this.snackbarsInQueue = true
- this.snackbarText = this.nextSnackbar
- this.shiftSnackbars()
- this.showSnackbar = true
- }
- },
mini (value) {
if (value) {
this.drawerMini = !this.drawerOpen
@@ -191,7 +157,7 @@ export default {
drawerOpen (value) { localStorage.setItem('drawerOpen', value) }
},
methods: {
- ...mapMutations(['shiftSnackbars', 'setLoginRedirect']),
+ ...mapMutations(['setLoginRedirect']),
toggleDrawer () {
if (this.settings.mini && this.desktop) this.drawerMini = !this.drawerMini
else this.drawerOpen = !this.drawerOpen
@@ -238,11 +204,13 @@ export default {
<style scoped>
-.topbar {
+.logo {
+ height: 100%;
overflow: hidden;
}
-.logo {
+.logo > img {
+ margin-top: -160px;
margin-left: 20px;
height: 400px;
}
diff --git a/webapp/src/components/NotificationsAlerts.vue b/webapp/src/components/NotificationsAlerts.vue
new file mode 100644
index 0000000..3f4f6d7
--- /dev/null
+++ b/webapp/src/components/NotificationsAlerts.vue
@@ -0,0 +1,132 @@
+<i18n>
+{
+ "en": {
+ "noNotifications": "No notifications.",
+ "new": "New",
+ "old": "Old",
+ "notifications": "Notifications"
+ },
+ "de": {
+ "noNotifications": "Keine Benachrichtigungen.",
+ "new": "Neu",
+ "old": "Alt",
+ "notifications": "Benachrichtigungen"
+ }
+}
+</i18n>
+
+<template>
+ <div>
+ <v-menu v-model="menu" offset-y left :close-on-content-click="false" :nudge-bottom="10" content-class="notifications">
+ <v-btn icon slot="activator">
+ <v-badge :value="newAlertCount" color="red darken-3" bottom>
+ <span slot="badge">{{newAlertCount}}</span>
+ <v-icon>notifications</v-icon>
+ </v-badge>
+ </v-btn>
+
+ <v-card>
+ <v-card-text>
+ <div style="max-height: 70vh; overflow: scroll">
+ <div v-if="alerts.length === 0" class="text-xs-center" style="width: 100%">{{ $t('noNotifications') }}</div>
+ <div v-else class="flex-between-center">
+ <v-subheader v-if="newCount > 0">{{ $t('new') }}</v-subheader>
+ <v-subheader v-else>{{ $t('notifications') }}</v-subheader>
+ <v-btn @click="clearAll" icon small style="margin-right: 16px"><v-icon>clear_all</v-icon></v-btn>
+ </div>
+ <template v-for="(a, index) in alerts">
+ <v-alert
+ :key="'alert' + index"
+ transition="scale-transition"
+ :value="a.show"
+ :type="a.type"
+ :color="a.color"
+ :icon="a.icon"
+ >
+ <div class="flex-between-center">
+ <div>{{ a.text }}</div>
+ <v-btn @click="closeAlert(a)" small :ripple="false" style="margin: 0; margin-top: -4px" icon><v-icon>close</v-icon></v-btn>
+ </div>
+ </v-alert>
+ <v-subheader :key="'subheader' + index" v-if="newCount < alerts.length && index == newCount - 1">{{ $t('old') }}</v-subheader>
+ </template>
+ </div>
+ </v-card-text>
+ </v-card>
+ </v-menu>
+ </div>
+</template>
+
+<script>
+import { mapState, mapMutations } from 'vuex'
+
+export default {
+ name: 'NotificationsAlerts',
+ data () {
+ return {
+ menu: false,
+ newCount: 0
+ }
+ },
+ computed: {
+ ...mapState('notifications', ['newAlertCount', 'alerts']),
+ alertsShown () {
+ return this.alerts.some(a => a.show)
+ }
+ },
+ watch: {
+ menu (value) {
+ if (value) {
+ this.newCount = this.newAlertCount
+ this.resetNewAlertCount()
+ }
+ }
+ },
+ methods: {
+ ...mapMutations('notifications', ['resetNewAlertCount', 'removeAlert']),
+ closeAlert (a) {
+ this.removeAlert(a)
+ if (!this.alertsShown) this.menu = false
+ },
+ clearAll () {
+ var timeout = this.alerts.length * 50
+ setTimeout(this.closeMenu, timeout)
+ var removeAlert = this.removeAlert
+ this.alerts.forEach(a => {
+ timeout -= 50
+ setTimeout(function () {
+ removeAlert(a)
+ }, timeout)
+ })
+ },
+ closeMenu () {
+ this.menu = false
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+@media (max-width: 960px) {
+ .notifications {
+ right: 12px;
+ max-width: 100%;
+ }
+}
+
+@media (min-width: 961px) {
+ .notifications {
+ width: 600px;
+
+ }
+}
+
+.flex-between-center {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+</style>
diff --git a/webapp/src/components/NotificationsSnackbars.vue b/webapp/src/components/NotificationsSnackbars.vue
new file mode 100644
index 0000000..ce768d6
--- /dev/null
+++ b/webapp/src/components/NotificationsSnackbars.vue
@@ -0,0 +1,93 @@
+<i18n>
+{
+ "en": {
+ "close": "Close"
+ },
+ "de": {
+ "close": "Schließen"
+ }
+}
+</i18n>
+
+<template>
+ <v-snackbar
+ v-model="showSnackbar"
+ bottom
+ right
+ :timeout="currentSnackbar.timeout || 2000"
+ :color="currentSnackbar.color"
+ >
+ {{ currentSnackbar.text }}
+ <v-btn
+ flat
+ @click="showSnackbar = false"
+ >
+ {{ $t('close') }}
+ </v-btn>
+ </v-snackbar>
+</template>
+
+<script>
+import { mapState, mapGetters, mapMutations } from 'vuex'
+
+export default {
+ name: 'NotificationsSnackbars',
+ data () {
+ return {
+ snackbarsInQueue: false,
+ currentSnackbar: {},
+ showSnackbar: false
+ }
+ },
+ computed: {
+ ...mapState('notifications', ['snackbars']),
+ ...mapGetters('notifications', ['nextSnackbar'])
+ },
+ watch: {
+ showSnackbar (value) {
+ if (!value) {
+ var nextSnackbar = this.nextSnackbar
+ if (nextSnackbar) {
+ this.shiftSnackbars()
+ setTimeout(() => {
+ this.currentSnackbar = nextSnackbar
+ this.showSnackbar = true
+ }, 500)
+ } else {
+ this.snackbarsInQueue = false
+ }
+ }
+ },
+ snackbars (value) {
+ if (!this.snackbarsInQueue) {
+ this.snackbarsInQueue = true
+ this.currentSnackbar = this.nextSnackbar
+ this.shiftSnackbars()
+ this.showSnackbar = true
+ }
+ }
+ },
+ methods: {
+ ...mapMutations('notifications', ['shiftSnackbars'])
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+@media (max-width: 960px) {
+ .notifications {
+ right: 12px;
+ max-width: 100%;
+ }
+}
+
+@media (min-width: 961px) {
+ .notifications {
+ width: 600px;
+
+ }
+}
+
+</style>
diff --git a/webapp/src/config/store.js b/webapp/src/config/store.js
index 710a07b..f83328b 100644
--- a/webapp/src/config/store.js
+++ b/webapp/src/config/store.js
@@ -1,3 +1,4 @@
+import notifications from '@/store/notifications'
import groups from '@/store/groups'
import configurator from '@/store/configurator'
import registration from '@/store/registration'
@@ -5,6 +6,7 @@ import backends from '@/store/backends'
import permissions from '@/store/permissions'
export default {
+ notifications,
groups,
configurator,
registration,
diff --git a/webapp/src/main.js b/webapp/src/main.js
index 030f9f7..3f59ede 100644
--- a/webapp/src/main.js
+++ b/webapp/src/main.js
@@ -59,7 +59,14 @@ axios.interceptors.response.use(null, error => {
}
return Promise.reject(error)
})
+
Vue.prototype.$http = axios
+Vue.prototype.$alert = function (data) {
+ store.commit('notifications/newAlert', data)
+}
+Vue.prototype.$snackbar = function (data) {
+ store.commit('notifications/newSnackbar', data)
+}
new Vue({
store,
diff --git a/webapp/src/store/global.js b/webapp/src/store/global.js
index bf31ce9..3277cea 100644
--- a/webapp/src/store/global.js
+++ b/webapp/src/store/global.js
@@ -19,26 +19,15 @@ export default {
clipped: loadSetting('clipped', true),
mini: loadSetting('mini', true)
},
- snackbars: [],
loginRedirect: null
},
getters: {
tabsDark: state => state.settings.dark || state.settings.coloredTabs,
tabsColor: state => state.settings.coloredTabs ? 'primary' : '',
- tabsSliderColor: state => state.settings.coloredTabs ? 'white' : 'primary',
- nextSnackbar (state) {
- if (state.snackbars) return state.snackbars[0]
- else return ''
- }
+ tabsSliderColor: state => state.settings.coloredTabs ? 'white' : 'primary'
},
mutations: {
setLoginRedirect: (state, value) => { state.loginRedirect = value },
- saveSetting (state, { name, value }) { if (name in state.settings) state.settings[name] = value; localStorage.setItem('settings.' + name, value) },
- shiftSnackbars (state) {
- state.snackbars.shift()
- },
- newSnackbar (state, text) {
- state.snackbars.push(text)
- }
+ saveSetting (state, { name, value }) { if (name in state.settings) state.settings[name] = value; localStorage.setItem('settings.' + name, value) }
}
}
diff --git a/webapp/src/store/notifications.js b/webapp/src/store/notifications.js
new file mode 100644
index 0000000..9f41d07
--- /dev/null
+++ b/webapp/src/store/notifications.js
@@ -0,0 +1,36 @@
+export default {
+ namespaced: true,
+ state: {
+ alerts: [],
+ snackbars: [],
+ newAlertCount: 0
+ },
+ getters: {
+ nextSnackbar (state) {
+ if (state.snackbars) return state.snackbars[0]
+ else return null
+ }
+ },
+ mutations: {
+ shiftSnackbars (state) {
+ state.snackbars.shift()
+ },
+ newSnackbar (state, data) {
+ state.snackbars.push(data)
+ },
+ newAlert (state, data) {
+ data.show = true
+ state.newAlertCount++
+ state.alerts.unshift(data)
+ },
+ removeAlert (state, a) {
+ a.show = false
+ setTimeout(function () {
+ state.alerts.splice(state.alerts.indexOf(a), 1)
+ }, 200)
+ },
+ resetNewAlertCount (state) {
+ state.newAlertCount = 0
+ }
+ }
+}