Added sockets for notifications

This commit is contained in:
2026-06-30 20:49:36 +10:00
parent e551e202d5
commit 6cadd3d9e6
25 changed files with 1639 additions and 193 deletions
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useNotificationStore } from '@/stores/notificationStore';
import { Notification } from '@/Types/types';
const props = defineProps<{
userId: number;
}>();
interface NotificationPayload {
notification: Notification;
}
const store = useNotificationStore();
let channel: ReturnType<typeof window.Echo.private> | null = null;
onMounted(() => {
channel = window.Echo.private(`App.Models.User.${props.userId}`)
.listen('.notification.created', (e: NotificationPayload) => {
store.push(e.notification);
});
});
onUnmounted(() => {
channel?.stopListening('.notification.created');
window.Echo.leave(`App.Models.User.${props.userId}`);
});
</script>
<template></template>
@@ -1,19 +1,19 @@
<script setup lang="ts">
import {ref, watch} from 'vue'
import { ref, watch, onMounted } from 'vue'
import axios from "axios";
import {Notification} from "@/Types/types";
import {Link} from "@inertiajs/vue3";
import {local} from "laravel-vite-plugin/fonts";
import { Notification } from "@/Types/types";
import { Link } from "@inertiajs/vue3";
import { useNotificationStore } from '@/stores/notificationStore';
const props = defineProps<{
unreadCount: number
}>()
const localUnreadCount = ref(props.unreadCount)
watch(() => props.unreadCount, (val) => {
localUnreadCount.value = val
})
const store = useNotificationStore();
onMounted(() => {
store.setInitialUnreadCount(props.unreadCount);
});
const open = ref(false)
const notifications = ref<Notification[]>([])
@@ -27,15 +27,10 @@ const markAllRead = async (notifications: Notification[]) => {
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)
store.unreadCount = 0
if (notifications.value.length) return
@@ -50,7 +45,7 @@ watch(open, async (isOpen) => {
<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-badge :content="store.unreadCount" :model-value="store.unreadCount > 0" color="primary">
<v-icon>mdi-bell-outline</v-icon>
</v-badge>
</v-btn>
@@ -1,54 +1,14 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { router } from "@inertiajs/vue3";
import { Notification } from "@/Types/types";
import axios from "axios";
import { useNotificationStore } from '@/stores/notificationStore';
const activeToasts = ref<Notification[]>([])
const seenNotificationIds = ref<Set<number>>(new Set())
const achievementSound = new Audio('/sounds/seatBelt.wav')
function handleNewNotifications(notifications: Notification[]) {
if (!notifications?.length) return
const newToasts: Notification[] = []
for (const n of notifications) {
if (!seenNotificationIds.value.has(n.id)) {
seenNotificationIds.value.add(n.id)
newToasts.push(n)
}
}
if (!newToasts.length) return
activeToasts.value.push(...newToasts)
achievementSound.play().catch(() => {})
}
async function dismissToast(notification: Notification) {
activeToasts.value = activeToasts.value.filter(n => n.id !== notification.id)
await axios.patch(`/notifications/${notification.id}/read`)
}
router.on('success', (event) => {
handleNewNotifications(event.detail.page.props.achievement_notifications as Notification[] ?? [])
})
// Handle initial page load notifications
import { usePage } from "@inertiajs/vue3";
import { SharedProps } from "@/Types/types";
const page = usePage<SharedProps>().props;
watch(
() => page.achievement_notifications,
handleNewNotifications,
{ immediate: true }
)
const store = useNotificationStore();
</script>
<template>
<div class="toast-stack">
<TransitionGroup name="toast">
<v-card
v-for="notification in activeToasts"
v-for="notification in store.toasts"
:key="notification.id"
class="toast-card glass"
rounded="lg"
@@ -66,7 +26,7 @@ watch(
variant="text"
size="small"
class="ml-auto"
@click="dismissToast(notification)"
@click="store.dismiss(notification)"
/>
</v-card-text>
</v-card>
+6 -2
View File
@@ -4,11 +4,14 @@ import MainFooter from "@/Components/FlightsGoneBy/MainFooter.vue";
import Radar from "@/Components/FlightsGoneBy/Radar.vue";
import StatusNotifications from "@/Components/FlightsGoneBy/StatusNotifications.vue";
import ToastNotifications from "@/Components/FlightsGoneBy/ToastNotifications.vue";
import { router } from "@inertiajs/vue3";
import {router, usePage} from "@inertiajs/vue3";
import { ref } from "vue";
import NotificationListener from "@/Components/FlightsGoneBy/NotificationListener.vue";
import {SharedProps} from "@/Types/types";
const transitionKey = ref(0);
const page = usePage<SharedProps>().props
router.on('success', () => {
transitionKey.value++;
});
@@ -25,6 +28,7 @@ router.on('success', () => {
</Transition>
<MainFooter :key="`footer-${transitionKey}`" />
</div>
<NotificationListener v-if="page.auth.user?.id" :user-id="page.auth.user?.id" />
<StatusNotifications />
<ToastNotifications />
</Radar>
+8
View File
@@ -0,0 +1,8 @@
import Echo from 'laravel-echo';
declare global {
interface Window {
Echo: Echo<'reverb'>;
Pusher: unknown;
}
}
+7
View File
@@ -6,9 +6,15 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
import { createApp, h, DefineComponent } from 'vue';
import vuetify from './plugins/vuetify';
import { configureEcho } from '@laravel/echo-vue';
const pinia = createPinia();
configureEcho({
broadcaster: 'reverb',
});
import '@mdi/font/css/materialdesignicons.css'
import 'flag-icons/css/flag-icons.min.css'
import VueApexCharts from 'vue3-apexcharts'
import {createPinia} from "pinia";
const appName = document.querySelector('meta[name="app-name"]')?.getAttribute('content') || 'Laravel';
@@ -25,6 +31,7 @@ createInertiaApp({
.use(plugin)
.use(ZiggyVue)
.use(vuetify)
.use(pinia)
.use(VueApexCharts)
.mount(el);
},
+14
View File
@@ -2,3 +2,17 @@ import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
+33
View File
@@ -0,0 +1,33 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { Notification } from '@/Types/types';
import axios from 'axios';
export const useNotificationStore = defineStore('notifications', () => {
const toasts = ref<Notification[]>([]);
const unreadCount = ref(0);
const latestNotification = ref<Notification | null>(null);
const achievementSound = new Audio('/sounds/seatBelt.wav');
function push(notification: Notification) {
toasts.value.push(notification);
latestNotification.value = notification;
unreadCount.value++;
if (notification.is_achievement) {
achievementSound.play().catch(() => {});
}
}
async function dismiss(notification: Notification) {
toasts.value = toasts.value.filter(n => n.id !== notification.id);
await axios.patch(`/notifications/${notification.id}/read`);
unreadCount.value = Math.max(0, unreadCount.value - 1);
}
function setInitialUnreadCount(count: number) {
unreadCount.value = count;
}
return { toasts, unreadCount, latestNotification, push, dismiss, setInitialUnreadCount};
});