added lots of animations, added debounce to search query, fixed faulty refetching when logging out from profile page
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
27
src/App.vue
27
src/App.vue
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user