v1 Release
This commit is contained in:
@@ -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">, </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(() => `© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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>
|
||||
+5
-2
@@ -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'" />
|
||||
|
||||
Reference in New Issue
Block a user