Session api token validation

This commit is contained in:
2026-06-29 21:42:47 +10:00
parent 164b590a6c
commit e551e202d5
18 changed files with 273 additions and 154 deletions
@@ -24,8 +24,8 @@ const showColumn = (column: string) => columnsToShow.value.includes(column)
const allHeaders = [
{ title: '', key: 'airline', sortable: true },
{ title: 'FLIGHT', key: 'flight_number', sortable: true },
{ title: 'FROM', key: 'from', sortable: true },
{ title: 'TO', key: 'to', sortable: true },
{ title: 'FROM', key: 'departure_airport', sortable: true },
{ title: 'TO', key: 'arrival_airport', sortable: true },
{ title: 'DATE', key: 'departure_date', sortable: true },
{ title: 'DEPART', key: 'departure_time', sortable: false },
{ title: 'ARRIVE', key: 'arrival_time', sortable: false },
@@ -49,14 +49,14 @@ const showColumn = (column: string) => props.columnsToShow.includes(column)
</div>
</td>
<td v-if="showColumn('from')" class="v-data-table__td">
<td v-if="showColumn('departure_airport')" class="v-data-table__td">
<AirportToolTip :airport="flight.departure_airport">
<Mono class="iata">{{ flight.departure_airport.display_code }}</Mono><br/>
</AirportToolTip>
<span class="city-name">{{ flight.departure_airport.municipality }}</span>
</td>
<td v-if="showColumn('to')" class="v-data-table__td">
<td v-if="showColumn('arrival_airport')" class="v-data-table__td">
<AirportToolTip :airport="flight.arrival_airport">
<span class="iata"><Mono>{{ flight.arrival_airport.display_code }}</Mono></span><br/>
</AirportToolTip>
@@ -40,26 +40,4 @@ defineProps<{
gap: 0.75rem;
margin-bottom: 0.25rem;
}
.airline-logo-placeholder {
width: 42px;
height: 42px;
border-radius: 8px;
background: var(--accent-glow);
border: 1px solid rgba(56, 189, 248, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-family: 'Share Tech Mono', monospace;
color: var(--accent);
flex-shrink: 0;
}
.airline-name {
font-size: 1rem;
font-weight: 600;
color: var(--text);
}
</style>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import PanelLabel from "@/Components/FlightsGoneBy/Panels/PanelLabel.vue";
defineProps<{
label?: string
}>()
@@ -6,30 +8,16 @@ defineProps<{
<template>
<div class="panel glass glass-border">
<div v-if="label" class="panel-label">{{label}}</div>
<PanelLabel v-if="label">{{label}}</PanelLabel>
<slot />
</div>
</template>
<style scoped>
/* Panels */
.panel {
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.panel-label {
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 0.25rem;
opacity: 0.8;
}
</style>
@@ -0,0 +1,21 @@
<script setup lang="ts">
</script>
<template>
<div class="panel-label">
<slot/>
</div>
</template>
<style scoped>
.panel-label {
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 0.25rem;
opacity: 0.8;
}
</style>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { usePage } from "@inertiajs/vue3";
import { computed } from "vue";
import { SharedProps } from "@/Types/types";
const page = usePage<SharedProps>().props;
const errors = computed(() => Object.values(page.errors ?? {}));
const success = computed(() => page.flash?.success);
</script>
<template>
<div class="status-notifications">
<TransitionGroup name="toast">
<v-alert
v-for="(error, i) in errors"
:key="`error-${i}`"
type="error"
variant="flat"
rounded="0"
closable
class="mb-2"
>
{{ error }}
</v-alert>
<v-alert
v-if="success"
key="success"
type="success"
variant="flat"
rounded="0"
closable
class="mb-2"
>
{{ success }}
</v-alert>
</TransitionGroup>
</div>
</template>
<style scoped>
.status-notifications {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
width: 360px;
}
</style>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { router } from "@inertiajs/vue3";
import { Notification } from "@/Types/types";
import axios from "axios";
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 }
)
</script>
<template>
<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>
</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; }
.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>
@@ -1,9 +1,11 @@
<script setup lang="ts">
import {computed, ref} from "vue";
import {Flight, User} from "@/Types/types";
import {router} from "@inertiajs/vue3";
import {Flight, SharedProps, User} from "@/Types/types";
import {router, usePage} from "@inertiajs/vue3";
import { copyToClipboard } from "@/Composables/useClipboard";
const loggedInUser = usePage<SharedProps>().props.auth.user?.id
defineProps<{
profileUser: User
flight: Flight
@@ -56,6 +58,12 @@ function copyFlightToClipboard(flight: Flight) {
title="View Details"
:href="route('profile.flight', { userFlight: flight.id, user: profileUser.name })"
/>
<v-list-item
v-if="loggedInUser && loggedInUser !== profileUser.id"
prepend-icon="mdi-plus"
title="Log Flight for Myself"
:href="route('flights.copy', { flight: flight.id })"
/>
<v-list-item
prepend-icon="mdi-content-copy"
title="Copy to Clipboard"
@@ -72,6 +80,7 @@ function copyFlightToClipboard(flight: Flight) {
@click="flightToDelete = flight"
/>
</v-list>
</v-menu>
<v-dialog v-if="canEdit" v-model="showDeleteDialog" max-width="400">
+8 -98
View File
@@ -2,54 +2,18 @@
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";
import StatusNotifications from "@/Components/FlightsGoneBy/StatusNotifications.vue";
import ToastNotifications from "@/Components/FlightsGoneBy/ToastNotifications.vue";
import { router } from "@inertiajs/vue3";
import { ref } from "vue";
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 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(() => {})
}
// ── 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) => {
router.on('success', () => {
transitionKey.value++;
handleNewNotifications(event.detail.page.props.achievement_notifications as Notification[] ?? [])
});
</script>
<template>
<Radar>
<div class="layoutContainer">
@@ -61,65 +25,11 @@ router.on('success', (event) => {
</Transition>
<MainFooter :key="`footer-${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>
<StatusNotifications />
<ToastNotifications />
</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;
+18 -2
View File
@@ -2,7 +2,7 @@
import MainLayout from "@/Layouts/MainLayout.vue";
import {Head, Link} from "@inertiajs/vue3";
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
import {Achievement, Flight, User, UserAchievement} from "@/Types/types";
import {Flight, User} from "@/Types/types";
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPasses/BoardingPass.vue";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
import AirportPanel from "@/Components/FlightsGoneBy/Panels/AirportPanel.vue";
@@ -11,6 +11,7 @@ import AircraftPanel from "@/Components/FlightsGoneBy/Panels/AircraftPanel.vue";
import RoutePanel from "@/Components/FlightsGoneBy/Panels/RoutePanel.vue";
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
import PanelLabel from "@/Components/FlightsGoneBy/Panels/PanelLabel.vue";
defineOptions({ layout: MainLayout })
@@ -21,6 +22,7 @@ const props = defineProps<{
followStatus: string
canEdit: boolean
canView: boolean
otherUsersOnFlight: string[]
}>()
</script>
@@ -42,7 +44,21 @@ const props = defineProps<{
<RoutePanel :flight="flight" />
<Panel label="Flight Details">
<BoardingPass :user="user" :showToolTips="false" style="width:100%;max-width:600px; margin:0 auto" :flight="flight" :canEdit="canEdit" />
<DetailRows/>
<DetailRows>
<PanelLabel>Other Users On This Flight</PanelLabel>
<div class="d-flex flex-wrap ga-2">
<v-chip
v-for="userName in otherUsersOnFlight"
:key="userName"
:href="route('profile.view', userName)"
color="primary"
variant="tonal"
link
>
{{ userName }}
</v-chip>
</div>
</DetailRows>
</Panel>
<AircraftPanel :flight="flight"/>
<AirportPanel :airport="flight.departure_airport" label="Departure" />
+1 -1
View File
@@ -28,7 +28,7 @@ const props = defineProps<{
flightCount: number
}>()
const hideFilters = ref(page.auth?.user?.resolved_settings?.hide_map_filters ?? false)
const hideFilters = ref(page.auth?.user?.resolved_settings?.hide_map_filters ?? true)
watch(hideFilters, (value) => {
updateSetting('hide_map_filters', value).catch(() => {})
+4
View File
@@ -100,6 +100,10 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
permissions: string[];
apiToken: string | null;
},
flash: {
success?: string;
error?: string;
},
logo_api_url: string
achievement_notifications: Notification[]
unread_notification_count: number