Added Notifications

This commit is contained in:
2026-05-16 23:48:18 +10:00
parent 69d72e0912
commit 1d5b9f340f
61 changed files with 4204 additions and 182 deletions
@@ -0,0 +1,51 @@
import { computed, ComputedRef } from 'vue'
import { Flight } from '@/Types/types'
// ── Types ──────────────────────────────────────────────────────────────────
export interface AircraftFamilyEntry {
family: string
designators: string[]
flights: Flight[]
}
// ── Composable ─────────────────────────────────────────────────────────────
export function useAircraftFamilies(
flights: ComputedRef<Flight[]>,
families: Record<string, string[]>,
) {
// Build a lookup from designator → family name for O(1) matching
const designatorToFamily = Object.entries(families).reduce<Record<string, string>>(
(acc, [family, designators]) => {
for (const d of designators) acc[d] = family
return acc
},
{}
)
const entries = computed<AircraftFamilyEntry[]>(() => {
// Pre-populate all families with empty flight lists
const map = new Map<string, AircraftFamilyEntry>(
Object.entries(families).map(([family, designators]) => [
family,
{ family, designators, flights: [] },
])
)
for (const flight of flights.value) {
const designator = flight.aircraft?.designator
if (!designator) continue
const family = designatorToFamily[designator]
if (!family) continue
map.get(family)!.flights.push(flight)
}
return [...map.values()]
})
const completedCount = computed(() => entries.value.filter(e => e.flights.length > 0).length)
const totalCount = computed(() => entries.value.length)
return { entries, completedCount, totalCount }
}
@@ -0,0 +1,43 @@
import { computed, type Ref } from 'vue'
import type { Flight } from '@/Types/types'
export type CodeType = 'iata' | 'icao'
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
const digits = '0123456789'.split('')
export function getAllLetters(showNumbers: boolean): string[] {
return showNumbers ? [...digits, ...letters] : letters
}
export function useAlphabetAirlines(
flights: Ref<Flight[]>,
codeType: Ref<CodeType>,
showNumbers: Ref<boolean>,
) {
const allLetters = computed(() => getAllLetters(showNumbers.value))
const flightsByLetter = computed(() => {
const map: Record<string, Flight[]> = {}
for (const flight of flights.value) {
const raw = codeType.value === 'iata'
? flight.airline?.IATA_code
: flight.airline?.ICAO_code
const code = raw?.trim().toUpperCase()
if (!code) continue
const key = code[0]
if (!allLetters.value.includes(key)) continue
;(map[key] ??= []).push(flight)
}
return map
})
const visitedLetters = computed(() => new Set(Object.keys(flightsByLetter.value)))
return { flightsByLetter, visitedLetters, allLetters }
}
@@ -0,0 +1,81 @@
import { computed, type ComputedRef, type Ref } from 'vue'
import type { Flight } from '@/Types/types'
export type CodeType = 'iata' | 'icao'
export interface AlphabetFlightData {
/** All 26 uppercase letters AZ */
allLetters: string[]
/** Letters the user has visited (has at least one qualifying airport) */
visitedLetters: ComputedRef<Set<string>>
/** Map of letter → flights that touch an airport starting with that letter */
flightsByLetter: ComputedRef<Record<string, Flight[]>>
/** The code type being used */
codeType: CodeType
}
/**
* Returns which letters of the alphabet have been "visited" based on
* the IATA or ICAO code of the departure or arrival airport on each flight.
*
* A letter is considered visited when at least one flight departs from or
* arrives at an airport whose chosen code starts with that letter.
*
* Airports without a code of the chosen type are ignored.
*/
export function useAlphabetFlights(
flights: Ref<Flight[]> | ComputedRef<Flight[]>,
codeType: CodeType = 'iata',
): AlphabetFlightData {
const allLetters = Array.from({ length: 26 }, (_, i) =>
String.fromCharCode(65 + i), // 'A' … 'Z'
)
/** Pick the right code from an airport, uppercased first character or null */
function getCode(airport: Flight['departure_airport'] | Flight['arrival_airport']): string | null {
const raw = codeType === 'iata' ? airport.iata_code : airport.icao_code
return raw ? raw.trim().toUpperCase() : null
}
const flightsByLetter = computed<Record<string, Flight[]>>(() => {
const map: Record<string, Flight[]> = {}
for (const letter of allLetters) {
map[letter] = []
}
for (const flight of flights.value) {
const codes = new Set<string>()
const depCode = getCode(flight.departure_airport)
if (depCode) codes.add(depCode[0])
const arrCode = getCode(flight.arrival_airport)
if (arrCode) codes.add(arrCode[0])
for (const letter of codes) {
if (letter >= 'A' && letter <= 'Z') {
map[letter]?.push(flight)
}
}
}
return map
})
const visitedLetters = computed<Set<string>>(() => {
const visited = new Set<string>()
for (const [letter, letterFlights] of Object.entries(flightsByLetter.value)) {
if (letterFlights.length > 0) {
visited.add(letter)
}
}
return visited
})
return {
allLetters,
visitedLetters,
flightsByLetter,
codeType,
}
}
@@ -0,0 +1,164 @@
import { computed, ComputedRef } from 'vue'
import { Flight } from '@/Types/types'
export interface Continent {
id: number
name: string
code: string
internal_name: string
}
export interface ContinentPairEntry {
key: string
label: string
flights: Flight[]
}
/** Stable undirected key — always alphabetically sorted so A|B === B|A */
export function undirectedKey(a: string, b: string): string {
return [a, b].sort().join('|')
}
/** Directed key — order preserved */
export function directedKey(dep: string, arr: string): string {
return `${dep}|${arr}`
}
export function labelFor(a: string, b: string): string {
return `${a}${b}`
}
export function continentNameOf(flight: Flight, side: 'departure' | 'arrival'): string | null {
const airport = side === 'departure' ? flight.departure_airport : flight.arrival_airport
return airport.region?.continent?.name ?? null
}
export function isInternational(flight: Flight): boolean {
const depCountry = flight.departure_airport.region?.country_id
const arrCountry = flight.arrival_airport.region?.country_id
if (depCountry == null || arrCountry == null) return true
return depCountry !== arrCountry
}
/** Filter flights to only those that qualify for continent-pair achievements */
export function qualifyingFlights(flights: Flight[]): Flight[] {
return flights.filter(flight => {
const dep = continentNameOf(flight, 'departure')
const arr = continentNameOf(flight, 'arrival')
if (!dep || !arr) return false
if (dep === arr && !isInternational(flight)) return false
return true
})
}
// ── One-way (undirected) pairs ─────────────────────────────────────────────
export function useUndirectedContinentPairs(
flights: ComputedRef<Flight[]>,
continents: ComputedRef<Continent[]>,
) {
const allKeys = computed<string[]>(() => {
const names = continents.value.map(c => c.name).sort()
const keys: string[] = []
for (let i = 0; i < names.length; i++) {
for (let j = i; j < names.length; j++) {
keys.push(undirectedKey(names[i], names[j]))
}
}
return keys.sort((a, b) => {
const [a1, a2] = a.split('|')
const [b1, b2] = b.split('|')
return labelFor(a1, a2).localeCompare(labelFor(b1, b2))
})
})
const flightsByKey = computed<Map<string, Flight[]>>(() => {
const map = new Map(allKeys.value.map(k => [k, [] as Flight[]]))
for (const flight of qualifyingFlights(flights.value)) {
const dep = continentNameOf(flight, 'departure')!
const arr = continentNameOf(flight, 'arrival')!
const key = undirectedKey(dep, arr)
map.get(key)?.push(flight)
}
return map
})
const entries = computed<ContinentPairEntry[]>(() =>
allKeys.value.map(key => {
const [a, b] = key.split('|')
return { key, label: labelFor(a, b), flights: flightsByKey.value.get(key) ?? [] }
})
)
const completedCount = computed(() => entries.value.filter(e => e.flights.length > 0).length)
const totalCount = computed(() => entries.value.length)
return { entries, completedCount, totalCount }
}
// ── Both-ways (directed) pairs ─────────────────────────────────────────────
export function useDirectedContinentPairs(
flights: ComputedRef<Flight[]>,
continents: ComputedRef<Continent[]>,
) {
/** All directed keys grouped by the departure continent name */
const keysByDeparture = computed<Map<string, string[]>>(() => {
const names = continents.value.map(c => c.name).sort()
const map = new Map<string, string[]>()
for (const dep of names) {
map.set(dep, names.map(arr => directedKey(dep, arr)).sort((a, b) => {
const [, a2] = a.split('|')
const [, b2] = b.split('|')
return a2.localeCompare(b2)
}))
}
return map
})
const flightsByKey = computed<Map<string, Flight[]>>(() => {
const map = new Map<string, Flight[]>()
for (const keys of keysByDeparture.value.values()) {
for (const key of keys) map.set(key, [])
}
for (const flight of qualifyingFlights(flights.value)) {
const dep = continentNameOf(flight, 'departure')!
const arr = continentNameOf(flight, 'arrival')!
const key = directedKey(dep, arr)
map.get(key)?.push(flight)
}
return map
})
/** Entries grouped by departure continent, each sorted by arrival continent name */
const entriesByDeparture = computed<Map<string, ContinentPairEntry[]>>(() => {
const map = new Map<string, ContinentPairEntry[]>()
for (const [dep, keys] of keysByDeparture.value) {
map.set(dep, keys.map(key => {
const [a, b] = key.split('|')
return { key, label: `${a}${b}`, flights: flightsByKey.value.get(key) ?? [] }
}))
}
return map
})
const departureNames = computed(() => [...keysByDeparture.value.keys()])
const completedCount = computed(() => {
let count = 0
for (const entries of entriesByDeparture.value.values()) {
count += entries.filter(e => e.flights.length > 0).length
}
return count
})
const totalCount = computed(() => {
let count = 0
for (const entries of entriesByDeparture.value.values()) {
count += entries.length
}
return count
})
return { entriesByDeparture, departureNames, completedCount, totalCount }
}
@@ -33,7 +33,6 @@ function getDateParts(f: Flight): { year: number; month: number; day: number } {
// ── Per year / month / day ────────────────────────────────────────────────────
export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getFlightsPerYear')
const allFlights = [...flights, ...upcomingFlights]
const allYears = new Set(allFlights.map(f => getDateParts(f).year))
@@ -57,12 +56,10 @@ export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[])
{ name: 'Upcoming', data: countByYear(upcomingFlights) },
],
}
console.timeEnd('getFlightsPerYear')
return result
}
export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getFlightsPerMonth')
const countByMonth = (list: Flight[]) =>
MONTHS.map((_, i) => list.filter(f => getDateParts(f).month === i).length)
@@ -73,12 +70,10 @@ export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[])
{ name: 'Upcoming', data: countByMonth(upcomingFlights) },
],
}
console.timeEnd('getFlightsPerMonth')
return result
}
export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getFlightsPerDay')
const countByDay = (list: Flight[]) =>
DAYS.map((_, i) => list.filter(f => getDateParts(f).day === i).length)
@@ -89,7 +84,6 @@ export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) {
{ name: 'Upcoming', data: countByDay(upcomingFlights) },
],
}
console.timeEnd('getFlightsPerDay')
return result
}
@@ -109,23 +103,17 @@ function groupByName(flights: Flight[], accessor: (f: Flight) => string) {
}
export function getFlightReasons(flights: Flight[]) {
console.time('getFlightReasons')
const result = groupByName(flights, f => f.flight_reason?.name ?? 'Unknown')
console.timeEnd('getFlightReasons')
return result
}
export function getFlightClasses(flights: Flight[]) {
console.time('getFlightClasses')
const result = groupByName(flights, f => f.flight_class?.name ?? 'Unknown')
console.timeEnd('getFlightClasses')
return result
}
export function getSeatTypes(flights: Flight[]) {
console.time('getSeatTypes')
const result = groupByName(flights, f => f.seat_type?.name ?? 'Unknown')
console.timeEnd('getSeatTypes')
return result
}
@@ -152,7 +140,6 @@ function countCountries(flights: Flight[]) {
}
export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getCountries')
const past = countCountries(flights)
const upcoming = countCountries(upcomingFlights)
const allNames = new Set([...past.keys(), ...upcoming.keys()])
@@ -173,14 +160,12 @@ export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
],
}
console.timeEnd('getCountries')
return result
}
// ── Continents ────────────────────────────────────────────────────────────────
export function getContinents(flights: Flight[]) {
console.time('getContinents')
const counts = new Map<string, number>()
flights.forEach(f => {
const continents = new Set<string>()
@@ -195,7 +180,6 @@ export function getContinents(flights: Flight[]) {
labels: sorted.map(([name]) => name),
series: sorted.map(([, count]) => count),
}
console.timeEnd('getContinents')
return result
}
@@ -214,7 +198,6 @@ function countAirlines(flights: Flight[]) {
}
export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getTopAirlines')
const past = countAirlines(flights)
const upcoming = countAirlines(upcomingFlights)
const allNames = new Set([...past.keys(), ...upcoming.keys()])
@@ -236,7 +219,6 @@ export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
],
}
console.timeEnd('getTopAirlines')
return result
}
@@ -256,7 +238,6 @@ function airportLabel(airport: Airport | null | undefined): string | null {
}
export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
console.time('getTopAirports')
const map = new Map<string, AirportItem>()
const empty = (): AirportItem => ({ departures: 0, arrivals: 0, upcoming: 0, label: '', fullName: '' })
@@ -313,7 +294,6 @@ export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
],
}
console.timeEnd('getTopAirports')
return result
}
@@ -421,7 +401,6 @@ export function getTopRoutes(flights: Flight[], upcomingFlights: Flight[]) {
// ── Flight types ──────────────────────────────────────────────────────────────
export function getFlightTypes(flights: Flight[]) {
console.time('getFlightTypes')
const counts = { International: 0, Domestic: 0 }
flights.forEach(f => {
const dep = f.departure_airport.region?.country?.id
@@ -435,7 +414,6 @@ export function getFlightTypes(flights: Flight[]) {
labels: sorted.map(([name]) => name),
series: sorted.map(([, count]) => count),
}
console.timeEnd('getFlightTypes')
return result
}
@@ -447,22 +425,16 @@ export function useFlightStats(flights: Ref<Flight[]>) {
const now = new Date()
watch(flights, (list) => {
console.time('dateCache warm')
list.forEach(f => getDateParts(f))
console.timeEnd('dateCache warm')
}, { immediate: true })
const pastFlights = computed(() => {
console.time('pastFlights')
const result = flights.value.filter(f => new Date(f.departure_date) <= now)
console.timeEnd('pastFlights')
return result
})
const upcomingFlights = computed(() => {
console.time('upcomingFlights')
const result = flights.value.filter(f => new Date(f.departure_date) > now)
console.timeEnd('upcomingFlights')
return result
})
+4 -2
View File
@@ -3,13 +3,15 @@ import {onMounted, ref} from "vue";
import {Flight} from "@/Types/types";
import axios from "axios";
export function useFlights(url: string) {
export function useFlights(url: string, departedOnly: boolean = false) {
const flights = ref<Flight[]>([])
const flightsLoading = ref(true)
onMounted(async () => {
try {
const response = await axios.get(url)
const response = await axios.get(url, {
params: departedOnly ? { departed_only: true } : {}
})
flights.value = response.data
} finally {
flightsLoading.value = false
+9 -1
View File
@@ -21,7 +21,7 @@ export function useRegionFlights(
})
const flightsByRegion = computed(() => {
return filteredFlights.value.reduce((grouped, flight) => {
const grouped = filteredFlights.value.reduce((grouped, flight) => {
const dep = flight.departure_airport?.region
const arr = flight.arrival_airport?.region
@@ -37,6 +37,14 @@ export function useRegionFlights(
return grouped
}, {} as Record<string, Flight[]>)
return Object.fromEntries(
Object.entries(grouped).sort(([codeA], [codeB]) => {
const nameA = regionNames.value[codeA] ?? codeA
const nameB = regionNames.value[codeB] ?? codeB
return nameA.localeCompare(nameB)
})
)
})
const regionNames = computed(() => {