188 lines
4.4 KiB
Vue
188 lines
4.4 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";
|
|
|
|
const props = defineProps<{
|
|
unreadCount: number
|
|
}>()
|
|
|
|
|
|
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 || notifications.value.length) return
|
|
loading.value = true
|
|
const { data } = await axios.get('/notifications')
|
|
notifications.value = data
|
|
loading.value = false
|
|
await markAllRead(notifications.value)
|
|
emit('update:unreadCount', 0) // <-- add this
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="notif-wrapper">
|
|
<v-btn icon variant="text" @click="open = !open" aria-label="Notifications">
|
|
<v-badge :content="unreadCount" :model-value="unreadCount > 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"
|
|
: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>
|