Files
FlightsAPI/resources/js/Pages/Profile/Achievements/fun_challenges.airline_alphabet.vue
T
2026-05-16 23:48:18 +10:00

279 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>