Added Notifications
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<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())
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
</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);
|
||||
right: 0;
|
||||
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>
|
||||
Reference in New Issue
Block a user