<template> <div class="fixed right-0 flex flex-col gap-4 p-2 w-full md:w-80 z-50" :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: string) { this.revoke(id); }, hovering(id: string, value: boolean, timeout?: number) { if (value) { clearTimeout(this.timeouts[id]); } else { this.timeouts[id] = setTimeout(() => { this.revoke(id); }, timeout); } }, }, }); </script>