190 lines
5.8 KiB
Vue
190 lines
5.8 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from "vue";
|
||
import { Flight } from "@/Types/types";
|
||
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||
|
||
const props = defineProps<{
|
||
flights: Flight[]
|
||
canEdit: boolean
|
||
}>()
|
||
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
const upcomingFlights = computed(() =>
|
||
props.flights
|
||
.filter(f => new Date(f.departure_date) >= today)
|
||
.sort((a, b) => new Date(a.departure_date).getTime() - new Date(b.departure_date).getTime())
|
||
)
|
||
|
||
const departedFlights = computed(() =>
|
||
props.flights
|
||
.filter(f => new Date(f.departure_date) < today)
|
||
.sort((a, b) => new Date(b.departure_date).getTime() - new Date(a.departure_date).getTime())
|
||
)
|
||
|
||
const allFlights = computed(() => [...upcomingFlights.value, ...departedFlights.value])
|
||
|
||
function isFirstDeparted(flight: Flight, pageItems: Flight[]): boolean {
|
||
const firstDeparted = pageItems.find(f => departedFlights.value.includes(f))
|
||
return flight === firstDeparted
|
||
}
|
||
|
||
function isUpcoming(flight: Flight): boolean {
|
||
return upcomingFlights.value.includes(flight)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<v-data-iterator :items="allFlights" :items-per-page="25">
|
||
<template #default="{ items }">
|
||
<div class="passes-root">
|
||
<template v-if="items.some(i => isUpcoming(i.raw))">
|
||
<div class="section-header upcoming">
|
||
<span class="section-icon">◈</span>
|
||
<span class="section-label">UPCOMING FLIGHTS</span>
|
||
<span class="section-count">{{ upcomingFlights.length }} scheduled</span>
|
||
<div class="section-line" />
|
||
</div>
|
||
</template>
|
||
|
||
<div class="passes-grid">
|
||
<template v-for="{ raw: flight } in items" :key="flight.id">
|
||
<template v-if="isFirstDeparted(flight, items.map(i => i.raw))">
|
||
<div class="section-header departed grid-span-full">
|
||
<span class="section-icon">◉</span>
|
||
<span class="section-label">DEPARTED FLIGHTS</span>
|
||
<span class="section-count">{{ departedFlights.length }} logged</span>
|
||
<div class="section-line" />
|
||
</div>
|
||
</template>
|
||
|
||
<BoardingPass :flight="flight" />
|
||
</template>
|
||
</div>
|
||
|
||
<template v-if="!items.some(i => isUpcoming(i.raw)) && items[0]?.raw === departedFlights[0]">
|
||
<div class="section-header departed departed-only">
|
||
<span class="section-icon">◉</span>
|
||
<span class="section-label">DEPARTED FLIGHTS</span>
|
||
<span class="section-count">{{ departedFlights.length }} logged</span>
|
||
<div class="section-line" />
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<template #footer="{ page, pageCount, prevPage, nextPage }">
|
||
<div class="iterator-footer">
|
||
<span class="footer-info">Page {{ page }} of {{ pageCount }}</span>
|
||
<div class="footer-nav">
|
||
<button class="nav-btn" :disabled="page === 1" @click="prevPage">‹</button>
|
||
<button class="nav-btn" :disabled="page === pageCount" @click="nextPage">›</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</v-data-iterator>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.passes-root {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.departed-only {
|
||
order: -1;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
padding: 1.5rem 0 0.75rem;
|
||
}
|
||
.section-icon { font-size: 0.7rem; flex-shrink: 0; }
|
||
.section-label {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.72rem;
|
||
letter-spacing: 0.22em;
|
||
font-weight: 400;
|
||
flex-shrink: 0;
|
||
text-transform: uppercase;
|
||
}
|
||
.section-count {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.1em;
|
||
color: #445566;
|
||
flex-shrink: 0;
|
||
text-transform: uppercase;
|
||
}
|
||
.section-line { flex: 1; height: 1px; min-width: 2rem; }
|
||
|
||
.section-header.upcoming .section-icon,
|
||
.section-header.upcoming .section-label { color: #ffc107; }
|
||
.section-header.upcoming .section-line { background: linear-gradient(to right, rgba(255,193,7,0.4), transparent); }
|
||
.section-header.departed .section-icon,
|
||
.section-header.departed .section-label { color: #778899; }
|
||
.section-header.departed .section-line { background: linear-gradient(to right, rgba(119,136,153,0.35), transparent); }
|
||
|
||
.passes-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||
gap: 1.25rem;
|
||
padding-bottom: 1.5rem;
|
||
}
|
||
|
||
.grid-span-full {
|
||
grid-column: 1 / -1;
|
||
padding: 0.75rem 0 0;
|
||
}
|
||
|
||
.iterator-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 1rem;
|
||
padding: 1rem 0 2rem;
|
||
border-top: 1px solid rgba(255,255,255,0.06);
|
||
}
|
||
|
||
.footer-info {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.72rem;
|
||
letter-spacing: 0.08em;
|
||
color: #445566;
|
||
}
|
||
|
||
.footer-nav {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.nav-btn {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
background: transparent;
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 2px;
|
||
color: #778;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-btn:hover:not(:disabled) {
|
||
background: rgba(255,193,7,0.05);
|
||
border-color: rgba(255,193,7,0.2);
|
||
color: #ffc107;
|
||
}
|
||
|
||
.nav-btn:disabled {
|
||
opacity: 0.25;
|
||
cursor: default;
|
||
}
|
||
</style>
|