diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
index d44fe97..628a33b 100644
--- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php
+++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
@@ -33,7 +33,7 @@ class AuthenticatedSessionController extends Controller
$request->session()->regenerate();
- return redirect()->intended(route('dashboard', absolute: false));
+ return redirect()->intended(route('home', absolute: false));
}
/**
diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php
index c14f9ee..ea07a43 100644
--- a/app/Http/Controllers/FlightProfileController.php
+++ b/app/Http/Controllers/FlightProfileController.php
@@ -10,6 +10,25 @@ use Inertia\Inertia;
class FlightProfileController extends Controller
{
+ public function index(){
+ if (auth()->check()) {
+ $user = auth()->user();
+ $defaultPage = $user->resolved_settings['default_login_page'];
+
+ $route = match ($defaultPage) {
+ 'feed_first' => $user->following()->count() > 0 ? 'feed' : 'profile.view',
+ 'feed' => 'feed',
+ 'profile' => 'profile.view',
+ 'dashboard' => 'dashboard',
+ };
+
+ $args = $route == 'profile.view' ? $user->name : null;
+
+ return redirect()->route($route, $args);
+ }
+ return redirect()->route('login');
+ }
+
public static function getUserFlightApiURL(User $user){
return '/data/user/'.$user->name.'/flights';
}
@@ -43,7 +62,25 @@ class FlightProfileController extends Controller
public function view(User $user)
{
- return $this->departureBoard($user);
+ $loggedInUser = auth()->user();
+
+ $isPrivate = $user->resolved_settings['private_profile'];
+
+ if ($isPrivate && $user->id !== $loggedInUser?->id) {
+ if (!$loggedInUser || !$loggedInUser->isFollowing($user)) {
+ abort(404);
+ }
+ }
+
+ $defaultView = $loggedInUser ? $loggedInUser->resolved_settings['default_profile_view'] : 'map';
+
+ return match($defaultView) {
+ 'boarding-passes' => $this->boardingPasses($user),
+ 'map' => $this->map($user),
+ 'departure-board' => $this->departureBoard($user),
+ 'achievements' => redirect()->route('profile.achievements', $user->name),
+ };
+
}
public function flight(User $user, UserFlight $userFlight)
diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php
index 9794d8c..8e186fe 100644
--- a/app/Settings/SettingsRegistry.php
+++ b/app/Settings/SettingsRegistry.php
@@ -44,14 +44,14 @@ class SettingsRegistry
[
'key' => 'default_login_page',
'type' => 'select',
- 'label' => 'Default Page After Login',
+ 'label' => 'Default Page',
'category' => 'FlightsGoneBy Settings',
'default' => 'feed_first',
'options' => [
- ['value' => 'feed_first', 'label' => 'Feed if Following People, Profile if Not'],
- ['value' => 'profile', 'label' => 'Your Profile'],
- ['value' => 'feed', 'label' => 'Your Feed'],
- ['value' => 'dashboard', 'label' => 'Your Dashboard'],
+ ['value' => 'feed_first', 'label' => 'My Feed if Following People, My Profile if Not'],
+ ['value' => 'profile', 'label' => 'My Profile'],
+ ['value' => 'feed', 'label' => 'My Feed'],
+ ['value' => 'dashboard', 'label' => 'My Dashboard'],
],
],
[
diff --git a/package-lock.json b/package-lock.json
index adde092..013aa90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@inertiajs/vue3": "^2.0.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/vite": "^4.0.0",
+ "@types/geojson": "^7946.0.16",
"@types/leaflet": "^1.9.21",
"@types/node": "^25.5.0",
"@vitejs/plugin-vue": "^6.0.0",
diff --git a/package.json b/package.json
index 11d27c8..9a6de63 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"@inertiajs/vue3": "^2.0.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/vite": "^4.0.0",
+ "@types/geojson": "^7946.0.16",
"@types/leaflet": "^1.9.21",
"@types/node": "^25.5.0",
"@vitejs/plugin-vue": "^6.0.0",
diff --git a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue
index 951193b..3c6c907 100644
--- a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue
+++ b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue
@@ -4,6 +4,11 @@ import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import WakeTurbulence from "@/Components/FlightsGoneBy/WakeTurbulence.vue";
import LiveryImage from "@/Components/FlightsGoneBy/LiveryImage.vue";
+import ToolTipDivider from "@/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue";
+import ToolTipRows from "@/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue";
+import ToolTipRow from "@/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue";
+import ToolTipName from "@/Components/FlightsGoneBy/Tooltips/ToolTipName.vue";
+import ToolTipHeader from "@/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue";
defineProps<{
aircraft: Aircraft
@@ -26,99 +31,36 @@ function formatEngineType(type: string): string {
-
-
-
+
{{aircraft.designator }}
-
+
-
+
-
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue
index e405588..dfed17a 100644
--- a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue
+++ b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue
@@ -5,6 +5,11 @@ import {usePage} from "@inertiajs/vue3";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue";
+import ToolTipHeader from "@/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue";
+import ToolTipDivider from "@/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue";
+import ToolTipName from "@/Components/FlightsGoneBy/Tooltips/ToolTipName.vue";
+import ToolTipRows from "@/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue";
+import ToolTipRow from "@/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue";
const page = usePage().props;
@@ -33,26 +38,21 @@ const size = computed(() => props.size ? props.size + 'px' : '30px');
-
-
-
-
- {{ airline.country.name }}
-
+
+
+
+
+
@@ -77,45 +77,4 @@ span.airline-logo {
align-items: center;
gap: 10px;
}
-
-.codes{
- width:100%;
- display: flex;
- gap: 4px;
- padding-top:1em;
- justify-content:flex-start;
-}
-
-
-.airline-name {
- letter-spacing: 0.03em;
-}
-
-.tooltip-divider {
- height: 1px;
- background: var(--table-border);
-}
-
-.tooltip-meta {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--muted);
-}
-
-.country-flag {
- font-size: 1rem;
-}
-
-.country-name {
- flex: 1;
- font-size: 0.8rem;
-}
-
-.codes {
- display: flex;
- gap: 4px;
- margin-left: auto;
-}
-
diff --git a/resources/js/Components/FlightsGoneBy/AirportToolTip.vue b/resources/js/Components/FlightsGoneBy/AirportToolTip.vue
index fbd753e..68219bf 100644
--- a/resources/js/Components/FlightsGoneBy/AirportToolTip.vue
+++ b/resources/js/Components/FlightsGoneBy/AirportToolTip.vue
@@ -2,6 +2,12 @@
import { Airport } from "@/Types/types";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
+import ToolTipDivider from "@/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue";
+import ToolTipRows from "@/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue";
+import ToolTipHeader from "@/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue";
+import ToolTipRow from "@/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue";
+import CountryRow from "@/Components/FlightsGoneBy/Tooltips/CountryRow.vue";
+import ToolTipName from "@/Components/FlightsGoneBy/Tooltips/ToolTipName.vue";
defineProps<{
airport: Airport
@@ -12,9 +18,6 @@ function formatElevation(ft: number | null): string | null {
return `${ft.toLocaleString()} ft`
}
-function formatType(type: string): string {
- return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
-}
@@ -26,46 +29,21 @@ function formatType(type: string): string {
@@ -74,78 +52,4 @@ function formatType(type: string): string {
.airport-tooltip {
display: contents;
}
-
-.fi{
- padding:0.25em;
-}
-
-.tooltip-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 0.75rem;
-}
-
-.iata{
- font-size:1.2em;
-}
-
-.tooltip-codes {
- display: flex;
- flex-direction: column;
- align-items: baseline;
- gap: 0.5rem;
- flex-wrap: wrap;
-}
-
-.code-badges {
- display: flex;
- gap: 0.3rem;
- flex-wrap: wrap;
-}
-
-.tooltip-divider {
- height: 1px;
- background: var(--table-border, rgba(255,255,255,0.08));
-}
-
-.tooltip-rows {
- display: flex;
- flex-direction: column;
- gap: 0.3rem;
-}
-
-.tooltip-row {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 1rem;
-}
-
-.tooltip-label {
- font-family: 'Share Tech Mono', monospace;
- font-size: 0.6rem;
- letter-spacing: 0.15em;
- color: var(--muted, #445566);
- flex-shrink: 0;
-}
-
-.tooltip-value {
- font-size: 0.78rem;
- color: var(--text, #c8cdd8);
- text-align: right;
-}
-
-.tz {
- font-family: 'Share Tech Mono', monospace;
- font-size: 0.7rem;
- letter-spacing: 0.03em;
-}
-
-.coords {
- font-family: 'Share Tech Mono', monospace;
- font-size: 0.68rem;
- color: var(--muted, #556677);
- letter-spacing: 0.03em;
-}
diff --git a/resources/js/Components/FlightsGoneBy/FlightMap.vue b/resources/js/Components/FlightsGoneBy/FlightMap.vue
index 6f9db36..71031d8 100644
--- a/resources/js/Components/FlightsGoneBy/FlightMap.vue
+++ b/resources/js/Components/FlightsGoneBy/FlightMap.vue
@@ -2,10 +2,34 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
@@ -13,23 +37,26 @@
import { defineComponent, ref, onMounted, onBeforeUnmount, watch, nextTick, PropType } from 'vue'
import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
-import { SharedProps } from '@/Types/types'
import { usePage } from '@inertiajs/vue3'
-import { Flight, Airport } from '@/Types/types'
-import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
+import { Flight, Airport, SharedProps } from '@/Types/types'
+import PlaneLoader from '@/Components/FlightsGoneBy/PlaneLoader.vue'
+import type { Feature, FeatureCollection, LineString, Point } from 'geojson'
+
+// ── Types ─────────────────────────────────────────────────────────────────────
-interface RouteFlightBucket {
- historical: Flight[]
- future: Flight[]
-}
type LngLat = [number, number]
+interface RouteFlightBucket {
+ historical: Flight[]
+ future: Flight[]
+}
+
interface RouteFeatureProperties {
- color?: string
- routeKey: string
- depId: number
- arrId: number
+ color?: string
+ routeKey: string
+ depId: number
+ arrId: number
}
interface AirportFeatureProperties {
@@ -37,105 +64,166 @@ interface AirportFeatureProperties {
}
interface RoutesGeoJSON {
- historical: GeoJSON.FeatureCollection
- future: GeoJSON.FeatureCollection
+ historical: FeatureCollection
+ future: FeatureCollection
}
-function greatCircleGeoJSON(from: LngLat, to: LngLat, steps?: number): LngLat[] {
- const dist = Math.sqrt((to[0] - from[0]) ** 2 + (to[1] - from[1]) ** 2)
- const s = steps ?? (dist > 60 ? 64 : dist > 20 ? 32 : 16)
- const toRad = (d: number): number => d * Math.PI / 180
- const toDeg = (r: number): number => r * 180 / Math.PI
+// ── Constants ─────────────────────────────────────────────────────────────────
+
+const PULSE_PHASES = 6
+const PULSE_PERIOD = 2200
+const TOUCH_RADIUS = 20
+
+const ROUTE_COLORS: [number, string][] = [
+ [5, '#f97316'],
+ [3, '#eab308'],
+ [2, '#22c55e'],
+ [1, '#a150d5'],
+]
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+function routeKey(a: { id: number }, b: { id: number }): string {
+ return [a.id, b.id].sort().join('-')
+}
+
+function routeColor(flightCount: number): string {
+ return ROUTE_COLORS.find(([min]) => flightCount >= min)![1]
+}
+
+function isTouchDevice(): boolean {
+ return window.matchMedia('(pointer: coarse)').matches
+}
+
+function greatCirclePoints(from: LngLat, to: LngLat, steps?: number): LngLat[] {
+ const dist = Math.sqrt((to[0] - from[0]) ** 2 + (to[1] - from[1]) ** 2)
+ const s = steps ?? (dist > 60 ? 64 : dist > 20 ? 32 : 16)
+
+ const toRad = (d: number) => d * Math.PI / 180
+ const toDeg = (r: number) => r * 180 / Math.PI
+
const lat1 = toRad(from[1]), lng1 = toRad(from[0])
const lat2 = toRad(to[1]), lng2 = toRad(to[0])
- const d = 2 * Math.asin(Math.sqrt(
+
+ const angularDist = 2 * Math.asin(Math.sqrt(
Math.sin((lat2 - lat1) / 2) ** 2 +
Math.cos(lat1) * Math.cos(lat2) * Math.sin((lng2 - lng1) / 2) ** 2
))
- if (d === 0) return [[from[0], from[1]], [to[0], to[1]]]
+
+ if (angularDist === 0) return [from, to]
+
const points: LngLat[] = []
for (let i = 0; i <= s; i++) {
const f = i / s
- const A = Math.sin((1 - f) * d) / Math.sin(d)
- const B = Math.sin(f * d) / Math.sin(d)
+ const A = Math.sin((1 - f) * angularDist) / Math.sin(angularDist)
+ const B = Math.sin(f * angularDist) / Math.sin(angularDist)
const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2)
const y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2)
const z = A * Math.sin(lat1) + B * Math.sin(lat2)
points.push([toDeg(Math.atan2(y, x)), toDeg(Math.atan2(z, Math.sqrt(x * x + y * y)))])
}
+
+ // Unwrap antimeridian crossings
for (let i = 1; i < points.length; i++) {
const diff = points[i][0] - points[i - 1][0]
- if (diff > 180) points[i][0] -= 360
+ if (diff > 180) points[i][0] -= 360
if (diff < -180) points[i][0] += 360
}
+
return points
}
+// ── Popup HTML ────────────────────────────────────────────────────────────────
+
function airportPopupHTML(airport: Airport): string {
+ const row = (label: string, value: string) =>
+ `${label}${value}
`
+
const elevation = airport.elevation_ft !== null
- ? `Elevation${airport.elevation_ft.toLocaleString()} ft
`
- : ''
- const city = airport.municipality
- ? `City${airport.municipality}
`
- : ''
+ ? row('Elevation', `${airport.elevation_ft.toLocaleString()} ft`) : ''
+ const city = airport.municipality
+ ? row('City', airport.municipality) : ''
const country = airport.region?.country
- ? `Country${airport.region.country.name}
`
- : ''
- const iata = airport.iata_code ? `${airport.iata_code}` : ''
- const icao = airport.icao_code ? `${airport.icao_code}` : ''
+ ? row('Country', `${airport.region.country.name} `) : ''
+ const badges = [airport.iata_code, airport.icao_code]
+ .filter(Boolean)
+ .map(code => `${code}`)
+ .join('')
+
return `
`
}
function routePopupHTML(historical: Flight[], future: Flight[]): string {
- interface DirectionGroup {
- label: string
- airlines: string[]
- }
+ interface AirlineEntry { html: string; count: number }
+ interface DirectionGroup { label: string; airlines: Map }
- const groupByDirection = (list: Flight[]): DirectionGroup[] => {
- const logoApiUrl = usePage().props.logo_api_url
+ const groupByDirection = (flights: Flight[]): DirectionGroup[] => {
+ const groups = new Map()
- const dirs = new Map()
- list.forEach(f => {
- const key = `${f.departure_airport.id}-${f.arrival_airport.id}`
- const label = `${f.departure_airport.municipality} to ${f.arrival_airport.municipality}`
- if (!dirs.has(key)) dirs.set(key, { label, airlines: [] })
- const airline = f.airline ? `
-
- ${f.airline?.name}
- ` : ''
- if (airline && !dirs.get(key)!.airlines.includes(airline)) {
- dirs.get(key)!.airlines.push(airline)
+ flights.forEach(flight => {
+ const key = `${flight.departure_airport.id}-${flight.arrival_airport.id}`
+ const label = `${flight.departure_airport.municipality} to ${flight.arrival_airport.municipality}`
+
+ if (!groups.has(key)) groups.set(key, { label, airlines: new Map() })
+
+ if (!flight.airline) return
+
+ const { iata_code, logo_url, name } = flight.airline
+ const airlineKey = iata_code ?? ''
+ const airlines = groups.get(key)!.airlines
+
+ if (airlines.has(airlineKey)) {
+ airlines.get(airlineKey)!.count++
+ } else {
+ airlines.set(airlineKey, {
+ html: `
+
+ ${name}
+ `,
+ count: 1,
+ })
}
})
- return [...dirs.values()]
+
+ return [...groups.values()]
}
- const renderSection = (title: string, list: Flight[]): string => {
- if (!list.length) return ''
- const rows = groupByDirection(list).map(({ label, airlines }) => `
-
-
${label}
-
${airlines.join('
') || '—'}
-
`).join('')
+ const renderSection = (title: string, flights: Flight[]): string => {
+ if (!flights.length) return ''
+
+ const rows = groupByDirection(flights).map(({ label, airlines }) => {
+ const airlineLines = [...airlines.values()]
+ .map(({ html, count }) => count > 1
+ ? `${html}(x${count})`
+ : html)
+ .join('
')
+
+ return `
+
+
${label}
+
${airlineLines || '—'}
+
`
+ }).join('')
+
return ``
}
const divider = historical.length && future.length ? '' : ''
+
return `
${renderSection('Flown', historical)}
@@ -144,76 +232,97 @@ function routePopupHTML(historical: Flight[], future: Flight[]): string {
`
}
+// ── Component ─────────────────────────────────────────────────────────────────
+
export default defineComponent({
name: 'FlightMap',
- components: {PlaneLoader},
+ components: { PlaneLoader },
props: {
flights: {
- type: Array as PropType,
+ type: Array as PropType,
default: (): Flight[] => [],
},
+ showLegend: {
+ type: Boolean,
+ default: true,
+ }
+
},
setup(props) {
const mapContainer = ref(null)
- let map: maplibregl.Map | null = null
- let popup: maplibregl.Popup | null = null
- let pulseFrame: number | null = null
+ const mapReady = ref(false)
+
+ let map: maplibregl.Map | null = null
+ let popup: maplibregl.Popup | null = null
+ let pulseFrame: number | null = null
+
+ const airportById = new Map()
+ const routeFlights = new Map()
+ const arcCache = new Map()
- const PULSE_PHASES = 6
- const TOUCH_RADIUS = 20
- const airportById = new Map()
- const routeFlights = new Map()
- const arcCache = new Map()
let selectedAirportId: number | null = null
- const isTouchDevice = (): boolean => window.matchMedia('(pointer: coarse)').matches
+ const legendOpen = ref(true)
- const showPopup = (lngLat: maplibregl.LngLatLike, html: string): void => {
- popup!.setLngLat(lngLat).setHTML(html).addTo(map!)
+ const legendItems = [
+ { color: '#f97316', label: '5+ flights' },
+ { color: '#eab308', label: '3–4 flights' },
+ { color: '#22c55e', label: '2 flights' },
+ { color: '#a150d5', label: '1 flight' },
+ { color: '#ffffff', label: 'Upcoming', dashed: true },
+ ]
+
+
+ // ── Arc cache ─────────────────────────────────────────────────────────
+
+ const exportMap = (targetWidth = 7680, targetHeight = 4320): void => {
+ if (!map) return
+
+ const container = map.getContainer()
+ const originalWidth = container.offsetWidth
+ const originalHeight = container.offsetHeight
+ const currentCenter = map.getCenter()
+ const currentZoom = map.getZoom()
+
+ container.style.width = `${targetWidth}px`
+ container.style.height = `${targetHeight}px`
+ map.resize()
+ map.jumpTo({ center: [100, 20], zoom: 2 })// low zoom, centered roughly on your data
+
+ setTimeout(() => {
+ map!.triggerRepaint()
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ const dataURL = map!.getCanvas().toDataURL('image/jpeg', 0.95)
+ const link = document.createElement('a')
+ link.href = dataURL
+ link.download = 'flight-map.jpg'
+ link.click()
+
+ container.style.width = `${originalWidth}px`
+ container.style.height = `${originalHeight}px`
+ map!.resize()
+ map!.jumpTo({ center: currentCenter, zoom: currentZoom })
+ })
+ })
+ }, 10000)
}
- // ── Filter ────────────────────────────────────────────────────────────
- const applyFilter = (): void => {
- if (!map || !map.isStyleLoaded()) return
- const id = selectedAirportId
-
- const routeFilter: maplibregl.FilterSpecification | null = id
- ? ['any', ['==', ['get', 'depId'], id], ['==', ['get', 'arrId'], id]]
- : null
-
- map.setFilter('routes-line', routeFilter)
- map.setFilter('routes-hit', routeFilter)
- map.setFilter('routes-future-line', routeFilter)
- map.setFilter('routes-future-hit', routeFilter)
-
- // Use setFilter on airport layers — GPU-side, no style recompilation
- for (let i = 0; i < PULSE_PHASES; i++) {
- const airportFilter: maplibregl.FilterSpecification | null = id
- ? ['==', ['get', 'id'], id]
- : null
- map.setFilter(`airports-dot-${i}`, airportFilter)
- map.setFilter(`airports-pulse-${i}`, airportFilter)
- }
+ const exportMapBasic = (): void => {
+ if (!map) return
+ const canvas = map.getCanvas()
+ const link = document.createElement('a')
+ link.href = canvas.toDataURL('image/jpeg', 0.95)
+ link.download = 'flight-map.jpg'
+ link.click()
}
- // ── Route flight lookup ───────────────────────────────────────────────
- const buildRouteFlights = (): void => {
- routeFlights.clear()
- const now = new Date()
- props.flights.forEach((flight) => {
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
- if (!routeFlights.has(key)) routeFlights.set(key, { historical: [], future: [] })
- routeFlights.get(key)![new Date(flight.departure_date) > now ? 'future' : 'historical'].push(flight)
- })
- }
-
- // ── Arc cache helper ──────────────────────────────────────────────────
const getArc = (flight: Flight): LngLat[] => {
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
+ const key = routeKey(flight.departure_airport, flight.arrival_airport)
if (!arcCache.has(key)) {
- arcCache.set(key, greatCircleGeoJSON(
+ arcCache.set(key, greatCirclePoints(
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
))
@@ -221,97 +330,152 @@ export default defineComponent({
return arcCache.get(key)!
}
+ // ── Popup ─────────────────────────────────────────────────────────────
+
+ const showPopup = (lngLat: maplibregl.LngLatLike, html: string) =>
+ popup!.setLngLat(lngLat).setHTML(html).addTo(map!)
+
+ // ── Filter ────────────────────────────────────────────────────────────
+
+ const applyFilter = (): void => {
+ if (!map?.isStyleLoaded()) return
+ const id = selectedAirportId
+
+ const routeFilter: maplibregl.FilterSpecification | null = id
+ ? ['any', ['==', ['get', 'depId'], id], ['==', ['get', 'arrId'], id]]
+ : null
+
+ for (const layerId of ['routes-line', 'routes-hit', 'routes-future-line', 'routes-future-hit']) {
+ map.setFilter(layerId, routeFilter)
+ }
+
+ // Build the set of airports that should remain visible
+ let visibleAirportIds: Set | null = null
+ if (id) {
+ visibleAirportIds = new Set([id])
+ routeFlights.forEach((bucket, key) => {
+ const allFlights = [...bucket.historical, ...bucket.future]
+ if (allFlights.some(f =>
+ f.departure_airport.id === id || f.arrival_airport.id === id
+ )) {
+ allFlights.forEach(f => {
+ visibleAirportIds!.add(f.departure_airport.id)
+ visibleAirportIds!.add(f.arrival_airport.id)
+ })
+ }
+ })
+ }
+
+ const airportFilter: maplibregl.FilterSpecification | null = visibleAirportIds
+ ? ['in', ['get', 'id'], ['literal', [...visibleAirportIds]]]
+ : null
+
+ for (let i = 0; i < PULSE_PHASES; i++) {
+ map.setFilter(`airports-dot-${i}`, airportFilter)
+ map.setFilter(`airports-pulse-${i}`, airportFilter)
+ }
+ }
+
// ── GeoJSON builders ──────────────────────────────────────────────────
+
+ const buildRouteFlights = (): void => {
+ routeFlights.clear()
+ const now = new Date()
+
+ props.flights.forEach(flight => {
+ const key = routeKey(flight.departure_airport, flight.arrival_airport)
+ const bucket = routeFlights.get(key) ?? { historical: [], future: [] }
+ bucket[new Date(flight.departure_date) > now ? 'future' : 'historical'].push(flight)
+ routeFlights.set(key, bucket)
+ })
+ }
+
const buildRoutesGeoJSON = (): RoutesGeoJSON => {
buildRouteFlights()
const now = new Date()
- const routeCounts = new Map()
- props.flights.forEach((flight) => {
- if (new Date(flight.departure_date) > now) return
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
- routeCounts.set(key, (routeCounts.get(key) ?? 0) + 1)
- })
-
- const routeColor = (count: number): string => {
- if (count >= 5) return '#f97316'
- if (count >= 3) return '#eab308'
- if (count >= 2) return '#22c55e'
- return '#a150d5'
- }
-
- const seenHistorical = new Set()
- const historicalFeatures: GeoJSON.Feature[] = []
+ // Count historical flights per route for colour coding
+ const flightCountByRoute = new Map()
props.flights
.filter(f => new Date(f.departure_date) <= now)
- .forEach((flight) => {
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
- if (seenHistorical.has(key)) return
- seenHistorical.add(key)
- const count = routeCounts.get(key) ?? 1
- historicalFeatures.push({
- type: 'Feature',
- properties: {
- color: routeColor(count), routeKey: key,
- depId: flight.departure_airport.id,
- arrId: flight.arrival_airport.id,
- },
- geometry: { type: 'LineString', coordinates: getArc(flight) },
- })
+ .forEach(f => {
+ const key = routeKey(f.departure_airport, f.arrival_airport)
+ flightCountByRoute.set(key, (flightCountByRoute.get(key) ?? 0) + 1)
})
- const historicalKeys = new Set(routeCounts.keys())
- const seenFuture = new Set()
- const futureFeatures: GeoJSON.Feature[] = []
- props.flights
- .filter((flight) => {
- if (new Date(flight.departure_date) <= now) return false
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
- return !historicalKeys.has(key)
- })
- .forEach((flight) => {
- const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
- if (seenFuture.has(key)) return
- seenFuture.add(key)
- futureFeatures.push({
- type: 'Feature',
- properties: { routeKey: key, depId: flight.departure_airport.id, arrId: flight.arrival_airport.id },
- geometry: { type: 'LineString', coordinates: getArc(flight) },
- })
- })
+ const makeFeature = (
+ flight: Flight,
+ extraProps: Partial = {}
+ ): Feature => ({
+ type: 'Feature',
+ properties: {
+ routeKey: routeKey(flight.departure_airport, flight.arrival_airport),
+ depId: flight.departure_airport.id,
+ arrId: flight.arrival_airport.id,
+ ...extraProps,
+ },
+ geometry: { type: 'LineString', coordinates: getArc(flight) },
+ })
+
+ // One feature per unique route, deduped by key
+ const historicalFeatures = [...new Map(
+ props.flights
+ .filter(f => new Date(f.departure_date) <= now)
+ .map(f => [
+ routeKey(f.departure_airport, f.arrival_airport),
+ makeFeature(f, { color: routeColor(flightCountByRoute.get(routeKey(f.departure_airport, f.arrival_airport)) ?? 1) }),
+ ])
+ ).values()]
+
+ const historicalRouteKeys = new Set(flightCountByRoute.keys())
+ const futureFeatures = [...new Map(
+ props.flights
+ .filter(f => new Date(f.departure_date) > now)
+ .filter(f => !historicalRouteKeys.has(routeKey(f.departure_airport, f.arrival_airport)))
+ .map(f => [routeKey(f.departure_airport, f.arrival_airport), makeFeature(f)])
+ ).values()]
+
+ const toCollection = (features: Feature[]) =>
+ ({ type: 'FeatureCollection' as const, features })
return {
- historical: { type: 'FeatureCollection', features: historicalFeatures },
- future: { type: 'FeatureCollection', features: futureFeatures },
+ historical: toCollection(historicalFeatures),
+ future: toCollection(futureFeatures),
}
}
- const buildAirportsGeoJSON = (): GeoJSON.FeatureCollection[] => {
- const seen = new Map()
+ const buildAirportsGeoJSON = (): FeatureCollection[] => {
+ const uniqueAirports = new Map()
props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
- if (!seen.has(dep.id)) seen.set(dep.id, dep)
- if (!seen.has(arr.id)) seen.set(arr.id, arr)
+ uniqueAirports.set(dep.id, dep)
+ uniqueAirports.set(arr.id, arr)
})
+
+ // Spread airports across pulse phase buckets for staggered animation
const buckets: Airport[][] = Array.from({ length: PULSE_PHASES }, () => [])
- ;[...seen.values()].forEach(airport => buckets[airport.id % PULSE_PHASES].push(airport))
+ uniqueAirports.forEach(airport => buckets[airport.id % PULSE_PHASES].push(airport))
+
return buckets.map(airports => ({
type: 'FeatureCollection',
features: airports.map(airport => ({
- type: 'Feature',
+ type: 'Feature',
properties: { id: airport.id },
- geometry: { type: 'Point', coordinates: [airport.longitude_deg, airport.latitude_deg] },
+ geometry: { type: 'Point', coordinates: [airport.longitude_deg, airport.latitude_deg] },
})),
}))
}
// ── Map layers ────────────────────────────────────────────────────────
+
const addLayers = (): void => {
const { historical, future } = buildRoutesGeoJSON()
+ const isTouch = isTouchDevice()
+ // Routes
map!.addSource('routes', { type: 'geojson', data: historical })
map!.addLayer({
id: 'routes-line', type: 'line', source: 'routes',
- paint: { 'line-color': ['get', 'color'], 'line-opacity': 0.75, 'line-width': 1.8 },
+ paint: { 'line-color': ['get', 'color'], 'line-opacity': 0.75, 'line-width': 1.8 },
layout: { 'line-join': 'round', 'line-cap': 'round' },
})
map!.addLayer({
@@ -319,10 +483,11 @@ export default defineComponent({
paint: { 'line-color': 'transparent', 'line-width': 12 },
})
+ // Future routes
map!.addSource('routes-future', { type: 'geojson', data: future })
map!.addLayer({
id: 'routes-future-line', type: 'line', source: 'routes-future',
- paint: { 'line-color': '#ffffff', 'line-opacity': 0.5, 'line-width': 1.5, 'line-dasharray': [3, 3] },
+ paint: { 'line-color': '#ffffff', 'line-opacity': 0.5, 'line-width': 1.5, 'line-dasharray': [3, 3] },
layout: { 'line-join': 'round', 'line-cap': 'round' },
})
map!.addLayer({
@@ -330,8 +495,8 @@ export default defineComponent({
paint: { 'line-color': 'transparent', 'line-width': 12 },
})
- const airportBuckets = buildAirportsGeoJSON()
- airportBuckets.forEach((data, i) => {
+ // Airport dots + pulse rings (one source per phase bucket)
+ buildAirportsGeoJSON().forEach((data, i) => {
map!.addSource(`airports-${i}`, { type: 'geojson', data })
map!.addLayer({
id: `airports-pulse-${i}`, type: 'circle', source: `airports-${i}`,
@@ -350,7 +515,6 @@ export default defineComponent({
})
})
- const isTouch = isTouchDevice()
popup = new maplibregl.Popup({
closeButton: isTouch,
closeOnClick: false,
@@ -361,16 +525,15 @@ export default defineComponent({
const airportLayerIds = Array.from({ length: PULSE_PHASES }, (_, i) => `airports-dot-${i}`)
- // ── Desktop hover events ──────────────────────────────────────────
+ // ── Hover events (desktop only) ───────────────────────────────────
+
if (!isTouch) {
for (let i = 0; i < PULSE_PHASES; i++) {
- map!.on('mouseenter', `airports-dot-${i}`, (e) => {
+ map!.on('mouseenter', `airports-dot-${i}`, e => {
map!.getCanvas().style.cursor = 'pointer'
const airport = airportById.get(Number(e.features![0].properties.id))
- const geom = e.features![0].geometry
- if (airport && geom.type === 'Point') {
- showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
- }
+ const geom = e.features![0].geometry
+ if (airport && geom.type === 'Point') showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
})
map!.on('mouseleave', `airports-dot-${i}`, () => {
map!.getCanvas().style.cursor = ''
@@ -378,74 +541,65 @@ export default defineComponent({
})
}
- map!.on('mouseenter', 'routes-hit', (e) => {
- map!.getCanvas().style.cursor = 'pointer'
- const rf = routeFlights.get(e.features![0].properties.routeKey as string)
- if (rf) showPopup(e.lngLat, routePopupHTML(rf.historical, rf.future))
- })
- map!.on('mouseleave', 'routes-hit', () => {
- map!.getCanvas().style.cursor = ''
- popup!.remove()
- })
-
- map!.on('mouseenter', 'routes-future-hit', (e) => {
- map!.getCanvas().style.cursor = 'pointer'
- const rf = routeFlights.get(e.features![0].properties.routeKey as string)
- if (rf) showPopup(e.lngLat, routePopupHTML(rf.historical, rf.future))
- })
- map!.on('mouseleave', 'routes-future-hit', () => {
- map!.getCanvas().style.cursor = ''
- popup!.remove()
- })
+ for (const layerId of ['routes-hit', 'routes-future-hit']) {
+ map!.on('mouseenter', layerId, e => {
+ map!.getCanvas().style.cursor = 'pointer'
+ const bucket = routeFlights.get(e.features![0].properties.routeKey as string)
+ if (bucket) showPopup(e.lngLat, routePopupHTML(bucket.historical, bucket.future))
+ })
+ map!.on('mouseleave', layerId, () => {
+ map!.getCanvas().style.cursor = ''
+ popup!.remove()
+ })
+ }
}
- // ── Unified click handler ─────────────────────────────────────────
- map!.on('click', (e) => {
+ // ── Click handler ─────────────────────────────────────────────────
+
+ map!.on('click', e => {
+ // Widen hit area on touch
const airportQuery = isTouch
? [[e.point.x - TOUCH_RADIUS, e.point.y - TOUCH_RADIUS], [e.point.x + TOUCH_RADIUS, e.point.y + TOUCH_RADIUS]] as [maplibregl.PointLike, maplibregl.PointLike]
: e.point as maplibregl.PointLike
- const airportFeatures = map!.queryRenderedFeatures(airportQuery, { layers: airportLayerIds })
+ const clickedAirport = map!.queryRenderedFeatures(airportQuery, { layers: airportLayerIds })[0]
- if (airportFeatures.length) {
- const id = Number(airportFeatures[0].properties.id)
+ if (clickedAirport) {
+ const id = Number(clickedAirport.properties.id)
selectedAirportId = selectedAirportId === id ? null : id
applyFilter()
if (isTouch) {
const airport = airportById.get(id)
- const geom = airportFeatures[0].geometry
- if (airport && geom.type === 'Point') {
- showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
- }
+ const geom = clickedAirport.geometry
+ if (airport && geom.type === 'Point') showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
}
return
}
- const routeFeatures = map!.queryRenderedFeatures(e.point, {
- layers: ['routes-hit', 'routes-future-hit'],
- })
+ const clickedRoute = map!.queryRenderedFeatures(e.point, { layers: ['routes-hit', 'routes-future-hit'] })[0]
- if (routeFeatures.length) {
- const rf = routeFlights.get(routeFeatures[0].properties.routeKey as string)
- if (rf) showPopup(e.lngLat, routePopupHTML(rf.historical, rf.future))
+ if (clickedRoute) {
+ const bucket = routeFlights.get(clickedRoute.properties.routeKey as string)
+ if (bucket) showPopup(e.lngLat, routePopupHTML(bucket.historical, bucket.future))
return
}
+ // Clicked empty space — deselect
selectedAirportId = null
applyFilter()
popup!.remove()
})
// ── Pulse animation ───────────────────────────────────────────────
- const period = 2200
+
const animate = (): void => {
const now = Date.now()
for (let i = 0; i < PULSE_PHASES; i++) {
- const t = ((now + (i / PULSE_PHASES) * period) % period) / period
- map!.setPaintProperty(`airports-pulse-${i}`, 'circle-radius', 5 + t * 13)
+ const progress = ((now + (i / PULSE_PHASES) * PULSE_PERIOD) % PULSE_PERIOD) / PULSE_PERIOD
+ map!.setPaintProperty(`airports-pulse-${i}`, 'circle-radius', 5 + progress * 13)
if (!selectedAirportId) {
- map!.setPaintProperty(`airports-pulse-${i}`, 'circle-stroke-opacity', 0.7 * (1 - t))
+ map!.setPaintProperty(`airports-pulse-${i}`, 'circle-stroke-opacity', 0.7 * (1 - progress))
}
}
pulseFrame = requestAnimationFrame(animate)
@@ -453,102 +607,102 @@ export default defineComponent({
animate()
}
- const fitBounds = (): void => {
+ // ── Fit bounds ────────────────────────────────────────────────────────
+
+ const fitBounds = (padding = 60): void => {
if (!props.flights.length) return
- // Collect all airport coordinates
- const lngs: number[] = []
- const lats: number[] = []
- props.flights.forEach(f => {
- lngs.push(f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg)
- lats.push(f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg)
- })
+ const lngs = props.flights.flatMap(f => [f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg])
+ const lats = props.flights.flatMap(f => [f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg])
const minLat = Math.min(...lats)
const maxLat = Math.max(...lats)
- // Check if wrapping across the dateline gives a tighter span
- const minLngNormal = Math.min(...lngs)
- const maxLngNormal = Math.max(...lngs)
- const spanNormal = maxLngNormal - minLngNormal
-
- // Shift negative longitudes into the 0–360 range and recompute span
- const lngs360 = lngs.map(lng => lng < 0 ? lng + 360 : lng)
- const minLng360 = Math.min(...lngs360)
- const maxLng360 = Math.max(...lngs360)
- const span360 = maxLng360 - minLng360
-
- let minLng: number
- let maxLng: number
+ // Compare normal span vs dateline-crossing span to pick the tighter fit
+ const spanNormal = Math.max(...lngs) - Math.min(...lngs)
+ const lngs360 = lngs.map(lng => lng < 0 ? lng + 360 : lng)
+ const span360 = Math.max(...lngs360) - Math.min(...lngs360)
+ let minLng: number, maxLng: number
if (span360 < spanNormal) {
- // Dateline-crossing view is tighter — use the shifted coords
- minLng = minLng360 > 180 ? minLng360 - 360 : minLng360
- maxLng = maxLng360 > 180 ? maxLng360 - 360 : maxLng360
+ const min360 = Math.min(...lngs360)
+ const max360 = Math.max(...lngs360)
+ minLng = min360 > 180 ? min360 - 360 : min360
+ maxLng = max360 > 180 ? max360 - 360 : max360
} else {
- minLng = minLngNormal
- maxLng = maxLngNormal
+ minLng = Math.min(...lngs)
+ maxLng = Math.max(...lngs)
}
- map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding: 60, duration: 0 })
+ map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding, duration: 0 })
}
// ── Map init ──────────────────────────────────────────────────────────
+
const initMap = (): void => {
nextTick(() => {
map = new maplibregl.Map({
- container: mapContainer.value!,
+ container: mapContainer.value!,
cooperativeGestures: true,
- attributionControl: false,
+ attributionControl: false,
+ center: [0, 20],
+ zoom: 2,
+ renderWorldCopies: true,
+ canvasContextAttributes: { preserveDrawingBuffer: true },
style: {
version: 8,
sources: {
'carto-dark': {
- type: 'raster',
+ type: 'raster',
+ tileSize: 256,
+ maxzoom: 19,
tiles: [
'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
'https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
],
- tileSize: 256,
- maxzoom: 19,
},
},
layers: [{ id: 'carto-dark', type: 'raster', source: 'carto-dark' }],
},
- center: [0, 20],
- zoom: 2,
- renderWorldCopies: true,
})
+
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
map.addControl(new maplibregl.FullscreenControl(), 'top-right')
map.on('load', () => {
addLayers()
fitBounds()
- mapReady.value = true // ← add this
+ mapReady.value = true
})
})
}
+
// ── Data updates ──────────────────────────────────────────────────────
+
const updateData = (): void => {
- if (!map || !map.isStyleLoaded()) return
+ if (!map?.isStyleLoaded()) return
+
arcCache.clear()
airportById.clear()
props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
airportById.set(dep.id, dep)
airportById.set(arr.id, arr)
})
- const { historical, future } = buildRoutesGeoJSON();
- (map.getSource('routes') as maplibregl.GeoJSONSource)?.setData(historical);
- (map.getSource('routes-future') as maplibregl.GeoJSONSource)?.setData(future)
+
+ const { historical, future } = buildRoutesGeoJSON()
+ ;(map.getSource('routes') as maplibregl.GeoJSONSource)?.setData(historical)
+ ;(map.getSource('routes-future') as maplibregl.GeoJSONSource)?.setData(future)
buildAirportsGeoJSON().forEach((data, i) =>
(map!.getSource(`airports-${i}`) as maplibregl.GeoJSONSource)?.setData(data)
)
+
fitBounds()
}
+ // ── Lifecycle ─────────────────────────────────────────────────────────
+
onMounted(() => {
props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
airportById.set(dep.id, dep)
@@ -564,84 +718,88 @@ export default defineComponent({
if (map) { map.remove(); map = null }
})
- const mapReady = ref(false)
-
- return { mapContainer, mapReady }
+ return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems }
},
})
+
+
diff --git a/resources/js/Components/FlightsGoneBy/FlightStatsBar.vue b/resources/js/Components/FlightsGoneBy/FlightStatsBar.vue
index 0db9e4a..7d9979b 100644
--- a/resources/js/Components/FlightsGoneBy/FlightStatsBar.vue
+++ b/resources/js/Components/FlightsGoneBy/FlightStatsBar.vue
@@ -40,7 +40,9 @@
-
+
+ upcoming
+
diff --git a/resources/js/Components/FlightsGoneBy/FlightToolTip.vue b/resources/js/Components/FlightsGoneBy/FlightToolTip.vue
index b95a797..6a5940f 100644
--- a/resources/js/Components/FlightsGoneBy/FlightToolTip.vue
+++ b/resources/js/Components/FlightsGoneBy/FlightToolTip.vue
@@ -3,6 +3,12 @@ import { Flight } from "@/Types/types";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
+import LiveryImage from "@/Components/FlightsGoneBy/LiveryImage.vue";
+import ToolTipDivider from "@/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue";
+import ToolTipRow from "@/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue";
+import ToolTipRows from "@/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue";
+import ToolTipName from "@/Components/FlightsGoneBy/Tooltips/ToolTipName.vue";
+import ToolTipHeader from "@/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue";
defineProps<{
flight: Flight
@@ -17,101 +23,45 @@ defineProps<{
-
+
-
+
{{ flight.departure_airport.display_code }}
→
{{ flight.arrival_airport.display_code }}
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
index 9fe617d..85136e5 100644
--- a/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
+++ b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
@@ -13,7 +13,7 @@ defineProps<{
-
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/CountryRow.vue b/resources/js/Components/FlightsGoneBy/Tooltips/CountryRow.vue
new file mode 100644
index 0000000..5effe7a
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/CountryRow.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue
new file mode 100644
index 0000000..af56661
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipDivider.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue
new file mode 100644
index 0000000..c17cbb2
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipHeader.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipName.vue b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipName.vue
new file mode 100644
index 0000000..8144277
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipName.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue
new file mode 100644
index 0000000..fe8e7e0
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRow.vue
@@ -0,0 +1,46 @@
+
+
+
+
+ {{label}}
+ {{value}}
+
+ {{value}}
+
+
+
+
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue
new file mode 100644
index 0000000..bfd5258
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/ToolTipRows.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index 1e830ea..583928e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -29,13 +29,7 @@ use Inertia\Inertia;
*/
Route::domain(config('app.domain'))->group(
function() {
- Route::get('/', function () {
- if (auth()->check()) {
- return redirect()->route('profile.view', auth()->user()->name);
- }
- return redirect()->route('login');
- });
-
+ Route::get('/', [FlightProfileController::class, 'index'])->name('home');
Route::get('/dashboard', function () {