vue · 5099 bytes Raw Blame History
1 <template>
2 <div class="notification-container fixed top-4 right-4 z-50 space-y-2">
3 <TransitionGroup name="notification" tag="div">
4 <div
5 v-for="notification in notifications"
6 :key="notification.id"
7 class="notification max-w-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg p-4"
8 :class="{
9 'border-green-200 dark:border-green-800': notification.type === 'success',
10 'border-blue-200 dark:border-blue-800': notification.type === 'info',
11 'border-yellow-200 dark:border-yellow-800': notification.type === 'warning',
12 'border-red-200 dark:border-red-800': notification.type === 'error'
13 }"
14 >
15 <div class="flex items-start">
16 <!-- Icon -->
17 <div class="flex-shrink-0 mr-3">
18 <CheckCircleIcon
19 v-if="notification.type === 'success'"
20 class="w-5 h-5 text-green-500"
21 />
22 <InformationCircleIcon
23 v-else-if="notification.type === 'info'"
24 class="w-5 h-5 text-blue-500"
25 />
26 <ExclamationTriangleIcon
27 v-else-if="notification.type === 'warning'"
28 class="w-5 h-5 text-yellow-500"
29 />
30 <ExclamationCircleIcon
31 v-else-if="notification.type === 'error'"
32 class="w-5 h-5 text-red-500"
33 />
34 </div>
35
36 <!-- Content -->
37 <div class="flex-1 min-w-0">
38 <h4
39 v-if="notification.title"
40 class="text-sm font-medium text-gray-900 dark:text-white"
41 >
42 {{ notification.title }}
43 </h4>
44 <p class="text-sm text-gray-600 dark:text-gray-300">
45 {{ notification.message }}
46 </p>
47 </div>
48
49 <!-- Close button -->
50 <button
51 @click="removeNotification(notification.id)"
52 class="flex-shrink-0 ml-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
53 >
54 <XMarkIcon class="w-4 h-4" />
55 </button>
56 </div>
57
58 <!-- Progress bar for auto-dismiss -->
59 <div
60 v-if="notification.duration && notification.duration > 0"
61 class="mt-2 h-1 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"
62 >
63 <div
64 class="h-full bg-gray-400 dark:bg-gray-500 transition-all duration-100 ease-linear"
65 :style="`width: ${getProgressWidth(notification)}%`"
66 ></div>
67 </div>
68 </div>
69 </TransitionGroup>
70 </div>
71 </template>
72
73 <script setup lang="ts">
74 import { ref, onMounted } from 'vue'
75 import {
76 CheckCircleIcon,
77 InformationCircleIcon,
78 ExclamationTriangleIcon,
79 ExclamationCircleIcon,
80 XMarkIcon,
81 } from '@heroicons/vue/24/outline'
82
83 interface Notification {
84 id: string
85 type: 'success' | 'error' | 'warning' | 'info'
86 title?: string
87 message: string
88 duration?: number
89 createdAt: number
90 }
91
92 const notifications = ref<Notification[]>([])
93
94 function addNotification(notification: Omit<Notification, 'id' | 'createdAt'>) {
95 const id = crypto.randomUUID()
96 const newNotification: Notification = {
97 ...notification,
98 id,
99 createdAt: Date.now(),
100 duration: notification.duration ?? 5000,
101 }
102
103 notifications.value.push(newNotification)
104
105 // Auto-remove after duration
106 if (newNotification.duration && newNotification.duration > 0) {
107 setTimeout(() => {
108 removeNotification(id)
109 }, newNotification.duration)
110 }
111 }
112
113 function removeNotification(id: string) {
114 const index = notifications.value.findIndex(n => n.id === id)
115 if (index > -1) {
116 notifications.value.splice(index, 1)
117 }
118 }
119
120 function getProgressWidth(notification: Notification): number {
121 if (!notification.duration) return 0
122
123 const elapsed = Date.now() - notification.createdAt
124 const progress = Math.max(0, (notification.duration - elapsed) / notification.duration * 100)
125 return progress
126 }
127
128 // Global notification methods
129 function showSuccess(message: string, title?: string, duration?: number) {
130 addNotification({ type: 'success', title, message, duration })
131 }
132
133 function showError(message: string, title?: string, duration?: number) {
134 addNotification({ type: 'error', title, message, duration })
135 }
136
137 function showWarning(message: string, title?: string, duration?: number) {
138 addNotification({ type: 'warning', title, message, duration })
139 }
140
141 function showInfo(message: string, title?: string, duration?: number) {
142 addNotification({ type: 'info', title, message, duration })
143 }
144
145 // Expose methods globally
146 onMounted(() => {
147 window.$notify = {
148 success: showSuccess,
149 error: showError,
150 warning: showWarning,
151 info: showInfo,
152 }
153 })
154 </script>
155
156 <style scoped>
157 .notification-enter-active,
158 .notification-leave-active {
159 transition: all 0.3s ease;
160 }
161
162 .notification-enter-from {
163 opacity: 0;
164 transform: translateX(100%);
165 }
166
167 .notification-leave-to {
168 opacity: 0;
169 transform: translateX(100%);
170 }
171
172 .notification-move {
173 transition: transform 0.3s ease;
174 }
175 </style>