Added achievement data
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
#[Signature('app:update-departed-flights')]
|
||||
#[Description('Command description')]
|
||||
class UpdateDepartedFlights extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$now = now()->utc();
|
||||
$oneHourAgo = $now->copy()->subHours(1);
|
||||
|
||||
$userFlights = UserFlight::whereBetween('arrival_date', [
|
||||
$oneHourAgo->toDateTimeString(),
|
||||
$now->toDateTimeString(),
|
||||
])
|
||||
->where('auto_update', true)
|
||||
->whereNotNull('flight_number')
|
||||
->get();
|
||||
|
||||
$this->info("Found {$userFlights->count()} flights.");
|
||||
|
||||
foreach ($userFlights as $flight) {
|
||||
// Split "QF22" into ["QF", "22"]
|
||||
preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches);
|
||||
|
||||
if (empty($matches)) {
|
||||
$this->warn("Could not parse flight number: {$flight->flight_number}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$airlineCode = strtoupper($matches[1]);
|
||||
$flightNumber = $matches[2];
|
||||
|
||||
$arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
|
||||
$year = $arrivalDate->year;
|
||||
$month = $arrivalDate->month;
|
||||
$day = $arrivalDate->day;
|
||||
|
||||
$url = "https://www.flightstats.com/v2/api-next/flight-tracker/{$airlineCode}/{$flightNumber}/{$year}/{$month}/{$day}";
|
||||
|
||||
$response = Http::get($url);
|
||||
|
||||
if (!$response->successful()) {
|
||||
$this->warn("Failed to fetch data for {$flight->flight_number}: HTTP {$response->status()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
$flightData = $data['data'] ?? [];
|
||||
|
||||
if (empty($flightData)) {
|
||||
$this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$tailNumber = $flightData['positional']['flexTrack']['tailNumber'] ?? null;
|
||||
$estimatedDepartureUtc = $flightData['schedule']['estimatedActualDepartureUTC'] ?? null;
|
||||
$estimatedArrivalUtc = $flightData['schedule']['estimatedActualArrivalUTC'] ?? null;
|
||||
$equipmentCode = $flightData['additionalFlightInfo']['equipment']['iata'] ?? null;
|
||||
|
||||
$this->info("Flight {$airlineCode}{$flightNumber} — Tail: {$tailNumber}, Equipment: {$equipmentCode}");
|
||||
$this->info("Departure: {$estimatedDepartureUtc} | Arrival: {$estimatedArrivalUtc}");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ class FlightController extends Controller
|
||||
{
|
||||
$this->authorize('delete', $flight);
|
||||
|
||||
$snapshot = $this->flightSnapshot($flight->id);
|
||||
$snapshot = $flight->snapshot($flight->id);
|
||||
|
||||
if(now()->utc()->isBefore($flight->departure_date)){
|
||||
$action = 'flight_deleted';
|
||||
|
||||
@@ -145,7 +145,7 @@ class UserFlight extends Model
|
||||
protected function distance(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->calculateGreatCircleDistance()
|
||||
get: fn() => round($this->calculateGreatCircleDistance())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ class UserFlight extends Model
|
||||
public function liveryUrl(): Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if($this->airline) {
|
||||
if($this->airline && $this->aircraft) {
|
||||
$fileName = "{$this->airline->internal_name}_{$this->aircraft->designator}.png";
|
||||
$file = public_path("img/liveries/generated/$fileName");
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
php artisan config:clear
|
||||
php artisan config:cache
|
||||
php artisan migrate --force
|
||||
|
||||
crond -f -l 8 &
|
||||
php-fpm -D
|
||||
nginx -g 'daemon off;'
|
||||
+8
-4
@@ -1,10 +1,8 @@
|
||||
FROM php:8.4-fpm-alpine
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache nginx curl zip unzip git postgresql-dev nodejs npm \
|
||||
RUN apk add --no-cache nginx curl zip unzip git postgresql-dev nodejs npm dcron \
|
||||
&& docker-php-ext-install pdo pdo_pgsql pgsql opcache
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www
|
||||
@@ -19,6 +17,12 @@ COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
|
||||
|
||||
RUN echo "* * * * * cd /var/www && php artisan schedule:run" \
|
||||
> /etc/crontabs/root
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["sh", "-c", "php-fpm -D && nginx -g 'daemon off;'"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
||||
@@ -2,21 +2,14 @@
|
||||
import { Aircraft } from "@/Types/types";
|
||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import WakeTurbulence from "@/Components/FlightsGoneBy/WakeTurbulence.vue";
|
||||
|
||||
defineProps<{
|
||||
aircraft: Aircraft
|
||||
showTooltips?: boolean
|
||||
}>()
|
||||
|
||||
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())
|
||||
@@ -41,7 +34,7 @@ function formatEngineType(type: string): string {
|
||||
{{aircraft.designator }}
|
||||
</InlineBadge>
|
||||
<InlineBadge variant="generic">
|
||||
{{ formatWtc(aircraft.wtc) }}
|
||||
<WakeTurbulence :value="aircraft.wtc" />
|
||||
</InlineBadge>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const size = computed(() => props.size ? props.size + 'px' : '30px');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="alliance-logo"></span>
|
||||
<span :title="alliance.name" class="alliance-logo"></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import {Flight} from "@/Types/types";
|
||||
import {Flight, User} from "@/Types/types";
|
||||
import { computed, ref, watch, nextTick } from "vue";
|
||||
import type { DataTableSortItem } from 'vuetify';
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
@@ -21,11 +21,17 @@ const props = defineProps<{
|
||||
flightId?: number | null
|
||||
}>()
|
||||
|
||||
console.log(props.user)
|
||||
|
||||
function editRoute(id: number) {
|
||||
return route('flights.edit', { flight: id })
|
||||
}
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
const flightToDelete = ref<Flight | null>(null)
|
||||
const showDeleteDialog = computed({
|
||||
get: () => flightToDelete.value !== null,
|
||||
set: (val) => { if (!val) flightToDelete.value = null }
|
||||
})
|
||||
|
||||
const ITEMS_PER_PAGE = 25
|
||||
|
||||
@@ -263,7 +269,6 @@ watch(
|
||||
</td>
|
||||
|
||||
<td class="v-data-table__td actions-cell">
|
||||
<template>
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
@@ -275,8 +280,8 @@ watch(
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact" bg-color="#1a1e2e">
|
||||
<v-list-item v-if="canEdit"
|
||||
prepend-icon="mdi-pencil-outline"
|
||||
<v-list-item
|
||||
prepend-icon="mdi-magnify"
|
||||
title="View Details"
|
||||
:href="`/u/${user.name}/flight/${(item as Flight).id}`"
|
||||
/>
|
||||
@@ -286,30 +291,32 @@ watch(
|
||||
:href="editRoute((item as Flight).id)"
|
||||
/>
|
||||
<v-list-item v-if="canEdit"
|
||||
prepend-icon="mdi-trash-can-outline"
|
||||
title="Delete"
|
||||
@click="showDeleteDialog = true"
|
||||
prepend-icon="mdi-trash-can-outline"
|
||||
title="Delete"
|
||||
@click="flightToDelete = (item as Flight)"
|
||||
/>
|
||||
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||
<v-dialog v-if="canEdit" v-model="showDeleteDialog" max-width="400">
|
||||
<v-card title="Delete Flight">
|
||||
<v-card-text>Are you sure you want to delete this flight?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn v-if="!deleting" @click="showDeleteDialog = false">Cancel</v-btn>
|
||||
<v-btn v-if="!deleting" @click="flightToDelete = null">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:loading="deleting"
|
||||
@click="deleting = true; router.delete(route('flights.delete', { flight: (item as Flight).id }), { onFinish: () => deleting = false })"
|
||||
@click="deleting = true; router.delete(
|
||||
route('flights.delete', { flight: flightToDelete!.id }),
|
||||
{ onFinish: () => { deleting = false; flightToDelete = null } }
|
||||
)"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
@@ -31,7 +31,6 @@ defineProps<{
|
||||
.glass-tooltip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
|
||||
@@ -26,7 +26,6 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
<header class="glass">
|
||||
<Link href="/" class="brand">FlightsGoneBy</Link>
|
||||
|
||||
<!-- Notification icon (always visible) -->
|
||||
<button v-if="props.auth.user" class="notif-btn" aria-label="Notifications">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -47,9 +46,8 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link">Profile</Link>
|
||||
<Link href="/feed" class="nav-link">Feed</Link>
|
||||
|
||||
<!-- User dropdown -->
|
||||
<div class="dropdown" ref="dropdownRef">
|
||||
<button class="nav-link dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
|
||||
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
||||
{{ props.auth.user.name }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -66,23 +64,23 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
</nav>
|
||||
|
||||
<!-- Hamburger (mobile only) -->
|
||||
<button class="hamburger" :class="{ open: menuOpen }" @click="menuOpen = !menuOpen" aria-label="Toggle menu">
|
||||
<button class="hamburger" :class="{ open: menuOpen }" @click.stop="menuOpen = !menuOpen" aria-label="Toggle menu">
|
||||
<span /><span /><span />
|
||||
</button>
|
||||
|
||||
<!-- Mobile drawer -->
|
||||
<Transition name="slide">
|
||||
<nav v-if="menuOpen" class="nav-mobile" @click="menuOpen = false">
|
||||
<nav v-if="menuOpen" class="nav-mobile" @click.stop>
|
||||
<template v-if="!props.auth.user">
|
||||
<Link :href="route('login')" class="nav-link">Log In</Link>
|
||||
<Link :href="route('register')" class="nav-link nav-link--highlight">Register</Link>
|
||||
<Link :href="route('login')" class="nav-link" @click="menuOpen = false">Log In</Link>
|
||||
<Link :href="route('register')" class="nav-link nav-link--highlight" @click="menuOpen = false">Register</Link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="nav-greeting">Welcome, {{ props.auth.user.name }}</span>
|
||||
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
||||
<Link :href="route('profile.view', { username: props.auth.user.name })" class="nav-link">Profile</Link>
|
||||
<Link href="/feed" class="nav-link nav-link--highlight">Feed</Link>
|
||||
<Link :href="route('import.fr24')" class="nav-link">Import from FR24</Link>
|
||||
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
||||
<Link href="/feed" class="nav-link nav-link--highlight" @click="menuOpen = false">Feed</Link>
|
||||
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
||||
<div class="dropdown-divider" />
|
||||
<button class="nav-link nav-link--danger" @click="logout">Log Out</button>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
|
||||
import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
|
||||
import type {Flight} from "@/Types/types";
|
||||
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
|
||||
import PanelSubHeader from "@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue";
|
||||
|
||||
defineProps<{
|
||||
flight: Flight
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel label="Aircraft">
|
||||
<div class="livery" v-if="flight.livery_url" :style="{ backgroundImage: `url('${flight.livery_url}')` }">
|
||||
</div>
|
||||
<PanelHeader>{{ flight.aircraft?.display_name_short }}</PanelHeader>
|
||||
<PanelSubHeader v-if="flight.aircraft?.manufacturer_code">{{ flight.aircraft.manufacturer_code }}</PanelSubHeader>
|
||||
<DetailRows>
|
||||
<DetailRow v-if="flight.aircraft?.designator" label="Designator" :value="flight.aircraft.designator" variant="Badge" />
|
||||
<DetailRow v-if="flight.aircraft_registration" label="Registration" :value="flight.aircraft_registration" />
|
||||
<DetailRow class="detail-row" v-if="flight.aircraft?.engine_type" label="Engine Type" :value="flight.aircraft?.engine_type" />
|
||||
<DetailRow class="detail-row" v-if="flight.aircraft?.engine_count" label="No. of Engines" :value="flight.aircraft?.engine_count.toString()" />
|
||||
<DetailRow class="detail-row" v-if="flight.aircraft?.wtc" label="Wake Turbulence Category" variant="WTC" :value="flight.aircraft?.wtc" />
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.livery{
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import {Airline} from "@/Types/types";
|
||||
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
|
||||
import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
|
||||
import PanelSubHeader from "@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue";
|
||||
|
||||
defineProps<{
|
||||
airline: Airline
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel label="Airline">
|
||||
<div class="airline-header">
|
||||
<AirlineLogo :airline="airline" class="airline-logo" />
|
||||
<div>
|
||||
<PanelHeader>{{ airline.name }}</PanelHeader>
|
||||
<PanelSubHeader v-if="airline.country?.name"><span class="fi" :class="`fi-${airline.country.code.toLowerCase()}`"></span> {{ airline.country.name }}</PanelSubHeader>
|
||||
</div>
|
||||
</div>
|
||||
<DetailRows>
|
||||
<DetailRow v-if="airline.alliance?.name" label="Alliance" :value="airline.alliance.name" variant="Alliance" :alliance="airline.alliance" />
|
||||
<DetailRow label="IATA" v-if="airline.IATA_code" :value="airline.IATA_code" variant="Badge" />
|
||||
<DetailRow label="ICAO" v-if="airline.IATA_code" :value="airline.ICAO_code" variant="Badge" />
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.airline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.airline-logo-placeholder {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-glow);
|
||||
border: 1px solid rgba(56, 189, 248, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.airline-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import { Airport } from "@/Types/types";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
|
||||
import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
|
||||
import PanelSubHeader from "@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue";
|
||||
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
|
||||
|
||||
defineProps<{
|
||||
label?: string
|
||||
airport: Airport
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel :label="label">
|
||||
<PanelHeader class="airport-name">{{ airport.name }}</PanelHeader>
|
||||
<PanelSubHeader>
|
||||
{{ airport.municipality }},
|
||||
{{ airport.region?.country?.name }} <span :class="`fi fi-${airport.region?.country.code.toLowerCase()}`"></span>
|
||||
</PanelSubHeader>
|
||||
<DetailRows>
|
||||
<DetailRow v-if="airport.iata_code" label="IATA" variant="Badge" :value="airport.iata_code" />
|
||||
<DetailRow v-if="airport.icao_code" label="ICAO" variant="Badge" :value="airport.icao_code" />
|
||||
<DetailRow label="City Served" :value="airport.municipality" />
|
||||
<DetailRow label="Region" v-if="airport.region?.name" :value="airport.region.name" />
|
||||
<DetailRow label="Country" v-if="airport.region?.country" variant="Country" :country="airport.region.country" :value="airport.region.country.name" />
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import {Alliance, Country} from "@/Types/types";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue";
|
||||
import WakeTurbulence from "@/Components/FlightsGoneBy/WakeTurbulence.vue";
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
value: string | null
|
||||
variant?: "Country" | "Badge" | "Alliance" | "WTC"
|
||||
country?: Country
|
||||
alliance?: Alliance
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-row">
|
||||
<span class="detail-key">{{label}}</span>
|
||||
<span class="detail-val" v-if="!variant">{{value ?? ''}}</span>
|
||||
<span class="detail-val" v-if="variant == 'Badge'"><InlineBadge variant="generic">{{value ?? ''}}</InlineBadge></span>
|
||||
<span class="detail-val" v-if="variant == 'Country' && country">{{country.name}} <span :class="`fi fi-${country.code.toLowerCase()}`"></span></span>
|
||||
<span class="detail-val" v-if="variant == 'Alliance' && alliance">{{alliance.name}} <AllianceLogo :alliance="alliance"/> </span>
|
||||
<span class="detail-val" v-if="variant == 'WTC'"><WakeTurbulence :value="value" /> </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-key {
|
||||
color: var(--muted);
|
||||
|
||||
}
|
||||
|
||||
.detail-val {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
display:inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-rows">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel glass glass-border">
|
||||
<div v-if="label" class="panel-label">{{label}}</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.panel-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 0.25rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-header"><slot /></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel-header {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-sub-header"><slot/></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel-sub-header {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight} from "@/Types/types";
|
||||
import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
|
||||
import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
|
||||
|
||||
defineProps<{
|
||||
flight: Flight
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel label="Route">
|
||||
<div class="map-placeholder">
|
||||
<FlightMap :flights="[flight]" />
|
||||
</div>
|
||||
<DetailRows>
|
||||
<DetailRow label="Distance" :value="flight.distance + 'km'" />
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
value: string | null
|
||||
}>()
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{formatWtc(value ?? '')}}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -4,9 +4,12 @@ import {Head} from "@inertiajs/vue3";
|
||||
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
|
||||
import {Achievement, Flight, User, UserAchievement} from "@/Types/types";
|
||||
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||||
import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue";
|
||||
import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import AirportPanel from "@/Components/FlightsGoneBy/Panels/AirportPanel.vue";
|
||||
import AirlinePanel from "@/Components/FlightsGoneBy/Panels/AirlinePanel.vue";
|
||||
import AircraftPanel from "@/Components/FlightsGoneBy/Panels/AircraftPanel.vue";
|
||||
import RoutePanel from "@/Components/FlightsGoneBy/Panels/RoutePanel.vue";
|
||||
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
|
||||
@@ -19,466 +22,60 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`${flight.flight_number ?? user.name + '\'s Flight'}`" />
|
||||
<ProfileLayout
|
||||
:user="user"
|
||||
:is-following="isFollowing"
|
||||
:flight-count="flightCount"
|
||||
:loading="false">
|
||||
<Head :title="`${flight.flight_number ?? user.name + '\'s Flight'}`" />
|
||||
|
||||
<div class="flight-profile">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flight-header glass glass-border">
|
||||
<BoardingPass :showToolTips="false" style="width:100%;max-width:600px" :flight="flight"/>
|
||||
</div>
|
||||
|
||||
<!-- Main grid -->
|
||||
<div class="profile-grid">
|
||||
<RoutePanel :flight="flight" />
|
||||
<Panel label="Flight Details">
|
||||
<BoardingPass :showToolTips="false" style="width:100%;max-width:600px" :flight="flight"/>
|
||||
<DetailRows>
|
||||
|
||||
<!-- Departure -->
|
||||
<div class="panel glass glass-border">
|
||||
<div class="panel-label">Departure</div>
|
||||
<div class="airport-name">{{ flight.departure_airport?.name }}</div>
|
||||
<div class="airport-location">
|
||||
{{ flight.departure_airport?.municipality }},
|
||||
{{ flight.departure_airport?.region?.country?.name }}
|
||||
</div>
|
||||
<div class="detail-rows">
|
||||
<div class="detail-row" v-if="flight.departure_airport?.iata_code">
|
||||
<span class="detail-key">IATA</span>
|
||||
<span class="detail-val mono">{{ flight.departure_airport.iata_code }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.departure_airport?.icao_code">
|
||||
<span class="detail-key">ICAO</span>
|
||||
<span class="detail-val mono">{{ flight.departure_airport.icao_code }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.departure_airport?.region?.name">
|
||||
<span class="detail-key">Region</span>
|
||||
<span class="detail-val">{{ flight.departure_airport.region.name }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.departure_date">
|
||||
<span class="detail-key">Time</span>
|
||||
<span class="detail-val mono">{{ flight.departure_time_display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrival -->
|
||||
<div class="panel glass glass-border">
|
||||
<div class="panel-label">Arrival</div>
|
||||
<div class="airport-name">{{ flight.arrival_airport?.name }}</div>
|
||||
<div class="airport-location">
|
||||
{{ flight.arrival_airport?.municipality }},
|
||||
{{ flight.arrival_airport?.region?.country?.name }}
|
||||
</div>
|
||||
<div class="detail-rows">
|
||||
<div class="detail-row" v-if="flight.arrival_airport?.iata_code">
|
||||
<span class="detail-key">IATA</span>
|
||||
<span class="detail-val mono">{{ flight.arrival_airport.iata_code }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.arrival_airport?.icao_code">
|
||||
<span class="detail-key">ICAO</span>
|
||||
<span class="detail-val mono">{{ flight.arrival_airport.icao_code }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.arrival_airport?.region?.name">
|
||||
<span class="detail-key">Region</span>
|
||||
<span class="detail-val">{{ flight.arrival_airport.region.name }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.arrival_date">
|
||||
<span class="detail-key">Time</span>
|
||||
<span class="detail-val mono">{{ flight.arrival_time_display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Route Map -->
|
||||
<div class="panel glass glass-border panel-map">
|
||||
<div class="panel-label">Route</div>
|
||||
<div class="map-placeholder">
|
||||
<FlightMap :flights="[flight]" />
|
||||
</div>
|
||||
<!-- Slot for a real map library integration -->
|
||||
<!-- <MapboxMap :departure="departureCoords" :arrival="arrivalCoords" /> -->
|
||||
</div>
|
||||
|
||||
<!-- Airline -->
|
||||
<div class="panel glass glass-border" v-if="flight.airline">
|
||||
<div class="panel-label">Airline</div>
|
||||
<div class="airline-header">
|
||||
<AirlineLogo :airline="flight.airline" class="airline-logo" />
|
||||
<div>
|
||||
<div class="airline-name">{{ flight.airline.name }}</div>
|
||||
<div class="airport-location" v-if="flight.airline?.country?.name"><span class="fi" :class="`fi-${flight.airline.country.code.toLowerCase()}`"></span> {{ flight.airline.country.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-rows">
|
||||
<div class="detail-row" v-if="flight.airline?.alliance?.name">
|
||||
<span class="detail-key">Alliance</span>
|
||||
<span class="detail-val"><AllianceLogo :alliance="flight.airline.alliance" />{{ flight.airline.alliance.name }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.airline?.IATA_code">
|
||||
<span class="detail-key">IATA</span>
|
||||
<span class="detail-val mono">{{ flight.airline.IATA_code }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.airline?.ICAO_code">
|
||||
<span class="detail-key">ICAO</span>
|
||||
<span class="detail-val mono">{{ flight.airline.ICAO_code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aircraft -->
|
||||
<div v-if="flight.aircraft" class="panel glass glass-border">
|
||||
<div class="panel-label">Aircraft</div>
|
||||
<div class="livery" v-if="flight.livery_url" :style="{ backgroundImage: `url('${flight.livery_url}')` }">
|
||||
</div>
|
||||
<div class="airport-name">{{ flight.aircraft?.display_name_short }}</div>
|
||||
<div class="airport-location" v-if="flight.aircraft?.manufacturer_code">{{ flight.aircraft.manufacturer_code }}</div>
|
||||
<div class="detail-rows">
|
||||
<div class="detail-row" v-if="flight.aircraft?.designator">
|
||||
<span class="detail-key">Designator</span>
|
||||
<span class="detail-val mono">{{ flight.aircraft?.designator }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.aircraft_registration">
|
||||
<span class="detail-key">Tail No.</span>
|
||||
<span class="detail-val mono">{{ flight.aircraft_registration }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="flight.aircraft?.engine_type">
|
||||
<span class="detail-key">Engines</span>
|
||||
<span class="detail-val">{{ flight.aircraft.engine_type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flight Details -->
|
||||
<div class="panel glass glass-border panel-details">
|
||||
<div class="panel-label">Flight Details</div>
|
||||
<div class="details-grid">
|
||||
<div class="detail-card" v-if="flight.flight_class">
|
||||
<div class="detail-card-label">Class</div>
|
||||
<div class="detail-card-value"
|
||||
:class="flight.flight_class?.internal_name ? `class-${flight.flight_class.internal_name}-global` : ''"
|
||||
style="padding: 0.25rem 0.6rem; border-radius: 4px; display: inline-block;">
|
||||
{{ flight.flight_class?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.seat_type">
|
||||
<div class="detail-card-label">Seat Type</div>
|
||||
<div class="detail-card-value">{{ flight.seat_type?.name }}</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.seat_number">
|
||||
<div class="detail-card-label">Seat</div>
|
||||
<div class="detail-card-value mono">{{ flight.seat_number }}</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.flight_reason">
|
||||
<div class="detail-card-label">Reason</div>
|
||||
<div class="detail-card-value">{{ flight.flight_reason?.name }}</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.crew_type">
|
||||
<div class="detail-card-label">Travelled As</div>
|
||||
<div class="detail-card-value">{{ flight.crew_type?.name }}</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.duration">
|
||||
<div class="detail-card-label">Duration</div>
|
||||
<div class="detail-card-value mono">{{ flight.duration_display }}</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.distance">
|
||||
<div class="detail-card-label">Distance</div>
|
||||
<div class="detail-card-value mono">{{ flight.distance.toLocaleString() }} km</div>
|
||||
</div>
|
||||
<div class="detail-card" v-if="flight.note">
|
||||
<div class="detail-card-label">Notes</div>
|
||||
<div class="detail-card-value notes-text">{{ flight.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DetailRows>
|
||||
</Panel>
|
||||
<AircraftPanel :flight="flight"/>
|
||||
<AirportPanel :airport="flight.departure_airport" label="Departure" />
|
||||
<AirportPanel :airport="flight.arrival_airport" label="Arrival" />
|
||||
<AirlinePanel :airline="flight.airline" v-if="flight.airline" />
|
||||
</div>
|
||||
</div>
|
||||
</ProfileLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.livery{
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.flight-profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
padding: 0; /* removed padding */
|
||||
font-family: 'Barlow', sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.flight-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.route-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.airport-code {
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
font-size: 2.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.route-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--accent);
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.route-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.route-dashes {
|
||||
flex: 1;
|
||||
border-top: 1.5px dashed rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
|
||||
.plane-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flight-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta-sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.flight-number {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: 1fr; /* single column by default (mobile) */
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel-map {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.panel-details {
|
||||
grid-column: span 2;
|
||||
@media (min-width: 600px) {
|
||||
.profile-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.profile-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.panel-map {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.panel-details {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.panel-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 0.25rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.airport-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.airport-location {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Detail rows */
|
||||
.detail-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-key {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.detail-val {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Map placeholder */
|
||||
.map-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem 0;
|
||||
min-height: 160px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.map-bg-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: var(--accent);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.map-route-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.map-arrow {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.map-distance {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Airline */
|
||||
.airline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.airline-logo-placeholder {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-glow);
|
||||
border: 1px solid rgba(56, 189, 248, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.airline-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Details grid */
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
.detail-card-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.detail-card-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.notes-text {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.mono {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -135,7 +135,7 @@ function switchView(view: ProfileView) {
|
||||
<Head :title="`${user.name}'s Flights`" />
|
||||
<ProfileLayout :is-following="isFollowing" :flightCount="flights.length" :user="user" :loading="flightsLoading">
|
||||
<ProfileViewSwitcher :user="user" :active-view="activeView" @update:active-view="switchView" />
|
||||
<DepartureBoard v-if="activeView === 'board'" :flight-id="localSelectedFlightId" :flight-stats="stats" :canEdit="canEdit" />
|
||||
<DepartureBoard v-if="activeView === 'board'" :flight-id="localSelectedFlightId" :flight-stats="stats" :canEdit="canEdit" :user="user" />
|
||||
<BoardingPasses v-if="activeView === 'passes'" :flight-stats="stats" :canEdit="canEdit" />
|
||||
<FlightMapAndCharts
|
||||
v-if="activeView === 'map'"
|
||||
|
||||
@@ -6,3 +6,5 @@ use Illuminate\Support\Facades\Artisan;
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('app:update-departed-flights')->hourly()->runInBackground();
|
||||
|
||||
Reference in New Issue
Block a user