feat : ajout des notifications
This commit is contained in:
@@ -2,7 +2,42 @@
|
||||
<header class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
||||
<div class="flex h-full items-center justify-end">
|
||||
<div class="flex gap-12 text-xl text-white">
|
||||
<Icon name="mdi:bell-plus" class="self-center cursor-pointer" size="36" />
|
||||
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
||||
<button type="button" class="relative self-center cursor-pointer" @click="toggleNotifications">
|
||||
<Icon name="mdi:bell-plus" size="36" />
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="absolute -right-1 -top-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="isNotificationsOpen"
|
||||
class="absolute right-0 top-full z-30 mt-2 w-80 rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
||||
>
|
||||
<div class="border-b border-neutral-200 px-3 py-2 text-sm font-semibold">
|
||||
Notifications
|
||||
</div>
|
||||
<div v-if="isLoadingNotifications" class="px-3 py-3 text-sm text-neutral-500">
|
||||
Chargement...
|
||||
</div>
|
||||
<div v-else-if="notifications.length === 0" class="px-3 py-3 text-sm text-neutral-500">
|
||||
Aucune notification.
|
||||
</div>
|
||||
<div v-else class="max-h-80 overflow-auto">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="border-b border-neutral-100 px-3 py-2 last:border-b-0"
|
||||
>
|
||||
<p class="text-sm font-semibold text-neutral-900">{{ notification.title }}</p>
|
||||
<p class="text-xs text-neutral-600">{{ notification.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group relative flex gap-4">
|
||||
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
|
||||
<p class="self-center cursor-pointer">{{ user?.username }}</p>
|
||||
@@ -29,15 +64,79 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { User } from '~/services/dto/user'
|
||||
import type { NotificationItem } from '~/services/dto/notification'
|
||||
import { listUnreadNotifications, markAllNotificationsRead } from '~/services/notifications'
|
||||
|
||||
defineProps<{
|
||||
user?: User
|
||||
}>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const bellRoot = ref<HTMLElement | null>(null)
|
||||
const notifications = ref<NotificationItem[]>([])
|
||||
const isNotificationsOpen = ref(false)
|
||||
const isLoadingNotifications = ref(false)
|
||||
const unreadCount = computed(() => notifications.value.length)
|
||||
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
await auth.logout()
|
||||
await navigateTo('/login')
|
||||
}
|
||||
|
||||
const loadNotifications = async () => {
|
||||
isLoadingNotifications.value = true
|
||||
try {
|
||||
notifications.value = await listUnreadNotifications()
|
||||
} finally {
|
||||
isLoadingNotifications.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeNotifications = async () => {
|
||||
if (!isNotificationsOpen.value) return
|
||||
isNotificationsOpen.value = false
|
||||
if (notifications.value.length > 0) {
|
||||
await markAllNotificationsRead()
|
||||
notifications.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNotifications = async () => {
|
||||
if (isNotificationsOpen.value) {
|
||||
await closeNotifications()
|
||||
return
|
||||
}
|
||||
|
||||
isNotificationsOpen.value = true
|
||||
await loadNotifications()
|
||||
}
|
||||
|
||||
const handleClickOutside = async (event: MouseEvent) => {
|
||||
const target = event.target as Node | null
|
||||
if (!target || !bellRoot.value) return
|
||||
if (!bellRoot.value.contains(target)) {
|
||||
await closeNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isAdmin.value) {
|
||||
await loadNotifications()
|
||||
}
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
if (!isAdmin.value) return
|
||||
await loadNotifications()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineNuxtConfig({
|
||||
devServer: {port: 3001},
|
||||
toast: {
|
||||
settings: {
|
||||
timeout: 10000,
|
||||
timeout: 2000,
|
||||
closeOnClick: true,
|
||||
progressBar: false
|
||||
}
|
||||
|
||||
7
frontend/services/dto/notification.ts
Normal file
7
frontend/services/dto/notification.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type NotificationItem = {
|
||||
id: number
|
||||
title: string
|
||||
message: string
|
||||
isRead: boolean
|
||||
createdAt: string
|
||||
}
|
||||
18
frontend/services/notifications.ts
Normal file
18
frontend/services/notifications.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NotificationItem } from './dto/notification'
|
||||
import { extractItems } from '~/utils/api'
|
||||
|
||||
export const listUnreadNotifications = async () => {
|
||||
const api = useApi()
|
||||
const data = await api.get<NotificationItem[] | { 'hydra:member'?: NotificationItem[] }>(
|
||||
'/notifications/unread',
|
||||
{},
|
||||
{ toast: false }
|
||||
)
|
||||
|
||||
return extractItems<NotificationItem>(data)
|
||||
}
|
||||
|
||||
export const markAllNotificationsRead = async () => {
|
||||
const api = useApi()
|
||||
return api.post('/notifications/mark-all-read', {}, { toast: false })
|
||||
}
|
||||
Reference in New Issue
Block a user