310 lines
10 KiB
Vue
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>
|