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
@@ -5,6 +5,13 @@ import {computed} from "vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import {Link} from '@inertiajs/vue3'
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
import Distance from "@/Components/Distance.vue";
const distanceAchievements = [
'general_flying.circumference_of_the_earth',
'general_flying.to_the_moon',
];
const props = defineProps<{
achievement: Achievement
@@ -85,13 +92,11 @@ const difficultyVariant = computed(() => {
</div>
<p class="achievement-description">{{ achievement.short_description }}</p>
<Link v-if="achievement.has_page && user" :href="route('profile.achievement', { user: user.name, achievement: achievement.internal_name })">
View Details
</Link>
<template v-if="achievement.progressive && progress">
<div class="progress-label">
<span>{{ Math.min(progress.current, progress.threshold) }} / {{ progress.threshold }}</span>
<span><distance :showUnits="false" :value="Math.min(progress.current, progress.threshold)" /> / <distance :value="progress.threshold" :showUnits="distanceAchievements.includes(achievement.internal_name)" /></span>
<span>{{ progress.percentage }}%</span>
</div>
<v-progress-linear
@@ -102,11 +107,17 @@ const difficultyVariant = computed(() => {
bg-color="rgba(255,255,255,0.1)"
/>
</template>
</div>
</div>
<br/>
<ButtonLink
variant="outlined"
label="View Details"
icon="mdi-magnify"
v-if="achievement.has_page && user" :href="route('profile.achievement', { user: user.name, achievement: achievement.internal_name })" />
</v-card-text>
</v-card>
</template>
@@ -115,19 +126,17 @@ const difficultyVariant = computed(() => {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.achievement-card.locked {
.achievement-card.locked .achievement-inner {
opacity: 0.45;
}
.achievement-card:hover {
opacity: 1;
transform: translateY(-1px);
}
.achievement-card:hover .achievement-inner,
.achievement-inner {
display: flex;
gap: 1rem;
align-items: flex-start;
opacity: 1;
}
.achievement-icon-wrap {
@@ -0,0 +1,112 @@
<script setup lang="ts">
import { Flight, Airline } from '@/Types/types'
import type { CodeType } from '@/Composables/useAlphabetAirlines'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
const props = defineProps<{
letters: string[]
flightsByLetter: Record<string, Flight[]>
codeType: CodeType
selectedYear: number | null
}>()
interface AirlineEntry {
airline: Airline
code: string
firstYear: number
}
function getCode(airline: Airline): string | null {
const raw = props.codeType === 'iata' ? airline.IATA_code : airline.ICAO_code
return raw?.trim().toUpperCase() ?? null
}
function airlineEntriesForLetter(letter: string): AirlineEntry[] {
const flights = props.flightsByLetter[letter] ?? []
const seen = new Map<string, AirlineEntry>()
for (const flight of flights) {
const airline = flight.airline
if (!airline) continue
const code = getCode(airline)
if (!code?.startsWith(letter)) continue
const year = new Date(flight.departure_date).getFullYear()
const existing = seen.get(code)
if (!existing || year < existing.firstYear) {
seen.set(code, { airline, code, firstYear: year })
}
}
return [...seen.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, entry]) => entry)
}
function isHighlighted({ firstYear }: AirlineEntry): boolean {
return props.selectedYear !== null && firstYear === props.selectedYear
}
function toBBCode(): string {
return props.letters
.map(letter => {
const entries = airlineEntriesForLetter(letter)
if (!entries.length) return letter
return entries
.map(entry =>
isHighlighted(entry)
? `[b][color=#00BF00]${entry.code}[/color][/b]`
: entry.code
)
.join(', ')
})
.join('\n')
}
defineExpose({ toBBCode })
</script>
<template>
<BadgeTable
:rows="letters"
:rowKey="letter => letter"
:hasItems="letter => !!flightsByLetter[letter]?.length"
labelWidth="4em"
>
<template #label="{ row: letter }">
<div
style="width:100%;display:flex;justify-content:center;align-items:center"
:class="flightsByLetter[letter]?.length ? 'visited' : 'unvisited'"
>
{{ letter }}
</div>
</template>
<template #items="{ row: letter }">
<div
v-for="entry in airlineEntriesForLetter(letter)"
:key="entry.airline.IATA_code!"
>
<InlineBadge style="align-items:center;gap:0.2em" :variant="isHighlighted(entry) ? 'business' : undefined">
<AirlineLogo :airline="entry.airline" /> {{ entry.code }}
</InlineBadge>
</div>
</template>
</BadgeTable>
</template>
<style scoped>
.visited {
font-weight: 700;
color: var(--accent);
}
.unvisited {
color: var(--muted);
}
</style>
@@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Achievement, Airline, Alliance, 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 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 FlightBadge from "@/Components/FlightsGoneBy/FlightBadge.vue";
interface AirlineEntry {
airline: Airline
flights: Flight[]
}
const props = defineProps<{
achievement: Achievement
user: User
isFollowing: boolean
alliance: Alliance
airlines: Airline[]
flights: Flight[]
}>()
const flightsByAirline = computed(() => {
const allianceAirlineIds = new Set(props.airlines.map(a => a.id))
const map = new Map<string, AirlineEntry>()
// Pre-populate every alliance airline so unflown ones still appear
for (const airline of props.airlines) {
map.set(rowKey(airline), { airline, flights: [] })
}
for (const flight of props.flights) {
const airline = flight.airline
if (!airline || !allianceAirlineIds.has(airline.id)) continue
map.get(rowKey(airline))?.flights.push(flight)
}
return map
})
function rowKey(airline: Airline): string {
return airline.IATA_code ?? airline.internal_name
}
const rows = computed(() => [...flightsByAirline.value.keys()])
function entryFor(key: string): AirlineEntry {
return flightsByAirline.value.get(key)!
}
</script>
<template>
<!-- Header -->
<Panel>
<div class="alliance-header">
<AllianceLogo :alliance="alliance" size="56" />
<div>
<PanelHeader centered>{{ alliance.name }}</PanelHeader>
<PanelSubHeader centered>
<slot />
</PanelSubHeader>
</div>
</div>
</Panel>
<!-- Airlines table -->
<Panel>
<div class="table-toolbar">
<PanelHeader>Airlines</PanelHeader>
</div>
<BadgeTable
:rows="rows"
:rowKey="key => key"
:hasItems="key => entryFor(key).flights.length > 0"
labelWidth="14em"
>
<template #label="{ row: key }">
<div class="airline-label" >
<AirlineLogo :airline="entryFor(key).airline" size="24" />
<span>{{ entryFor(key).airline.name }}</span>
</div>
</template>
<template #items="{ row: key }">
<FlightBadge
v-for="flight in entryFor(key).flights"
:key="flight.id"
:title="`${flight.departure_airport?.iata_code} → ${flight.arrival_airport?.iata_code}`"
:flight="flight" />
</template>
</BadgeTable>
</Panel>
<!-- Slot for alliance-specific panels -->
<slot name="extra" />
<!-- Requirements -->
<Panel>
<PanelHeader centered>Requirements</PanelHeader>
<p>
To complete this challenge you must fly with every current member airline of
<strong>{{ alliance.name }}</strong>. Alliance membership changes over time, so the
required airlines reflect the current roster.
</p>
<p>
Codeshare flights do not count, the operating carrier must be a member of {{alliance.name}}.
</p>
</Panel>
</template>
<style scoped>
.alliance-header {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.airline-label {
width: 100%;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.25rem;
}
</style>
@@ -0,0 +1,115 @@
<script setup lang="ts">
import {Airport, Flight} from '@/Types/types'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
import AirportToolTip from '@/Components/FlightsGoneBy/AirportToolTip.vue'
type CodeType = 'iata' | 'icao'
const props = defineProps<{
letters: string[]
flightsByLetter: Record<string, Flight[]>
codeType: CodeType
selectedYear: number | null
}>()
function getCode(airport: Airport): string | null {
const raw = props.codeType === 'iata' ? airport.iata_code : airport.icao_code
return raw?.trim().toUpperCase() ?? null
}
interface AirportEntry {
airport: Airport
firstYear: number
}
function airportEntriesForLetter(letter: string): AirportEntry[] {
const flights = props.flightsByLetter[letter] ?? []
const seen = new Map<string, AirportEntry>()
for (const flight of flights) {
const year = new Date(flight.departure_date).getFullYear()
for (const airport of [flight.departure_airport, flight.arrival_airport]) {
const code = getCode(airport)
if (!code?.startsWith(letter)) continue
const existing = seen.get(code)
if (!existing || year < existing.firstYear) {
seen.set(code, { airport, firstYear: year })
}
}
}
return [...seen.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, entry]) => entry)
}
function isHighlighted({ firstYear }: AirportEntry): boolean {
return props.selectedYear !== null && firstYear === props.selectedYear
}
function toBBCode(): string {
return props.letters
.map(letter => {
const entries = airportEntriesForLetter(letter)
if (!entries.length) return letter
return entries
.map(entry => {
const code = getCode(entry.airport)!
return isHighlighted(entry) ? `[b][color=#00BF00]${code}[/color][/b]` : code
})
.join(', ')
})
.join('\n')
}
defineExpose({ toBBCode })
</script>
<template>
<BadgeTable
:rows="letters"
:rowKey="letter => letter"
:hasItems="letter => !!flightsByLetter[letter]?.length"
labelWidth="4em"
>
<template #label="{ row: letter }">
<div style="width:100%;display:flex;justify-content: center; align-items: center" :class="flightsByLetter[letter]?.length ? 'visited' : 'unvisited'">
{{ letter }}
</div>
</template>
<template #items="{ row: letter }">
<AirportToolTip
v-for="entry in airportEntriesForLetter(letter)"
:key="getCode(entry.airport)!"
:airport="entry.airport"
>
<InlineBadge :variant="isHighlighted(entry) ? 'business' : undefined">
{{ getCode(entry.airport) }}
</InlineBadge>
</AirportToolTip>
</template>
</BadgeTable>
</template>
<style scoped>
.visited {
font-weight: 700;
color: var(--accent);
}
.unvisited {
color: var(--muted);
}
.airport-count {
font-size: 0.7rem;
font-weight: 400;
color: var(--muted);
margin-left: 0.2em;
}
</style>
@@ -3,6 +3,7 @@ import { Flight } from "@/Types/types";
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
import Distance from "@/Components/Distance.vue";
defineProps<{
flight: Flight
@@ -70,7 +71,7 @@ defineProps<{
<span class="pass-stat-divider" v-if="flight.duration_display && flight.distance">·</span>
<span v-if="flight.distance" class="pass-stat">
<span class="pass-stat-label">DISTANCE</span>
<span class="pass-stat-value">{{ Math.round(flight.distance).toLocaleString() }} km</span>
<span class="pass-stat-value"><Distance :value="Math.round(flight.distance)" /></span>
</span>
</div>
</div>
@@ -80,16 +81,6 @@ defineProps<{
<style scoped>
.feed-boarding-pass{
max-width: 600px;
margin: 1em auto;
}
@media (max-width: 1200px) {
.boarding-pass{
margin: 0;
}
}
.pass-stats-row {
display: flex;
@@ -0,0 +1,31 @@
<script setup lang="ts">
import {Link} from "@inertiajs/vue3";
type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
defineProps<{
href: string | {
url: string;
method: Method;
},
label: string;
icon?: string;
variant?: "flat" | "text" | "elevated" | "outlined" | "plain" | "tonal" | undefined
}>()
</script>
<template>
<Link :href="href">
<v-btn
style="width:100%;"
:prepend-icon="icon"
:variant="variant"
>
{{ label }}
</v-btn>
</Link>
</template>
<style scoped>
a{
width: 100%;
display: block;
}
</style>
@@ -13,6 +13,7 @@ import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import CrewTooltip from "@/Components/FlightsGoneBy/CrewTooltip.vue";
import {Link, router} from "@inertiajs/vue3";
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue";
import Distance from "@/Components/Distance.vue";
const props = defineProps<{
flightStats: FlightStats
@@ -238,7 +239,7 @@ watch(
<td class="v-data-table__td">
<span class="mono-tag distance-cell">
{{ (item as Flight).distance ? Math.round((item as Flight).distance).toLocaleString() + ' km' : '' }}
<Distance :value="Math.round((item as Flight).distance)" />
</span>
</td>
@@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div class="flight-booked">
<BoardingPass :class="`feed-boarding-pass`" :flight="flight" />
<BoardingPass style="max-width:90%; margin: 0 auto" :class="`feed-boarding-pass`" :flight="flight" />
</div>
</template>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import {Flight} from "@/Types/types";
defineProps<{
flight: Flight
}>()
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import FlightToolTip from "@/Components/FlightsGoneBy/FlightToolTip.vue";
</script>
<template>
<FlightToolTip
:flight="flight"
>
<InlineBadge class="flight-badge">
<AirlineLogo hideTooltip :airline="flight.airline" />
{{ flight.flight_number || `${flight.departure_airport.display_code}-${flight.arrival_airport.display_code}` }}
</InlineBadge>
</FlightToolTip>
</template>
<style scoped>
.flight-badge {
display: inline-flex;
align-items: center;
gap: 0.25em;
flex-shrink: 0;
}
</style>
@@ -471,6 +471,7 @@ export default defineComponent({
map = new maplibregl.Map({
container: mapContainer.value!,
cooperativeGestures: true,
attributionControl: false,
style: {
version: 8,
sources: {
@@ -483,7 +484,6 @@ export default defineComponent({
'https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
],
tileSize: 256,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>',
maxzoom: 19,
},
},
@@ -1,49 +1,47 @@
<script setup lang="ts">
import {Flight} from "@/Types/types";
import FlightToolTip from "@/Components/FlightsGoneBy/FlightToolTip.vue";
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import { Flight } from '@/Types/types'
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
import FlightToolTip from '@/Components/FlightsGoneBy/FlightToolTip.vue'
import AirlineLogo from '@/Components/FlightsGoneBy/AirlineLogo.vue'
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
import {computed} from "vue";
import FlightBadge from "@/Components/FlightsGoneBy/FlightBadge.vue";
defineProps<{
const props = defineProps<{
regionCodes: string[]
flightsByRegion: Record<string, Flight[]>
regionNames?: Record<string, string>
}>()
const sortedRegionCodes = computed(() =>
[...props.regionCodes].sort((a, b) => {
const nameA = props.regionNames?.[a] ?? a
const nameB = props.regionNames?.[b] ?? b
return nameA.localeCompare(nameB)
})
)
</script>
<template>
<table>
<tr v-for="code in regionCodes" :key="code">
<td>{{ regionNames?.[code] ?? code }}</td>
<td>
<template v-if="flightsByRegion[code]?.length">
<span style="display:inline-flex; align-items:center; gap:0.25em; flex-wrap:wrap;">
<FlightToolTip
v-for="(flight, index) in flightsByRegion[code].slice(0, 5)"
:key="flight.id"
:flight="flight"
>
<InlineBadge style="display:inline-flex; align-items:center; gap:0.25em;">
<AirlineLogo hideTooltip :airline="flight.airline" />
{{ flight.flight_number || `${flight.departure_airport.display_code}-${flight.arrival_airport.display_code}` }}
</InlineBadge>
</FlightToolTip>
</span>
</template>
<template v-else>
</template>
</td>
</tr>
</table>
<BadgeTable
:rows="sortedRegionCodes"
:rowKey="code => code"
:hasItems="code => !!flightsByRegion[code]?.length"
>
<template #label="{ row: code }">
{{ regionNames?.[code] ?? code }}
</template>
<template #items="{ row: code }">
<FlightBadge
v-for="flight in flightsByRegion[code]"
:key="flight.id"
:flight="flight"
/>
</template>
</BadgeTable>
</template>
<style scoped>
table {
border-spacing: 0;
width: 100%;
}
table td {
border: solid 1px;
padding: 0.5em;
}
</style>
@@ -35,18 +35,13 @@
<div class="stat">
<template v-if="totalDistanceKm">
<div class="stat-primary">
<span class="stat-num">{{ totalDistanceKm.toLocaleString() }}</span>
<span class="unit">km</span>
<span class="stat-num"><Distance includeSpace :value="totalDistanceKm" /></span>
</div>
<div class="stat-sub">{{ totalDistanceMi.toLocaleString() }} miles</div>
</template>
<template v-if="upcomingDistanceKm">
<div :class="totalDistanceKm ? 'stat-upcoming' : 'stat-primary'">
<span :class="totalDistanceKm ? 'stat-upcoming-num' : 'stat-num'">{{ upcomingDistanceKm.toLocaleString() }}</span>
<span :class="totalDistanceKm ? 'stat-upcoming-lbl' : 'unit'">{{ totalDistanceKm ? 'km upcoming' : 'km' }}</span>
<span v-if="!totalDistanceKm" class="upcoming-badge">upcoming</span>
<span :class="totalDistanceKm ? 'stat-upcoming-num' : 'stat-num'"><Distance includeSpace :value="upcomingDistanceKm"/></span>
</div>
<div v-if="!totalDistanceKm" class="stat-sub">{{ upcomingDistanceMi.toLocaleString() }} miles</div>
</template>
</div>
@@ -105,6 +100,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { Flight } from '@/Types/types'
import Distance from "@/Components/Distance.vue";
const props = defineProps<{
flights: Flight[]
@@ -117,10 +113,6 @@ const totalDistanceKm = computed(() =>
Math.round(props.flights.reduce((sum, f) => sum + (f.distance ?? 0), 0))
)
const totalDistanceMi = computed(() =>
Math.round(totalDistanceKm.value * 0.621371)
)
const uniqueRoutes = computed(() => {
const keys = new Set(props.flights.map(f =>
[f.departure_airport.id, f.arrival_airport.id].sort().join('-')
@@ -164,10 +156,6 @@ const upcomingDistanceKm = computed(() =>
Math.round(props.upcomingFlights.reduce((sum, f) => sum + (f.distance ?? 0), 0))
)
const upcomingDistanceMi = computed(() =>
Math.round(upcomingDistanceKm.value * 0.621371)
)
const uniqueUpcomingRoutes = computed(() => {
const keys = new Set(props.upcomingFlights.map(f =>
[f.departure_airport.id, f.arrival_airport.id].sort().join('-')
@@ -3,7 +3,6 @@ import { Flight } from "@/Types/types";
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
defineProps<{
flight: Flight
@@ -0,0 +1,90 @@
<script setup lang="ts" generic="T">
defineProps<{
rows: T[]
rowKey: (row: T) => string | number
hasItems: (row: T) => boolean
labelWidth?: string
}>()
</script>
<template>
<div class="badge-table">
<div
v-for="row in rows"
:key="rowKey(row)"
class="badge-row"
>
<!-- Label column: consumer provides content -->
<div class="badge-label" :style="labelWidth ? `width: ${labelWidth}` : ''">
<slot name="label" :row="row" />
</div>
<!-- Scrollable badge strip -->
<div class="badge-scroll-container">
<div v-if="hasItems(row)" class="badge-strip">
<slot name="items" :row="row" />
</div>
<span v-else class="badge-empty"></span>
</div>
</div>
</div>
</template>
<style scoped>
.badge-table {
width: 100%;
border: 1px solid var(--table-border);
border-bottom: none;
container-type: inline-size;
}
.badge-row {
display: flex;
align-items: center;
border-bottom: 1px solid var(--table-border);
min-height: 2.5em;
}
.badge-label {
flex: 0 0 auto;
width: 15em;
padding: 0.5em;
border-right: 1px solid var(--table-border);
align-self: stretch;
display: flex;
align-items: center;
font-size:0.9rem;
}
@container (max-width: 480px) {
.badge-label {
font-size: 0.75rem;
width: 9em;
}
}
.badge-scroll-container {
flex: 1 1 0;
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
display: flex;
align-items: center;
}
.badge-strip {
display: inline-flex;
align-items: center;
gap: 0.25em;
padding: 0.5em;
white-space: nowrap;
}
.badge-empty {
padding: 0.5em;
color: var(--muted);
}
</style>
@@ -29,8 +29,6 @@ defineProps<{
}
.glass-tooltip {
background: var(--surface);
border: 1px solid var(--table-border);
padding: 10px 14px;
min-width: 180px;
display: flex;
@@ -38,5 +36,6 @@ defineProps<{
gap: 8px;
color: var(--text);
font-size: 0.85rem;
z-index: 20000
}
</style>
@@ -17,7 +17,7 @@ withDefaults(defineProps<{
<style scoped>
.class-badge {
display: inline-block;
display: inline-flex;
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.1em;
@@ -1,9 +1,11 @@
<script setup lang="ts">
defineProps<{
centered?: boolean
}>()
</script>
<template>
<div class="panel-sub-header"><slot/></div>
<div class="panel-sub-header" :class="centered ? 'centered' : ''"><slot/></div>
</template>
<style scoped>
@@ -12,4 +14,8 @@
color: var(--muted);
margin-bottom: 0.5rem;
}
.centered{
text-align: center;
}
</style>
@@ -9,6 +9,7 @@ const props = defineProps<{
flightCount?: number
achievementCount?: number
isFollowing?: boolean
show: "flights" | "achievements"
}>()
const auth = usePage<SharedProps>().props.auth
@@ -20,6 +21,13 @@ const processing = ref(false)
const snackbar = ref(false)
const snackbarMessage = ref('')
const counts = computed(() => {
return {
flights: props.flightCount ?? 0,
achievements: props.achievementCount ?? 0,
} as Record<"flights" | "achievements", number>
})
const follow = async () => {
processing.value = true
const response = await fetch(route('profile.follow', { user: props.user.name }), {
@@ -61,8 +69,8 @@ const follow = async () => {
</div>
<div class="board-count">
<span class="count-number">{{ flightCount ?? achievementCount }}</span>
<span class="count-label">{{achievementCount ? 'Achievements' : 'Flights'}}</span>
<span class="count-number">{{ counts[show] }}</span>
<span class="count-label">{{show.toUpperCase()}}</span>
</div>
</div>
@@ -114,6 +122,12 @@ const follow = async () => {
margin: 0;
}
@media (max-width: 768px) {
.board-header {
padding: 1em
}
}
.follow-btn {
font-family: 'Share Tech Mono', monospace;
font-size: 0.75rem;
@@ -14,7 +14,7 @@ defineProps<{
<template>
<div class="board-wrapper">
<ProfileHeader :is-following="isFollowing" :user="user" :flightCount="flightCount" />
<ProfileHeader :show="achievementCount && achievementCount > 0 ? 'achievements' : 'flights'" :is-following="isFollowing" :user="user" :flightCount="flightCount" :achievementCount="achievementCount" />
<div v-if="loading" class="loading-state">
<PlaneLoader />
</div>