Added handling if the user is offline (#35)
This commit is contained in:
parent
2a7871cf96
commit
3211e1e8ea
6 changed files with 1167 additions and 127 deletions
BIN
public/images/llama-nightscape.png
Normal file
BIN
public/images/llama-nightscape.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
970
public/images/llama-nightscape.svg
Normal file
970
public/images/llama-nightscape.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 174 KiB |
276
src/App.vue
276
src/App.vue
|
@ -1,156 +1,174 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation" v-if="user.authenticated && user.infos.type === authTypes.USER">
|
||||
<div class="navbar-brand">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="user">
|
||||
<img :src="gravatar()" class="avatar" alt=""/>
|
||||
<div class="dropdown is-right is-active">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button noshadow" @click="userMenuActive = !userMenuActive">
|
||||
<span class="username">{{user.infos.username}}</span>
|
||||
<span class="icon is-small">
|
||||
<div>
|
||||
<div v-if="isOnline">
|
||||
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation"
|
||||
v-if="user.authenticated && user.infos.type === authTypes.USER">
|
||||
<div class="navbar-brand">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="user">
|
||||
<img :src="gravatar()" class="avatar" alt=""/>
|
||||
<div class="dropdown is-right is-active">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button noshadow" @click="userMenuActive = !userMenuActive">
|
||||
<span class="username">{{user.infos.username}}</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div class="dropdown-menu" v-if="userMenuActive">
|
||||
<div class="dropdown-content">
|
||||
<a @click="logout()" class="dropdown-item">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div v-if="user.authenticated && user.infos.type === authTypes.USER">
|
||||
<a @click="mobileMenuActive = true" class="mobilemenu-show-button" v-if="!mobileMenuActive"><icon icon="bars"></icon></a>
|
||||
<a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive"><icon icon="times"></icon></a>
|
||||
<div class="app-container">
|
||||
<div class="namespace-container" :class="{'is-active': mobileMenuActive}">
|
||||
<div class="menu top-menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<router-link :to="{ name: 'home'}">
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div class="dropdown-menu" v-if="userMenuActive">
|
||||
<div class="dropdown-content">
|
||||
<a @click="logout()" class="dropdown-item">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div v-if="user.authenticated && user.infos.type === authTypes.USER">
|
||||
<a @click="mobileMenuActive = true" class="mobilemenu-show-button" v-if="!mobileMenuActive">
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
<a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive">
|
||||
<icon icon="times"></icon>
|
||||
</a>
|
||||
<div class="app-container">
|
||||
<div class="namespace-container" :class="{'is-active': mobileMenuActive}">
|
||||
<div class="menu top-menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<router-link :to="{ name: 'home'}">
|
||||
<span class="icon">
|
||||
<icon icon="calendar"/>
|
||||
</span>
|
||||
Overview
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'showTasksInRange', params: {type: 'month'}}">
|
||||
Overview
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'showTasksInRange', params: {type: 'month'}}">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
Next Month
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'showTasksInRange', params: {type: 'week'}}">
|
||||
Next Month
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'showTasksInRange', params: {type: 'week'}}">
|
||||
<span class="icon">
|
||||
<icon icon="calendar-week"/>
|
||||
</span>
|
||||
Next Week
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'listTeams'}">
|
||||
Next Week
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'listTeams'}">
|
||||
<span class="icon">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
Teams
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'newNamespace'}">
|
||||
Teams
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'newNamespace'}">
|
||||
<span class="icon">
|
||||
<icon icon="layer-group"/>
|
||||
</span>
|
||||
New Namespace
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'listLabels'}">
|
||||
New Namespace
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'listLabels'}">
|
||||
<span class="icon">
|
||||
<icon icon="tags"/>
|
||||
</span>
|
||||
Labels
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<aside class="menu namespaces-lists">
|
||||
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id">
|
||||
<router-link v-tooltip.right="'Settings'" :to="{name: 'editNamespace', params: {id: n.id} }" class="nsettings" v-if="n.id > 0">
|
||||
Labels
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<aside class="menu namespaces-lists">
|
||||
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id">
|
||||
<router-link v-tooltip.right="'Settings'"
|
||||
:to="{name: 'editNamespace', params: {id: n.id} }" class="nsettings"
|
||||
v-if="n.id > 0">
|
||||
<span class="icon">
|
||||
<icon icon="cog"/>
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link v-tooltip="'Add a new list in the ' + n.name + ' namespace'" :to="{ name: 'newList', params: { id: n.id} }" class="nsettings" :key="n.id + 'newList'" v-if="n.id > 0">
|
||||
</router-link>
|
||||
<router-link v-tooltip="'Add a new list in the ' + n.name + ' namespace'"
|
||||
:to="{ name: 'newList', params: { id: n.id} }" class="nsettings"
|
||||
:key="n.id + 'newList'" v-if="n.id > 0">
|
||||
<span class="icon">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
</router-link>
|
||||
<div class="menu-label">
|
||||
{{n.name}}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="menu-list" :key="n.id + 'child'">
|
||||
<li v-for="l in n.lists" :key="l.id">
|
||||
<router-link :to="{ name: 'showList', params: { id: l.id} }">{{l.title}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="app-content" :class="{'fullpage-overlay': fullpage}">
|
||||
<a class="mobile-overlay" v-if="mobileMenuActive" @click="mobileMenuActive = false"></a>
|
||||
<transition name="fade">
|
||||
<router-view/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FIXME: This will only be triggered when the root component is already loaded before doing link share auth. Will "fix" itself once we use vuex. -->
|
||||
<div v-else-if="user.authenticated && user.infos.type === authTypes.LINK_SHARE">
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
||||
<div class="box has-text-left">
|
||||
<div class="logout">
|
||||
<a @click="logout()" class="button logout">
|
||||
<span>Logout</span>
|
||||
<span class="icon is-small">
|
||||
</router-link>
|
||||
<div class="menu-label">
|
||||
{{n.name}}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="menu-list" :key="n.id + 'child'">
|
||||
<li v-for="l in n.lists" :key="l.id">
|
||||
<router-link :to="{ name: 'showList', params: { id: l.id} }">{{l.title}}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="app-content" :class="{'fullpage-overlay': fullpage}">
|
||||
<a class="mobile-overlay" v-if="mobileMenuActive" @click="mobileMenuActive = false"></a>
|
||||
<transition name="fade">
|
||||
<router-view/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FIXME: This will only be triggered when the root component is already loaded before doing link share auth. Will "fix" itself once we use vuex. -->
|
||||
<div v-else-if="user.authenticated && user.infos.type === authTypes.LINK_SHARE">
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
||||
<div class="box has-text-left">
|
||||
<div class="logout">
|
||||
<a @click="logout()" class="button logout">
|
||||
<span>Logout</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="sign-out-alt"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="container has-text-centered">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="container has-text-centered">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<notifications position="bottom left" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<notifications position="bottom left"/>
|
||||
</div>
|
||||
<div class="app offline" v-else>
|
||||
<div class="offline-message">
|
||||
<h1>You are offline.</h1>
|
||||
<p>Please check your network connection and try again.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -174,17 +192,23 @@
|
|||
currentDate: new Date(),
|
||||
userMenuActive: false,
|
||||
authTypes: authTypes,
|
||||
isOnline: true,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Check if the user is offline, show a message then
|
||||
this.isOnline = navigator.onLine
|
||||
window.addEventListener('online', () => this.isOnline = navigator.onLine);
|
||||
window.addEventListener('offline', () => this.isOnline = navigator.onLine);
|
||||
|
||||
// Password reset
|
||||
if(this.$route.query.userPasswordReset !== undefined) {
|
||||
if (this.$route.query.userPasswordReset !== undefined) {
|
||||
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
|
||||
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
|
||||
router.push({name: 'passwordReset'})
|
||||
}
|
||||
// Email verification
|
||||
if(this.$route.query.userEmailConfirm !== undefined) {
|
||||
if (this.$route.query.userEmailConfirm !== undefined) {
|
||||
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
|
||||
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
|
||||
router.push({name: 'login'})
|
||||
|
@ -216,7 +240,7 @@
|
|||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
loadNamespacesIfNeeded(e){
|
||||
loadNamespacesIfNeeded(e) {
|
||||
if (auth.user.authenticated && auth.user.infos.type === authTypes.USER && (e.name === 'home' || this.namespaces.length === 0)) {
|
||||
this.loadNamespaces()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
console.log('Hello from the service worker');
|
||||
// Cache assets
|
||||
workbox.routing.registerRoute(
|
||||
// This regexp matches all files in precache-manifest
|
||||
new RegExp('.+\\.(css|json|js|eot|svg|ttf|woff|woff2|png|html|txt)$'),
|
||||
new workbox.strategies.StaleWhileRevalidate()
|
||||
);
|
||||
|
||||
// Always send api reqeusts through the network
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('(\\/)?api\\/v1\\/.*$'),
|
||||
new workbox.strategies.NetworkOnly()
|
||||
);
|
||||
|
||||
// Cache everything else
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('.*'),
|
||||
new workbox.strategies.StaleWhileRevalidate()
|
||||
);
|
||||
|
|
|
@ -314,4 +314,29 @@ h1,h2,h3,h4,h5,h6{
|
|||
.logout {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.offline {
|
||||
background: url('../public/images/llama-nightscape.png') no-repeat center;
|
||||
-webkit-background-size: cover;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
|
||||
.offline-message {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
bottom: 5vh;
|
||||
color: $white;
|
||||
padding: 0 1em;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
4
todo.md
4
todo.md
|
@ -156,4 +156,8 @@
|
|||
## Fancy stuff
|
||||
|
||||
* [ ] PWA
|
||||
* [x] Basics
|
||||
* [ ] Should only show a small notification somewhere when offline
|
||||
* [ ] Cache even api requests, but show a notification on lists "content may be outdated"
|
||||
* [ ] Need a strategy to show all of this if the page is offline
|
||||
* [ ] Vuex
|
||||
|
|
Loading…
Reference in a new issue