Files
2026-04-23 21:32:25 +10:00

310 lines
10 KiB
Vue

<template>
<div class="stats-bar glass">
<div class="stat">
<template v-if="flights.length">
<div class="stat-primary">
<span class="stat-num">{{ flights.length.toLocaleString() }}</span>
<span class="unit">flights</span>
</div>
</template>
<template v-if="upcomingFlights.length">
<div :class="flights.length ? 'stat-upcoming' : 'stat-primary'">
<span :class="flights.length ? 'stat-upcoming-num' : 'stat-num'">{{ upcomingFlights.length.toLocaleString() }}</span>
<span :class="flights.length ? 'stat-upcoming-lbl' : 'unit'">{{ flights.length ? 'upcoming' : 'flights' }}</span>
<span v-if="!flights.length" class="upcoming-badge">upcoming</span>
</div>
</template>
</div>
<div class="stat">
<template v-if="uniqueRoutes">
<div class="stat-primary">
<span class="stat-num">{{ uniqueRoutes.toLocaleString() }}</span>
<span class="unit">routes</span>
</div>
</template>
<template v-if="uniqueUpcomingRoutes">
<div :class="uniqueRoutes ? 'stat-upcoming' : 'stat-primary'">
<span :class="uniqueRoutes ? 'stat-upcoming-num' : 'stat-num'">{{ uniqueUpcomingRoutes.toLocaleString() }}</span>
<span :class="uniqueRoutes ? 'stat-upcoming-lbl' : 'unit'">{{ uniqueRoutes ? 'upcoming' : 'routes' }}</span>
<span v-if="!uniqueRoutes" class="upcoming-badge">upcoming</span>
</div>
</template>
</div>
<div class="stat">
<template v-if="totalDistanceKm">
<div class="stat-primary">
<span class="stat-num">{{ totalDistanceKm.toLocaleString() }}</span>
<span class="unit">km</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>
</div>
<div v-if="!totalDistanceKm" class="stat-sub">{{ upcomingDistanceMi.toLocaleString() }} miles</div>
</template>
</div>
<div class="stat">
<template v-if="uniqueCountries">
<div class="stat-primary">
<span class="stat-num">{{ uniqueCountries }}</span>
<span class="unit">countries</span>
</div>
</template>
<template v-if="uniqueUpcomingCountries">
<div :class="uniqueCountries ? 'stat-upcoming' : 'stat-primary'">
<span :class="uniqueCountries ? 'stat-upcoming-num' : 'stat-num'">{{ uniqueUpcomingCountries }}</span>
<span :class="uniqueCountries ? 'stat-upcoming-lbl' : 'unit'">{{ uniqueCountries ? 'upcoming' : 'countries' }}</span>
<span v-if="!uniqueCountries" class="upcoming-badge">upcoming</span>
</div>
</template>
</div>
<div class="stat">
<template v-if="uniqueAirports">
<div class="stat-primary">
<span class="stat-num">{{ uniqueAirports }}</span>
<span class="unit">airports</span>
</div>
</template>
<template v-if="uniqueUpcomingAirports">
<div :class="uniqueAirports ? 'stat-upcoming' : 'stat-primary'">
<span :class="uniqueAirports ? 'stat-upcoming-num' : 'stat-num'">{{ uniqueUpcomingAirports }}</span>
<span :class="uniqueAirports ? 'stat-upcoming-lbl' : 'unit'">{{ uniqueAirports ? 'upcoming' : 'airports' }}</span>
<span v-if="!uniqueAirports" class="upcoming-badge">upcoming</span>
</div>
</template>
</div>
<div class="stat">
<template v-if="durationDisplay.hours">
<div class="stat-primary">
<span class="stat-num">{{ durationDisplay.hours.toLocaleString() }}</span>
<span class="unit">hours in the air</span>
</div>
<div class="stat-sub">{{ durationDisplay.days }} days · {{ durationDisplay.weeks }} weeks</div>
</template>
<template v-if="upcomingDurationDisplay.hours">
<div :class="durationDisplay.hours ? 'stat-upcoming' : 'stat-primary'">
<span :class="durationDisplay.hours ? 'stat-upcoming-num' : 'stat-num'">{{ upcomingDurationDisplay.hours.toLocaleString() }}</span>
<span :class="durationDisplay.hours ? 'stat-upcoming-lbl' : 'unit'">{{ durationDisplay.hours ? 'hrs upcoming' : 'hours in the air' }}</span>
<span v-if="!durationDisplay.hours" class="upcoming-badge">upcoming</span>
</div>
<div v-if="!durationDisplay.hours" class="stat-sub">{{ upcomingDurationDisplay.days }} days · {{ upcomingDurationDisplay.weeks }} weeks</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Flight } from '@/Types/types'
const props = defineProps<{
flights: Flight[]
upcomingFlights: Flight[]
}>()
// ── Past ──────────────────────────────────────────────────────────────────────
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('-')
))
return keys.size
})
const uniqueCountries = computed(() => {
const codes = new Set<string>()
props.flights.forEach(f => {
const depCode = f.departure_airport.region?.country?.code
const arrCode = f.arrival_airport.region?.country?.code
if (depCode) codes.add(depCode)
if (arrCode) codes.add(arrCode)
})
return codes.size
})
const uniqueAirports = computed(() => {
const ids = new Set<number>()
props.flights.forEach(f => {
ids.add(f.departure_airport.id)
ids.add(f.arrival_airport.id)
})
return ids.size
})
const durationDisplay = computed(() => {
const totalMinutes = props.flights.reduce((sum, f) => sum + (f.duration ?? 0), 0)
const totalHours = Math.floor(totalMinutes / 60)
return {
hours: totalHours,
days: Math.floor(totalHours / 24),
weeks: Math.floor(totalHours / 24 / 7),
}
})
// ── Upcoming ──────────────────────────────────────────────────────────────────
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('-')
))
return keys.size
})
const uniqueUpcomingCountries = computed(() => {
const codes = new Set<string>()
props.upcomingFlights.forEach(f => {
const depCode = f.departure_airport.region?.country?.code
const arrCode = f.arrival_airport.region?.country?.code
if (depCode) codes.add(depCode)
if (arrCode) codes.add(arrCode)
})
return codes.size
})
const uniqueUpcomingAirports = computed(() => {
const ids = new Set<number>()
props.upcomingFlights.forEach(f => {
ids.add(f.departure_airport.id)
ids.add(f.arrival_airport.id)
})
return ids.size
})
const upcomingDurationDisplay = computed(() => {
const totalMinutes = props.upcomingFlights.reduce((sum, f) => sum + (f.duration ?? 0), 0)
const totalHours = Math.floor(totalMinutes / 60)
return {
hours: totalHours,
days: Math.floor(totalHours / 24),
weeks: Math.floor(totalHours / 24 / 7),
}
})
</script>
<style scoped>
.stats-bar {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 1px;
border-radius: 10px;
overflow: hidden;
margin-top: 12px;
}
.stat {
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-primary {
display: flex;
align-items: baseline;
gap: 6px;
flex-wrap: wrap;
}
.stat-num {
font-size: 26px;
font-weight: 500;
color: #e0e6f0;
letter-spacing: -0.5px;
line-height: 1;
}
.unit {
font-size: 13px;
font-weight: 400;
color: #3a5566;
}
.stat-sub {
font-size: 11px;
color: #334455;
margin-top: 1px;
}
.stat-upcoming {
display: flex;
align-items: baseline;
gap: 5px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.stat-upcoming-num {
font-size: 13px;
font-weight: 500;
color: #4a8fa8;
}
.stat-upcoming-lbl {
font-size: 12px;
color: #335566;
}
.upcoming-badge {
font-size: 10px;
font-weight: 500;
color: #4a8fa8;
background: rgba(74, 143, 168, 0.12);
border: 1px solid rgba(74, 143, 168, 0.2);
border-radius: 4px;
padding: 1px 6px;
letter-spacing: 0.06em;
text-transform: uppercase;
align-self: center;
}
@media (max-width: 1024px) {
.stats-bar {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.stat {
padding: 14px 16px;
}
}
@media (max-width: 640px) {
.stats-bar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat {
padding: 12px 14px;
}
.stat-num {
font-size: 22px;
}
}
</style>