Files
FlightsAPI/resources/js/Components/FlightsGoneBy/NotificationMenu.vue
T
2026-06-20 22:21:17 +10:00

200 lines
4.7 KiB
Vue

<script setup lang="ts">
import {ref, watch} from 'vue'
import axios from "axios";
import {Notification} from "@/Types/types";
import {Link} from "@inertiajs/vue3";
import {local} from "laravel-vite-plugin/fonts";
const props = defineProps<{
unreadCount: number
}>()
const localUnreadCount = ref(props.unreadCount)
watch(() => props.unreadCount, (val) => {
localUnreadCount.value = val
})
const open = ref(false)
const notifications = ref<Notification[]>([])
const loading = ref(false)
const markAllRead = async (notifications: Notification[]) => {
const unread = notifications.filter(n => !n.read_at)
await Promise.all(
unread.map(n => axios.patch(`/notifications/${n.id}/read`))
)
unread.forEach(n => n.read_at = new Date().toISOString())
}
const emit = defineEmits<{
(e: 'update:unreadCount', value: number): void
}>()
watch(open, async (isOpen) => {
if (!isOpen) return
localUnreadCount.value = 0
emit('update:unreadCount', 0)
if (notifications.value.length) return
loading.value = true
const { data } = await axios.get('/notifications')
notifications.value = data
loading.value = false
await markAllRead(notifications.value)
})
</script>
<template>
<div class="notif-wrapper">
<v-btn icon variant="text" @click="open = !open" aria-label="Notifications">
<v-badge :content="localUnreadCount" :model-value="localUnreadCount > 0" color="primary">
<v-icon>mdi-bell-outline</v-icon>
</v-badge>
</v-btn>
<div v-if="open" class="notif-menu">
<div class="notif-header">
<span>Notifications</span>
</div>
<div v-if="loading" class="notif-empty">
<v-progress-circular indeterminate size="20" width="2" />
</div>
<div v-else-if="notifications.length === 0" class="notif-empty">
No notifications yet.
</div>
<div v-else class="notif-list">
<Link
v-for="notification in notifications"
:key="notification.id"
class="notif-item"
:href="notification.url || undefined"
:class="{ 'notif-item--unread': !notification.read_at }"
>
<v-icon v-if="notification.is_achievement" size="18" color="amber" class="notif-icon">
mdi-trophy-outline
</v-icon>
<v-icon v-else size="18" class="notif-icon">
mdi-information-outline
</v-icon>
<div class="notif-content">
<p class="notif-title">{{ notification.title }}</p>
<p class="notif-body">{{ notification.body }}</p>
</div>
</Link>
</div>
</div>
</div>
</template>
<style scoped>
.notif-wrapper {
position: relative;
}
.notif-menu {
position: absolute;
top: calc(100% + 0.4rem);
left: 50%;
transform: translateX(-50%);
width: 320px;
background: var(--bg);
border: 1px solid rgba(56, 189, 248, 0.12);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
z-index: 30;
overflow: hidden;
}
.notif-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.5;
border-bottom: 1px solid rgba(56, 189, 248, 0.1);
}
.notif-empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
opacity: 0.5;
padding: 1.5rem;
}
.notif-list {
max-height: 400px;
overflow-y: auto;
}
.notif-item {
display: flex;
gap: 0.6rem;
padding: 0.65rem 0.75rem;
border-bottom: 1px solid rgba(56, 189, 248, 0.06);
transition: background 0.15s ease;
}
.notif-item:last-child {
border-bottom: none;
}
.notif-item:hover {
background: rgba(56, 189, 248, 0.04);
}
.notif-item--unread {
background: rgba(56, 189, 248, 0.05);
}
.notif-item--unread:hover {
background: rgba(56, 189, 248, 0.09);
}
.notif-icon {
flex-shrink: 0;
margin-top: 2px;
opacity: 0.7;
}
.notif-content {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.notif-title {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
}
.notif-body {
margin: 0;
font-size: 0.8rem;
opacity: 0.7;
color: var(--text);
white-space: pre-line;
}
.notif-time {
margin: 0;
font-size: 0.72rem;
opacity: 0.4;
color: var(--text);
}
</style>