152 lines
4.5 KiB
Vue
152 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { useRouter } from 'vue-router'
|
|
import { Box } from '@/components/ui/box'
|
|
import { Lucide, type Icon } from '@/components/ui/lucide'
|
|
import {
|
|
resolveNotificationRoute,
|
|
useNotifications,
|
|
type NotificationItem,
|
|
} from '@/modules/notification'
|
|
|
|
interface Props {
|
|
class?: string
|
|
boxClass?: string
|
|
open?: boolean
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const router = useRouter()
|
|
|
|
const {
|
|
notifications,
|
|
unreadCount,
|
|
loading,
|
|
error,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
} = useNotifications()
|
|
|
|
function getNotificationIcon(notification: NotificationItem): Icon {
|
|
if (notification.type === 'user_activation_required') {
|
|
return 'UserPlus'
|
|
}
|
|
|
|
return 'Bell'
|
|
}
|
|
|
|
function getIconClass(color: string): string {
|
|
switch (color) {
|
|
case 'success':
|
|
return 'bg-success/15 text-success'
|
|
case 'warning':
|
|
return 'bg-warning/15 text-warning'
|
|
case 'info':
|
|
return 'bg-info/15 text-info'
|
|
case 'purple':
|
|
return 'bg-purple-500/15 text-purple-600 dark:text-purple-400'
|
|
case 'orange':
|
|
return 'bg-orange-500/15 text-orange-600 dark:text-orange-400'
|
|
default:
|
|
return 'bg-primary/15 text-primary'
|
|
}
|
|
}
|
|
|
|
async function handleNotificationClick(
|
|
event: MouseEvent,
|
|
notification: NotificationItem,
|
|
) {
|
|
event.preventDefault()
|
|
|
|
await markAsRead(notification.id)
|
|
|
|
const route = resolveNotificationRoute(notification)
|
|
if (route) {
|
|
await router.push(route)
|
|
}
|
|
}
|
|
|
|
async function handleMarkAllAsRead(event: MouseEvent) {
|
|
event.preventDefault()
|
|
await markAllAsRead()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-bind="$attrs"
|
|
:class="[
|
|
'invisible opacity-0 scale-95 transition-all duration-200 delay-0',
|
|
'xl:group-hover/notifications:visible xl:group-hover/notifications:opacity-100 xl:group-hover/notifications:scale-100 xl:group-hover/notifications:delay-200',
|
|
props.open && 'max-xl:visible max-xl:opacity-100 max-xl:scale-100',
|
|
props.class,
|
|
]"
|
|
>
|
|
<Box
|
|
:class="`before:shadow-foreground/5 z-50 flex w-96 flex-col gap-2.5 px-6 py-5 before:rounded-2xl before:shadow-xl before:backdrop-blur after:rounded-2xl ${props.boxClass ?? ''}`"
|
|
>
|
|
<div class="flex place-content-between items-center">
|
|
<div class="font-medium">
|
|
Notifikasi
|
|
<span v-if="unreadCount > 0" class="text-primary ml-1 text-sm">({{ unreadCount }})</span>
|
|
</div>
|
|
<button
|
|
v-if="unreadCount > 0"
|
|
type="button"
|
|
class="text-primary text-sm hover:underline"
|
|
@click="handleMarkAllAsRead"
|
|
>
|
|
Tandakan semua dibaca
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-foreground/70 mt-1 py-6 text-center text-sm">
|
|
Memuatkan notifikasi...
|
|
</div>
|
|
|
|
<div v-else-if="error" class="text-danger mt-1 py-4 text-center text-sm">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="notifications.length === 0"
|
|
class="text-foreground/70 mt-1 flex flex-col items-center gap-2 py-8 text-center text-sm"
|
|
>
|
|
<Lucide class="size-8 opacity-40" icon="Bell" />
|
|
<span>Tiada notifikasi buat masa ini.</span>
|
|
</div>
|
|
|
|
<div v-else class="mt-1 flex max-h-96 flex-col gap-1 overflow-y-auto">
|
|
<a
|
|
v-for="notification in notifications"
|
|
:key="notification.id"
|
|
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-2 flex items-start gap-3.5 rounded-2xl border border-transparent p-2"
|
|
:class="!notification.is_read && 'bg-primary/5'"
|
|
href=""
|
|
@click="handleNotificationClick($event, notification)"
|
|
>
|
|
<div
|
|
class="flex size-11 flex-none items-center justify-center rounded-full"
|
|
:class="getIconClass(notification.color)"
|
|
>
|
|
<Lucide class="size-5" :icon="getNotificationIcon(notification)" />
|
|
</div>
|
|
<div class="min-w-0 flex-1 flex-col gap-1">
|
|
<div class="flex place-content-between items-start gap-2">
|
|
<div class="font-medium leading-snug">{{ notification.title }}</div>
|
|
<div class="shrink-0 text-xs opacity-70">{{ notification.time_ago }}</div>
|
|
</div>
|
|
<div class="line-clamp-2 text-sm opacity-70">
|
|
{{ notification.message }}
|
|
</div>
|
|
</div>
|
|
<span
|
|
v-if="!notification.is_read"
|
|
class="bg-primary mt-2 size-2 shrink-0 rounded-full"
|
|
aria-hidden="true"
|
|
/>
|
|
</a>
|
|
</div>
|
|
</Box>
|
|
</div>
|
|
</template>
|