v1 Release
This commit is contained in:
@@ -129,10 +129,12 @@ class GetAircraftInfoForArrivedFlights extends Command
|
||||
|
||||
if ($data->estimated_departure_utc?->ne($originalDeparture)) {
|
||||
$updates['departure_date'] = $data->estimated_departure_utc;
|
||||
$updates['departure_processed_at'] = $data->estimated_departure_utc;
|
||||
}
|
||||
|
||||
if ($data->estimated_arrival_utc?->ne($originalArrival)) {
|
||||
$updates['arrival_date'] = $data->estimated_arrival_utc;
|
||||
$updates['arrival_processed_at'] = $data->estimated_arrival_utc;
|
||||
}
|
||||
|
||||
if ($data->equipment_iata && $originalAircraft?->iata_code !== $data->equipment_iata) {
|
||||
|
||||
@@ -93,7 +93,6 @@ class UserProfileController extends Controller
|
||||
'canView' => Gate::allows('viewProfileData', $user),
|
||||
'user' => $user,
|
||||
'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
|
||||
'otherUsersOnFlight' => $userFlight->otherUsersOnFlight(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+34
-17
@@ -55,11 +55,12 @@ class UserFlight extends Model
|
||||
'duration',
|
||||
'duration_display',
|
||||
'distance',
|
||||
'livery_url',
|
||||
'livery',
|
||||
'scope',
|
||||
'range',
|
||||
'region_range',
|
||||
'class_seat_combined'
|
||||
'class_seat_combined',
|
||||
'other_users_on_flight'
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
@@ -207,19 +208,23 @@ class UserFlight extends Model
|
||||
return !$this->isDomestic();
|
||||
}
|
||||
|
||||
public function otherUsersOnFlight(): Collection
|
||||
protected function otherUsersOnFlight(): Attribute
|
||||
{
|
||||
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');
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return once(fn() => 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
|
||||
@@ -286,16 +291,19 @@ class UserFlight extends Model
|
||||
return $this->belongsTo(FlightReason::class);
|
||||
}
|
||||
|
||||
public function liveryUrl(): Attribute{
|
||||
public function livery(): Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
|
||||
$user = auth()?->user();
|
||||
|
||||
$useAi = !$user || $user->getSetting('ai_liveries');
|
||||
|
||||
$apiUrl = config('app.logo_api_url');
|
||||
|
||||
$attribution = '';
|
||||
$attributionUrl = '';
|
||||
$isAi = false;
|
||||
|
||||
if (!$this->aircraft) {
|
||||
return null;
|
||||
}
|
||||
@@ -305,6 +313,8 @@ class UserFlight extends Model
|
||||
$path = "images/liveries/{$this->airline->internal_name}_{$this->aircraft->designator}.png";
|
||||
if (Storage::disk('local')->exists($path)) {
|
||||
$finalPath = $apiUrl."/airline/{$this->airline->internal_name}/livery/{$this->aircraft->designator}";
|
||||
$attribution = 'This image is AI generated and may not be an accurate representation of the aircraft.';
|
||||
$isAi = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +322,8 @@ class UserFlight extends Model
|
||||
$path = "images/livery_templates/{$this->aircraft->designator}.png";
|
||||
if (Storage::disk('local')->exists($path)) {
|
||||
$finalPath = $apiUrl."/aircraft/{$this->aircraft->designator}/livery";
|
||||
$attribution = '© NoRebbo Blank Aircraft Templates';
|
||||
$attributionUrl = 'https://www.norebbo.com/category/aircraft-templates/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +331,12 @@ class UserFlight extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
return $finalPath;
|
||||
return [
|
||||
'image_url' => $finalPath,
|
||||
'attribution' => $attribution,
|
||||
'attribution_url' => $attributionUrl,
|
||||
'is_ai' => $isAi,
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'" />
|
||||
|
||||
@@ -148,11 +148,10 @@ const copied = ref(false)
|
||||
to places you might never have otherwise considered.
|
||||
</p>
|
||||
<p>
|
||||
Most letters are possible to visit somewhat organically if you fly enough, but some will be difficult. <b>Q</b> is probably the most difficult letter to obtain -
|
||||
there's no rules against having a code starting with Q, but due to many aviation terms starting with Q (such as QNH), very few airports use it.
|
||||
Most letters are possible to visit somewhat organically if you fly enough, but some will be difficult. Based on our airport database, it is actually possible to obtain every letter except <b>V</b> without leaving China.
|
||||
</p>
|
||||
<p>
|
||||
Based on our airport database, it is actually possible to obtain every letter except <b>V</b> without leaving China.
|
||||
<b>Q</b> is probably the most difficult letter to obtain - there's no rules against having a code starting with Q, but due to many aviation terms starting with Q (such as QNH), very few airports use it.
|
||||
</p>
|
||||
<p>
|
||||
At time of writing, there are just 2 major airports that have daily scheduled service by major airlines, and 4 airports with any commercial service at all.
|
||||
@@ -162,7 +161,7 @@ const copied = ref(false)
|
||||
<li><b>QRO</b> in Mexico - a fantastic place to visit and with flights from around Mexico and the US.</li>
|
||||
<li><b>QSZ</b> in China is also a very interesting place to visit and is served well from Urumqi and Xi'an.</li>
|
||||
<li><b>QBC</b> is a small airport in Canada and has daily runs on commuter aircraft from Vancouver.</li>
|
||||
<li><b>QSR</b> is probably most promising recently - it did not have commercial service for 10 years and is having good growth now with European low cost carriers.</li>
|
||||
<li><b>QSR</b> in Italy is probably most promising recently - it did not have commercial service for 10 years and is having good growth now with European low cost carriers.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Other potential candidates do not seem so promising
|
||||
|
||||
@@ -22,7 +22,6 @@ const props = defineProps<{
|
||||
followStatus: string
|
||||
canEdit: boolean
|
||||
canView: boolean
|
||||
otherUsersOnFlight: string[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -44,11 +43,11 @@ 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 v-if="otherUsersOnFlight.length > 0">
|
||||
<DetailRows v-if="flight.other_users_on_flight.length > 0">
|
||||
<PanelLabel>Other Users On This Flight</PanelLabel>
|
||||
<div class="d-flex flex-wrap ga-2">
|
||||
<v-chip
|
||||
v-for="userName in otherUsersOnFlight"
|
||||
v-for="userName in flight.other_users_on_flight"
|
||||
:key="userName"
|
||||
:href="route('profile.view', userName)"
|
||||
color="primary"
|
||||
|
||||
@@ -57,6 +57,7 @@ const selectedSeatTypes = ref<number[]>([])
|
||||
const selectedManufacturers = ref<string[]>([])
|
||||
const selectedAircraftModels = ref<number[]>([])
|
||||
const selectedAirportRegions = ref<number[]>([])
|
||||
const selectedOtherUsers = ref<string[]>([])
|
||||
|
||||
const activeFilterCount = computed(() =>
|
||||
selectedYears.value.length +
|
||||
@@ -73,7 +74,8 @@ const activeFilterCount = computed(() =>
|
||||
selectedSeatTypes.value.length +
|
||||
selectedManufacturers.value.length +
|
||||
selectedAircraftModels.value.length +
|
||||
selectedAirportRegions.value.length
|
||||
selectedAirportRegions.value.length +
|
||||
selectedOtherUsers.value.length
|
||||
)
|
||||
|
||||
function onFiltersChange(filters: {
|
||||
@@ -92,6 +94,7 @@ function onFiltersChange(filters: {
|
||||
manufacturers: string[]
|
||||
aircraftModels: number[]
|
||||
airportRegions: number[]
|
||||
otherUsers: string[]
|
||||
}) {
|
||||
localSelectedFlightId.value = null
|
||||
selectedYears.value = filters.years
|
||||
@@ -109,6 +112,7 @@ function onFiltersChange(filters: {
|
||||
selectedManufacturers.value = filters.manufacturers
|
||||
selectedAircraftModels.value = filters.aircraftModels
|
||||
selectedAirportRegions.value = filters.airportRegions
|
||||
selectedOtherUsers.value = filters.otherUsers
|
||||
}
|
||||
|
||||
// ── Filtering ─────────────────────────────────────────────────────────────────
|
||||
@@ -143,6 +147,10 @@ function matchesFilters(f: Flight): boolean {
|
||||
if (!selectedAirportRegions.value.includes(depRegion ?? -1) &&
|
||||
!selectedAirportRegions.value.includes(arrRegion ?? -1)) return false
|
||||
}
|
||||
if (selectedOtherUsers.value.length) {
|
||||
const names = f.other_users_on_flight ?? []
|
||||
if (!selectedOtherUsers.value.some(name => names.includes(name))) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Vendored
+9
-1
@@ -278,6 +278,13 @@ export type RegionRange = "intraregional" | "interregional"
|
||||
export type FlightScope = 'domestic' | 'international'
|
||||
export type FlightRange = 'intracontinental' | 'intercontinental'
|
||||
|
||||
export interface Livery{
|
||||
image_url: string,
|
||||
is_ai: boolean,
|
||||
attribution: string,
|
||||
attribution_url: string,
|
||||
}
|
||||
|
||||
export interface Flight {
|
||||
id: number
|
||||
flight_number: string | null
|
||||
@@ -305,9 +312,10 @@ export interface Flight {
|
||||
scope: FlightScope
|
||||
range: FlightRange
|
||||
region_range: RegionRange
|
||||
livery_url?: string
|
||||
livery: Livery | null
|
||||
user?: User | null
|
||||
class_seat_combined: number
|
||||
other_users_on_flight: string[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user