added lots of animations, added debounce to search query, fixed faulty refetching when logging out from profile page

This commit is contained in:
Sockenklaus
2021-11-14 11:20:01 +01:00
parent a92342d445
commit 0d00d73eb4
11 changed files with 174 additions and 118 deletions

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title> <title>Vite App</title>
</head> </head>
<body class="bg-light"> <body style="overflow-Y: scroll;" class="bg-light">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@@ -3,8 +3,12 @@
<v-main> <v-main>
<Notification /> <Notification />
<div class="container shadow px-4 pt-3 pb-5 text-center"> <div class="container shadow px-4 pt-3 pb-5 text-center">
<VNavigation /> <VNavigation />
<router-view></router-view> <router-view v-slot="{ Component, route }">
<transition name="fade-slide-y" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</div> </div>
</v-main> </v-main>
@@ -18,11 +22,28 @@ import VNavigation from './components/VNavigation.vue';
</script> </script>
<style> <style lang="scss" scoped>
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #2c3e50; color: #2c3e50;
} }
.fade-slide-y- {
&enter-active {
transition: all .2s ease
}
&leave-active {
transition: all .2s cubic-bezier(1,.5,.8,1)
// transition: all .3s ease
}
&enter-from,
&leave-to {
transform: translateY(10px);
opacity: 0;
}
}
</style> </style>

View File

@@ -29,6 +29,7 @@
import { ref, computed} from 'vue'; import { ref, computed} from 'vue';
import type { PropType, Ref } from 'vue'; import type { PropType, Ref } from 'vue';
import { union } from 'lodash'; import { union } from 'lodash';
import _debounce from 'lodash/debounce'
const props = defineProps({ const props = defineProps({
columns: { columns: {
@@ -50,9 +51,9 @@ const unionColumns = computed(() => {
return union(props.columns, columnsChecked.value) return union(props.columns, columnsChecked.value)
}) })
async function search() { const search = _debounce(() => {
emit('search', queryString.value) emit('search', queryString.value)
} }, 150)
</script> </script>

View File

@@ -1,19 +1,3 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useUser } from '@/stores/user';
const userStore = useUser()
const router = useRouter()
async function onLogout() {
if(await userStore.logout()){
router.push({name: 'Login'})
}
}
</script>
<template> <template>
<nav class="nav justify-content-center border-bottom mb-4 pb-2 d-flex"> <nav class="nav justify-content-center border-bottom mb-4 pb-2 d-flex">
<router-link class="nav-link" :to="{name: 'Home'}">Home</router-link> <router-link class="nav-link" :to="{name: 'Home'}">Home</router-link>
@@ -32,6 +16,20 @@ async function onLogout() {
</nav> </nav>
</template> </template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useUser } from '@/stores/user';
const userStore = useUser()
const router = useRouter()
function onLogout() {
userStore.logout(router)
}
</script>
<style scoped> <style scoped>
</style> </style>

View File

@@ -29,25 +29,50 @@ function onCancel() {
Zurück Zurück
</router-link> </router-link>
<button v-if="!isActive" type="button" @click="onEdit" class="btn btn-primary ms-auto"> <transition name="button-save" mode="out-in">
<i class="bi bi-pen"></i> <div class="ms-auto" v-if="isActive">
Mitarbeiter bearbeiten <button v-if="isActive" type="submit" @click.prevent="onSave" class="btn btn-success">
</button> <i class="bi bi-save"></i>
Mitarbeiter speichern
<button v-if="isActive" type="submit" @click.prevent="onSave" class="btn btn-success ms-auto"> </button>
<i class="bi bi-save"></i> <button v-if="isActive" type="button" @click="onCancel" class="btn btn-outline-secondary ms-3">
Mitarbeiter speichern <i class="bi bi-x-lg"></i>
</button> Abbrechen
</button>
<button v-if="isActive" type="button" @click="onCancel" class="btn btn-outline-secondary ms-3"> </div>
<i class="bi bi-x-lg"></i> <div v-else class="ms-auto">
Abbrechen <button v-if="!isActive" type="button" @click="onEdit" key="button-not-active" class="btn btn-primary">
</button> <i class="bi bi-pen"></i>
Mitarbeiter bearbeiten
</button>
</div>
</transition>
</div> </div>
</template> </template>
<style scoped> <style lang="scss" scoped>
@mixin animation-target($offset) {
opacity: 0;
transform: translateX($offset);
}
.button-save-{
&enter-active,
&leave-active {
transition: all .1s ease-in-out
}
&enter-from,
&leave-to {
@include animation-target(15px)
}
&leave-to {
@include animation-target(-15px)
}
}
</style> </style>

View File

@@ -23,7 +23,7 @@ export const useNotifications = defineStore('notifications', {
text:text, text:text,
} }
) )
if (timeout !== -1) setTimeout(() => this.remove(id), timeout) if (timeout > -1) setTimeout(() => this.remove(id), timeout)
} }
} }
}) })

