v1 Release

This commit is contained in:
2026-07-11 18:44:40 +10:00
parent 85eeba982f
commit 5ac580bb06
15 changed files with 171 additions and 72 deletions
@@ -3,7 +3,7 @@ import {Aircraft, Airline, Flight} from "@/Types/types";
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 LiveryImage from "@/Components/FlightsGoneBy/Liveries/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";
@@ -50,8 +50,8 @@ function formatEngineType(type: string): string {
<ToolTipRow label="Engines" :value="aircraft.engine_count + 'x ' + formatEngineType(aircraft.engine_type)" />
</ToolTipRows>
<ToolTipDivider v-if="flight?.livery_url" />
<ToolTipRows v-if="flight?.livery_url">
<ToolTipDivider v-if="flight?.livery" />
<ToolTipRows v-if="flight?.livery">
<LiveryImage :flight="flight" />
</ToolTipRows>
<ToolTipDivider v-if="flight?.aircraft_registration" />
@@ -24,6 +24,7 @@ const emit = defineEmits<{
manufacturers: string[]
aircraftModels: number[]
airportRegions: number[]
otherUsers: string[]
}]
}>()
@@ -62,10 +63,11 @@ function buildOptions(flights: Flight[]) {
const manufacturers = new Map<string, { name: string }>()
const aircraftModels = new Map<number, { id: number; name: string }>()
const airportRegions = new Map<number, { id: number; name: string; countryCodes: Set<string> }>()
const otherUsers = new Set<string>()
flights.forEach(f => {
years.add(new Date(f.departure_date).getFullYear())
f.other_users_on_flight?.forEach(name => otherUsers.add(name))
if (f.airline) {
airlines.set(f.airline.id, { id: f.airline.id, name: f.airline.name, airline: f.airline })
@@ -143,6 +145,7 @@ function buildOptions(flights: Flight[]) {
manufacturers: [...manufacturers.values()].sort((a, b) => a.name.localeCompare(b.name)),
aircraftModels: [...aircraftModels.values()].sort((a, b) => a.name.localeCompare(b.name)),
airportRegions: [...airportRegions.values()].sort((a, b) => a.name.localeCompare(b.name)),
otherUsers: [...otherUsers].sort((a, b) => a.localeCompare(b)),
}
}
@@ -165,6 +168,7 @@ const selectedSeatTypes = ref<number[]>([])
const selectedManufacturers = ref<string[]>([])
const selectedAircraftModels = ref<number[]>([])
const selectedAirportRegions = ref<number[]>([])
const selectedOtherUsers = ref<string[]>([])
// When selected countries change, drop any selected regions that no longer
// belong to any of the selected countries.
@@ -194,6 +198,7 @@ function emitFilters() {
manufacturers: selectedManufacturers.value,
aircraftModels: selectedAircraftModels.value,
airportRegions: selectedAirportRegions.value,
otherUsers: selectedOtherUsers.value,
})
}
@@ -508,7 +513,22 @@ const countryFlagClass = (code: string) =>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCrewTypes.length - 2 }}</span>
</template>
</v-select>
<v-select
v-if="availableOptions.otherUsers.length > 0"
v-model="selectedOtherUsers"
:items="availableOptions.otherUsers"
label="Flew With"
multiple clearable hide-details
density="compact" variant="outlined"
@update:model-value="emitFilters"
>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
{{ item }}<span v-if="index < Math.min(selectedOtherUsers.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedOtherUsers.length - 2 }}</span>
</template>
</v-select>
</div>
</template>
@@ -1,43 +1,46 @@
<template>
<div class="flight-map-wrapper">
<PlaneLoader v-if="!mapReady" class="map-loader" />
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
<button @click="exportMapBasic()" class="export-btn" title="Download as Image">
<span class="mdi mdi-download" />
</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">
<span class="mdi mdi-earth-off" />
<p>No flight data available</p>
</div>
<div v-if="showLegend" class="map-legend" :class="{ 'map-legend--open': legendOpen }">
<button class="map-legend__toggle" @click="toggleLegend">
<span class="mdi mdi-format-list-bulleted" />
<span class="map-legend__toggle-label">Legend</span>
<span class="mdi" :class="legendOpen ? 'mdi-chevron-down' : 'mdi-chevron-up'" />
<div>
<div class="flight-map-wrapper">
<PlaneLoader v-if="!mapReady" class="map-loader" />
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
<button @click="exportMapBasic()" class="export-btn" title="Download as Image">
<span class="mdi mdi-download" />
</button>
<div class="map-legend__body">
<div class="map-legend__row" v-for="item in legendItems" :key="item.label">
<svg width="28" height="10" class="map-legend__swatch">
<line
x1="2" y1="5" x2="26" y2="5"
:stroke="item.color"
:stroke-dasharray="item.dashed ? '4 3' : undefined"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<span class="map-legend__label">{{ item.label }}</span>
<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">
<span class="mdi mdi-earth-off" />
<p>No flight data available</p>
</div>
<div v-if="showLegend" class="map-legend" :class="{ 'map-legend--open': legendOpen }">
<button class="map-legend__toggle" @click="toggleLegend">
<span class="mdi mdi-format-list-bulleted" />
<span class="map-legend__toggle-label">Legend</span>
<span class="mdi" :class="legendOpen ? 'mdi-chevron-down' : 'mdi-chevron-up'" />
</button>
<div class="map-legend__body">
<div class="map-legend__row" v-for="item in legendItems" :key="item.label">
<svg width="28" height="10" class="map-legend__swatch">
<line
x1="2" y1="5" x2="26" y2="5"
:stroke="item.color"
:stroke-dasharray="item.dashed ? '4 3' : undefined"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<span class="map-legend__label">{{ item.label }}</span>
</div>
</div>
</div>
</div>
<small class="attribution" v-if="compact" v-html="cartoAttribution" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onBeforeUnmount, watch, nextTick, PropType } from 'vue'
import {defineComponent, ref, onMounted, onBeforeUnmount, watch, nextTick, PropType, computed} from 'vue'
import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { Flight, Airport, SharedProps } from '@/Types/types'
@@ -72,6 +75,7 @@ interface RoutesGeoJSON {
const cartoAttribution = computed(() => `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>`)
// ── Constants ─────────────────────────────────────────────────────────────────
const PULSE_PHASES = 6
@@ -688,7 +692,7 @@ export default defineComponent({
map = new maplibregl.Map({
container: mapContainer.value!,
cooperativeGestures: true,
attributionControl: false,
attributionControl: props.compact ? false : {compact: true},
center: [0, 20],
zoom: 2,
renderWorldCopies: true,
@@ -700,6 +704,7 @@ export default defineComponent({
type: 'raster',
tileSize: 256,
maxzoom: 19,
attribution: cartoAttribution.value,
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',
@@ -767,7 +772,7 @@ export default defineComponent({
if (map) { map.remove(); map = null }
})
return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems, isGlobe, toggleProjection, toggleLegend }
return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems, isGlobe, toggleProjection, toggleLegend, cartoAttribution }
},
})
@@ -816,6 +821,19 @@ export default defineComponent({
}
}
small.attribution{
font-size: 0.7em;
display: flex;
align-items: center;
justify-content: center;
color: var(--muted);
gap: 2px;
}
small.attribution * {
color: var(--muted)
}
.map-container { position: absolute; inset: 0; }
.map-loader { z-index: 10; }
.map-hidden { visibility: hidden; }
@@ -991,4 +1009,9 @@ export default defineComponent({
}
.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); }
.maplibregl-ctrl-bottom-right {
left: 50%;
right: auto;
transform: translateX(-50%);
}
</style>
@@ -3,7 +3,7 @@ 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 LiveryImage from "@/Components/FlightsGoneBy/Liveries/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";
@@ -52,8 +52,8 @@ defineProps<{
<ToolTipRow label="Aircraft" :value="flight.aircraft?.display_name_short" v-if="flight.aircraft" />
<ToolTipRow label="Date" :value="flight.departure_date_display" />
</ToolTipRows>
<ToolTipDivider v-if="flight.livery_url" />
<ToolTipRows v-if="flight.livery_url">
<ToolTipDivider v-if="flight.livery" />
<ToolTipRows v-if="flight.livery">
<LiveryImage :flight="flight" />
</ToolTipRows>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import {computed} from "vue";
import {Link} from "@inertiajs/vue3";
import {Livery} from "@/Types/types";
defineProps<{
livery: Livery
}>()
</script>
<template>
<small v-if="livery.is_ai">{{livery.attribution}}</small>
<Link v-else :href="livery.attribution_url" target="_blank"><small>{{livery.attribution}}</small></Link>
</template>
<style scoped>
small{
font-size: 0.7em;
color: var(--muted);
}
</style>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import {Flight} from "@/Types/types";
import Mono from "@/Components/FlightsGoneBy/Mono.vue";
import LiveryAttribution from "@/Components/FlightsGoneBy/Liveries/LiveryAttribution.vue";
defineProps<{
flight: Flight
@@ -8,8 +9,8 @@ defineProps<{
</script>
<template>
<div class="livery" v-if="flight.livery_url" :style="{ backgroundImage: `url('${flight.livery_url ?? ''}')` }">
<div class="livery" v-if="flight.livery?.image_url" :style="{ backgroundImage: `url('${flight.livery.image_url ?? ''}')` }">
<LiveryAttribution :livery="flight.livery" />
</div>
</template>
@@ -18,9 +19,11 @@ defineProps<{
display:flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
width: 100%;
aspect-ratio: 16/9;
background-size: cover;
min-width: 300px;
}
</style>
@@ -5,7 +5,7 @@ import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
import type {Flight} from "@/Types/types";
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
import PanelSubHeader from "@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue";
import LiveryImage from "@/Components/FlightsGoneBy/LiveryImage.vue";
import LiveryImage from "@/Components/FlightsGoneBy/Liveries/LiveryImage.vue";
defineProps<{
flight: Flight
@@ -13,7 +13,7 @@ defineProps<{
<template>
<Panel label="Route">
<div class="map-placeholder">
<FlightMap :flights="[flight]" :showLegend="false" />
<FlightMap :flights="[flight]" :showLegend="false" compact />
</div>
<DetailRows>
<DetailRow label="Distance" :value="flight.distance + 'km'" />