Compare commits

..
8 Commits
Author SHA1 Message Date
dredgy 9d175a1525 Allowed filters in the url 2026-08-11 16:11:31 +10:00
dredgy c602393c95 Adds a user search box 2026-08-09 20:08:49 +10:00
dredgy 8832028304 Following Select Box Shows only When User Is Following People 2026-07-22 21:46:02 +10:00
dredgy 7cecdb6bde Added engine count/type filters 2026-07-22 21:25:54 +10:00
dredgy 39c1f78b20 Compact map padding improved 2026-07-22 19:08:27 +10:00
dredgy 923d9a2c3e Added user select box to achievement pages 2026-07-21 21:03:46 +10:00
dredgy 3abd66001b Fixed Chart Axes 2026-07-20 20:34:43 +10:00
dredgy fe642300b6 Bug fix for individual alliance challenges 2026-07-20 15:59:15 +10:00
18 changed files with 765 additions and 413 deletions
+20
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Aircraft; use App\Models\Aircraft;
use App\Models\Airline; use App\Models\Airline;
use App\Models\Airport; use App\Models\Airport;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class SearchController extends Controller class SearchController extends Controller
@@ -82,4 +83,23 @@ class SearchController extends Controller
]) ])
->values(); ->values();
} }
public function users()
{
$q = request('q', '');
if (strlen($q) < 2) return [];
return User::where('name', 'ilike', "%{$q}%")
->orderBy('name')
->limit(50)
->get(['id', 'name'])
->map(fn($user) => [
'value' => $user->id,
'title' => $user->name,
])
->values();
}
} }
@@ -38,6 +38,7 @@ class HandleInertiaRequests extends Middleware
'roles' => $request->user()?->getRoleNames() ?? [], 'roles' => $request->user()?->getRoleNames() ?? [],
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [], 'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
'apiToken' => session('api_token'), 'apiToken' => session('api_token'),
'following' => $request->user()?->following()->with('followee')->get()->pluck('followee')->filter()->pluck('name')->toArray() ?? [],
], ],
'flash' => [ 'flash' => [
'success' => $request->session()->get('success'), 'success' => $request->session()->get('success'),
@@ -1,115 +1,125 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref } from 'vue'
import { Achievement, Alliance, Flight, User } from '@/Types/types' import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue' import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue' import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue' import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue' import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
import AirlineLogo from '@/Components/FlightsGoneBy/AirlineLogo.vue'
import AllianceLogo from '@/Components/FlightsGoneBy/AllianceLogo.vue' import AllianceLogo from '@/Components/FlightsGoneBy/AllianceLogo.vue'
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue' import FlightBadge from "@/Components/FlightsGoneBy/FlightBadge.vue";
defineOptions({ inheritAttrs: false })
interface AirlineEntry {
airline: Airline
flights: Flight[]
}
const props = defineProps<{ const props = defineProps<{
achievement: Achievement achievement: Achievement
user: User user: User
followStatus: string followStatus: string
majorAlliances: Alliance[] alliance: Alliance
airlines: Airline[]
flights: Flight[] flights: Flight[]
}>() }>()
interface AllianceEntry { const flightsByAirline = computed(() => {
alliance: Alliance const map = new Map<string, AirlineEntry>()
flights: Flight[] const idToKey = new Map<number, string>()
}
const alliances = computed<AllianceEntry[]>(() => { for (const airline of props.airlines) {
const entries = props.majorAlliances.map(alliance => ({ alliance, flights: [] as Flight[] })) const key = rowKey(airline)
const byName = new Map(entries.map(e => [e.alliance.internal_name, e])) map.set(key, { airline, flights: [] })
idToKey.set(airline.id, key)
for (const flight of props.flights) { for (const alias of airline.merged_from ?? []) {
const key = flight.airline?.alliance?.internal_name idToKey.set(alias.id, key)
if (!key) continue }
byName.get(key)?.flights.push(flight)
} }
return entries for (const flight of props.flights) {
const airline = flight.airline
if (!airline) continue
const key = idToKey.get(airline.id)
if (!key) continue
map.get(key)?.flights.push(flight)
}
return map
}) })
const completedCount = computed( function rowKey(airline: Airline): string {
() => alliances.value.filter(a => a.flights.length > 0).length return airline.iata_code ?? airline.internal_name
)
function flightTitle(flight: Flight): string {
const name = flight.airline?.name ?? 'Unknown airline'
return `${name}: ${flight.departure_airport?.iata_code}${flight.arrival_airport?.iata_code}`
} }
const rows = computed(() => [...flightsByAirline.value.keys()])
function entryFor(key: string): AirlineEntry {
return flightsByAirline.value.get(key)!
}
</script> </script>
<template> <template>
<!-- Header --> <!-- Header -->
<Panel> <Panel>
<div class="alliance-header"> <div class="alliance-header">
<div class="logo-row"> <AllianceLogo :alliance="alliance" size="56" />
<AllianceLogo
v-for="entry in alliances"
:key="entry.alliance.internal_name"
:alliance="entry.alliance"
size="56"
/>
</div>
<div> <div>
<PanelHeader centered>Major Alliances</PanelHeader> <PanelHeader centered>{{ alliance.name }}</PanelHeader>
<PanelSubHeader centered> <PanelSubHeader centered>
{{ completedCount }} / {{ alliances.length }} alliances flown <slot />
</PanelSubHeader> </PanelSubHeader>
</div> </div>
</div> </div>
</Panel> </Panel>
<!-- Per-alliance breakdown -->
<Panel v-for="entry in alliances" :key="entry.alliance.internal_name"> <!-- Airlines table -->
<Panel>
<div class="table-toolbar"> <div class="table-toolbar">
<PanelHeader>{{ entry.alliance.name }}</PanelHeader> <PanelHeader>Airlines</PanelHeader>
</div> </div>
<BadgeTable <BadgeTable
:rows="[entry.alliance.internal_name]" :rows="rows"
:rowKey="key => key" :rowKey="key => key"
:hasItems="() => entry.flights.length > 0" :hasItems="key => entryFor(key).flights.length > 0"
labelWidth="14em" labelWidth="14em"
> >
<template #label> <template #label="{ row: key }">
<div class="airline-label"> <div class="airline-label" >
<AllianceLogo :alliance="entry.alliance" size="24" /> <AirlineLogo :airline="entryFor(key).airline" size="24" />
<span>{{ entry.alliance.name }}</span> <span>{{ entryFor(key).airline.name }}</span>
</div> </div>
</template> </template>
<template #items> <template #items="{ row: key }">
<FlightBadge <FlightBadge
v-for="flight in entry.flights" v-for="flight in entryFor(key).flights"
:key="flight.id" :key="flight.id"
:title="flightTitle(flight)" :title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight" :flight="flight" />
/>
</template> </template>
</BadgeTable> </BadgeTable>
</Panel> </Panel>
<!-- Slot for alliance-specific panels -->
<slot name="extra" />
<!-- Requirements --> <!-- Requirements -->
<Panel> <Panel>
<PanelHeader centered>Requirements</PanelHeader> <PanelHeader centered>Requirements</PanelHeader>
<p> <p>
To complete this challenge you must fly with at least one member airline from each of To complete this challenge you must fly with every current member airline of
the three major alliances. Unlike the individual alliance challenges, you don't need <strong>{{ alliance.name }}</strong>. Alliance membership changes over time, so the
to fly every airline in an alliance just one flight per alliance is enough. required airlines reflect the current roster.
</p> </p>
<p> <p>
Codeshare flights do not count, the operating carrier must be a member of the Codeshare flights do not count, the operating carrier must be a member of {{alliance.name}}.
alliance.
</p> </p>
</Panel> </Panel>
</template> </template>
@@ -123,10 +133,6 @@ function flightTitle(flight: Flight): string {
flex-wrap: wrap; flex-wrap: wrap;
} }
.logo-row {
display: flex;
gap: 0.75rem;
}
.table-toolbar { .table-toolbar {
display: flex; display: flex;
@@ -136,6 +142,7 @@ function flightTitle(flight: Flight): string {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.airline-label { .airline-label {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -143,4 +150,5 @@ function flightTitle(flight: Flight): string {
gap: 0.5rem; gap: 0.5rem;
padding: 0 0.25rem; padding: 0 0.25rem;
} }
</style> </style>
@@ -250,7 +250,7 @@ const legendItems = computed(() => {
<div v-if="mode === 'all'" class="ring-labels"> <div v-if="mode === 'all'" class="ring-labels">
<span v-for="(r, i) in normalizedRings" :key="r.name"> <span v-for="(r, i) in normalizedRings" :key="r.name">
&#9899; {{ i === 0 ? 'Outer' : i === normalizedRings.length - 1 ? 'Inner' : `Ring ${i + 1}` }} {{ i === 0 ? 'Outer' : i === normalizedRings.length - 1 ? 'Inner' : `Ring ${i + 1}` }}
Ring = {{ r.name }} Ring = {{ r.name }}
</span> </span>
</div> </div>
@@ -7,10 +7,9 @@ const props = defineProps<{
</script> </script>
<template> <template>
<DonutChart <DonutChart
title="Flight Scopes" title="Flight Scope"
:height="280" :height="280"
center-mode="count" total-label="Flights"
total-label="Flight Scopes"
:labels="flightStats.flightTypes.value.labels" :labels="flightStats.flightTypes.value.labels"
:series="flightStats.flightTypes.value.series" :series="flightStats.flightTypes.value.series"
/> />
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Airline, Alliance, Flight, RegionRange, FlightRange, FlightScope } from '@/Types/types' import type { Airline, Alliance, Flight, RegionRange, FlightRange, FlightScope } from '@/Types/types'
import { ref, watch } from 'vue' import { reactive, ref, watch, onMounted, nextTick } from 'vue'
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue" import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue"
const props = defineProps<{ const props = defineProps<{
@@ -23,11 +23,63 @@ const emit = defineEmits<{
seatTypes: number[] seatTypes: number[]
manufacturers: string[] manufacturers: string[]
aircraftModels: number[] aircraftModels: number[]
engineTypes: string[]
engineCounts: number[]
airportRegions: number[] airportRegions: number[]
otherUsers: string[] otherUsers: string[]
}] }]
}>() }>()
// ── Filter registry ──────────────────────────────────────────────────────────
//
// This is the single source of truth for every filter's identity. To add a
// new filter:
// 1. Add one entry here (name = the key used in `filters` + the URL param,
// emitKey = the key expected by the `change` event, kind = how to parse
// values coming back out of the URL).
// 2. Add a <v-select> in the template with :name="'yourName'" and
// v-model="filters.yourName" (initialise it in `filters` below, or just
// let step 1 do it for you since `filters` is derived from this list).
//
// Nothing else needs to know about the new filter — URL loading, URL syncing,
// and the emitted payload are all generic over this list.
type FilterKind = 'number' | 'string'
interface FilterDef {
name: string // key in `filters`, and the URL query param name
emitKey: string // key in the payload sent via the `change` event
kind: FilterKind // how to coerce values parsed back out of the URL
}
const FILTER_DEFS: FilterDef[] = [
{ name: 'year', emitKey: 'years', kind: 'number' },
{ name: 'airline', emitKey: 'airlines', kind: 'number' },
{ name: 'alliance', emitKey: 'alliances', kind: 'number' },
{ name: 'country', emitKey: 'countries', kind: 'string' },
{ name: 'continent', emitKey: 'continents', kind: 'string' },
{ name: 'flightClass', emitKey: 'flightClasses', kind: 'number' },
{ name: 'crewType', emitKey: 'crewTypes', kind: 'number' },
{ name: 'flightScope', emitKey: 'flightScopes', kind: 'string' },
{ name: 'flightRange', emitKey: 'flightRanges', kind: 'string' },
{ name: 'regionRange', emitKey: 'regionRanges', kind: 'string' },
{ name: 'flightReason', emitKey: 'flightReasons', kind: 'number' },
{ name: 'seatType', emitKey: 'seatTypes', kind: 'number' },
{ name: 'manufacturer', emitKey: 'manufacturers', kind: 'string' },
{ name: 'aircraftModel', emitKey: 'aircraftModels', kind: 'number' },
{ name: 'engineType', emitKey: 'engineTypes', kind: 'string' },
{ name: 'engineCount', emitKey: 'engineCounts', kind: 'number' },
{ name: 'airportRegion', emitKey: 'airportRegions', kind: 'number' },
{ name: 'otherUsers', emitKey: 'otherUsers', kind: 'string' },
]
// Single reactive bag of filter state, one array per FILTER_DEFS entry.
// `filters.year`, `filters.airline`, etc. This is what every v-select
// v-models against.
const filters = reactive<Record<string, any[]>>(
Object.fromEntries(FILTER_DEFS.map(d => [d.name, []])),
)
// ── Available options ───────────────────────────────────────────────────────── // ── Available options ─────────────────────────────────────────────────────────
type AirlineData = { id: number; name: string; airline: Airline } type AirlineData = { id: number; name: string; airline: Airline }
@@ -62,6 +114,8 @@ function buildOptions(flights: Flight[]) {
const regionRanges = new Set<RegionRange>() const regionRanges = new Set<RegionRange>()
const manufacturers = new Map<string, { name: string }>() const manufacturers = new Map<string, { name: string }>()
const aircraftModels = new Map<number, { id: number; name: string }>() const aircraftModels = new Map<number, { id: number; name: string }>()
const engineTypes = new Map<string, { name: string }>()
const engineCounts = new Map<number, { count: number }>()
const airportRegions = new Map<number, { id: number; name: string; countryCodes: Set<string> }>() const airportRegions = new Map<number, { id: number; name: string; countryCodes: Set<string> }>()
const otherUsers = new Set<string>() const otherUsers = new Set<string>()
@@ -120,6 +174,12 @@ function buildOptions(flights: Flight[]) {
if (f.aircraft?.id != null && f.aircraft?.display_name_short) if (f.aircraft?.id != null && f.aircraft?.display_name_short)
aircraftModels.set(f.aircraft.id, { id: f.aircraft.id, name: f.aircraft.display_name_short }) aircraftModels.set(f.aircraft.id, { id: f.aircraft.id, name: f.aircraft.display_name_short })
if (f.aircraft?.engine_type)
engineTypes.set(f.aircraft.engine_type, { name: f.aircraft.engine_type })
if (f.aircraft?.engine_count != null)
engineCounts.set(f.aircraft.engine_count, { count: f.aircraft.engine_count })
flightScopes.add(f.scope) flightScopes.add(f.scope)
flightRanges.add(f.range) flightRanges.add(f.range)
regionRanges.add(f.region_range) regionRanges.add(f.region_range)
@@ -144,6 +204,8 @@ function buildOptions(flights: Flight[]) {
regionRanges: regionRangeOrder.filter(r => regionRanges.has(r)).map(r => ({ value: r, label: REGION_RANGE_LABELS[r] })), regionRanges: regionRangeOrder.filter(r => regionRanges.has(r)).map(r => ({ value: r, label: REGION_RANGE_LABELS[r] })),
manufacturers: [...manufacturers.values()].sort((a, b) => a.name.localeCompare(b.name)), manufacturers: [...manufacturers.values()].sort((a, b) => a.name.localeCompare(b.name)),
aircraftModels: [...aircraftModels.values()].sort((a, b) => a.name.localeCompare(b.name)), aircraftModels: [...aircraftModels.values()].sort((a, b) => a.name.localeCompare(b.name)),
engineTypes: [...engineTypes.values()].sort((a, b) => a.name.localeCompare(b.name)),
engineCounts: [...engineCounts.values()].sort((a, b) => a.count - b.count),
airportRegions: [...airportRegions.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)), otherUsers: [...otherUsers].sort((a, b) => a.localeCompare(b)),
} }
@@ -151,55 +213,87 @@ function buildOptions(flights: Flight[]) {
const availableOptions = buildOptions(props.flights) const availableOptions = buildOptions(props.flights)
// ── Filter state ──────────────────────────────────────────────────────────────
const selectedYears = ref<number[]>([])
const selectedAirlines = ref<number[]>([])
const selectedAlliances = ref<number[]>([])
const selectedCountries = ref<string[]>([])
const selectedContinents = ref<string[]>([])
const selectedFlightClasses = ref<number[]>([])
const selectedCrewTypes = ref<number[]>([])
const selectedFlightScopes = ref<FlightScope[]>([])
const selectedFlightRanges = ref<FlightRange[]>([])
const selectedRegionRanges = ref<RegionRange[]>([])
const selectedFlightReasons = ref<number[]>([])
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 // When selected countries change, drop any selected regions that no longer
// belong to any of the selected countries. // belong to any of the selected countries.
watch(selectedCountries, (countries) => { watch(() => filters.country, (countries) => {
if (countries.length === 0) return if (countries.length === 0) return
selectedAirportRegions.value = selectedAirportRegions.value.filter(regionId => { filters.airportRegion = filters.airportRegion.filter((regionId: number) => {
const region = availableOptions.airportRegions.find(r => r.id === regionId) const region = availableOptions.airportRegions.find(r => r.id === regionId)
if (!region) return false if (!region) return false
return [...(region as any).countryCodes].some((code: string) => countries.includes(code)) return [...region.countryCodes].some((code: string) => countries.includes(code))
}) })
}) })
// ── URL query sync ──────────────────────────────────────────────────────────
function loadFiltersFromQuery(): boolean {
const params = new URLSearchParams(window.location.search)
let foundAny = false
for (const def of FILTER_DEFS) {
const raw = params.get(def.name)
if (!raw) continue
const values = raw.split(',').filter(Boolean)
filters[def.name] = def.kind === 'number'
? values.map(Number).filter(n => !Number.isNaN(n))
: values
foundAny = true
}
return foundAny
}
function syncUrlWithFilters() {
const params = new URLSearchParams()
for (const def of FILTER_DEFS) {
const value = filters[def.name]
if (value.length > 0)
params.set(def.name, value.join(','))
}
const query = params.toString()
const newUrl = `${window.location.pathname}${query ? `?${query}` : ''}`
window.history.replaceState(window.history.state, '', newUrl)
}
// ── Emit ──────────────────────────────────────────────────────────────────────
function emitFilters() { function emitFilters() {
emit('change', { syncUrlWithFilters()
years: selectedYears.value, const payload = Object.fromEntries(
airlines: selectedAirlines.value, FILTER_DEFS.map(def => [def.emitKey, filters[def.name]]),
alliances: selectedAlliances.value, )
countries: selectedCountries.value, emit('change', payload as Parameters<typeof emit>[1] extends [infer T] ? T : never)
continents: selectedContinents.value, }
flightClasses: selectedFlightClasses.value,
crewTypes: selectedCrewTypes.value, onMounted(async () => {
flightScopes: selectedFlightScopes.value, const hasQueryFilters = loadFiltersFromQuery()
flightRanges: selectedFlightRanges.value,
regionRanges: selectedRegionRanges.value, // Only emit if the URL actually seeded some filters - a plain page load
flightReasons: selectedFlightReasons.value, // (no query params) should behave exactly like before and not emit at all,
seatTypes: selectedSeatTypes.value, // since the parent already renders its own initial, unfiltered state.
manufacturers: selectedManufacturers.value, if (!hasQueryFilters) return
aircraftModels: selectedAircraftModels.value,
airportRegions: selectedAirportRegions.value, // Wait a beat before emitting. Charts further down the tree (ApexCharts,
otherUsers: selectedOtherUsers.value, // in particular) initialize asynchronously on their own mount; firing our
}) // update in the same synchronous pass can hand them new series data before
// their internal chart instance exists yet, which throws inside the
// charting library. A couple of ticks is enough to let that settle.
await nextTick()
await nextTick()
emitFilters()
})
// ── Copy link ─────────────────────────────────────────────────────────────────
const linkCopied = ref(false)
async function copyFilterLink() {
try {
await navigator.clipboard.writeText(window.location.href)
linkCopied.value = true
setTimeout(() => { linkCopied.value = false }, 2000)
} catch {
// Clipboard API unavailable (e.g. non-secure context) - nothing to fall
// back to here, the button just silently no-ops.
}
} }
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -209,10 +303,12 @@ const countryFlagClass = (code: string) =>
</script> </script>
<template> <template>
<div class="flight-filters-wrapper">
<div class="flight-filters"> <div class="flight-filters">
<v-select <v-select
v-model="selectedYears" name="year"
v-model="filters.year"
:items="availableOptions.years" :items="availableOptions.years"
label="Year" label="Year"
multiple clearable hide-details multiple clearable hide-details
@@ -221,14 +317,15 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ item }}<span v-if="index < Math.min(selectedYears.length, 2) - 1">,&nbsp;</span> {{ item }}<span v-if="index < Math.min(filters.year.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedYears.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.year.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-model="selectedAirlines" name="airline"
v-model="filters.airline"
:items="availableOptions.airlines" :items="availableOptions.airlines"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Airline" label="Airline"
@@ -254,15 +351,16 @@ const countryFlagClass = (code: string) =>
</template> </template>
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedAirlines.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.airline.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAirlines.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.airline.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.alliances.length > 0" v-if="availableOptions.alliances.length > 0"
v-model="selectedAlliances" name="alliance"
v-model="filters.alliance"
:items="availableOptions.alliances" :items="availableOptions.alliances"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Alliance" label="Alliance"
@@ -283,14 +381,15 @@ const countryFlagClass = (code: string) =>
</template> </template>
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedAlliances.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.alliance.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAlliances.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.alliance.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-model="selectedCountries" name="country"
v-model="filters.country"
:items="availableOptions.countries" :items="availableOptions.countries"
item-title="name" item-value="code" item-title="name" item-value="code"
label="Country" label="Country"
@@ -312,15 +411,16 @@ const countryFlagClass = (code: string) =>
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
<span :class="countryFlagClass((item as any).code)" style="margin-right: 4px;" /> <span :class="countryFlagClass((item as any).code)" style="margin-right: 4px;" />
{{ (item as any).name }}<span v-if="index < Math.min(selectedCountries.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.country.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCountries.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.country.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.flightScopes.length > 1" v-if="availableOptions.flightScopes.length > 1"
v-model="selectedFlightScopes" name="flightScope"
v-model="filters.flightScope"
:items="availableOptions.flightScopes" :items="availableOptions.flightScopes"
item-title="label" item-value="value" item-title="label" item-value="value"
label="Flight Scope" label="Flight Scope"
@@ -330,15 +430,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).label }}<span v-if="index < Math.min(selectedFlightScopes.length, 2) - 1">,&nbsp;</span> {{ (item as any).label }}<span v-if="index < Math.min(filters.flightScope.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightScopes.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.flightScope.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.airportRegions.length > 0" v-if="availableOptions.airportRegions.length > 0"
v-model="selectedAirportRegions" name="airportRegion"
v-model="filters.airportRegion"
:items="availableOptions.airportRegions" :items="availableOptions.airportRegions"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Region" label="Region"
@@ -348,15 +449,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedAirportRegions.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.airportRegion.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAirportRegions.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.airportRegion.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.regionRanges.length > 1" v-if="availableOptions.regionRanges.length > 1"
v-model="selectedRegionRanges" name="regionRange"
v-model="filters.regionRange"
:items="availableOptions.regionRanges" :items="availableOptions.regionRanges"
item-title="label" item-value="value" item-title="label" item-value="value"
label="Region Range" label="Region Range"
@@ -366,14 +468,15 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).label }}<span v-if="index < Math.min(selectedRegionRanges.length, 2) - 1">,&nbsp;</span> {{ (item as any).label }}<span v-if="index < Math.min(filters.regionRange.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedRegionRanges.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.regionRange.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-model="selectedContinents" name="continent"
v-model="filters.continent"
:items="availableOptions.continents" :items="availableOptions.continents"
item-title="name" item-value="code" item-title="name" item-value="code"
label="Continent" label="Continent"
@@ -383,15 +486,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedContinents.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.continent.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedContinents.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.continent.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.flightRanges.length > 1" v-if="availableOptions.flightRanges.length > 1"
v-model="selectedFlightRanges" name="flightRange"
v-model="filters.flightRange"
:items="availableOptions.flightRanges" :items="availableOptions.flightRanges"
item-title="label" item-value="value" item-title="label" item-value="value"
label="Flight Range" label="Flight Range"
@@ -401,14 +505,15 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).label }}<span v-if="index < Math.min(selectedFlightRanges.length, 2) - 1">,&nbsp;</span> {{ (item as any).label }}<span v-if="index < Math.min(filters.flightRange.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightRanges.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.flightRange.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-model="selectedFlightClasses" name="flightClass"
v-model="filters.flightClass"
:items="availableOptions.classes" :items="availableOptions.classes"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Flight Class" label="Flight Class"
@@ -418,15 +523,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedFlightClasses.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.flightClass.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightClasses.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.flightClass.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.flightReasons.length > 0" v-if="availableOptions.flightReasons.length > 0"
v-model="selectedFlightReasons" name="flightReason"
v-model="filters.flightReason"
:items="availableOptions.flightReasons" :items="availableOptions.flightReasons"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Flight Reason" label="Flight Reason"
@@ -436,15 +542,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedFlightReasons.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.flightReason.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightReasons.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.flightReason.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.seatTypes.length > 0" v-if="availableOptions.seatTypes.length > 0"
v-model="selectedSeatTypes" name="seatType"
v-model="filters.seatType"
:items="availableOptions.seatTypes" :items="availableOptions.seatTypes"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Seat Type" label="Seat Type"
@@ -454,15 +561,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedSeatTypes.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.seatType.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedSeatTypes.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.seatType.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.manufacturers.length > 0" v-if="availableOptions.manufacturers.length > 0"
v-model="selectedManufacturers" name="manufacturer"
v-model="filters.manufacturer"
:items="availableOptions.manufacturers" :items="availableOptions.manufacturers"
item-title="name" item-value="name" item-title="name" item-value="name"
label="Aircraft Manufacturer" label="Aircraft Manufacturer"
@@ -472,15 +580,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedManufacturers.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.manufacturer.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedManufacturers.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.manufacturer.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.aircraftModels.length > 0" v-if="availableOptions.aircraftModels.length > 0"
v-model="selectedAircraftModels" name="aircraftModel"
v-model="filters.aircraftModel"
:items="availableOptions.aircraftModels" :items="availableOptions.aircraftModels"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Aircraft Model" label="Aircraft Model"
@@ -490,15 +599,54 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedAircraftModels.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.aircraftModel.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAircraftModels.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.aircraftModel.length - 2 }}</span>
</template>
</v-select>
<v-select
v-if="availableOptions.engineTypes.length > 0"
name="engineType"
v-model="filters.engineType"
:items="availableOptions.engineTypes"
item-title="name" item-value="name"
label="Engine Type"
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 as any).name }}<span v-if="index < Math.min(filters.engineType.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.engineType.length - 2 }}</span>
</template>
</v-select>
<v-select
v-if="availableOptions.engineCounts.length > 0"
name="engineCount"
v-model="filters.engineCount"
:items="availableOptions.engineCounts"
item-title="count" item-value="count"
label="Engine Count"
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 as any).count }}<span v-if="index < Math.min(filters.engineCount.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.engineCount.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.crewTypes.length > 0" v-if="availableOptions.crewTypes.length > 0"
v-model="selectedCrewTypes" name="crewType"
v-model="filters.crewType"
:items="availableOptions.crewTypes" :items="availableOptions.crewTypes"
item-title="name" item-value="id" item-title="name" item-value="id"
label="Crew Type" label="Crew Type"
@@ -508,14 +656,16 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedCrewTypes.length, 2) - 1">,&nbsp;</span> {{ (item as any).name }}<span v-if="index < Math.min(filters.crewType.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCrewTypes.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.crewType.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
<v-select <v-select
v-if="availableOptions.otherUsers.length > 0" v-if="availableOptions.otherUsers.length > 0"
v-model="selectedOtherUsers" name="otherUsers"
v-model="filters.otherUsers"
:items="availableOptions.otherUsers" :items="availableOptions.otherUsers"
label="Flew With" label="Flew With"
multiple clearable hide-details multiple clearable hide-details
@@ -524,11 +674,25 @@ const countryFlagClass = (code: string) =>
> >
<template #selection="{ item, index }"> <template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text"> <span v-if="index < 2" class="v-select__selection-text">
{{ item }}<span v-if="index < Math.min(selectedOtherUsers.length, 2) - 1">,&nbsp;</span> {{ item }}<span v-if="index < Math.min(filters.otherUsers.length, 2) - 1">,&nbsp;</span>
</span> </span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedOtherUsers.length - 2 }}</span> <span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ filters.otherUsers.length - 2 }}</span>
</template> </template>
</v-select> </v-select>
</div>
<div class="flight-filters-actions">
<v-btn
:prepend-icon="linkCopied ? 'mdi-check' : 'mdi-link-variant'"
:color="linkCopied ? 'success' : undefined"
variant="outlined"
size="small"
@click="copyFilterLink"
>
{{ linkCopied ? 'Copied!' : 'Copy Link to This Page With Filters' }}
</v-btn>
</div>
</div> </div>
</template> </template>
@@ -543,4 +707,8 @@ const countryFlagClass = (code: string) =>
.flight-filters .v-select { .flight-filters .v-select {
flex: 1 1 160px; flex: 1 1 160px;
} }
.flight-filters-actions {
margin-top: 12px;
}
</style> </style>
@@ -659,7 +659,9 @@ export default defineComponent({
const fitBounds = (): void => { const fitBounds = (): void => {
if (!props.flights.length) return if (!props.flights.length) return
const padding = props.compact ? 50 : 60 const padding = props.compact
? { top: 20, bottom: 20, left: 55, right: 55 }
: { top: 60, bottom: 60, left: 60, right: 60 }
const lngs = props.flights.flatMap(f => [f.departure_airport.longitude_deg, f.arrival_airport.longitude_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 lats = props.flights.flatMap(f => [f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg])
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { usePage, router } from "@inertiajs/vue3";
import { SharedProps } from "@/Types/types";
const props = withDefaults(
defineProps<{
redirectRoute: string;
routeParams?: Record<string, string | number>;
placeholder?: string;
}>(),
{
routeParams: () => ({}),
}
);
const page = usePage<SharedProps>().props;
const following = [...page.auth?.following ?? [], page.auth?.user?.name]
.filter((x, i, a) => a.indexOf(x) === i);
function onUpdate(user: string) {
if (!user) return;
router.get(
route(props.redirectRoute, {
user,
...props.routeParams,
})
);
}
</script>
<template>
<VSelect
v-if="following.length > 1"
:placeholder="placeholder"
variant="outlined"
density="comfortable"
:items="following"
@update:model-value="onUpdate"
>
</VSelect>
</template>
<style scoped>
</style>
@@ -25,5 +25,4 @@ defineProps<{
background-size: cover; background-size: cover;
min-width: 300px; min-width: 300px;
} }
</style> </style>
@@ -5,6 +5,7 @@ import type { SharedProps } from '@/Types/types'
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import NotificationMenu from "@/Components/FlightsGoneBy/NotificationMenu.vue"; import NotificationMenu from "@/Components/FlightsGoneBy/NotificationMenu.vue";
import Logo from "@/Components/FlightsGoneBy/Logo.vue"; import Logo from "@/Components/FlightsGoneBy/Logo.vue";
import UserSearchBox from "@/Components/UserSearchBox.vue";
const page = usePage<SharedProps>() const page = usePage<SharedProps>()
const menuOpen = ref(false) const menuOpen = ref(false)
@@ -30,6 +31,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
<Logo variant="color" /> <Logo variant="color" />
</Link> </Link>
<UserSearchBox density="compact" style="max-width:400px; width: 400px;" />
<NotificationMenu v-if="page.props.auth?.user" :unread-count="page.props.unread_notification_count" /> <NotificationMenu v-if="page.props.auth?.user" :unread-count="page.props.unread_notification_count" />
<!-- Desktop nav --> <!-- Desktop nav -->
+83
View File
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Link } from '@inertiajs/vue3'
defineProps<{
density?: null | 'default' | 'comfortable' | 'compact';
}>()
interface UserOption {
value: number
title: string
}
const search = ref('')
const selectedUser = ref<UserOption | null>(null)
const items = ref<UserOption[]>([])
const loading = ref(false)
let debounceTimeout: ReturnType<typeof setTimeout> | undefined
async function fetchUsers(query: string) {
if (!query) {
items.value = []
return
}
loading.value = true
try {
const response = await fetch(`/search/users?q=${encodeURIComponent(query)}`)
if (!response.ok) throw new Error('Failed to fetch users')
items.value = await response.json()
} catch (error) {
console.error('User search failed:', error)
items.value = []
} finally {
loading.value = false
}
}
watch(search, (newQuery) => {
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(() => {
fetchUsers(newQuery)
}, 300)
})
</script>
<template>
<div class="user-search-box">
<v-autocomplete
v-model="selectedUser"
v-model:search="search"
:items="items"
:loading="loading"
item-title="title"
item-value="value"
return-object
label="Search users"
placeholder="Type a name..."
prepend-inner-icon="mdi-magnify"
:density="density ?? 'default'"
variant="outlined"
no-filter
clearable
hide-details
autocomplete="off"
>
<template #item="{ item }">
<Link :href="`/u/${item.title}`" class="user-search-result">
<v-list-item :title="item.title" />
</Link>
</template>
</v-autocomplete>
</div>
</template>
<style scoped>
.user-search-box {
display: block;
align-self: center;
width: 100%;
}
</style>
+1 -1
View File
@@ -175,7 +175,7 @@ export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
const result = { const result = {
countries: sorted, countries: sorted,
series: [ series: [
{ name: 'Departed', data: sorted.map(s => s.past) }, { name: 'Flown', data: sorted.map(s => s.past) },
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) }, { name: 'Upcoming', data: sorted.map(s => s.upcoming) },
], ],
} }
+3 -1
View File
@@ -9,6 +9,7 @@ import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
import { useFlights } from "@/Composables/useFlights"; import { useFlights } from "@/Composables/useFlights";
import { useApiResource } from "@/Composables/useApiResource"; import { useApiResource } from "@/Composables/useApiResource";
import Avatar from "@/Components/FlightsGoneBy/Feed/Avatar.vue"; import Avatar from "@/Components/FlightsGoneBy/Feed/Avatar.vue";
import UserSearchBox from "@/Components/UserSearchBox.vue";
defineOptions({ layout: MainLayout }) defineOptions({ layout: MainLayout })
@@ -46,8 +47,9 @@ const { data: following, loading: followingLoading } = useApiResource<User[]>('/
</div> </div>
</section> </section>
<UserSearchBox />
<div class="feed-grid"> <div class="feed-grid">
<!-- FOLLOWING: secondary, supporting content -->
<aside class="following-box"> <aside class="following-box">
<h2>Following</h2> <h2>Following</h2>
<div v-if="followingLoading" class="following-loading"> <div v-if="followingLoading" class="following-loading">
@@ -12,6 +12,7 @@ import {useFlights} from "@/Composables/useFlights";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue"; import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue"; import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue"; import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
import FollowingSelectBox from "@/Components/FlightsGoneBy/FollowingSelectBox.vue";
defineOptions({ layout: MainLayout }) defineOptions({ layout: MainLayout })
@@ -119,6 +120,12 @@ const unlocked = computed(() => {
:families="aircraft_families" :families="aircraft_families"
:major-alliances="majorAlliances" :major-alliances="majorAlliances"
/> />
<FollowingSelectBox
redirect-route="profile.achievement"
:route-params="{ achievement: achievement.internal_name }"
placeholder="View the Progress of Someone You Follow"
/>
</div> </div>
</ProfileLayout> </ProfileLayout>
</template> </template>
+3
View File
@@ -9,6 +9,7 @@ import MainLayout from "@/Layouts/MainLayout.vue";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue"; import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue"; import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
import {useUpdateSetting} from "@/Composables/useUpdateSetting"; import {useUpdateSetting} from "@/Composables/useUpdateSetting";
import FollowingSelectBox from "@/Components/FlightsGoneBy/FollowingSelectBox.vue";
const {updateSetting} = useUpdateSetting() const {updateSetting} = useUpdateSetting()
@@ -121,6 +122,8 @@ const filteredUnlockedCount = computed(() =>
/> />
</div> </div>
</Panel> </Panel>
<FollowingSelectBox redirect-route="profile.achievements" placeholder="View The Achievements of Someone You Follow" />
</div> </div>
</ProfileLayout> </ProfileLayout>
</template> </template>
+10
View File
@@ -56,6 +56,8 @@ const selectedFlightReasons = ref<number[]>([])
const selectedSeatTypes = ref<number[]>([]) const selectedSeatTypes = ref<number[]>([])
const selectedManufacturers = ref<string[]>([]) const selectedManufacturers = ref<string[]>([])
const selectedAircraftModels = ref<number[]>([]) const selectedAircraftModels = ref<number[]>([])
const selectedEngineTypes = ref<string[]>([])
const selectedEngineCounts = ref<number[]>([])
const selectedAirportRegions = ref<number[]>([]) const selectedAirportRegions = ref<number[]>([])
const selectedOtherUsers = ref<string[]>([]) const selectedOtherUsers = ref<string[]>([])
@@ -74,6 +76,8 @@ const activeFilterCount = computed(() =>
selectedSeatTypes.value.length + selectedSeatTypes.value.length +
selectedManufacturers.value.length + selectedManufacturers.value.length +
selectedAircraftModels.value.length + selectedAircraftModels.value.length +
selectedEngineTypes.value.length +
selectedEngineCounts.value.length +
selectedAirportRegions.value.length + selectedAirportRegions.value.length +
selectedOtherUsers.value.length selectedOtherUsers.value.length
) )
@@ -93,6 +97,8 @@ function onFiltersChange(filters: {
seatTypes: number[] seatTypes: number[]
manufacturers: string[] manufacturers: string[]
aircraftModels: number[] aircraftModels: number[]
engineTypes: string[]
engineCounts: number[]
airportRegions: number[] airportRegions: number[]
otherUsers: string[] otherUsers: string[]
}) { }) {
@@ -111,6 +117,8 @@ function onFiltersChange(filters: {
selectedSeatTypes.value = filters.seatTypes selectedSeatTypes.value = filters.seatTypes
selectedManufacturers.value = filters.manufacturers selectedManufacturers.value = filters.manufacturers
selectedAircraftModels.value = filters.aircraftModels selectedAircraftModels.value = filters.aircraftModels
selectedEngineTypes.value = filters.engineTypes
selectedEngineCounts.value = filters.engineCounts
selectedAirportRegions.value = filters.airportRegions selectedAirportRegions.value = filters.airportRegions
selectedOtherUsers.value = filters.otherUsers selectedOtherUsers.value = filters.otherUsers
} }
@@ -141,6 +149,8 @@ function matchesFilters(f: Flight): boolean {
if (selectedSeatTypes.value.length && !selectedSeatTypes.value.includes(f.seat_type?.id ?? -1)) return false if (selectedSeatTypes.value.length && !selectedSeatTypes.value.includes(f.seat_type?.id ?? -1)) return false
if (selectedManufacturers.value.length && !selectedManufacturers.value.includes(f.aircraft?.manufacturer_code ?? '')) return false if (selectedManufacturers.value.length && !selectedManufacturers.value.includes(f.aircraft?.manufacturer_code ?? '')) return false
if (selectedAircraftModels.value.length && !selectedAircraftModels.value.includes(f.aircraft?.id ?? -1)) return false if (selectedAircraftModels.value.length && !selectedAircraftModels.value.includes(f.aircraft?.id ?? -1)) return false
if (selectedEngineTypes.value.length && !selectedEngineTypes.value.includes(f.aircraft?.engine_type ?? '')) return false
if (selectedEngineCounts.value.length && !selectedEngineCounts.value.includes(f.aircraft?.engine_count ?? -1)) return false
if (selectedAirportRegions.value.length) { if (selectedAirportRegions.value.length) {
const depRegion = f.departure_airport.region?.id const depRegion = f.departure_airport.region?.id
const arrRegion = f.arrival_airport.region?.id const arrRegion = f.arrival_airport.region?.id
+1
View File
@@ -101,6 +101,7 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
roles: string[]; roles: string[];
permissions: string[]; permissions: string[];
apiToken: string | null; apiToken: string | null;
following: string[];
}, },
flash: { flash: {
success?: string; success?: string;
+2 -1
View File
@@ -103,10 +103,11 @@ Route::middleware(['auth'])->group(function () {
//Search Routes //Search Routes
Route::get('/search/airlines', [SearchController::class, 'airlines'])->name('search.airlines'); Route::get('/search/airlines', [SearchController::class, 'airlines'])->name('search.airlines');
Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft'); Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft');
Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports'); Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports');
Route::get('/search/users', [SearchController::class, 'users'])->name('search.users');
Route::get('/u/{user}', [UserProfileController::class, 'view'])->name('profile.view'); Route::get('/u/{user}', [UserProfileController::class, 'view'])->name('profile.view');
Route::get('/u/{user}/map', [UserProfileController::class, 'map'])->name('profile.map'); Route::get('/u/{user}/map', [UserProfileController::class, 'map'])->name('profile.map');