Updated Map View
This commit is contained in:
+70
-6
@@ -1,8 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Spatie\Permission\Middleware\PermissionMiddleware;
|
||||||
|
use Spatie\Permission\Middleware\RoleMiddleware;
|
||||||
|
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
|
||||||
|
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@@ -13,16 +22,71 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
'role' => RoleMiddleware::class,
|
||||||
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
'permission' => PermissionMiddleware::class,
|
||||||
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
'role_or_permission' => RoleOrPermissionMiddleware::class,
|
||||||
]);
|
]);
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
$exceptions->respond(function (Response $response, Throwable $e, Request $request) {
|
||||||
|
$status = $response->getStatusCode();
|
||||||
|
|
||||||
|
$errors = [
|
||||||
|
403 => [
|
||||||
|
'title' => "The Cockpit is Off Limits",
|
||||||
|
'message' => 'You don\'t have permission to access this page.',
|
||||||
|
],
|
||||||
|
404 => [
|
||||||
|
'title' => 'You Flight Has Been Cancelled',
|
||||||
|
'message' => 'The page you are looking for doesn\'t exist or has been moved.',
|
||||||
|
],
|
||||||
|
419 => [
|
||||||
|
'title' => 'Page Expired',
|
||||||
|
'message' => 'Your session has expired. Please refresh the page and try again.',
|
||||||
|
],
|
||||||
|
429 => [
|
||||||
|
'title' => 'Too Many Requests',
|
||||||
|
'message' => 'You\'re making too many requests. Please slow down and try again.',
|
||||||
|
],
|
||||||
|
500 => [
|
||||||
|
'title' => 'This Plane Has Made An Emergency Landing',
|
||||||
|
'message' => 'Something went wrong on our end. Please try again later.',
|
||||||
|
],
|
||||||
|
503 => [
|
||||||
|
'title' => 'Service Unavailable',
|
||||||
|
'message' => 'We\'re down for maintenance. Please check back soon.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$isLocal = app()->environment(['local', 'testing']);
|
||||||
|
$handled = array_keys($errors);
|
||||||
|
$friendlyErrorsOnLocal = [404, 403];
|
||||||
|
|
||||||
|
// In local/testing, only handle 404. In production, handle all.
|
||||||
|
$shouldHandle = isset($errors[$status]) && (
|
||||||
|
!$isLocal || in_array($status, $friendlyErrorsOnLocal)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$shouldHandle) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('Error', [
|
||||||
|
'statusCode' => $status,
|
||||||
|
'statusTitle' => $errors[$status]['title'],
|
||||||
|
'statusMessage' => $errors[$status]['message'],
|
||||||
|
'auth' => [
|
||||||
|
'user' => $request->user(),
|
||||||
|
'roles' => $request->user()?->getRoleNames() ?? [],
|
||||||
|
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->toResponse($request)
|
||||||
|
->setStatusCode($status);
|
||||||
|
});
|
||||||
})->create();
|
})->create();
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
<div class="flight-map-wrapper">
|
<div class="flight-map-wrapper">
|
||||||
<PlaneLoader v-if="!mapReady" class="map-loader" />
|
<PlaneLoader v-if="!mapReady" class="map-loader" />
|
||||||
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
|
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
|
||||||
<button @click="exportMapBasic()" class="export-btn">
|
<button @click="exportMapBasic()" class="export-btn" title="Download as Image">
|
||||||
<span class="mdi mdi-download" />
|
<span class="mdi mdi-download" />
|
||||||
</button>
|
</button>
|
||||||
|
<button class="projection-btn" @click="toggleProjection" :title="isGlobe ? 'Switch to flat map' : 'Switch to globe'">
|
||||||
|
<span class="mdi" :class="isGlobe ? 'mdi-map' : 'mdi-earth'"/>
|
||||||
|
</button>
|
||||||
<div v-if="!flights.length" class="empty-state">
|
<div v-if="!flights.length" class="empty-state">
|
||||||
<span class="mdi mdi-earth-off" />
|
<span class="mdi mdi-earth-off" />
|
||||||
<p>No flight data available</p>
|
<p>No flight data available</p>
|
||||||
@@ -266,6 +269,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
const legendOpen = ref(true)
|
const legendOpen = ref(true)
|
||||||
|
|
||||||
|
const isGlobe = ref(false)
|
||||||
|
|
||||||
|
const toggleProjection = (): void => {
|
||||||
|
if (!map?.isStyleLoaded()) return
|
||||||
|
isGlobe.value = !isGlobe.value
|
||||||
|
map.setProjection({ type: isGlobe.value ? 'globe' : 'mercator' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const legendItems = [
|
const legendItems = [
|
||||||
{ color: '#f97316', label: '5+ flights' },
|
{ color: '#f97316', label: '5+ flights' },
|
||||||
{ color: '#eab308', label: '3–4 flights' },
|
{ color: '#eab308', label: '3–4 flights' },
|
||||||
@@ -670,6 +682,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
|
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
|
||||||
map.addControl(new maplibregl.FullscreenControl(), 'top-right')
|
map.addControl(new maplibregl.FullscreenControl(), 'top-right')
|
||||||
|
|
||||||
|
map.on('style.load', () => {
|
||||||
|
map?.setProjection({ type: 'mercator' })
|
||||||
|
})
|
||||||
|
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
addLayers()
|
addLayers()
|
||||||
fitBounds()
|
fitBounds()
|
||||||
@@ -718,7 +735,7 @@ export default defineComponent({
|
|||||||
if (map) { map.remove(); map = null }
|
if (map) { map.remove(); map = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems }
|
return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems, isGlobe, toggleProjection }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -901,4 +918,25 @@ export default defineComponent({
|
|||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
color: #c8cdd8;
|
color: #c8cdd8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projection-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 48px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(10,14,22,0.85);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
color: #a0b4c8;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.projection-btn:hover { background: rgba(77,166,255,0.15); color: #4da6ff; }
|
||||||
|
.projection-btn.globe-active { color: #4da6ff; border-color: rgba(77,166,255,0.35); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { SharedProps } from '@/Types/types'
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import NotificationMenu from "@/Components/FlightsGoneBy/NotificationMenu.vue";
|
import NotificationMenu from "@/Components/FlightsGoneBy/NotificationMenu.vue";
|
||||||
|
|
||||||
const props = usePage<SharedProps>().props
|
const page = usePage<SharedProps>()
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
const dropdownOpen = ref(false)
|
const dropdownOpen = ref(false)
|
||||||
const dropdownRef = ref<HTMLElement | null>(null)
|
const dropdownRef = ref<HTMLElement | null>(null)
|
||||||
@@ -27,23 +27,23 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|||||||
<header class="glass">
|
<header class="glass">
|
||||||
<Link href="/" class="brand">FlightsGoneBy</Link>
|
<Link href="/" class="brand">FlightsGoneBy</Link>
|
||||||
|
|
||||||
<NotificationMenu v-if="props.auth.user" :unread-count="props.unread_notification_count" />
|
<NotificationMenu v-if="page.props.auth?.user" :unread-count="page.props.unread_notification_count" />
|
||||||
|
|
||||||
<!-- Desktop nav -->
|
<!-- Desktop nav -->
|
||||||
<nav class="nav-desktop">
|
<nav class="nav-desktop">
|
||||||
<template v-if="!props.auth.user">
|
<template v-if="!page.props.auth?.user">
|
||||||
<Link :href="route('login')" class="nav-link">Log In</Link>
|
<Link :href="route('login')" class="nav-link">Log In</Link>
|
||||||
<Link :href="route('register')" class="nav-link nav-link--highlight">Register</Link>
|
<Link :href="route('register')" class="nav-link nav-link--highlight">Register</Link>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
||||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link">Profile</Link>
|
<Link :href="route('profile.view', { user: page.props.auth?.user.name })" class="nav-link">Profile</Link>
|
||||||
<Link :href="route('feed')" class="nav-link">Feed</Link>
|
<Link :href="route('feed')" class="nav-link">Feed</Link>
|
||||||
<Link v-if="props.auth.roles.includes('admin')" :href="route('admin.dashboard')" class="nav-link">Admin</Link>
|
<Link v-if="page.props.auth.roles.includes('admin')" :href="route('admin.dashboard')" class="nav-link">Admin</Link>
|
||||||
|
|
||||||
<div class="dropdown" ref="dropdownRef">
|
<div class="dropdown" ref="dropdownRef">
|
||||||
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
||||||
{{ props.auth.user.name }}
|
{{ page.props.auth.user.name }}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="m6 9 6 6 6-6"/>
|
<path d="m6 9 6 6 6-6"/>
|
||||||
@@ -67,14 +67,14 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|||||||
<!-- Mobile drawer -->
|
<!-- Mobile drawer -->
|
||||||
<Transition name="slide">
|
<Transition name="slide">
|
||||||
<nav v-if="menuOpen" class="nav-mobile" @click.stop>
|
<nav v-if="menuOpen" class="nav-mobile" @click.stop>
|
||||||
<template v-if="!props.auth.user">
|
<template v-if="!page.props.auth?.user">
|
||||||
<Link :href="route('login')" class="nav-link" @click="menuOpen = false">Log In</Link>
|
<Link :href="route('login')" class="nav-link" @click="menuOpen = false">Log In</Link>
|
||||||
<Link :href="route('register')" class="nav-link nav-link--highlight" @click="menuOpen = false">Register</Link>
|
<Link :href="route('register')" class="nav-link nav-link--highlight" @click="menuOpen = false">Register</Link>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="nav-greeting">Welcome, {{ props.auth.user.name }}</span>
|
<span class="nav-greeting">Welcome, {{ page.props.auth.user.name }}</span>
|
||||||
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
||||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
<Link :href="route('profile.view', { user: page.props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
||||||
<Link :href="route('feed')" class="nav-link nav-link" @click="menuOpen = false">Feed</Link>
|
<Link :href="route('feed')" class="nav-link nav-link" @click="menuOpen = false">Feed</Link>
|
||||||
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
||||||
<Link :href="route('profile.settings')" class="nav-link" @click="menuOpen = false">Settings</Link>
|
<Link :href="route('profile.settings')" class="nav-link" @click="menuOpen = false">Settings</Link>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||||
|
import {usePage} from "@inertiajs/vue3";
|
||||||
|
import {SharedProps} from "@/Types/types";
|
||||||
|
const page = usePage<SharedProps>()
|
||||||
|
defineOptions({ layout: MainLayout })
|
||||||
|
defineProps<{
|
||||||
|
statusCode : number
|
||||||
|
statusTitle : string
|
||||||
|
statusMessage : string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GlassBox :title="statusTitle" :blurb="statusMessage">
|
||||||
|
|
||||||
|
</GlassBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user