Added Notifications
This commit is contained in:
@@ -3,6 +3,7 @@ import { Link, useForm } from "@inertiajs/vue3";
|
||||
import { usePage } from '@inertiajs/vue3'
|
||||
import type { SharedProps } from '@/Types/types'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import NotificationMenu from "@/Components/FlightsGoneBy/NotificationMenu.vue";
|
||||
|
||||
const props = usePage<SharedProps>().props
|
||||
const menuOpen = ref(false)
|
||||
@@ -26,14 +27,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
<header class="glass">
|
||||
<Link href="/" class="brand">FlightsGoneBy</Link>
|
||||
|
||||
<button v-if="props.auth.user" class="notif-btn" aria-label="Notifications">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
||||
</svg>
|
||||
<span class="notif-dot" />
|
||||
</button>
|
||||
<NotificationMenu v-if="props.auth.user" :unread-count="props.unread_notification_count" />
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="nav-desktop">
|
||||
|
||||
@@ -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>
|
||||
@@ -17,6 +17,7 @@ defineProps<{
|
||||
</div>
|
||||
<PanelHeader>{{ flight.aircraft?.display_name_short }}</PanelHeader>
|
||||
<PanelSubHeader v-if="flight.aircraft?.manufacturer_code">{{ flight.aircraft.manufacturer_code }}</PanelSubHeader>
|
||||
<PanelSubHeader v-if="!flight.aircraft && !flight.aircraft_registration">No Aircraft Information Found</PanelSubHeader>
|
||||
<DetailRows>
|
||||
<DetailRow v-if="flight.aircraft?.designator" label="Designator" :value="flight.aircraft.designator" variant="Badge" />
|
||||
<DetailRow v-if="flight.aircraft_registration" label="Registration" :value="flight.aircraft_registration" />
|
||||
|
||||
@@ -17,6 +17,7 @@ defineProps<{
|
||||
</div>
|
||||
<DetailRows>
|
||||
<DetailRow label="Distance" :value="flight.distance + 'km'" />
|
||||
<DetailRow label="Duration" :value="flight.duration_display" />
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user