slidein notification

show error message to user
This commit is contained in:
Julian Krauser 2024-12-19 11:16:05 +01:00
parent 354d3b8972
commit b1dbb806c6
4 changed files with 183 additions and 1 deletions

View file

@ -7,6 +7,7 @@
<RouterView /> <RouterView />
</div> </div>
<Footer @contextmenu.prevent /> <Footer @contextmenu.prevent />
<Notification />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -19,6 +20,7 @@ import { useAuthStore } from "./stores/auth";
import { isAuthenticatedPromise } from "./router/authGuard"; import { isAuthenticatedPromise } from "./router/authGuard";
import ContextMenu from "./components/ContextMenu.vue"; import ContextMenu from "./components/ContextMenu.vue";
import Modal from "./components/Modal.vue"; import Modal from "./components/Modal.vue";
import Notification from "./components/Notification.vue";
</script> </script>
<script lang="ts"> <script lang="ts">

View 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>

View file

@ -1,6 +1,7 @@
import axios from "axios"; import axios from "axios";
import { isAuthenticatedPromise, type Payload } from "./router/authGuard"; import { isAuthenticatedPromise, type Payload } from "./router/authGuard";
import router from "./router"; import router from "./router";
import { useNotificationStore } from "./stores/notification";
let devMode = process.env.NODE_ENV === "development"; let devMode = process.env.NODE_ENV === "development";
@ -37,7 +38,7 @@ http.interceptors.response.use(
return response; return response;
}, },
async (error) => { async (error) => {
if (!error.config.url.includes("/admin")) { if (!error.config.url.includes("/admin") || !error.config.url.includes("/user")) {
return Promise.reject(error); return Promise.reject(error);
} }
@ -53,6 +54,9 @@ http.interceptors.response.use(
.catch(); .catch();
} }
const notificationStore = useNotificationStore();
notificationStore.push("Fehler", error.response.data, "error");
return Promise.reject(error); return Promise.reject(error);
} }
); );

View 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];
},
},
});