DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import fakers from '@/utils/faker'
|
||||
import _ from 'lodash'
|
||||
import { Lucide, type Icon } from '@/components/ui/lucide'
|
||||
import {
|
||||
resolveNotificationRoute,
|
||||
useNotifications,
|
||||
type NotificationItem,
|
||||
} from '@/modules/notification'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
@@ -11,6 +14,60 @@ interface Props {
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -25,29 +82,65 @@ const props = defineProps<Props>()
|
||||
: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">Notifications</div>
|
||||
<a class="text-primary" href="">View More</a>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-col gap-2.5">
|
||||
<a
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 5)"
|
||||
:key="fakerKey"
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-2 flex items-center gap-3.5 rounded-2xl border border-transparent p-2"
|
||||
href=""
|
||||
<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"
|
||||
>
|
||||
<AvatarRoot class="size-11 flex-none rounded-full">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][2]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex place-content-between items-center">
|
||||
<div class="font-medium">{{ faker['users'][0]!['name'] }}</div>
|
||||
<div class="text-xs opacity-70">{{ faker['times'][0] }}</div>
|
||||
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 opacity-70">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user