View File

@@ -1,20 +1,18 @@
import { defineStore, storeToRefs } from 'pinia' import { defineStore } from 'pinia'
import { useNotifications } from './notifications' import { useNotifications } from './notifications'
import { getUnixTime } from 'date-fns' import { ref, reactive } from 'vue'
import { AxiosError } from 'axios'
import axios from '@/axios'
import { apiUrl } from '@/axios'
import Axios from 'axios'
import { useRequest } from 'vue-request' import { useRequest } from 'vue-request'
import { json } from 'stream/consumers' import axios from '@/axios'
import Axios from 'axios'
import { useRouter } from 'vue-router'
import type { Router } from 'vue-router'
type AuthSuccResult = { type AuthSuccResult = {
user: string, user: string,
role: string, role: string,
token: string token: string
} }
export const useUser = defineStore({ export const useUser = defineStore({
id: 'storeUser', id: 'storeUser',
@@ -23,8 +21,9 @@ export const useUser = defineStore({
user: '', user: '',
role: '', role: '',
isLoggedIn: false, isLoggedIn: false,
rememberMe: false, token: '',
token: '' errorMessage: '',
loading: ref(false)
} }
}, },
@@ -36,18 +35,27 @@ export const useUser = defineStore({
}, },
actions: { actions: {
async login(username: string, password: string): async login(username: string, password: string, router: Router) {
Promise<boolean | this.loading = true
'E_INVALID_AUTH_PASSWORD: Password mis-match' |
'E_INVALID_AUTH_UID: User not found'>
{
const notifications = useNotifications() const notifications = useNotifications()
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/* async function axiosQuery<T>(username: string, password: string){
return await axios.post<T>('login',{
username,
password
})
}
const { loading } = useRequest(axiosQuery(username, password))
this.loading */
try { try {
const response = await axios.post<AuthSuccResult>('login', { const response = await axios.post<AuthSuccResult>('login', {
username: username, username,
password: password password
}) })
this.isLoggedIn = true this.isLoggedIn = true
@@ -57,53 +65,43 @@ export const useUser = defineStore({
notifications.add('success', 'Login successful.') notifications.add('success', 'Login successful.')
return true await router.push({name: 'Home'})
} catch(err : unknown) { this.loading = false
} catch(err : unknown) {
this.loading = false
if (Axios.isAxiosError(err) && err.response && err.response.data){ if (Axios.isAxiosError(err) && err.response && err.response.data){
const data = err.response.data this.errorMessage = err.response.data as string
if(
data === 'E_INVALID_AUTH_PASSWORD: Password mis-match' ||
data === 'E_INVALID_AUTH_UID: User not found'
) return data
} }
else if(err instanceof Error){ else if(err instanceof Error){
notifications.add('danger', err.message, -1) notifications.add('danger', err.message, -1)
return false
} }
}
notifications.add('danger', err as string, -1)
return false
}
}, },
async logout(): Promise<boolean> { async logout(router: Router) {
const notifications = useNotifications() const notifications = useNotifications()
try { try {
await axios.post('logout', '', { const result = await axios.post('logout', '', {
headers: { headers: {
'Authorization': 'Bearer '+useUser().token 'Authorization': 'Bearer '+useUser().token
} }
}) })
await router.push({name: 'Login'})
notifications.add('success','Logged out successfully') notifications.add('success','Logged out successfully')
this.$reset() this.$reset()
return true
} }
catch(error) { catch(error) {
if(error instanceof Error) { if(error instanceof Error) {
notifications.add('danger', error.message, -1) notifications.add('danger', error.message, -1)
} }
return false
} }
} }
}, },

View File

@@ -231,7 +231,7 @@ function onToggleEdit() {
} }
watch(() => [route.params, route.name], ([newParam, newName], [oldParam, oldName]) => { watch(() => [route.params, route.name], ([newParam, newName], [oldParam, oldName]) => {
if(newName === oldName && newParam !== oldParam) { if(newName === oldName && newParam?.toString() !== oldParam?.toString()) {
state.fetchFromApi(route.params.id) state.fetchFromApi(route.params.id)
createUser.value = false createUser.value = false
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div>
<IndexSettingsModal <IndexSettingsModal
id="indexSettingsModal" id="indexSettingsModal"
:left-column="settings.columnsSelected" :left-column="settings.columnsSelected"
@@ -13,8 +13,7 @@
@search="store.setSimpleSearch($event)" @search="store.setSimpleSearch($event)"
/> />
<table class="table table-hover mt-3 ">
<table class="table table-hover table-bordered mt-3 ">
<thead> <thead>
<tr> <tr>
<th <th
@@ -37,14 +36,13 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <transition-group name="list" tag="tbody">
<tr v-for="employee in rows" @click="router.push({name: 'Employees/Details', params: {id: employee.id}})"> <tr v-for="employee in rows" :key="employee.id" @click="router.push({name: 'Employees/Details', params: {id: employee.id}})">
<td v-for="col in settings.columnsSelected"> <td v-for="col in settings.columnsSelected">
{{ employee[col] }} {{ employee[col] }}
</td> </td>
</tr> </tr>
</tbody> </transition-group>
</table> </table>
<h5 v-show="rows.length === 0" class="text-muted">Keine Daten anzuzeigen...</h5> <h5 v-show="rows.length === 0" class="text-muted">Keine Daten anzuzeigen...</h5>
@@ -56,7 +54,7 @@
:last-page="store.meta.last_page" :last-page="store.meta.last_page"
@set-page="paginatorSetPage" @set-page="paginatorSetPage"
/> />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -110,10 +108,26 @@ function paginatorSetPage(e : number){
</script> </script>
<style scoped> <style lang="scss" scoped>
.list- {
&enter-active,
&leave-active {
transition: all .2s ease
}
&enter-from,
&leave-to {
opacity: 0;
}
&move {
transition: all .2s ease
}
}
table thead tr:first-child th:first-child { table thead tr:first-child th:first-child {
width: 51px; min-width: 51px;
} }
table th { table th {

View File

@@ -1,12 +1,12 @@
<template lang="pug"> <template lang="pug">
<div>
//- p {{ state }} //- p {{ state }}
MonthPicker(:selectedMonth="selectedMonth" :selectedYear="selectedYear" @getMonth="selectedMonth = $event" @getYear="selectedYear = $event") MonthPicker(:selectedMonth="selectedMonth" :selectedYear="selectedYear" @getMonth="selectedMonth = $event" @getYear="selectedYear = $event")
Schedule( :startDate="new Date(selectedYear, selectedMonth)") Schedule( :startDate="new Date(selectedYear, selectedMonth)")
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,5 +1,6 @@
<template> <template>
<img src="/src/assets/logo.png"> <div>
<img src="/src/assets/logo.png">
<form class="m-auto" novalidate> <form class="m-auto" novalidate>
<h1 class="h3 mb-4">Bitte einloggen</h1> <h1 class="h3 mb-4">Bitte einloggen</h1>
@@ -38,17 +39,25 @@
<button <button
type="submit" type="submit"
class="btn btn-lg btn-success w-100 mb-5" class="btn btn-success w-100 mb-5"
:class="{'opacity-75': userStore.loading || userStore.isLoggedIn}"
@click.prevent="onClick" @click.prevent="onClick"
> >
Einloggen <span
v-if="userStore.loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
{{userStore.loading ? "Lade..." : "Einloggen"}}
</button> </button>
</form> </form>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUser } from '@/stores/user' import { useUser } from '@/stores/user'
import { reactive, computed, ref, watch } from 'vue' import { reactive, computed, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { helpers, required } from '@vuelidate/validators' import { helpers, required } from '@vuelidate/validators'
@@ -61,11 +70,11 @@ const input = reactive({
password: '', password: '',
}) })
const valPasswordCorrect = () => { function valPasswordCorrect() {
return apiResponse.value !== 'E_INVALID_AUTH_PASSWORD: Password mis-match' return userStore.errorMessage !== 'E_INVALID_AUTH_PASSWORD: Password mis-match'
} }
const valUsernameCorrect = () => { function valUsernameCorrect() {
return apiResponse.value !== 'E_INVALID_AUTH_UID: User not found' return userStore.errorMessage !== 'E_INVALID_AUTH_UID: User not found'
} }
const rules = computed(() => ({ const rules = computed(() => ({
@@ -79,30 +88,20 @@ const rules = computed(() => ({
} }
})) }))
const v$ = useVuelidate(rules, input)
watch(input, () => { watch(input, () => {
apiResponse.value = '' userStore.errorMessage = ''
v$.value.$reset() v$.value.$reset()
}) })
const v$ = useVuelidate(rules, input) userStore.$subscribe((mutation, state) => {
// if(state.isLoggedIn) router.push({name: 'Home'})
const apiResponse = ref('') })
async function onClick() { async function onClick() {
if(!(await v$.value.$validate())) return if(await v$.value.$validate()) {
await userStore.login(input.username, input.password, router)
const result = await userStore.login(input.username, input.password)
console.log("back in login form: "+result)
switch(result) {
case true:
router.push({name: 'Home'})
break;
case false:
break;
default:
apiResponse.value = result
} }
} }