146 lines
4.3 KiB
Vue
146 lines
4.3 KiB
Vue
<script setup lang="ts">
|
|
import MainHeader from "@/Components/FlightsGoneBy/MainHeader.vue";
|
|
import MainFooter from "@/Components/FlightsGoneBy/MainFooter.vue";
|
|
import Radar from "@/Components/FlightsGoneBy/Radar.vue";
|
|
import { usePage, router } from "@inertiajs/vue3";
|
|
import { ref, watch } from "vue";
|
|
import { SharedProps, Notification } from "@/Types/types";
|
|
import axios from "axios";
|
|
|
|
const page = usePage<SharedProps>().props;
|
|
const transitionKey = ref(0);
|
|
|
|
const seenNotificationIds = ref<Set<number>>(new Set())
|
|
|
|
const achievementSound = new Audio('/sounds/seatBelt.wav')
|
|
|
|
function handleNewNotifications(notifications: Notification[]) {
|
|
if (!notifications?.length) return
|
|
const unseen = notifications.filter(n => !seenNotificationIds.value.has(n.id))
|
|
if (!unseen.length) return
|
|
unseen.forEach(n => seenNotificationIds.value.add(n.id))
|
|
activeToasts.value.push(...unseen)
|
|
achievementSound.play().catch(() => {})
|
|
}
|
|
|
|
|
|
|
|
// ── Toasts ────────────────────────────────────────────────────────────────────
|
|
const activeToasts = ref<Notification[]>([])
|
|
|
|
async function dismissToast(notification: Notification) {
|
|
activeToasts.value = activeToasts.value.filter(n => n.id !== notification.id)
|
|
await axios.patch(`/notifications/${notification.id}/read`)
|
|
}
|
|
|
|
watch(
|
|
() => page.achievement_notifications,
|
|
handleNewNotifications,
|
|
{ immediate: true }
|
|
)
|
|
|
|
router.on('success', (event) => {
|
|
transitionKey.value++;
|
|
handleNewNotifications(event.detail.page.props.achievement_notifications as Notification[] ?? [])
|
|
});
|
|
</script>
|
|
<template>
|
|
<Radar>
|
|
<div class="layoutContainer">
|
|
<MainHeader :key="transitionKey" />
|
|
<Transition name="fade" mode="out-in">
|
|
<main id="pageContainer" :key="transitionKey">
|
|
<slot />
|
|
</main>
|
|
</Transition>
|
|
<MainFooter :key="transitionKey" />
|
|
</div>
|
|
|
|
<div class="toast-stack">
|
|
<TransitionGroup name="toast">
|
|
<v-card
|
|
v-for="notification in activeToasts"
|
|
:key="notification.id"
|
|
class="toast-card glass"
|
|
rounded="lg"
|
|
elevation="4"
|
|
max-width="360"
|
|
>
|
|
<v-card-text class="d-flex align-center ga-3">
|
|
<v-icon icon="mdi-trophy" color="amber" size="32" />
|
|
<div>
|
|
<div class="text-subtitle-2 font-weight-bold">{{ notification.title }}</div>
|
|
<div class="text-body-2 text-medium-emphasis">{{ notification.body }}</div>
|
|
</div>
|
|
<v-btn
|
|
icon="mdi-close"
|
|
variant="text"
|
|
size="small"
|
|
class="ml-auto"
|
|
@click="dismissToast(notification)"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</TransitionGroup>
|
|
</div>
|
|
</Radar>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.toast-stack {
|
|
position: fixed;
|
|
bottom: 1.5rem;
|
|
right: 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
z-index: 9999;
|
|
max-height: 50dvh;
|
|
overflow-y: scroll;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.toast-stack::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.toast-card {
|
|
flex-shrink: 0;
|
|
background: rgb(var(--v-theme-surface)) !important;
|
|
}
|
|
|
|
.toast-enter-from { opacity: 0; transform: translateX(100%); }
|
|
.toast-leave-to { opacity: 0; transform: translateX(100%); }
|
|
.toast-enter-active,
|
|
.toast-leave-active { transition: all 0.3s ease; }
|
|
</style>
|
|
<style scoped>
|
|
.layoutContainer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100dvh;
|
|
color: var(--text);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
header,
|
|
footer {
|
|
flex: 0 0 5dvh;
|
|
min-height: 3rem;
|
|
}
|
|
|
|
main {
|
|
flex: 1 1 auto;
|
|
background: transparent;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.fade-enter-from,
|
|
.fade-leave-to { opacity: 0; }
|
|
.fade-enter-active,
|
|
.fade-leave-active { transition: opacity 0.2s ease; }
|
|
</style>
|