Files
FlightsAPI/resources/js/Pages/Profile/Achievements/fun_challenges.airport_alphabet.vue
T
2026-06-14 16:04:01 +10:00

261 lines
9.0 KiB
Vue

<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";
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
defineOptions({ inheritAttrs: false })
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)
</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"
/>
<CopyButton
title="Copy as BBCode"
size="large"
:text="alphabetTable?.bbCode ?? ''"
/>
</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>
Based on our airport database, it is actually possible to obtain every letter except <b>V</b> without leaving China.
</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>