Corrected Korea
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use App\Http\Resources\UserFlightResource;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -15,20 +16,20 @@ class FlightProfileController extends Controller
|
||||
|
||||
$flights = UserFlight::where('user_id', $user->id)
|
||||
->with([
|
||||
'departureAirport',
|
||||
'arrivalAirport',
|
||||
'departureAirport.region.country',
|
||||
'arrivalAirport.region.country',
|
||||
'airline.country',
|
||||
'aircraft',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'flightClass',
|
||||
])
|
||||
->orderBy('departure_date')
|
||||
->orderBy('departure_date', 'desc')
|
||||
->get();
|
||||
|
||||
return Inertia::render('FlightProfile', [
|
||||
'user' => $user,
|
||||
'flights' => $flights,
|
||||
'flights' => UserFlightResource::collection($flights)->resolve(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class LogoController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function getLogoById(int $id){
|
||||
public function getLogoById($id){
|
||||
$airline = Airline::where('id', $id)
|
||||
->first();
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin UserFlight */
|
||||
class UserFlightResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
$departureTz = $this->departureAirport->timezone;
|
||||
$arrivalTz = $this->arrivalAirport->timezone;
|
||||
$duration = $this->departure_date->diffInMinutes($this->arrival_date);
|
||||
$hours = intdiv($duration, 60);
|
||||
$minutes = $duration % 60;
|
||||
$durationDisplay = $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
||||
|
||||
$departureLocal = $this->departure_date->copy()->setTimezone($departureTz);
|
||||
$arrivalLocal = $this->arrival_date?->copy()->setTimezone($arrivalTz);
|
||||
|
||||
$distance = $this->calculateGreatCircleDistance();
|
||||
|
||||
$dayDifference = (int) abs(Carbon::parse($arrivalLocal->toDateString())
|
||||
->diffInDays(Carbon::parse($departureLocal->toDateString())));
|
||||
|
||||
|
||||
return [
|
||||
...$this->resource->toArray(),
|
||||
'departure_date_display' => $departureLocal->format('j M Y'),
|
||||
'departure_time_display' => $departureLocal->format('g:iA'),
|
||||
'arrival_date_display' => $arrivalLocal?->format('j M Y'),
|
||||
'arrival_time_display' => $arrivalLocal?->format('g:iA'),
|
||||
'arrival_day_difference' => $dayDifference,
|
||||
'duration' => $duration,
|
||||
'duration_display' => $durationDisplay,
|
||||
'distance' => $distance,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,24 @@ class UserFlight extends Model
|
||||
'arrival_date' => 'immutable_datetime',
|
||||
];
|
||||
|
||||
public function calculateGreatCircleDistance(): float{
|
||||
$earthRadiusKm = 6371;
|
||||
[$depLat, $depLong] = [$this->departureAirport->latitude_deg, $this->departureAirport->longitude_deg];
|
||||
[$arrLat, $arrLong] = [$this->arrivalAirport->latitude_deg, $this->arrivalAirport->longitude_deg];
|
||||
|
||||
|
||||
$latDelta = deg2rad($arrLat - $depLat);
|
||||
$longDelta = deg2rad($arrLong - $depLong);
|
||||
|
||||
$a = sin($latDelta / 2) * sin($latDelta / 2)
|
||||
+ cos(deg2rad($depLat)) * cos(deg2rad($arrLat))
|
||||
* sin($longDelta / 2) * sin($longDelta / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return $earthRadiusKm * $c;
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Airline;
|
||||
use App\Models\Country;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$northKorea = Country::where('code', 'KP')->first();
|
||||
|
||||
Airline::where('internal_name', 'air-koryo')->update(['country_id' => $northKorea->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$southKorea = Country::where('code', 'KR')->first();
|
||||
|
||||
Airline::where('internal_name', 'air-koryo')->update(['country_id' => $southKorea->id]);
|
||||
}
|
||||
};
|
||||
@@ -39,3 +39,100 @@ body {
|
||||
radial-gradient(ellipse at 20% 50%, rgba(56, 189, 248, 0.06) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(14, 165, 233, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* flight-classes
|
||||
* Apply .class-{name}-global to any element to get the themed background, border, and colour.
|
||||
*/
|
||||
|
||||
.class-economy-global {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #778;
|
||||
}
|
||||
|
||||
.class-premium-global {
|
||||
background: rgba(75, 32, 137, 0.35);
|
||||
border: 1px solid rgba(180, 130, 255, 0.25);
|
||||
color: #c49dff;
|
||||
}
|
||||
|
||||
/* ── Business: Gold ── */
|
||||
.class-business-global {
|
||||
background: rgba(184, 134, 11, 0.15);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
color: #ffc107;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── First: Platinum ── */
|
||||
.class-first-global {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(160, 175, 190, 0.15) 0%,
|
||||
rgba(210, 225, 240, 0.22) 45%,
|
||||
rgba(150, 168, 185, 0.12) 100%
|
||||
);
|
||||
border: 1px solid rgba(210, 225, 240, 0.45);
|
||||
color: #ddeeff;
|
||||
text-shadow: 0 0 10px rgba(200, 220, 245, 0.6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* ── Private: Platinum (more intense) ── */
|
||||
.class-private-global {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(160, 175, 190, 0.15) 0%,
|
||||
rgba(210, 225, 240, 0.22) 45%,
|
||||
rgba(150, 168, 185, 0.12) 100%
|
||||
);
|
||||
border: 1px solid rgba(210, 225, 240, 0.45);
|
||||
color: #ddeeff;
|
||||
text-shadow: 0 0 10px rgba(200, 220, 245, 0.6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.class-generic-global{
|
||||
background: var(--accent-glow, rgba(255,193,7,0.1));
|
||||
border: 1px solid var(--accent-soft, rgba(255,193,7,0.25));
|
||||
color: var(--accent, #ffc107);
|
||||
}
|
||||
|
||||
.class-private-global::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 30%,
|
||||
rgba(230, 242, 255, 0.6) 50%,
|
||||
transparent 70%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: class-shimmer 1.8s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.class-unspecified-global {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
color: #445566;
|
||||
}
|
||||
|
||||
@keyframes class-shimmer {
|
||||
0% { background-position: 200% center; }
|
||||
100% { background-position: -200% center; }
|
||||
}
|
||||
|
||||
.class-unspecified-global {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
color: #445566;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { Aircraft } from "@/Types/types";
|
||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
defineProps<{
|
||||
aircraft: Aircraft
|
||||
}>()
|
||||
|
||||
function formatWtc(wtc: string): string {
|
||||
switch (wtc.toUpperCase()) {
|
||||
case 'L': return 'Light'
|
||||
case 'M': return 'Medium'
|
||||
case 'H': return 'Heavy'
|
||||
case 'J': return 'Super'
|
||||
default: return wtc
|
||||
}
|
||||
}
|
||||
|
||||
function formatEngineType(type: string): string {
|
||||
return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GlassTooltip>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<div style="cursor:pointer" v-bind="tooltipProps">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="tooltip-header">
|
||||
<span class="designator">{{ aircraft.manufacturer_code }} {{ aircraft.model_full_name }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tooltip-name">
|
||||
<InlineBadge variant="generic">
|
||||
{{aircraft.designator }}
|
||||
</InlineBadge>
|
||||
<InlineBadge variant="generic">
|
||||
{{ formatWtc(aircraft.wtc) }}
|
||||
</InlineBadge>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-divider" />
|
||||
|
||||
<div class="tooltip-rows">
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Engines</span>
|
||||
<span class="tooltip-value">{{ aircraft.engine_count }}x {{ formatEngineType(aircraft.engine_type) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.designator {
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tooltip-name {
|
||||
display:flex;
|
||||
gap: 4px;
|
||||
margin-top: 0.2rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
|
||||
.tooltip-divider {
|
||||
height: 1px;
|
||||
background: var(--table-border, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.tooltip-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tooltip-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tooltip-label {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--muted, #445566);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tooltip-value {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text, #c8cdd8);
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,8 @@
|
||||
import {Airline, SharedProps} from "@/Types/types";
|
||||
import {computed} from "vue";
|
||||
import {usePage} from "@inertiajs/vue3";
|
||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
airline: Airline | null;
|
||||
@@ -22,44 +24,36 @@ const size = computed(() => props.size ? props.size + 'px' : '30px');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tooltip location="top" :open-delay="200">
|
||||
<GlassTooltip>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<span class="airline-logo" v-bind="tooltipProps"></span>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div v-if="airline" class="airline-tooltip glass glass-border">
|
||||
<div class="tooltip-header">
|
||||
<div v-if="airline" class="airline-tooltip-content">
|
||||
<div class="tooltip-header">
|
||||
<div class="logo-title">
|
||||
<span class="airline-logo-tooltip" :style="logoStyle"></span>
|
||||
<span class="airline-name">{{ airline.name }}</span>
|
||||
<span v-if="airline.IATA_code || airline.ICAO_code" class="codes">
|
||||
<span v-if="airline.IATA_code" class="code-badge">{{ airline.IATA_code }}</span>
|
||||
<span v-if="airline.ICAO_code" class="code-badge">{{ airline.ICAO_code }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tooltip-divider"></div>
|
||||
<div class="tooltip-meta">
|
||||
<span
|
||||
v-if="airline.country"
|
||||
:class="`fi fi-${airline.country.code.toLowerCase()}`"
|
||||
class="country-flag"
|
||||
></span>
|
||||
<span v-if="airline.country" class="country-name">{{ airline.country.name }}</span>
|
||||
<div v-if="airline.IATA_code || airline.ICAO_code" class="codes">
|
||||
<InlineBadge v-if="airline.IATA_code" variant="generic">{{ airline.IATA_code }}</InlineBadge>
|
||||
<InlineBadge v-if="airline.ICAO_code" variant="generic">{{ airline.ICAO_code }}</InlineBadge>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="airline-tooltip muted">Unknown airline</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div class="tooltip-divider"></div>
|
||||
<div class="tooltip-meta">
|
||||
<span
|
||||
v-if="airline.country"
|
||||
:class="`fi fi-${airline.country.code.toLowerCase()}`"
|
||||
class="country-flag"
|
||||
></span>
|
||||
<span v-if="airline.country" class="country-name">{{ airline.country.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.v-overlay__content) {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
span.airline-logo {
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
@@ -69,28 +63,26 @@ span.airline-logo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.airline-tooltip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
.airline-tooltip-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.tooltip-header {
|
||||
.logo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.codes{
|
||||
width:100%;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding-top:1em;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
|
||||
|
||||
.airline-name {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
@@ -121,18 +113,4 @@ span.airline-logo {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.code-badge {
|
||||
background: var(--accent-glow);
|
||||
border: 1px solid var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.72rem;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import { Airport } from "@/Types/types";
|
||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
defineProps<{
|
||||
airport: Airport
|
||||
}>()
|
||||
|
||||
function formatElevation(ft: number | null): string | null {
|
||||
if (ft === null) return null
|
||||
return `${ft.toLocaleString()} ft`
|
||||
}
|
||||
|
||||
function formatType(type: string): string {
|
||||
return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GlassTooltip>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<div v-bind="tooltipProps" style="cursor:pointer">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="airport-tooltip">
|
||||
<!-- Header -->
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-codes">
|
||||
<div><span class="iata">{{ airport.name }}</span></div>
|
||||
<div class="code-badges">
|
||||
<InlineBadge v-if="airport.iata_code" variant="generic">{{ airport.iata_code }}</InlineBadge>
|
||||
<InlineBadge v-if="airport.icao_code" variant="generic">{{ airport.icao_code }}</InlineBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip-divider" />
|
||||
|
||||
<!-- Details -->
|
||||
<div class="tooltip-rows">
|
||||
<div v-if="airport.municipality" class="tooltip-row">
|
||||
<span class="tooltip-label">City</span>
|
||||
<span class="tooltip-value">{{ airport.municipality }}</span>
|
||||
</div>
|
||||
<div v-if="airport.municipality" class="tooltip-row">
|
||||
<span class="tooltip-label">Country</span>
|
||||
<span class="tooltip-value" v-if="airport.region?.country">
|
||||
<span>{{airport.region.country.name}} </span>
|
||||
<span class="fi" :class="`fi-${airport.region.country.code.toLowerCase()}`"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="airport.elevation_ft !== null" class="tooltip-row">
|
||||
<span class="tooltip-label">Elevation</span>
|
||||
<span class="tooltip-value">{{ formatElevation(airport.elevation_ft) }}</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Timezone</span>
|
||||
<span class="tooltip-value tz">{{ airport.timezone }}</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Coordinates</span>
|
||||
<span class="tooltip-value coords">
|
||||
{{ airport.latitude_deg.toFixed(4) }}, {{ airport.longitude_deg.toFixed(4) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.airport-tooltip {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.fi{
|
||||
padding:0.25em;
|
||||
}
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.iata{
|
||||
font-size:1.2em;
|
||||
}
|
||||
|
||||
.tooltip-codes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.code-badges {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tooltip-divider {
|
||||
height: 1px;
|
||||
background: var(--table-border, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.tooltip-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tooltip-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tooltip-label {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--muted, #445566);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tooltip-value {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text, #c8cdd8);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tz {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.coords {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted, #556677);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
|
||||
defineProps<{
|
||||
flight: Flight
|
||||
}>()
|
||||
|
||||
function classKey(flight: Flight): string {
|
||||
const n = flight.flight_class?.name?.toLowerCase() ?? ''
|
||||
if (n.includes('private')) return 'private'
|
||||
if (n.includes('first')) return 'first'
|
||||
if (n.includes('business')) return 'business'
|
||||
if (n.includes('premium')) return 'premium'
|
||||
return 'economy'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="boarding-pass">
|
||||
<div class="pass-header" :class="`class-${classKey(flight)}-global`">
|
||||
<span v-if="flight.flight_class?.name !== 'Unspecified'" class="pass-header-class">
|
||||
{{ flight.flight_class?.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pass-body">
|
||||
<div class="pass-route">
|
||||
<div class="pass-endpoint">
|
||||
<AirportToolTip :airport="flight.departure_airport">
|
||||
<div class="pass-iata">{{ flight.departure_airport.iata_code }}</div>
|
||||
</AirportToolTip>
|
||||
<div class="pass-endpoint-city">{{ flight.departure_airport.municipality }}</div>
|
||||
<div class="pass-endpoint-date">{{ flight.departure_date_display }}</div>
|
||||
<div class="pass-endpoint-time">{{ flight.departure_time_display }}</div>
|
||||
</div>
|
||||
|
||||
<div class="pass-centre">
|
||||
<div class="pass-plane-icon">✈</div>
|
||||
<AirlineLogo :airline="flight.airline" size="44" class="pass-logo" />
|
||||
<div class="pass-flight-number">{{ flight.flight_number }}</div>
|
||||
<div class="pass-airline-name">{{ flight.airline?.name }}</div>
|
||||
<AircraftToolTip v-if="flight.aircraft?.designator" :aircraft="flight.aircraft">
|
||||
<div v-if="flight.aircraft?.designator" class="pass-aircraft">{{ flight.aircraft.manufacturer_code}} {{flight.aircraft.model_full_name}}</div>
|
||||
|
||||
</AircraftToolTip>
|
||||
</div>
|
||||
|
||||
<div class="pass-endpoint pass-endpoint--right">
|
||||
<AirportToolTip :airport="flight.arrival_airport">
|
||||
<div class="pass-iata">{{ flight.arrival_airport.iata_code }}</div>
|
||||
</AirportToolTip>
|
||||
<div class="pass-endpoint-city">{{ flight.arrival_airport.municipality }}</div>
|
||||
<div class="pass-endpoint-date">{{ flight.arrival_date_display ?? flight.departure_date_display }}</div>
|
||||
<div class="pass-endpoint-time">
|
||||
{{ flight.arrival_time_display }}
|
||||
<span v-if="flight.arrival_day_difference" class="pass-daydiff">+{{ flight.arrival_day_difference }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="flight.seat_number" class="pass-meta-row">
|
||||
<span class="pass-meta-pill pass-meta-pill--seat">SEAT {{ flight.seat_number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.boarding-pass {
|
||||
background: #181b24;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.pass-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0.5rem 0.85rem;
|
||||
min-height: 2rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.pass-header-class {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pass-body {
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
}
|
||||
|
||||
.pass-route {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pass-endpoint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.pass-endpoint--right {
|
||||
text-align: right;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.pass-iata {
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #f0f2f5;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pass-endpoint-city {
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
color: #778899;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pass-endpoint-date {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: #445566;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.pass-endpoint-time {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 1rem;
|
||||
color: #c8cdd8;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.pass-centre {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.pass-plane-icon {
|
||||
font-size: 0.9rem;
|
||||
color: #445566;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.pass-logo { opacity: 0.9; }
|
||||
|
||||
.pass-flight-number {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.pass-airline-name {
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-size: 0.72rem;
|
||||
color: #778899;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pass-aircraft {
|
||||
font-size: 0.65rem;
|
||||
color: #445566;
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.pass-daydiff {
|
||||
font-size: 0.65rem;
|
||||
color: #ffc107;
|
||||
margin-left: 0.15rem;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.pass-meta-row {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pass-meta-pill {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(255,193,7,0.25);
|
||||
background: rgba(255,193,7,0.07);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.pass-meta-pill--seat {
|
||||
border-color: rgba(200,205,216,0.2);
|
||||
background: rgba(200,205,216,0.06);
|
||||
color: #c8cdd8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<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[]
|
||||
}>()
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,451 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import {Flight} from "@/Types/types";
|
||||
import { computed, ref } from "vue";
|
||||
import type { DataTableSortItem } from 'vuetify';
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
flights: Flight[]
|
||||
}>()
|
||||
|
||||
const headers = [
|
||||
{ title: '', key: 'airline', sortable: true },
|
||||
{ title: 'FLIGHT', key: 'flight_number', sortable: true },
|
||||
{ title: 'FROM', key: 'departure_airport', sortable: true },
|
||||
{ title: 'TO', key: 'arrival_airport', sortable: true },
|
||||
{ title: 'DATE', key: 'departure_date', sortable: true },
|
||||
{ title: 'DEPART', key: 'departure_time_display', sortable: false },
|
||||
{ title: 'ARRIVE', key: 'arrival_time_display', sortable: false },
|
||||
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
||||
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
||||
]
|
||||
|
||||
const CLASS_ORDER: Record<string, number> = {
|
||||
'Unspecified': 0,
|
||||
'Economy': 1,
|
||||
'Premium Economy': 2,
|
||||
'Business': 3,
|
||||
'First': 4,
|
||||
'Private': 5
|
||||
}
|
||||
|
||||
const customKeySort = {
|
||||
flight_class: (a: Flight['flight_class'], b: Flight['flight_class']) => {
|
||||
return (CLASS_ORDER[a?.name ?? ''] ?? -1) - (CLASS_ORDER[b?.name ?? ''] ?? -1)
|
||||
},
|
||||
airline: (a: Flight['airline'], b: Flight['airline']) => {
|
||||
return (a?.IATA_code ?? '').localeCompare(b?.IATA_code ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
// Track active sort state
|
||||
const sortBy = ref<DataTableSortItem[]>([])
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const isSorting = computed(() => sortBy.value.length > 0)
|
||||
|
||||
// Split flights into upcoming vs departed, sorted by date within each group
|
||||
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())
|
||||
)
|
||||
|
||||
// Flat ordered list with group markers injected — used only when not sorting
|
||||
type GroupedFlight = Flight & { _group?: 'upcoming' | 'departed'; _groupHeader?: boolean; _groupLabel?: string }
|
||||
|
||||
const groupedItems = computed<GroupedFlight[]>(() => {
|
||||
const result: GroupedFlight[] = []
|
||||
|
||||
if (upcomingFlights.value.length > 0) {
|
||||
result.push({ _groupHeader: true, _groupLabel: 'UPCOMING FLIGHTS', _group: 'upcoming' } as GroupedFlight)
|
||||
upcomingFlights.value.forEach(f => result.push({ ...f, _group: 'upcoming' }))
|
||||
}
|
||||
|
||||
if (departedFlights.value.length > 0) {
|
||||
result.push({ _groupHeader: true, _groupLabel: 'DEPARTED FLIGHTS', _group: 'departed' } as GroupedFlight)
|
||||
departedFlights.value.forEach(f => result.push({ ...f, _group: 'departed' }))
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// When sorting, pass all flights flat; when not sorting, pass grouped (headers will be rendered via #item slot trick)
|
||||
const tableItems = computed(() =>
|
||||
isSorting.value ? props.flights : groupedItems.value
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:custom-key-sort="customKeySort"
|
||||
:items="tableItems"
|
||||
:items-per-page="25"
|
||||
v-model:sort-by="sortBy"
|
||||
class="departures-table"
|
||||
hover
|
||||
>
|
||||
<!-- ── GROUP HEADER ROW (injected as a fake item) ── -->
|
||||
<template #item="{ item, columns }">
|
||||
<!-- Section divider row -->
|
||||
<template v-if="(item as any)._groupHeader && !isSorting">
|
||||
<tr class="section-header-row">
|
||||
<td :colspan="columns.length" class="section-header-cell">
|
||||
<div class="section-header-inner" :class="(item as any)._group">
|
||||
<span class="section-header-icon">
|
||||
{{ (item as any)._group === 'upcoming' ? '◈' : '◉' }}
|
||||
</span>
|
||||
<span class="section-header-label">{{ (item as any)._groupLabel }}</span>
|
||||
<span class="section-header-count">
|
||||
{{
|
||||
(item as any)._group === 'upcoming'
|
||||
? upcomingFlights.length
|
||||
: departedFlights.length
|
||||
}} {{ (item as any)._group === 'upcoming' ? 'scheduled' : 'logged' }}
|
||||
</span>
|
||||
<div class="section-header-line" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Normal data row -->
|
||||
<template v-else-if="!(item as any)._groupHeader">
|
||||
<tr
|
||||
class="v-data-table__tr"
|
||||
:class="(item as any)._group && !isSorting ? `group-row--${(item as any)._group}` : ''"
|
||||
>
|
||||
<!-- Airline logo -->
|
||||
<td class="v-data-table__td airline-logo-cell">
|
||||
<AirlineLogo size="32" :airline="(item as any).airline" class="airline-logo-img" />
|
||||
</td>
|
||||
|
||||
<!-- Flight number -->
|
||||
<td class="v-data-table__td flight-number-cell">
|
||||
<div class="flight-cell">
|
||||
<span class="flight-number">{{ (item as any).flight_number }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Departure airport -->
|
||||
<td class="v-data-table__td">
|
||||
<AirportToolTip :airport="(item as Flight).departure_airport">
|
||||
<span class="iata">{{ (item as Flight).departure_airport.iata_code }}</span><br/>
|
||||
</AirportToolTip>
|
||||
<span class="city-name">{{ (item as Flight).departure_airport.municipality }}</span>
|
||||
|
||||
</td>
|
||||
|
||||
<!-- Arrival airport -->
|
||||
<td class="v-data-table__td">
|
||||
<AirportToolTip :airport="(item as Flight).arrival_airport">
|
||||
<span class="iata">{{ (item as Flight).arrival_airport.iata_code }}</span><br/>
|
||||
</AirportToolTip>
|
||||
<span class="city-name">{{ (item as Flight).arrival_airport.municipality }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Departure date -->
|
||||
<td class="v-data-table__td">
|
||||
<span class="date-cell">{{ (item as Flight).departure_date_display }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Departure time -->
|
||||
<td class="v-data-table__td">
|
||||
<span class="time-cell">{{ (item as Flight).departure_time_display }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Arrival time -->
|
||||
<td class="v-data-table__td">
|
||||
<span class="time-cell">{{ (item as Flight).arrival_time_display }}</span>
|
||||
<sup v-if="(item as Flight).arrival_day_difference" class="day-diff">
|
||||
+{{ (item as Flight).arrival_day_difference }}
|
||||
</sup>
|
||||
</td>
|
||||
|
||||
<!-- Aircraft -->
|
||||
<td class="v-data-table__td">
|
||||
<AircraftToolTip v-if="(item as any).aircraft" :aircraft="(item as any).aircraft">
|
||||
<span class="mono-tag">{{ (item as any).aircraft?.designator }}</span>
|
||||
</AircraftToolTip>
|
||||
</td>
|
||||
|
||||
<!-- Class -->
|
||||
<td class="v-data-table__td ">
|
||||
<span class="class-cell">
|
||||
<FlightClassBadge :flight="(item as Flight)" />
|
||||
<InlineBadge v-if="(item as Flight).seat_number" variant="economy">{{(item as Flight).seat_number}}</InlineBadge>
|
||||
<InlineBadge v-if="(item as Flight).seat_type?.name && (item as Flight).seat_type?.name !== 'Unspecified'" variant="economy">{{(item as Flight).seat_type?.name}}</InlineBadge>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Empty state -->
|
||||
<template #no-data>
|
||||
<div class="no-data">
|
||||
<span>NO FLIGHTS ON RECORD</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Vuetify table overrides ── */
|
||||
.departures-table {
|
||||
background: transparent !important;
|
||||
color: #c8cdd8 !important;
|
||||
font-family: 'Barlow', sans-serif !important;
|
||||
}
|
||||
|
||||
/* Header row */
|
||||
:deep(.v-data-table-header__content) {
|
||||
font-family: 'Share Tech Mono', monospace !important;
|
||||
font-size: 0.68rem !important;
|
||||
letter-spacing: 0.14em !important;
|
||||
color: #ffc107 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__th) {
|
||||
background: transparent !important;
|
||||
border-bottom: 1px solid rgba(255,193,7,0.15) !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
}
|
||||
|
||||
/* Body rows — alternating, no borders */
|
||||
:deep(.v-data-table__tr:nth-child(odd)) {
|
||||
background: rgba(255,255,255,0.025) !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__tr:nth-child(even)) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__tr:hover td) {
|
||||
background: rgba(255,193,7,0.05) !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__td) {
|
||||
border: none !important;
|
||||
padding: 0.7rem 1rem !important;
|
||||
font-size: 0.88rem !important;
|
||||
color: #c8cdd8 !important;
|
||||
}
|
||||
|
||||
/* Footer / pagination */
|
||||
:deep(.v-data-table-footer) {
|
||||
background: transparent !important;
|
||||
border-top: 1px solid rgba(255,255,255,0.06) !important;
|
||||
color: #556 !important;
|
||||
font-family: 'Share Tech Mono', monospace !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-footer .v-btn) {
|
||||
color: #778 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-footer .v-btn--active) {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* Sort icon colour */
|
||||
:deep(.v-data-table-header__sort-icon) {
|
||||
color: #ffc107 !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── Section header rows ── */
|
||||
.section-header-row {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.section-header-cell {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.section-header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem 1rem 0.6rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Upcoming = amber accent */
|
||||
.section-header-inner.upcoming .section-header-icon,
|
||||
.section-header-inner.upcoming .section-header-label {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.section-header-inner.upcoming .section-header-line {
|
||||
background: linear-gradient(to right, rgba(255,193,7,0.4), transparent);
|
||||
}
|
||||
|
||||
/* Departed = muted slate */
|
||||
.section-header-inner.departed .section-header-icon,
|
||||
.section-header-inner.departed .section-header-label {
|
||||
color: #778899;
|
||||
}
|
||||
|
||||
.section-header-inner.departed .section-header-line {
|
||||
background: linear-gradient(to right, rgba(119,136,153,0.35), transparent);
|
||||
}
|
||||
|
||||
.section-header-icon {
|
||||
font-size: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-header-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-header-count {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.1em;
|
||||
color: #445566;
|
||||
flex-shrink: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-header-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
/* ── Airline + flight number fused cells ── */
|
||||
:deep(.v-data-table__th:nth-child(1)) {
|
||||
width: 50px !important;
|
||||
min-width: 50px !important;
|
||||
max-width: 50px !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__th:nth-child(2)) {
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
|
||||
.airline-logo-cell {
|
||||
width: 50px !important;
|
||||
min-width: 50px !important;
|
||||
max-width: 50px !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.flight-number-cell {
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* ── Cell styles ── */
|
||||
.flight-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.airline-logo-img {
|
||||
opacity: 0.85;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flight-number {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.iata {
|
||||
display: inline-block;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.08em;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.city-name {
|
||||
font-size: 0.75rem;
|
||||
color: #556;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #9aa;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #c8cdd8;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.day-diff {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
color: #ffc107;
|
||||
letter-spacing: 0.04em;
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
|
||||
.mono-tag {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #778899;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.seat-cell {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #c8cdd8;
|
||||
}
|
||||
|
||||
.class-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Empty state ── */
|
||||
.no-data {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.2em;
|
||||
color: #445;
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {FlightClass} from "@/Types/types";
|
||||
import {Flight} from "@/Types/types";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
defineProps<{
|
||||
flightClass: FlightClass | null
|
||||
const props = defineProps<{
|
||||
flight: Flight
|
||||
}>();
|
||||
|
||||
type BadgeVariant = 'first' | 'business' | 'premium' | 'economy' | 'private' | 'unspecified';
|
||||
|
||||
function classVariant(name?: string | null): BadgeVariant {
|
||||
const n = name?.toLowerCase() ?? '';
|
||||
if (n.includes('private')) return 'private';
|
||||
if (n.includes('first')) return 'first';
|
||||
if (n.includes('business')) return 'business';
|
||||
if (n.includes('premium')) return 'premium';
|
||||
if (n.includes('economy')) return 'economy';
|
||||
return 'unspecified';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="class-badge"
|
||||
:class="{
|
||||
'class-first': flightClass?.name?.toLowerCase().includes('first'),
|
||||
'class-business': flightClass?.name?.toLowerCase().includes('business'),
|
||||
'class-premium': flightClass?.name?.toLowerCase().includes('premium'),
|
||||
'class-economy': flightClass?.name?.toLowerCase().includes('economy') && !flightClass?.name?.toLowerCase().includes('premium'),
|
||||
'class-private': flightClass?.name?.toLowerCase().includes('private'),
|
||||
'class-unspecified': flightClass?.name?.toLowerCase().includes('unspecified'),
|
||||
}"
|
||||
>
|
||||
{{ flightClass?.name ?? '—' }}
|
||||
</span>
|
||||
<InlineBadge :variant="classVariant(flight.flight_class?.name)">
|
||||
{{ flight.flight_class?.name }}
|
||||
</InlineBadge>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.class-badge {
|
||||
display: inline-block;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.class-first {
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
color: #ffc107;
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.class-business {
|
||||
background: rgba(100, 180, 255, 0.1);
|
||||
color: #64b4ff;
|
||||
border: 1px solid rgba(100, 180, 255, 0.25);
|
||||
}
|
||||
|
||||
.class-premium {
|
||||
background: rgb(75, 32, 137, 0.1);
|
||||
color: #c49dff;
|
||||
border: 1px solid rgba(180, 130, 255, 0.25);
|
||||
}
|
||||
|
||||
.class-economy {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #778;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.class-unspecified {
|
||||
display: none;
|
||||
background: transparent;
|
||||
color: #778;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.class-private {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 160, 0, 0.2) 0%,
|
||||
rgba(255, 220, 80, 0.3) 45%,
|
||||
rgba(255, 160, 0, 0.15) 100%
|
||||
);
|
||||
color: #ffc107;
|
||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||
text-shadow: 0 0 8px rgba(255, 193, 7, 0.6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.class-private::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 40%,
|
||||
rgba(255, 220, 100, 0.45) 50%,
|
||||
transparent 60%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import {Flight} from "@/Types/types";
|
||||
|
||||
const props = defineProps<{
|
||||
flights: Flight[]
|
||||
}>()
|
||||
|
||||
const headers = [
|
||||
{ title: 'FLIGHT', key: 'flight_number', sortable: true },
|
||||
{ title: 'FROM', key: 'departure_airport', sortable: true },
|
||||
{ title: 'TO', key: 'arrival_airport', sortable: true },
|
||||
{ title: 'DATE', key: 'departure_date', sortable: true },
|
||||
{ title: 'AIRLINE', key: 'airline', sortable: true },
|
||||
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
||||
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
||||
{ title: 'SEAT', key: 'seat_number', sortable: false },
|
||||
]
|
||||
|
||||
const CLASS_ORDER: Record<string, number> = {
|
||||
'Unspecified': 0,
|
||||
'Economy': 1,
|
||||
'Premium Economy': 2,
|
||||
'Business': 3,
|
||||
'First': 4,
|
||||
'Private': 5
|
||||
}
|
||||
|
||||
const customKeySort = {
|
||||
flight_class: (a: Flight['flight_class'], b: Flight['flight_class']) => {
|
||||
return (CLASS_ORDER[a?.name ?? ''] ?? -1) - (CLASS_ORDER[b?.name ?? ''] ?? -1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:custom-key-sort="customKeySort"
|
||||
:items="flights"
|
||||
:items-per-page="25"
|
||||
class="departures-table"
|
||||
hover
|
||||
>
|
||||
<!-- Flight number + logo -->
|
||||
<template #item.flight_number="{ item }">
|
||||
<div class="flight-cell">
|
||||
<AirlineLogo size="32" :airline="item.airline" class="airline-logo-img" />
|
||||
<span class="flight-number">{{ item.flight_number}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Departure -->
|
||||
<template #item.departure_airport="{ item }">
|
||||
<span class="iata">{{ item.departure_airport.iata_code }}</span><br/>
|
||||
<span class="city-name">{{ item.departure_airport.municipality }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Arrival -->
|
||||
<template #item.arrival_airport="{ item }">
|
||||
<span class="iata">{{ item.arrival_airport.iata_code }}</span><br/>
|
||||
<span class="city-name">{{ item.arrival_airport.municipality }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Date -->
|
||||
<template #item.departure_date="{ item }">
|
||||
<span class="date-cell">{{ item.departure_date}}</span>
|
||||
</template>
|
||||
|
||||
<!-- Airline -->
|
||||
<template #item.airline="{ item }">
|
||||
<span class="airline-name">{{ item.airline?.name }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Aircraft -->
|
||||
<template #item.aircraft="{ item }">
|
||||
<span class="mono-tag">{{ item.aircraft?.designator }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Class -->
|
||||
<template #item.flight_class="{ item }">
|
||||
<FlightClassBadge :flight-class="item.flight_class" />
|
||||
</template>
|
||||
|
||||
<!-- Seat -->
|
||||
<template #item.seat_number="{ item }">
|
||||
<span class="seat-cell">{{ item.seat_number}}</span>
|
||||
</template>
|
||||
|
||||
<!-- Empty state -->
|
||||
<template #no-data>
|
||||
<div class="no-data">
|
||||
<span>NO FLIGHTS ON RECORD</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Vuetify table overrides ── */
|
||||
.departures-table {
|
||||
background: transparent !important;
|
||||
color: #c8cdd8 !important;
|
||||
font-family: 'Barlow', sans-serif !important;
|
||||
}
|
||||
|
||||
/* Header row */
|
||||
:deep(.v-data-table-header__content) {
|
||||
font-family: 'Share Tech Mono', monospace !important;
|
||||
font-size: 0.68rem !important;
|
||||
letter-spacing: 0.14em !important;
|
||||
color: #ffc107 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__th) {
|
||||
background: transparent !important;
|
||||
border-bottom: 1px solid rgba(255,193,7,0.15) !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
}
|
||||
|
||||
/* Body rows — alternating, no borders */
|
||||
:deep(.v-data-table__tr:nth-child(odd)) {
|
||||
background: rgba(255,255,255,0.025) !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__tr:nth-child(even)) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__tr:hover td) {
|
||||
background: rgba(255,193,7,0.05) !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__td) {
|
||||
border: none !important;
|
||||
padding: 0.7rem 1rem !important;
|
||||
font-size: 0.88rem !important;
|
||||
color: #c8cdd8 !important;
|
||||
}
|
||||
|
||||
/* Footer / pagination */
|
||||
:deep(.v-data-table-footer) {
|
||||
background: transparent !important;
|
||||
border-top: 1px solid rgba(255,255,255,0.06) !important;
|
||||
color: #556 !important;
|
||||
font-family: 'Share Tech Mono', monospace !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-footer .v-btn) {
|
||||
color: #778 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-footer .v-btn--active) {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* Sort icon colour */
|
||||
:deep(.v-data-table-header__sort-icon) {
|
||||
color: #ffc107 !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── Cell styles ── */
|
||||
.flight-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.airline-logo-img {
|
||||
opacity: 0.85;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flight-number {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.iata {
|
||||
display: inline-block;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 0.08em;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.city-name {
|
||||
font-size: 0.75rem;
|
||||
color: #556;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #9aa;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.airline-name {
|
||||
font-size: 0.85rem;
|
||||
color: #b0b8c8;
|
||||
}
|
||||
|
||||
.mono-tag {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #778899;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.seat-cell {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #c8cdd8;
|
||||
}
|
||||
|
||||
/* ── Empty state ── */
|
||||
.no-data {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.2em;
|
||||
color: #445;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import {Anchor} from "vuetify";
|
||||
|
||||
defineProps<{
|
||||
location?: Anchor;
|
||||
openDelay?: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tooltip :location="location ?? 'top'" :open-delay="openDelay ?? 200">
|
||||
<template #activator="activatorScope">
|
||||
<slot name="activator" v-bind="activatorScope" />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="glass-tooltip glass glass-border">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.v-overlay__content) {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
.glass-tooltip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
variant?: 'first' | 'business' | 'premium' | 'economy' | 'private' | 'unspecified' | 'generic';
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="variant !== 'unspecified'" class="class-badge" :class="variant ? `class-${variant}-global` : 'class-economy-global'">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.class-badge {
|
||||
display: inline-block;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -2,9 +2,9 @@
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { Flight } from "@/Types/types";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
import FlightListTable from "@/Components/FlightsGoneBy/FlightListTable.vue";
|
||||
import DepartureBoard from "@/Components/FlightsGoneBy/DepartureBoard.vue";
|
||||
import BoardingPasses from "@/Components/FlightsGoneBy/BoardingPasses.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
defineOptions({
|
||||
layout: MainLayout
|
||||
@@ -19,6 +19,8 @@ defineProps<{
|
||||
flights: Flight[]
|
||||
}>()
|
||||
|
||||
type View = 'board' | 'passes'
|
||||
const activeView = ref<View>('board')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,7 +38,28 @@ defineProps<{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlightListTable :flights="flights" />
|
||||
<!-- View switcher toolbar -->
|
||||
<div class="view-toolbar">
|
||||
<button
|
||||
class="view-btn"
|
||||
:class="{ active: activeView === 'board' }"
|
||||
@click="activeView = 'board'"
|
||||
>
|
||||
<span class="view-btn-icon">▦</span>
|
||||
<span class="view-btn-label">DEPARTURE BOARD</span>
|
||||
</button>
|
||||
<button
|
||||
class="view-btn"
|
||||
:class="{ active: activeView === 'passes' }"
|
||||
@click="activeView = 'passes'"
|
||||
>
|
||||
<span class="view-btn-icon">◫</span>
|
||||
<span class="view-btn-label">BOARDING PASSES</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DepartureBoard v-if="activeView === 'board'" :flights="flights" />
|
||||
<BoardingPasses v-else :flights="flights" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,7 +72,7 @@ defineProps<{
|
||||
background: #0d0f14;
|
||||
padding: 2.5rem 2rem;
|
||||
font-family: 'Barlow', sans-serif;
|
||||
width:100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
@@ -100,5 +123,49 @@ defineProps<{
|
||||
color: #556;
|
||||
}
|
||||
|
||||
/* ── View toolbar ── */
|
||||
.view-toolbar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255,193,7,0.15);
|
||||
border-radius: 3px;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-right: 1px solid rgba(255,193,7,0.15);
|
||||
color: #556;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.15em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: rgba(255,193,7,0.05);
|
||||
color: #9aa;
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgba(255,193,7,0.08);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.view-btn-icon {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
Vendored
+21
@@ -23,6 +23,18 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
|
||||
logo_api_url: string
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
local_code: string
|
||||
country_id: number
|
||||
continent_id: number
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
country: Country
|
||||
}
|
||||
|
||||
export interface Airport {
|
||||
id: number
|
||||
name: string
|
||||
@@ -30,6 +42,7 @@ export interface Airport {
|
||||
longitude_deg: number
|
||||
elevation_ft: number | null
|
||||
region_id: number
|
||||
region?: Region
|
||||
municipality: string | null
|
||||
icao_code: string | null
|
||||
iata_code: string | null
|
||||
@@ -109,11 +122,19 @@ export interface Flight {
|
||||
note: string | null
|
||||
departure_airport: Airport
|
||||
arrival_airport: Airport
|
||||
departure_date_display: string,
|
||||
arrival_date_display: string,
|
||||
departure_time_display: string,
|
||||
arrival_time_display: string,
|
||||
arrival_day_difference: number,
|
||||
airline: Airline | null
|
||||
aircraft: Aircraft | null
|
||||
seat_type: SeatType | null
|
||||
flight_reason: FlightReason | null
|
||||
flight_class: FlightClass | null
|
||||
duration_display: string
|
||||
duration: number
|
||||
distance: number
|
||||
}
|
||||
|
||||
declare module '@inertiajs/vue3' {
|
||||
|
||||
+1
-1
@@ -79,5 +79,5 @@ Route::domain(config('app.api_domain'))->group(function () {
|
||||
});
|
||||
Route::get('airlines/logos/tail/{code}', [LogoController::class, 'getLogoByCode'])
|
||||
->where('code', '[A-Za-z0-9]{2,3}');
|
||||
Route::get('airlines/logos/tail/id/{id}', [LogoController::class, 'getLogoById']);
|
||||
Route::get('airlines/logos/tail/id/{id}', [LogoController::class, 'getLogoById'])->where('id', '[0-9]+');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user