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,101 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Achievement, Flight, User } from '@/Types/types'
import { useAircraftFamilies } from '@/Composables/useAircraftFamilies'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
families: Record<string, string[]>
}>()
const flights = computed(() => props.flights)
const { entries } = useAircraftFamilies(flights, props.families)
</script>
<template>
<Panel>
<div class="table-toolbar">
<PanelHeader>Families</PanelHeader>
</div>
<BadgeTable
:rows="entries"
:rowKey="entry => entry.family"
:hasItems="entry => entry.flights.length > 0"
labelWidth="8em"
>
<template #label="{ row: entry }">
<div class="family-label">
<span class="family-name">Airbus {{ entry.family }}</span>
</div>
</template>
<template #items="{ row: entry }">
<FlightBadge
v-for="flight in entry.flights"
:key="flight.id"
:title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight"
/>
</template>
</BadgeTable>
</Panel>
<slot name="extra" />
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly on at least one aircraft from every
Airbus A3xx family the A300, A310, A318, A319, A320, A321, A330, A340, A350,
and A380.
</p>
<p>
Any variant within a family counts. For example, an Airbus A320neo (A20N) and
a classic A320 both satisfy the A320 family requirement.
</p>
</Panel>
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
If you are starting this challenge today, then this challenge is near-impossible to complete and will soon be impossible. At the time of writing, the following aircraft are very hard to fly on:
</p>
<ul>
<li><b>Airbus A300</b> - Only in service in Iran, mostly domestically. Very hard to arrange.</li>
<li><b>Airbus A310</b> - In commercial service in Iran and Afghanistan. Afghanistan is far less sanctioned, so easier to book a flight. Dubai to Kabul is a reliabe route to see the A310. An A310 also acts as a vomit comet in Europe</li>
<li><b>Airbus A318</b> - Very few left operating for Air France. Will likely be retired before you have read this.</li>
<li><b>Airbus A340</b> - Rapidly being retired from service in the western world with Lufthansa being the last hold out (at time of writing). Iran and Venezuela are likely to hold on to them for sometime. South African always seems to bring them back into service too. A few more scattered around in charter or smaller national airlines</li>
</ul>
</Panel>
</template>
<style scoped>
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.family-label {
width: 100%;
display: flex;
align-items: center;
padding: 0 0.25rem;
}
.family-name {
font-size: 0.9rem;
font-weight: 500;
}
</style>
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Achievement, Flight, User } from '@/Types/types'
import { useAircraftFamilies } from '@/Composables/useAircraftFamilies'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
families: Record<string, string[]>
}>()
const flights = computed(() => props.flights)
const { entries, completedCount, totalCount } = useAircraftFamilies(flights, props.families)
</script>
<template>
<Panel>
<div class="table-toolbar">
<PanelHeader>Families</PanelHeader>
</div>
<BadgeTable
:rows="entries"
:rowKey="entry => entry.family"
:hasItems="entry => entry.flights.length > 0"
labelWidth="8em"
>
<template #label="{ row: entry }">
<div class="family-label">
<span class="family-name">Boeing {{ entry.family }}</span>
</div>
</template>
<template #items="{ row: entry }">
<FlightBadge
v-for="flight in entry.flights"
:key="flight.id"
:title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight"
/>
</template>
</BadgeTable>
</Panel>
<slot name="extra" />
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly on at least one aircraft from every
Boeing 7x7 family the 707, 717, 727, 737, 747, 757, 767, 777, and 787.
</p>
<p>
Any variant within a family counts. For example, a Boeing 737-800 (B738) and
a Boeing 737 MAX 8 (B38M) both satisfy the 737 family requirement.
</p>
</Panel>
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
If you are starting this challenge today, then this challenge is impossible to complete and will not count towards your achievement total. If you have historically completed it, it will.
</p>
<p>
Difficult types to fly on
</p>
<ul>
<li><b>Boeing 707</b> - Completely impossible to fly today. The US Military still operates some derivatives, but they do not count towards this challenge</li>
<li><b>Boeing 727</b> - No commercial service. One operates as a vomit comet in the USA. Very expensive, but technically possible to fly on</li>
<li><b>Boeing 717</b> - Still in service, mostly in the US, but being withdrawn rapidly.</li>
<li><b>Boeing 747</b> - Being pulled from service rapidly. Almost no 747-400s left in service (just Lufthansa). 747-8s are still active with Korean Air, Air China and Lufthansa but the US Air Force has acquired a large number from these airlines and they are dwindling</li>
<li><b>Boeing 757</b> - Still fairly common in the USA but quickly going extinct elsewhere.</li>
</ul>
</Panel>
</template>
<style scoped>
.family-name {
font-size: 0.9rem;
font-weight: 500;
}
</style>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
alliance: Alliance
airlines: Airline[]
flights: Flight[]
}>()
</script>
<template>
<AllianceChallenge
:achievement="achievement"
:user="user"
:isFollowing="isFollowing"
:alliance="alliance"
:airlines="airlines"
:flights="flights"
>
<p>
The <b>Oneworld</b> Alliance is currently the smallest of the big 3 major alliances, but with members all around the world it's no mean feat to get on all of them.
</p>
</AllianceChallenge>
</template>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
alliance: Alliance
airlines: Airline[]
flights: Flight[]
}>()
</script>
<template>
<AllianceChallenge
:achievement="achievement"
:user="user"
:isFollowing="isFollowing"
:alliance="alliance"
:airlines="airlines"
:flights="flights"
>
<p>
The SkyTeam Alliance is the youngest of the 3 major alliances, but does not have the smallest roster!
</p>
</AllianceChallenge>
</template>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
alliance: Alliance
airlines: Airline[]
flights: Flight[]
}>()
</script>
<template>
<AllianceChallenge
:achievement="achievement"
:user="user"
:isFollowing="isFollowing"
:alliance="alliance"
:airlines="airlines"
:flights="flights"
>
<p>
Fly with every member airline of <b>Star Alliance</b>, the world's largest airline
alliance by number of member carriers.
</p>
</AllianceChallenge>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
alliance: Alliance
airlines: Airline[]
flights: Flight[]
}>()
</script>
<template>
<AllianceChallenge
:achievement="achievement"
:user="user"
:isFollowing="isFollowing"
:alliance="alliance"
:airlines="airlines"
:flights="flights"
>
<p>
<b>Vanilla Alliance</b> is a small, regional alliance in the Vanilla Islands. It is a paper alliance that is effectively defunct, but it's still a fun one to try for!
</p>
</AllianceChallenge>
</template>
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Achievement, Flight, User } from '@/Types/types'
import { Continent, useDirectedContinentPairs } from '@/Composables/useContinentPairs'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
continents: Continent[]
}>()
const flights = computed(() => props.flights)
const continents = computed(() => props.continents.filter(c => c.internal_name !== 'antarctica'))
const { entriesByDeparture, departureNames, completedCount, totalCount } =
useDirectedContinentPairs(flights, continents)
</script>
<template>
<!-- One panel per departure continent -->
<Panel v-for="dep in departureNames" :key="dep">
<div class="table-toolbar">
<PanelHeader>Departing {{ dep }}</PanelHeader>
</div>
<BadgeTable
:rows="entriesByDeparture.get(dep) ?? []"
:rowKey="entry => entry.key"
:hasItems="entry => entry.flights.length > 0"
labelWidth="18em"
>
<template #label="{ row: entry }">
<div class="pair-label">
<span class="pair-name">{{ entry.label }}</span>
</div>
</template>
<template #items="{ row: entry }">
<FlightBadge
v-for="flight in entry.flights"
:key="flight.id"
:title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight"
/>
</template>
</BadgeTable>
</Panel>
<slot name="extra" />
<!-- Requirements -->
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly between every possible pair of continents
in <strong>both directions</strong> a flight from Europe to Asia and a separate
flight from Asia to Europe are each required.
</p>
<p>
For <strong>same-continent routes</strong> (e.g. Africa Africa), only
<strong>international flights</strong> count domestic flights within the same
country are excluded.
</p>
</Panel>
</template>
<style scoped>
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
</style>
@@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Achievement, Flight, User } from '@/Types/types'
import { Continent, useUndirectedContinentPairs } from '@/Composables/useContinentPairs'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
continents: Continent[]
}>()
const flights = computed(() => props.flights)
const continents = computed(() => props.continents.filter(c => c.internal_name !== 'antarctica'))
const { entries, completedCount, totalCount } = useUndirectedContinentPairs(flights, continents)
</script>
<template>
<!-- Continent pairs table -->
<Panel>
<div class="table-toolbar">
<PanelHeader>Continent Pairs</PanelHeader>
</div>
<BadgeTable
:rows="entries"
:rowKey="entry => entry.key"
:hasItems="entry => entry.flights.length > 0"
labelWidth="18em"
>
<template #label="{ row: entry }">
<div class="pair-label">
<span class="pair-name">{{ entry.label }}</span>
</div>
</template>
<template #items="{ row: entry }">
<FlightBadge
v-for="flight in entry.flights"
:key="flight.id"
:title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight"
/>
</template>
</BadgeTable>
</Panel>
<slot name="extra" />
<!-- Requirements -->
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly at least one flight between every possible
pair of continents including flights within the same continent.
</p>
<p>
For <strong>same-continent pairs</strong> (e.g. Africa Africa), only
<strong>international flights</strong> count domestic flights within the same
country are excluded.
</p>
</Panel>
</template>
<style scoped>
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.pair-label {
width: 100%;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.25rem;
}
.pair-name {
font-size: 0.9rem;
}
</style>
@@ -0,0 +1,278 @@
<script setup lang="ts">
import { Achievement, Flight, User } from '@/Types/types'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
import AirlineAlphabetTable from '@/Components/FlightsGoneBy/AirlineAlphabetTable.vue'
import { computed, ref } from 'vue'
import { useAlphabetAirlines, type CodeType } from '@/Composables/useAlphabetAirlines'
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
}>()
const codeType = ref<CodeType>('iata')
const showNumbers = ref(false)
const flightsRef = computed(() => props.flights)
const { flightsByLetter, visitedLetters, allLetters } = useAlphabetAirlines(
flightsRef,
codeType,
showNumbers,
)
// Only count letters (AZ) towards progress, never digits
const visitedLetterCount = computed(() =>
[...visitedLetters.value].filter(k => isNaN(Number(k))).length
)
const availableYears = computed(() => {
const years = new Set(props.flights.map(f => new Date(f.departure_date).getFullYear()))
return [...years].sort((a, b) => b - a)
})
const yearItems = computed(() => [
{ title: 'None', value: null },
...availableYears.value.map(y => ({ title: String(y), value: y })),
])
const currentYear = new Date().getFullYear()
const selectedYear = ref<number | null>(
availableYears.value.includes(currentYear) ? currentYear : (availableYears.value[0] ?? null)
)
// Copy BBCode
const airlineTable = ref<InstanceType<typeof AirlineAlphabetTable> | null>(null)
const copied = ref(false)
async function copyBBCode() {
const text = airlineTable.value?.toBBCode()
if (text == null) return
try {
await navigator.clipboard.writeText(text)
} catch {
const el = document.createElement('textarea')
el.value = text
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
document.body.appendChild(el)
el.focus()
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
copied.value = true
setTimeout(() => (copied.value = false), 2000)
}
</script>
<template>
<!-- Challenge description -->
<Panel>
<PanelHeader centered>The Airline Alphabet Challenge</PanelHeader>
<PanelSubHeader centered>
<p>
A twist on the classic Alphabet Challenge instead of airports, you need to fly with
airlines whose <b>IATA codes</b> begin with each letter of the alphabet.
</p>
<p>
You can copy your results as BBCode using the copy icon above the table, and select
a year to highlight airlines that are new for that given year.
</p>
</PanelSubHeader>
</Panel>
<!-- Progress grid -->
<Panel style="display: flex; flex-direction: column; align-items: center; gap: 1rem;">
<PanelHeader centered>Progress</PanelHeader>
<div class="toggle-row">
<div>
<button :class="['code-toggle', codeType === 'iata' ? 'active' : '']" @click="codeType = 'iata'">IATA</button>
<button :class="['code-toggle', codeType === 'icao' ? 'active' : '']" @click="codeType = 'icao'">ICAO</button>
</div>
<button :class="['code-toggle', showNumbers ? 'active' : '']" @click="showNumbers = !showNumbers">
Show 09
</button>
</div>
<p class="progress-summary">{{ visitedLetterCount }} / {{allLetters.length}} letters visited</p>
<div class="alphabet-grid">
<div
v-for="letter in allLetters"
:key="letter"
:class="['letter-tile', visitedLetters.has(letter) ? 'visited' : 'unvisited']"
:title="visitedLetters.has(letter) ? 'Visited' : 'Not yet visited'"
>
{{ letter }}
</div>
</div>
</Panel>
<!-- Airlines by letter -->
<Panel>
<div class="table-toolbar">
<PanelHeader>Airlines By Letter</PanelHeader>
<div class="toolbar-right">
<v-select
v-model="selectedYear"
:items="yearItems"
item-title="title"
item-value="value"
label="New For"
density="compact"
variant="outlined"
hide-details
class="year-select"
/>
<v-btn
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
:color="copied ? 'success' : undefined"
density="compact"
variant="text"
title="Copy as BBCode"
@click="copyBBCode"
/>
</div>
</div>
<AirlineAlphabetTable
ref="airlineTable"
:letters="allLetters"
:flightsByLetter="flightsByLetter"
:codeType="codeType"
:selectedYear="selectedYear"
/>
</Panel>
<!-- Requirements -->
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly with airlines whose IATA codes begin with each
of the 26 letters of the alphabet A through Z. Airlines without an IATA code do not count.
</p>
<p>
A single flight contributes one letter the operating airline's IATA code. So a flight
on <strong>QF</strong> (Qantas) counts toward <strong>Q</strong>.
</p>
<p>
ICAO codes do not count towards this challenge, but you can toggle to ICAO view to see
where you're at for fun. Similarly, numeric IATA codes (such as <strong>3U</strong>) exist
but do not count towards the challenge enable "Show 09" to see them.
</p>
<p>
It would unlikely to be possible to complete the challenge with both numbers and letters required.
</p>
</Panel>
<!-- Difficulty -->
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
Like the airport version, this challenge is <b>very</b> difficult, but it is slightly easier and most letters have a fair few options.
Some difficulty.
</p>
<ul>
<li><b>X</b> Very few airlines; worth researching regional carriers in your area.</li>
</ul>
<p>
Most other letters have a reasonable selection of options across different continents,
though some will require deliberate routing to achieve.
</p>
</Panel>
</template>
<style scoped>
.progress-summary {
font-size: 1.2rem;
font-weight: 600;
color: var(--text);
}
.toggle-row {
display: flex;
align-items: center;
gap: 1rem;
}
.code-toggle {
padding: 0.15rem 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
color: var(--muted);
cursor: pointer;
font-size: inherit;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.code-toggle.active {
background: var(--accent-glow);
color: var(--accent);
border-color: var(--accent);
}
.alphabet-grid {
display: grid;
grid-template-columns: repeat(13, 1fr);
gap: 0.4rem;
width: 100%;
max-width: 520px;
}
@media (max-width: 480px) {
.alphabet-grid {
grid-template-columns: repeat(7, 1fr);
}
}
.letter-tile {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
border-radius: 6px;
user-select: none;
}
.letter-tile.visited {
background: var(--accent-glow);
color: var(--accent);
border: 1px solid var(--accent-soft);
}
.letter-tile.unvisited {
background: var(--surface-alt);
color: var(--muted);
border: 1px solid var(--border);
}
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.year-select {
max-width: 140px;
}
</style>
@@ -0,0 +1,280 @@
<script setup lang="ts">
import { Achievement, Flight, User } from '@/Types/types'
import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
import AlphabetTable from '@/Components/FlightsGoneBy/AlphabetTable.vue'
import { computed, ref } from 'vue'
import { useAlphabetFlights, type CodeType } from '@/Composables/useAlphabetFlights'
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
}>()
const codeType = ref<CodeType>('iata')
const flightsRef = computed(() => props.flights)
const visited = computed(() => useAlphabetFlights(flightsRef, codeType.value).visitedLetters.value)
const byLetter = computed(() => useAlphabetFlights(flightsRef, codeType.value).flightsByLetter.value)
const { allLetters } = useAlphabetFlights(flightsRef, codeType.value)
const visitedCount = computed(() => visited.value.size)
const availableYears = computed(() => {
const years = new Set(props.flights.map(f => new Date(f.departure_date).getFullYear()))
return [...years].sort((a, b) => b - a)
})
// Year select items: "None" option + all flight years
const yearItems = computed(() => [
{ title: 'None', value: null },
...availableYears.value.map(y => ({ title: String(y), value: y })),
])
const currentYear = new Date().getFullYear()
const selectedYear = ref<number | null>(
availableYears.value.includes(currentYear) ? currentYear : (availableYears.value[0] ?? null)
)
// Copy to clipboard
const alphabetTable = ref<InstanceType<typeof AlphabetTable> | null>(null)
const copied = ref(false)
async function copyBBCode() {
const text = alphabetTable.value?.toBBCode()
if (text == null) return
try {
await navigator.clipboard.writeText(text)
} catch {
// Fallback for non-HTTPS or browsers that block clipboard API
const el = document.createElement('textarea')
el.value = text
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
document.body.appendChild(el)
el.focus()
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
copied.value = true
setTimeout(() => (copied.value = false), 2000)
}
</script>
<template>
<!-- Challenge description -->
<Panel>
<PanelHeader centered>The Alphabet Challenge</PanelHeader>
<PanelSubHeader centered>
<p>
Originating from aviation forums, as <b>Land at the Alphabet</b>, this challenge is a tad more lenient
as it allows for both landing and taking off to count towards a letter.
</p>
<p>
In honor of this challenge's forum roots, you can copy your results of this challenge as BBCode by pressing the copy icon above the table, and select
a year to highlight airports that are new for that given year.
</p>
</PanelSubHeader>
</Panel>
<!-- Progress grid -->
<Panel style="display: flex; flex-direction: column; align-items: center; gap: 1rem;">
<PanelHeader centered>Progress</PanelHeader>
<div>
<button :class="['code-toggle', codeType === 'iata' ? 'active' : '']" @click="codeType = 'iata'">IATA</button>
<button :class="['code-toggle', codeType === 'icao' ? 'active' : '']" @click="codeType = 'icao'">ICAO</button>
</div>
<p class="progress-summary">{{ visitedCount }} / {{ allLetters.length }} letters visited</p>
<div class="alphabet-grid">
<div
v-for="letter in allLetters"
:key="letter"
:class="['letter-tile', visited.has(letter) ? 'visited' : 'unvisited']"
:title="visited.has(letter) ? 'Visited' : 'Not yet visited'"
>
{{ letter }}
</div>
</div>
</Panel>
<!-- Airports by letter -->
<Panel>
<div class="table-toolbar">
<PanelHeader>Airports By Letter</PanelHeader>
<div class="toolbar-right">
<v-select
v-model="selectedYear"
:items="yearItems"
item-title="title"
item-value="value"
label="New For"
density="compact"
variant="outlined"
hide-details
class="year-select"
/>
<v-btn
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
:color="copied ? 'success' : undefined"
density="compact"
variant="text"
title="Copy as BBCode"
@click="copyBBCode"
/>
</div>
</div>
<AlphabetTable
ref="alphabetTable"
:letters="allLetters"
:flightsByLetter="byLetter"
:codeType="codeType"
:selectedYear="selectedYear"
/>
</Panel>
<!-- Requirements -->
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must take off from or land at airports whose
IATA codes begin with each of the 26 letters of the alphabet —
A through Z. Airports without an IATA code do not count.
</p>
<p>
Both the departure and arrival airport on a single flight can count toward different
letters simultaneously, so a flight from <strong>ABX</strong> to <strong>BNE</strong>
would count toward both A and B.
</p>
<p>
ICAO codes do not count towards this challenge (and it is not possible to visit an ICAO code for every letter), but you can
toggle to ICAO view to see where you're at for fun.
</p>
</Panel>
<!-- Difficulty -->
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
This is a <b>very</b> difficult challenge no matter how well travelled you are. However it one of the most fun to try and accomplish as it will get you travelling
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.
</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.
Others - such as seaplane ports - might have GA or charter service.
</p>
<ul>
<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>
</ul>
<p>
Other potential candidates do not seem so promising
</p>
<ul>
<li><b>QAH</b> was in India as a secondary Delhi airport, but the code was changed to <b>HDO</b> </li>
<li><b>QAJ</b> was proposed for Ajman Airport which might have acted as an alternate to Dubai and Sharjah, but construction has not moved forward.</li>
</ul>
<p>
Every other letter usually has a decent selection of options across most continents.
</p>
</Panel>
</template>
<style scoped>
.progress-summary {
font-size: 1.2rem;
font-weight: 600;
color: var(--text);
}
.code-toggle {
padding: 0.15rem 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
color: var(--muted);
cursor: pointer;
font-size: inherit;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.code-toggle.active {
background: var(--accent-glow);
color: var(--accent);
border-color: var(--accent);
}
.alphabet-grid {
display: grid;
grid-template-columns: repeat(13, 1fr);
gap: 0.4rem;
width: 100%;
max-width: 520px;
}
@media (max-width: 480px) {
.alphabet-grid {
grid-template-columns: repeat(7, 1fr);
}
}
.letter-tile {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
border-radius: 6px;
user-select: none;
}
.letter-tile.visited {
background: var(--accent-glow);
color: var(--accent);
border: 1px solid var(--accent-soft);
}
.letter-tile.unvisited {
background: var(--surface-alt);
color: var(--muted);
border: 1px solid var(--border);
}
/* Table toolbar */
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.year-select {
max-width: 140px;
}
</style>
@@ -36,9 +36,9 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<template>
<Panel>
<PanelHeader centered>The Challenge</PanelHeader>
<p>
<PanelSubHeader centered>
Either take off or land from an airport in each Australian State and Internal Territory.
</p>
</PanelSubHeader>
</Panel>
<Panel style="display:flex; flex-direction: column; align-items: center; justify-content: center;">
<PanelHeader centered>Progress</PanelHeader>
@@ -46,7 +46,7 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<RegionLegend :regionNames="regionNames" :visitedRegions="visitedRegions" :stateLocalCodes="stateLocalCodes"/>
<br/>
<PanelHeader center>Flights By State</PanelHeader>
<PanelSubHeader>5 Most Recent Flights By State</PanelSubHeader>
<FlightRegionTable
:regionCodes="stateLocalCodes"
:regionNames="regionNames"
@@ -56,7 +56,7 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
The 8 states and territories you need to visit to complete this challenge are Queensland, Victoria, New South Wales, South Australia, Tasmania, Western Australia, the Northern Territory
The 8 states and territories you need to visit to complete this challenge are Queensland, Victoria, New South Wales, South Australia, Tasmania, Western Australia, the Northern Territory
and the Australian Capital Territory.
</p>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import MainLayout from "@/Layouts/MainLayout.vue";
import {Achievement, Flight, Region, User} from "@/Types/types";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
import {computed} from "vue";
import PanelSubHeader from "@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue";
import FlightRegionTable from "@/Components/FlightsGoneBy/FlightRegionTable.vue";
import {useRegionFlights} from "@/Composables/useRegionFlights";
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
import Brazil from "@/Components/Maps/Brazil.vue";
defineOptions({ layout: MainLayout })
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
flights: Flight[]
regions: Region[]
}>()
const countryCode = 'BR'
const stateLocalCodes = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']
const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
computed(() => props.flights),
stateLocalCodes,
countryCode,
computed(() => props.regions)
)
</script>
<template>
<Panel>
<PanelHeader centered>The Challenge</PanelHeader>
<p>
Either take off or land from an airport in each Brazilian State plus the Distrito Federal (Brasilia).
</p>
</Panel>
<Panel style="display:flex; flex-direction: column; align-items: center; justify-content: center;">
<PanelHeader centered>Progress</PanelHeader>
<Brazil :visitedStates="visitedRegions" :flights="flightsByRegion" style="width: 100%;"/>
<RegionLegend :regionNames="regionNames" :visitedRegions="visitedRegions" :stateLocalCodes="stateLocalCodes"/>
<br/>
<PanelHeader center>Flights By State</PanelHeader>
<FlightRegionTable
:regionCodes="stateLocalCodes"
:regionNames="regionNames"
:flightsByRegion="flightsByRegion"
/>
</Panel>
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
This is a very difficult challenge - Brazil is a reasonably mature aviation market by South American standards, but flights can still be expensive and the major
hubs tend to be in the south so you can expect a lot of doubling back. Brasilia might not be a bad base as it's quite central and well connected, but if you're trying
to achieve this trophy organically, it's unlikely to just happen!
</p>
</Panel>
</template>
<style scoped>
</style>
@@ -49,8 +49,7 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<Canada :visitedStates="visitedRegions" :flights="flightsByRegion" style="width: 100%;"/>
<RegionLegend :regionNames="regionNames" :visitedRegions="visitedRegions" :stateLocalCodes="stateLocalCodes"/>
<br/>
<PanelHeader center>Flights By State</PanelHeader>
<PanelSubHeader>5 Most Recent Flights By State</PanelSubHeader>
<PanelHeader centered>Flights By Province</PanelHeader>
<FlightRegionTable
:regionCodes="stateLocalCodes"
:regionNames="regionNames"
@@ -60,7 +59,7 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<Panel>
<PanelHeader centered>Difficulty</PanelHeader>
<p>
This challenge is not extremely difficult bur Canada's geography does complicate it and it could get expensive!
This challenge is not extremely difficult but Canada's geography does complicate it and it could get expensive!
</p>
<p>
Canada's population is all in the South, so visiting the Northwest Territories, Yukon and Nanavut will likely require doubling back south to get connecting flights.
@@ -47,7 +47,6 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<RegionLegend :regionNames="regionNames" :visitedRegions="visitedRegions" :stateLocalCodes="stateLocalCodes"/>
<br/>
<PanelHeader center>Flights By Region</PanelHeader>
<PanelSubHeader>5 Most Recent Flights Per Region</PanelSubHeader>
<FlightRegionTable
:regionCodes="stateLocalCodes"
:regionNames="regionNames"
@@ -53,7 +53,6 @@ const { flightsByRegion, visitedRegions, regionNames } = useRegionFlights(
<RegionLegend :visitedRegions="visitedRegions" :stateLocalCodes="stateLocalCodes" :regionNames="regionNames"/>
<br/>
<PanelHeader center>Flights By State</PanelHeader>
<PanelSubHeader>5 Most Recent Flights By State</PanelSubHeader>
<FlightRegionTable
:regionCodes="stateLocalCodes"
:regionNames="regionNames"
+28 -11
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {Achievement, Region, User, UserAchievement} from "@/Types/types";
import {Achievement, Airline, Continent, Region, User, UserAchievement} from "@/Types/types";
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
import {computed, defineAsyncComponent} from 'vue';
@@ -11,6 +11,7 @@ import MainLayout from "@/Layouts/MainLayout.vue";
import {useFlights} from "@/Composables/useFlights";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
defineOptions({ layout: MainLayout })
@@ -18,13 +19,18 @@ const props = defineProps<{
achievement: Achievement
userAchievement: UserAchievement
user: User
loggedInUser: User | null
isFollowing: boolean
flight_api_url: string
regions: Region[]
alliance: string | null
airlines: Airline[]
continents: Continent[]
aircraft_families: Record<string, string[]>
}>()
const { flights, flightsLoading } = useFlights(props.flight_api_url)
const { flights, flightsLoading } = useFlights(props.flight_api_url, true)
const AchievementDetail = defineAsyncComponent(
() => import(`./Achievements/${props.achievement.internal_name}.vue`)
@@ -63,14 +69,14 @@ const difficultyVariant = computed(() => {
<ProfileLayout :user="user" :isFollowing="isFollowing" :loading="flightsLoading">
<Head :title="`${achievement.name}`" />
<div class="innerLayout">
<v-btn
prepend-icon="mdi-arrow-left"
variant="flat"
>
<Link :href="route('profile.achievements', { user: user.name })">
Back to {{ user.name }}'s Achievements
</Link>
</v-btn>
<ButtonLink variant="flat" icon="mdi-arrow-left" :label="`Back to ${user.name}'s Achievements`" :href="route('profile.achievements', { user: user.name })" />
<VAlert type="info" v-if="loggedInUser?.id !== user.id && loggedInUser">
You are viewing {{user.name}}'s progress in this achievement. If you would like to see your progress,
<Link :href="route('profile.achievement', {user: loggedInUser.name, achievement: achievement.internal_name})">please click here</Link>.
</VAlert>
<Panel>
<div class="achievement-hero">
@@ -110,7 +116,18 @@ const difficultyVariant = computed(() => {
</div>
</Panel>
<component :is="AchievementDetail" :regions="regions" :flights="flights" :achievement="achievement" :user="user" :isFollowing="isFollowing" />
<component
:is="AchievementDetail"
:regions="regions"
:flights="flights"
:achievement="achievement"
:user="user"
:isFollowing="isFollowing"
:airlines="airlines"
:alliance="alliance"
:continents="continents"
:families="aircraft_families"
/>
</div>
</ProfileLayout>
</template>
+4 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import MainLayout from "@/Layouts/MainLayout.vue";
import {Head} from "@inertiajs/vue3";
import {Head, Link} from "@inertiajs/vue3";
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
import {Achievement, Flight, User, UserAchievement} from "@/Types/types";
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
@@ -10,6 +10,7 @@ import AirlinePanel from "@/Components/FlightsGoneBy/Panels/AirlinePanel.vue";
import AircraftPanel from "@/Components/FlightsGoneBy/Panels/AircraftPanel.vue";
import RoutePanel from "@/Components/FlightsGoneBy/Panels/RoutePanel.vue";
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
defineOptions({ layout: MainLayout })
@@ -30,6 +31,8 @@ const props = defineProps<{
<Head :title="`${flight.flight_number ?? user.name + '\'s Flight'}`" />
<div class="flight-profile">
<ButtonLink variant="flat" icon="mdi-arrow-left" :label="`Back to ${user.name}'s Flights`" :href="route('profile.departure-board', { user: user.name, flight: flight.id })" />
<!-- Main grid -->
<div class="profile-grid">
<RoutePanel :flight="flight" />
+2 -1
View File
@@ -21,6 +21,7 @@ const props = defineProps<{
initialView?: ProfileView
isFollowing: boolean
flight_api_url: string
flightCount: number,
}>()
// ── Flights state ─────────────────────────────────────────────────────────────
@@ -120,7 +121,7 @@ function switchView(view: ProfileView) {
<template>
<Head :title="`${user.name}'s Flights`" />
<ProfileLayout :is-following="isFollowing" :flightCount="flights.length" :user="user" :loading="flightsLoading">
<ProfileLayout :is-following="isFollowing" :flightCount="flightCount" :user="user" :loading="flightsLoading">
<ProfileViewSwitcher :user="user" :active-view="activeView" @update:active-view="switchView" />
<DepartureBoard v-if="activeView === 'board'" :flight-id="localSelectedFlightId" :flight-stats="stats" :canEdit="canEdit" :user="user" />
<BoardingPasses v-if="activeView === 'passes'" :flight-stats="stats" :canEdit="canEdit" />