slidein notification
show error message to user
This commit is contained in:
parent
354d3b8972
commit
b1dbb806c6
4 changed files with 183 additions and 1 deletions
|
@ -7,6 +7,7 @@
|
|||
<RouterView />
|
||||
</div>
|
||||
<Footer @contextmenu.prevent />
|
||||
<Notification />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -19,6 +20,7 @@ import { useAuthStore } from "./stores/auth";
|
|||
import { isAuthenticatedPromise } from "./router/authGuard";
|
||||
import ContextMenu from "./components/ContextMenu.vue";
|
||||
import Modal from "./components/Modal.vue";
|
||||
import Notification from "./components/Notification.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
130
src/components/Notification.vue
Normal file
130
src/components/Notification.vue
Normal file
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<div
|
||||
class="fixed right-0 flex flex-col gap-4 p-2 w-full md:w-80"
|
||||
:class="position == 'bottom' ? 'bottom-0' : 'top-0'"
|
||||
>
|
||||
<TransitionGroup
|
||||
:enter-active-class="notifications.length > 1 ? [props.enter, props.moveDelay].join(' ') : props.enter"
|
||||
:enter-from-class="props.enterFrom"
|
||||
:enter-to-class="props.enterTo"
|
||||
:leave-active-class="props.leave"
|
||||
:leave-from-class="props.leaveFrom"
|
||||
:leave-to-class="props.leaveTo"
|
||||
:move-class="props.move"
|
||||
>
|
||||
<div
|
||||
v-for="notification in sortedNotifications"
|
||||
:key="notification.id"
|
||||
class="relative p-2 bg-white flex flex-row gap-2 w-full overflow-hidden rounded-lg shadow-md"
|
||||
:class="[
|
||||
notification.type == 'error' ? 'border border-red-400' : '',
|
||||
notification.type == 'warning' ? 'border border-red-400' : '',
|
||||
notification.type == 'info' ? 'border border-gray-400' : '',
|
||||
]"
|
||||
>
|
||||
<!-- @mouseover="hovering(notification.id, true)"
|
||||
@mouseleave="hovering(notification.id, false)" -->
|
||||
<ExclamationCircleIcon
|
||||
v-if="notification.type == 'error'"
|
||||
class="flex items-center justify-center min-w-12 w-12 h-12 bg-red-500 rounded-lg text-white p-1"
|
||||
/>
|
||||
<ExclamationTriangleIcon
|
||||
v-if="notification.type == 'warning'"
|
||||
class="flex items-center justify-center min-w-12 w-12 h-12 bg-red-500 rounded-lg text-white p-1"
|
||||
/>
|
||||
<InformationCircleIcon
|
||||
v-if="notification.type == 'info'"
|
||||
class="flex items-center justify-center min-w-12 w-12 h-12 bg-gray-500 rounded-lg text-white p-1"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="font-semibold"
|
||||
:class="[
|
||||
notification.type == 'error' ? 'text-red-500' : '',
|
||||
notification.type == 'warning' ? 'text-red-500' : '',
|
||||
notification.type == 'info' ? 'text-gray-700' : '',
|
||||
]"
|
||||
>{{ notification.title }}</span
|
||||
>
|
||||
<p class="text-sm text-gray-600">{{ notification.text }}</p>
|
||||
</div>
|
||||
<XMarkIcon
|
||||
@click="close(notification.id)"
|
||||
class="absolute top-2 right-2 w-6 h-6 cursor-pointer text-gray-500"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-0 bottom-0 h-1 bg-gray-500 transition-[width] duration-[4900ms] ease-linear"
|
||||
:class="notification.indicator ? 'w-0' : 'w-full'"
|
||||
></div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, TransitionGroup } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
|
||||
export interface Props {
|
||||
maxNotifications?: number;
|
||||
enter?: string;
|
||||
enterFrom?: string;
|
||||
enterTo?: string;
|
||||
leave?: string;
|
||||
leaveFrom?: string;
|
||||
leaveTo?: string;
|
||||
move?: string;
|
||||
moveDelay?: string;
|
||||
position?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxNotifications: 10,
|
||||
enter: "transform ease-out duration-300 transition",
|
||||
enterFrom: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4",
|
||||
enterTo: "translate-y-0 opacity-100 sm:translate-x-0",
|
||||
leave: "transition ease-in duration-500",
|
||||
leaveFrom: "opacity-100",
|
||||
leaveTo: "opacity-0",
|
||||
move: "transition duration-500",
|
||||
moveDelay: "delay-300",
|
||||
position: "bottom",
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useNotificationStore, ["notifications", "timeouts"]),
|
||||
sortedNotifications() {
|
||||
if (this.position === "bottom") {
|
||||
return [...this.notifications];
|
||||
}
|
||||
return [...this.notifications].reverse();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useNotificationStore, ["revoke"]),
|
||||
close(id: number) {
|
||||
this.revoke(id);
|
||||
},
|
||||
hovering(id: number, value: boolean, timeout?: number) {
|
||||
if (value) {
|
||||
clearTimeout(this.timeouts[id]);
|
||||
} else {
|
||||
this.timeouts[id] = setTimeout(() => {
|
||||
this.revoke(id);
|
||||
}, timeout);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
import axios from "axios";
|
||||
import { isAuthenticatedPromise, type Payload } from "./router/authGuard";
|
||||
import router from "./router";
|
||||
import { useNotificationStore } from "./stores/notification";
|
||||
|
||||
let devMode = process.env.NODE_ENV === "development";
|
||||
|
||||
|
@ -37,7 +38,7 @@ http.interceptors.response.use(
|
|||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
if (!error.config.url.includes("/admin")) {
|
||||
if (!error.config.url.includes("/admin") || !error.config.url.includes("/user")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,9 @@ http.interceptors.response.use(
|
|||
.catch();
|
||||
}
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
notificationStore.push("Fehler", error.response.data, "error");
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
46
src/stores/notification.ts
Normal file
46
src/stores/notification.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
type: NotificationType;
|
||||
indicator: boolean;
|
||||
}
|
||||
|
||||
export type NotificationType = "info" | "warning" | "error";
|
||||
|
||||
export const useNotificationStore = defineStore("notification", {
|
||||
state: () => {
|
||||
return {
|
||||
notifications: [] as Array<Notification>,
|
||||
timeouts: {} as { [key: string]: any },
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
push(title: string, text: string, type: NotificationType, timeout: number = 5000) {
|
||||
let id = Date.now();
|
||||
this.notifications.push({
|
||||
id,
|
||||
title,
|
||||
text,
|
||||
type,
|
||||
indicator: false,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.notifications[this.notifications.findIndex((n) => n.id === id)].indicator = true;
|
||||
}, 100);
|
||||
this.timeouts[id] = setTimeout(() => {
|
||||
this.revoke(id);
|
||||
}, timeout);
|
||||
},
|
||||
revoke(id: number) {
|
||||
this.notifications.splice(
|
||||
this.notifications.findIndex((n) => n.id === id),
|
||||
1
|
||||
);
|
||||
clearTimeout(this.timeouts[id]);
|
||||
delete this.timeouts[id];
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue