Session api token validation
This commit is contained in:
@@ -16,6 +16,10 @@ use Inertia\Inertia;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
public function currentUserCanEditProfile(User $profileUser){
|
||||
return auth()->check() && (auth()->id() === $profileUser->id || auth()->user()->hasRole('admin'));
|
||||
}
|
||||
|
||||
public function index(){
|
||||
if (auth()->check()) {
|
||||
$user = auth()->user();
|
||||
@@ -39,7 +43,7 @@ class UserProfileController extends Controller
|
||||
return [
|
||||
'user' => $user,
|
||||
'canView' => Gate::allows('viewProfileData', $user),
|
||||
'canEdit' => auth()->check() && (auth()->id() === $user->id || auth()->user()->hasRole('admin')),
|
||||
'canEdit' => $this->currentUserCanEditProfile($user),
|
||||
'initialView' => $view,
|
||||
'selectedFlightId' => $selectedFlightId,
|
||||
'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
|
||||
@@ -85,10 +89,11 @@ class UserProfileController extends Controller
|
||||
return Inertia::render('UserFlight', [
|
||||
'flightCount' => $user->departedFlights()->count(),
|
||||
'flight' => $userFlight->snapshot($userFlight->id),
|
||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||
'canEdit' => $this->currentUserCanEditProfile($user),
|
||||
'canView' => Gate::allows('viewProfileData', $user),
|
||||
'user' => $user,
|
||||
'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
|
||||
'otherUsersOnFlight' => $userFlight->otherUsersOnFlight(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -128,6 +133,26 @@ class UserProfileController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function copyFlight(UserFlight $flight)
|
||||
{
|
||||
$loggedInUser = auth()->user();
|
||||
|
||||
$exists = UserFlight::where('user_id', $loggedInUser->id)
|
||||
->where('departure_airport_id', $flight->departure_airport_id)
|
||||
->where('arrival_airport_id', $flight->arrival_airport_id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()->withErrors(['flight' => 'You already have a flight logged between these airports on this date.']);
|
||||
}
|
||||
|
||||
$newFlight = $flight->replicate();
|
||||
$newFlight->user_id = $loggedInUser->id;
|
||||
$newFlight->save();
|
||||
|
||||
return redirect()->route('flights.edit', ['flight' => $newFlight->id]);
|
||||
}
|
||||
|
||||
public function achievement(User $user, Achievement $achievement)
|
||||
{
|
||||
$regions = match($achievement->internal_name){
|
||||
|
||||
@@ -39,6 +39,10 @@ class HandleInertiaRequests extends Middleware
|
||||
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
|
||||
'apiToken' => session('api_token'),
|
||||
],
|
||||
'flash' => [
|
||||
'success' => $request->session()->get('success'),
|
||||
'error' => $request->session()->get('error'),
|
||||
],
|
||||
'achievement_notifications' => fn() => $request->user()
|
||||
? $request->user()
|
||||
->notifications()
|
||||
|
||||
@@ -182,6 +182,20 @@ class UserFlight extends Model
|
||||
return !$this->isDomestic();
|
||||
}
|
||||
|
||||
public function otherUsersOnFlight(){
|
||||
return UserFlight::where('user_id', '!=', $this->user_id)
|
||||
->where('departure_airport_id', $this->departure_airport_id)
|
||||
->where('arrival_airport_id', $this->arrival_airport_id)
|
||||
->where('flight_number', $this->flight_number)
|
||||
->where('airline_id', $this->airline_id)
|
||||
->whereRaw('DATE(departure_date AT TIME ZONE \'UTC\') = ?', [
|
||||
Carbon::parse($this->departure_date)->utc()->toDateString()
|
||||
])
|
||||
->with('user')
|
||||
->get()
|
||||
->pluck('user.name');
|
||||
}
|
||||
|
||||
public static function snapshot($userFlightId): array
|
||||
{
|
||||
return UserFlight::with([
|
||||
|
||||
@@ -14,7 +14,7 @@ class FlightObserver
|
||||
//Make queued task if the site gets big
|
||||
$flight->user->followers()
|
||||
->get()
|
||||
->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->id}"));
|
||||
->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->user_id}"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class SettingsRegistry
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Hide Map Filters By Default',
|
||||
'category' => 'FlightsGoneBy Settings',
|
||||
'default' => false,
|
||||
'default' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'hide_impossible_achievements',
|
||||
@@ -111,8 +111,8 @@ class SettingsRegistry
|
||||
'options' => [
|
||||
['value' => 'airline', 'label' => 'Airline'],
|
||||
['value' => 'flight_number', 'label' => 'Flight Number'],
|
||||
['value' => 'from', 'label' => 'From'],
|
||||
['value' => 'to', 'label' => 'To'],
|
||||
['value' => 'departure_airport', 'label' => 'From'],
|
||||
['value' => 'arrival_airport', 'label' => 'To'],
|
||||
['value' => 'departure_date', 'label' => 'Departure Date'],
|
||||
['value' => 'departure_time', 'label' => 'Departure Time'],
|
||||
['value' => 'arrival_time', 'label' => 'Arrival Time'],
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
Vendored
+4
@@ -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
|
||||
|
||||
+5
-4
@@ -39,6 +39,10 @@ use Inertia\Inertia;
|
||||
Route::put('/flights/{flight}', [FlightController::class, 'update'])->name('flights.update');
|
||||
Route::delete('/flights/{flight}/{referrer?}', [FlightController::class, 'delete'])->name('flights.delete');
|
||||
|
||||
Route::get('/flights/lookup', [FlightController::class, 'lookup'])->name('flights.lookup');
|
||||
Route::post('/flights/import', [FlightImportController::class, 'store'])->name('flights.import.store');
|
||||
Route::get('/flights/{flight}/copy', [UserProfileController::class, 'copyFlight'])->name('flights.copy');
|
||||
|
||||
Route::patch('/settings/{key}', [SettingsController::class, 'updateSingle'])
|
||||
->where('key', '[a-z_]+')
|
||||
->name('settings.update-single');
|
||||
@@ -47,10 +51,6 @@ use Inertia\Inertia;
|
||||
Route::get('/import/fr24', [FlightImportController::class, 'showFr24Import'])->name('import.fr24');
|
||||
Route::get('/reconcile', [FlightImportController::class, 'reconcile'])->name('reconcile');;
|
||||
|
||||
Route::get('/flights/lookup', [FlightController::class, 'lookup'])->name('flights.lookup');
|
||||
Route::post('/flights/import', [FlightImportController::class, 'store'])->name('flights.import.store');
|
||||
|
||||
|
||||
Route::get('/settings/{category?}', [UserController::class, 'settings'])->name('user.settings');
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ use Inertia\Inertia;
|
||||
|
||||
Route::post('/import/save', [FlightImportController::class, 'save'])->name('import.save');
|
||||
|
||||
|
||||
//Search Routes
|
||||
Route::get('/search/airlines', [SearchController::class, 'airlines'])->name('search.airlines');
|
||||
Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft');
|
||||
|
||||
Reference in New Issue
Block a user