diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php index 6d1447f..83b723b 100644 --- a/app/Http/Controllers/FlightProfileController.php +++ b/app/Http/Controllers/FlightProfileController.php @@ -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(), ]); } } diff --git a/app/Http/Controllers/LogoController.php b/app/Http/Controllers/LogoController.php index a04053a..237bd8e 100644 --- a/app/Http/Controllers/LogoController.php +++ b/app/Http/Controllers/LogoController.php @@ -21,7 +21,7 @@ class LogoController extends Controller ]); } - public function getLogoById(int $id){ + public function getLogoById($id){ $airline = Airline::where('id', $id) ->first(); diff --git a/app/Http/Resources/UserFlightResource.php b/app/Http/Resources/UserFlightResource.php new file mode 100644 index 0000000..20cdc3a --- /dev/null +++ b/app/Http/Resources/UserFlightResource.php @@ -0,0 +1,42 @@ +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, + ]; + } +} diff --git a/app/Models/UserFlight.php b/app/Models/UserFlight.php index 9d15676..09b9616 100644 --- a/app/Models/UserFlight.php +++ b/app/Models/UserFlight.php @@ -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); diff --git a/database/migrations/2026_04_09_011620_correct_korea.php b/database/migrations/2026_04_09_011620_correct_korea.php new file mode 100644 index 0000000..bd4d767 --- /dev/null +++ b/database/migrations/2026_04_09_011620_correct_korea.php @@ -0,0 +1,30 @@ +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]); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 009e84a..6e187ad 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; +} + diff --git a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue new file mode 100644 index 0000000..44f1721 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue @@ -0,0 +1,116 @@ + + + + + + + + + + + + {{ aircraft.manufacturer_code }} {{ aircraft.model_full_name }} + + + + + + {{aircraft.designator }} + + + {{ formatWtc(aircraft.wtc) }} + + + + + + + + Engines + {{ aircraft.engine_count }}x {{ formatEngineType(aircraft.engine_type) }} + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue index 75f203a..7fedc53 100644 --- a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue +++ b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue @@ -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'); - + - - - + + + {{ airline.name }} - - {{ airline.IATA_code }} - {{ airline.ICAO_code }} - - - - - {{ airline.country.name }} + + {{ airline.IATA_code }} + {{ airline.ICAO_code }} - Unknown airline - - + + + + {{ airline.country.name }} + + + diff --git a/resources/js/Components/FlightsGoneBy/AirportToolTip.vue b/resources/js/Components/FlightsGoneBy/AirportToolTip.vue new file mode 100644 index 0000000..fbd753e --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/AirportToolTip.vue @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + {{ airport.name }} + + {{ airport.iata_code }} + {{ airport.icao_code }} + + + + + + + + + City + {{ airport.municipality }} + + + Country + + {{airport.region.country.name}} + + + + + Elevation + {{ formatElevation(airport.elevation_ft) }} + + + Timezone + {{ airport.timezone }} + + + Coordinates + + {{ airport.latitude_deg.toFixed(4) }}, {{ airport.longitude_deg.toFixed(4) }} + + + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/BoardingPass.vue b/resources/js/Components/FlightsGoneBy/BoardingPass.vue new file mode 100644 index 0000000..93c90bc --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/BoardingPass.vue @@ -0,0 +1,218 @@ + + + + + + + {{ flight.flight_class?.name }} + + + + + + + + {{ flight.departure_airport.iata_code }} + + {{ flight.departure_airport.municipality }} + {{ flight.departure_date_display }} + {{ flight.departure_time_display }} + + + + ✈ + + {{ flight.flight_number }} + {{ flight.airline?.name }} + + {{ flight.aircraft.manufacturer_code}} {{flight.aircraft.model_full_name}} + + + + + + + {{ flight.arrival_airport.iata_code }} + + {{ flight.arrival_airport.municipality }} + {{ flight.arrival_date_display ?? flight.departure_date_display }} + + {{ flight.arrival_time_display }} + +{{ flight.arrival_day_difference }} + + + + + + SEAT {{ flight.seat_number }} + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/BoardingPasses.vue b/resources/js/Components/FlightsGoneBy/BoardingPasses.vue new file mode 100644 index 0000000..917e43c --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/BoardingPasses.vue @@ -0,0 +1,188 @@ + + + + + + + + + ◈ + UPCOMING FLIGHTS + {{ upcomingFlights.length }} scheduled + + + + + + + + + ◉ + DEPARTED FLIGHTS + {{ departedFlights.length }} logged + + + + + + + + + + + ◉ + DEPARTED FLIGHTS + {{ departedFlights.length }} logged + + + + + + + + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/DepartureBoard.vue b/resources/js/Components/FlightsGoneBy/DepartureBoard.vue new file mode 100644 index 0000000..7694be8 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/DepartureBoard.vue @@ -0,0 +1,451 @@ + + + + + + + + + + + + + {{ (item as any)._group === 'upcoming' ? '◈' : '◉' }} + + {{ (item as any)._groupLabel }} + + {{ + (item as any)._group === 'upcoming' + ? upcomingFlights.length + : departedFlights.length + }} {{ (item as any)._group === 'upcoming' ? 'scheduled' : 'logged' }} + + + + + + + + + + + + + + + + + + + {{ (item as any).flight_number }} + + + + + + + {{ (item as Flight).departure_airport.iata_code }} + + {{ (item as Flight).departure_airport.municipality }} + + + + + + + {{ (item as Flight).arrival_airport.iata_code }} + + {{ (item as Flight).arrival_airport.municipality }} + + + + + {{ (item as Flight).departure_date_display }} + + + + + {{ (item as Flight).departure_time_display }} + + + + + {{ (item as Flight).arrival_time_display }} + + +{{ (item as Flight).arrival_day_difference }} + + + + + + + {{ (item as any).aircraft?.designator }} + + + + + + + + {{(item as Flight).seat_number}} + {{(item as Flight).seat_type?.name}} + + + + + + + + + + + NO FLIGHTS ON RECORD + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/FlightClassBadge.vue b/resources/js/Components/FlightsGoneBy/FlightClassBadge.vue index f35719b..cb3279f 100644 --- a/resources/js/Components/FlightsGoneBy/FlightClassBadge.vue +++ b/resources/js/Components/FlightsGoneBy/FlightClassBadge.vue @@ -1,103 +1,26 @@ - - {{ flightClass?.name ?? '—' }} - + + {{ flight.flight_class?.name }} + - - diff --git a/resources/js/Components/FlightsGoneBy/FlightListTable.vue b/resources/js/Components/FlightsGoneBy/FlightListTable.vue deleted file mode 100644 index 8488cb7..0000000 --- a/resources/js/Components/FlightsGoneBy/FlightListTable.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - - - {{ item.flight_number}} - - - - - - {{ item.departure_airport.iata_code }} - {{ item.departure_airport.municipality }} - - - - - {{ item.arrival_airport.iata_code }} - {{ item.arrival_airport.municipality }} - - - - - {{ item.departure_date}} - - - - - {{ item.airline?.name }} - - - - - {{ item.aircraft?.designator }} - - - - - - - - - - {{ item.seat_number}} - - - - - - NO FLIGHTS ON RECORD - - - - - - diff --git a/resources/js/Components/FlightsGoneBy/GlassTooltip.vue b/resources/js/Components/FlightsGoneBy/GlassTooltip.vue new file mode 100644 index 0000000..2995dbe --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/GlassTooltip.vue @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/js/Components/FlightsGoneBy/InlineBadge.vue b/resources/js/Components/FlightsGoneBy/InlineBadge.vue new file mode 100644 index 0000000..f731357 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/InlineBadge.vue @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/resources/js/Pages/FlightProfile.vue b/resources/js/Pages/FlightProfile.vue index 2a71d31..2142f06 100644 --- a/resources/js/Pages/FlightProfile.vue +++ b/resources/js/Pages/FlightProfile.vue @@ -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('board') @@ -36,7 +38,28 @@ defineProps<{ - + + + + ▦ + DEPARTURE BOARD + + + ◫ + BOARDING PASSES + + + + + @@ -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; +} diff --git a/resources/js/Types/types.d.ts b/resources/js/Types/types.d.ts index 3a5fdd9..e23c42d 100644 --- a/resources/js/Types/types.d.ts +++ b/resources/js/Types/types.d.ts @@ -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' { diff --git a/routes/web.php b/routes/web.php index 904e6fc..92ed468 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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]+'); });