DONE: layout letter with letterhead and footer, notification for newly...

This commit is contained in:
ISMAIL MASSERAN
2026-06-28 02:16:49 +00:00
parent 94ecbe5887
commit 034ecf947f
400 changed files with 29802 additions and 5446 deletions
@@ -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>