custom_ui/frontend/src/components/common/NotificationDisplay.vue

439 lines
8.4 KiB
Vue

<template>
<div class="notification-container" :class="positionClass">
<TransitionGroup name="notification" tag="div" class="notification-list">
<div
v-for="notification in activeNotifications"
:key="notification.id"
:class="notificationClass(notification)"
class="notification"
@click="markAsSeen(notification.id)"
>
<!-- Notification Header -->
<div class="notification-header">
<div class="notification-icon">
<i :class="getIcon(notification.type)"></i>
</div>
<div class="notification-content">
<h4 v-if="notification.title" class="notification-title">
{{ notification.title }}
</h4>
<p class="notification-message">{{ notification.message }}</p>
</div>
<button
@click.stop="dismissNotification(notification.id)"
class="notification-close"
type="button"
>
<i class="mdi mdi-close"></i>
</button>
</div>
<!-- Notification Actions -->
<div
v-if="notification.actions && notification.actions.length > 0"
class="notification-actions"
>
<button
v-for="action in notification.actions"
:key="action.label"
@click.stop="handleAction(action, notification)"
:class="action.variant || 'primary'"
class="notification-action-btn"
type="button"
>
<i v-if="action.icon" :class="action.icon"></i>
{{ action.label }}
</button>
</div>
<!-- Progress Bar for timed notifications -->
<div
v-if="!notification.persistent && notification.duration > 0"
class="notification-progress"
>
<div
class="notification-progress-bar"
:style="{ animationDuration: notification.duration + 'ms' }"
></div>
</div>
</div>
</TransitionGroup>
</div>
</template>
<script>
import { computed } from "vue";
import { useNotificationStore } from "../../stores/notifications-primevue";
export default {
name: "NotificationDisplay",
setup() {
const notificationStore = useNotificationStore();
const activeNotifications = computed(() => notificationStore.activeNotifications);
const positionClass = computed(
() => `notification-container--${notificationStore.position}`,
);
const notificationClass = (notification) => [
`notification--${notification.type}`,
{
"notification--seen": notification.seen,
"notification--persistent": notification.persistent,
},
];
const getIcon = (type) => {
const icons = {
success: "mdi mdi-check-circle",
error: "mdi mdi-alert-circle",
warning: "mdi mdi-alert",
info: "mdi mdi-information",
};
return icons[type] || icons.info;
};
const dismissNotification = (id) => {
notificationStore.dismissNotification(id);
};
const markAsSeen = (id) => {
notificationStore.markAsSeen(id);
};
const handleAction = (action, notification) => {
if (action.handler) {
action.handler(notification);
}
// Auto-dismiss notification after action unless specified otherwise
if (action.dismissAfter !== false) {
dismissNotification(notification.id);
}
};
return {
activeNotifications,
positionClass,
notificationClass,
getIcon,
dismissNotification,
markAsSeen,
handleAction,
};
},
};
</script>
<style scoped>
.notification-container {
position: fixed;
z-index: 9999;
pointer-events: none;
max-width: 400px;
width: 100%;
padding: 1rem;
}
/* Position variants */
.notification-container--top-right {
top: 0;
right: 0;
}
.notification-container--top-left {
top: 0;
left: 0;
}
.notification-container--bottom-right {
bottom: 0;
right: 0;
}
.notification-container--bottom-left {
bottom: 0;
left: 0;
}
.notification-container--top-center {
top: 0;
left: 50%;
transform: translateX(-50%);
}
.notification-container--bottom-center {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.notification-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.notification {
pointer-events: auto;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-left: 4px solid;
overflow: hidden;
min-width: 320px;
max-width: 100%;
position: relative;
}
/* Notification type variants */
.notification--success {
border-left-color: #10b981;
}
.notification--error {
border-left-color: #ef4444;
}
.notification--warning {
border-left-color: #f59e0b;
}
.notification--info {
border-left-color: #3b82f6;
}
.notification-header {
display: flex;
align-items: flex-start;
padding: 1rem;
gap: 0.75rem;
}
.notification-icon {
flex-shrink: 0;
font-size: 1.25rem;
margin-top: 0.125rem;
}
.notification--success .notification-icon {
color: #10b981;
}
.notification--error .notification-icon {
color: #ef4444;
}
.notification--warning .notification-icon {
color: #f59e0b;
}
.notification--info .notification-icon {
color: #3b82f6;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
margin: 0 0 0.25rem 0;
font-size: 0.875rem;
font-weight: 600;
color: #1f2937;
}
.notification-message {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
line-height: 1.4;
word-wrap: break-word;
}
.notification-close {
flex-shrink: 0;
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s;
font-size: 1rem;
}
.notification-close:hover {
color: #6b7280;
background-color: #f3f4f6;
}
.notification-actions {
padding: 0 1rem 1rem 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: -0.25rem;
}
.notification-action-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: white;
color: #374151;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.25rem;
}
.notification-action-btn:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.notification-action-btn.primary {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.notification-action-btn.primary:hover {
background: #2563eb;
}
.notification-action-btn.danger {
background: #ef4444;
border-color: #ef4444;
color: white;
}
.notification-action-btn.danger:hover {
background: #dc2626;
}
.notification-progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: rgba(0, 0, 0, 0.1);
}
.notification-progress-bar {
height: 100%;
background: currentColor;
opacity: 0.3;
animation: progress-decrease linear forwards;
transform-origin: left;
}
.notification--success .notification-progress-bar {
background: #10b981;
}
.notification--error .notification-progress-bar {
background: #ef4444;
}
.notification--warning .notification-progress-bar {
background: #f59e0b;
}
.notification--info .notification-progress-bar {
background: #3b82f6;
}
@keyframes progress-decrease {
from {
width: 100%;
}
to {
width: 0%;
}
}
/* Transition animations */
.notification-enter-active {
transition: all 0.3s ease-out;
}
.notification-leave-active {
transition: all 0.3s ease-in;
}
.notification-enter-from {
opacity: 0;
transform: translateX(100%);
}
.notification-leave-to {
opacity: 0;
transform: translateX(100%);
}
/* Adjustments for left-positioned containers */
.notification-container--top-left .notification-enter-from,
.notification-container--bottom-left .notification-enter-from,
.notification-container--top-left .notification-leave-to,
.notification-container--bottom-left .notification-leave-to {
transform: translateX(-100%);
}
/* Adjustments for center-positioned containers */
.notification-container--top-center .notification-enter-from,
.notification-container--bottom-center .notification-enter-from,
.notification-container--top-center .notification-leave-to,
.notification-container--bottom-center .notification-leave-to {
transform: translateY(-100%);
}
.notification-container--bottom-center .notification-enter-from,
.notification-container--bottom-center .notification-leave-to {
transform: translateY(100%);
}
/* Hover effects */
.notification:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.notification--seen {
opacity: 0.95;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.notification-container {
left: 0;
right: 0;
max-width: none;
padding: 0.5rem;
}
.notification-container--top-center,
.notification-container--bottom-center {
transform: none;
}
.notification {
min-width: auto;
}
.notification-header {
padding: 0.75rem;
}
.notification-actions {
padding: 0 0.75rem 0.75rem 0.75rem;
}
}
</style>