Add more airlines and fix edit bugs
This commit is contained in:
@@ -10,8 +10,7 @@ use Inertia\Inertia;
|
|||||||
|
|
||||||
class FlightProfileController extends Controller
|
class FlightProfileController extends Controller
|
||||||
{
|
{
|
||||||
public function view(string $username)
|
public function profileData(string $username, string $view) : array {
|
||||||
{
|
|
||||||
$user = User::whereRaw(DB::raw('LOWER(name) = ?'), [strtolower($username)])->firstOrFail();
|
$user = User::whereRaw(DB::raw('LOWER(name) = ?'), [strtolower($username)])->firstOrFail();
|
||||||
|
|
||||||
$flights = UserFlight::where('user_id', $user->id)
|
$flights = UserFlight::where('user_id', $user->id)
|
||||||
@@ -29,10 +28,31 @@ class FlightProfileController extends Controller
|
|||||||
->orderBy('departure_date', 'desc')
|
->orderBy('departure_date', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return Inertia::render('FlightProfile', [
|
return [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||||
'flights' => UserFlightResource::collection($flights)->resolve(),
|
'flights' => UserFlightResource::collection($flights)->resolve(),
|
||||||
]);
|
'initialView' => $view,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function departureBoard(string $username){
|
||||||
|
$profileData = $this->profileData($username, 'board');
|
||||||
|
return Inertia::render('UserProfile', $profileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function map(string $username){
|
||||||
|
$profileData = $this->profileData($username, 'map');
|
||||||
|
return Inertia::render('UserProfile', $profileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boardingPasses(string $username){
|
||||||
|
$profileData = $this->profileData($username, 'passes');
|
||||||
|
return Inertia::render('UserProfile', $profileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(string $username)
|
||||||
|
{
|
||||||
|
return $this->departureBoard($username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,58 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Share Tech Mono */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Share Tech Mono';
|
||||||
|
src: url('/fonts/share-tech-mono-v16-latin-regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barlow */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow';
|
||||||
|
src: url('/fonts/barlow-v13-latin_latin-ext-regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow';
|
||||||
|
src: url('/fonts/barlow-v13-latin_latin-ext-500.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow';
|
||||||
|
src: url('/fonts/barlow-v13-latin_latin-ext-600.woff2') format('woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barlow Condensed */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
src: url('/fonts/barlow-condensed-v13-latin-500.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
src: url('/fonts/barlow-condensed-v13-latin-700.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
|
canEdit: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Continents visited</div>
|
title="Continents"
|
||||||
<apexchart
|
|
||||||
v-if="series.length"
|
v-if="series.length"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="280"
|
height="280"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="seriesData"
|
:series="seriesData"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-empty">No continent data available</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div v-if="series.length" class="chart-outer">
|
<div v-if="series.length" class="chart-outer">
|
||||||
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
||||||
<apexchart
|
<UnstyledFlightChart
|
||||||
type="bar"
|
type="bar"
|
||||||
:height="chartHeight"
|
:height="chartHeight"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
@@ -36,6 +36,7 @@ import { computed } from 'vue'
|
|||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
import { useChartTooltip} from "@/Composables/useChartTooltip";
|
import { useChartTooltip} from "@/Composables/useChartTooltip";
|
||||||
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
||||||
|
import UnstyledFlightChart from "@/Components/FlightsGoneBy/UnstyledFlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Flight classes</div>
|
title="Flight Classes"
|
||||||
<apexchart
|
|
||||||
v-if="series.length"
|
v-if="series.length"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="280"
|
height="280"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="seriesData"
|
:series="seriesData"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-empty">No flight class data available</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -87,26 +85,5 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-empty {
|
|
||||||
height: 280px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #445566;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Flight reasons</div>
|
title="Flight Reasons"
|
||||||
<apexchart
|
|
||||||
v-if="series.length"
|
v-if="series.length"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="280"
|
height="280"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="seriesData"
|
:series="seriesData"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-empty">No flight reason data available</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">International vs Domestic</div>
|
title="International vs Domestic"
|
||||||
<apexchart
|
|
||||||
v-if="series.length"
|
v-if="series.length"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="280"
|
height="280"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="seriesData"
|
:series="seriesData"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-empty">No flight data available</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -90,37 +88,4 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-empty {
|
|
||||||
height: 280px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #445566;
|
|
||||||
}
|
|
||||||
|
|
||||||
.charts-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.charts-row > * {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Flights per day of week</div>
|
title="Flights per day of week"
|
||||||
<apexchart
|
|
||||||
type="bar"
|
type="bar"
|
||||||
height="220"
|
height="220"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="series"
|
:series="series"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -97,17 +96,4 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Flights per month</div>
|
title="Flights per month"
|
||||||
<apexchart
|
|
||||||
type="bar"
|
type="bar"
|
||||||
height="220"
|
height="220"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="series"
|
:series="series"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -93,17 +92,4 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Flights per year</div>
|
title="Flights per year"
|
||||||
<apexchart
|
|
||||||
type="bar"
|
type="bar"
|
||||||
height="220"
|
height="220"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="series"
|
:series="series"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -107,17 +106,5 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart-wrap">
|
<FlightChart
|
||||||
<div class="chart-title">Seat types</div>
|
title="Seat Types"
|
||||||
<apexchart
|
|
||||||
v-if="series.length"
|
v-if="series.length"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="280"
|
height="280"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="seriesData"
|
:series="seriesData"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-empty">No seat type data available</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Flight } from '@/Types/types'
|
import type { Flight } from '@/Types/types'
|
||||||
|
import FlightChart from "@/Components/FlightsGoneBy/FlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
@@ -87,26 +85,4 @@ const chartOptions = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #556677;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-empty {
|
|
||||||
height: 280px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #445566;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div v-if="series.length" class="chart-outer">
|
<div v-if="series.length" class="chart-outer">
|
||||||
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
||||||
<apexchart
|
<UnstyledFlightChart
|
||||||
type="bar"
|
type="bar"
|
||||||
:height="chartHeight"
|
:height="chartHeight"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
@@ -34,6 +34,7 @@ import type {Flight, SharedProps} from '@/Types/types'
|
|||||||
import { useChartTooltip } from '@/Composables/useChartTooltip'
|
import { useChartTooltip } from '@/Composables/useChartTooltip'
|
||||||
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
||||||
import {usePage} from "@inertiajs/vue3";
|
import {usePage} from "@inertiajs/vue3";
|
||||||
|
import UnstyledFlightChart from "@/Components/FlightsGoneBy/UnstyledFlightChart.vue";
|
||||||
|
|
||||||
const page = usePage<SharedProps>().props
|
const page = usePage<SharedProps>().props
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div v-if="series.length" class="chart-outer">
|
<div v-if="series.length" class="chart-outer">
|
||||||
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
<div class="chart-scroll" :style="{ height: scrollHeight }" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
||||||
<apexchart
|
<UnstyledFlightChart
|
||||||
type="bar"
|
type="bar"
|
||||||
:height="chartHeight"
|
:height="chartHeight"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
@@ -44,6 +44,7 @@ import { computed } from 'vue'
|
|||||||
import type { Airport, Flight } from '@/Types/types'
|
import type { Airport, Flight } from '@/Types/types'
|
||||||
import { useChartTooltip } from '@/Composables/useChartTooltip'
|
import { useChartTooltip } from '@/Composables/useChartTooltip'
|
||||||
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
import ChartTooltip from '@/Components/FlightsGoneBy/Charts/ChartTooltip.vue'
|
||||||
|
import UnstyledFlightChart from "@/Components/FlightsGoneBy/UnstyledFlightChart.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
|
||||||
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
import {ChartType} from "@/Types/types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
type: ChartType
|
||||||
|
title: string
|
||||||
|
height: number | string
|
||||||
|
options: object
|
||||||
|
series: unknown[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const ready = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="chart-wrap">
|
||||||
|
<div class="chart-title">{{title}}</div>
|
||||||
|
<PlaneLoader v-if="!ready" />
|
||||||
|
<VueApexCharts
|
||||||
|
:type="type"
|
||||||
|
:height="height"
|
||||||
|
:options="options"
|
||||||
|
:series="series"
|
||||||
|
:class="{ 'chart-hidden': !ready }"
|
||||||
|
@mounted="ready = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #556677;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-empty {
|
||||||
|
height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #445566;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+141
-192
@@ -1,170 +1,45 @@
|
|||||||
<template>
|
|
||||||
<FlightMap :page="page.props" :flights="mappedFlights" class="profile-map__map" />
|
|
||||||
|
|
||||||
<div class="profile-map__toolbar">
|
|
||||||
<v-select
|
|
||||||
v-model="selectedYears"
|
|
||||||
:items="availableYears"
|
|
||||||
label="Year"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<template #selection="{ item, index }">
|
|
||||||
<span v-if="index < 2" class="v-select__selection-text">
|
|
||||||
{{ item }}<span v-if="index < Math.min(selectedYears.length, 2) - 1">, </span>
|
|
||||||
</span>
|
|
||||||
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedYears.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
|
|
||||||
<v-select
|
|
||||||
v-model="selectedAirlines"
|
|
||||||
:items="availableAirlines"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
label="Airline"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<template #item="{ item, props: itemProps }">
|
|
||||||
<v-list-item v-bind="itemProps">
|
|
||||||
<template #prepend="{ isSelected }">
|
|
||||||
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<img
|
|
||||||
:src="airlineLogoUrl((item as any).id)"
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
style="object-fit: contain; margin-right: 8px; vertical-align: middle;"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{{ (item as any).name }}
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template #selection="{ item, index }">
|
|
||||||
<span v-if="index < 2" class="v-select__selection-text">
|
|
||||||
{{ (item as any).name }}<span v-if="index < Math.min(selectedAirlines.length, 2) - 1">, </span>
|
|
||||||
</span>
|
|
||||||
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAirlines.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
|
|
||||||
<v-select
|
|
||||||
v-model="selectedCountries"
|
|
||||||
:items="availableCountries"
|
|
||||||
item-title="name"
|
|
||||||
item-value="code"
|
|
||||||
label="Country"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<template #item="{ item, props: itemProps }">
|
|
||||||
<v-list-item v-bind="itemProps">
|
|
||||||
<template #prepend="{ isSelected }">
|
|
||||||
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<span :class="countryFlagClass((item as any).code)" style="margin-right: 8px; font-size: 1.1em;" />
|
|
||||||
{{ (item as any).name }}
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template #selection="{ item, index }">
|
|
||||||
<span v-if="index < 2" class="v-select__selection-text">
|
|
||||||
<span :class="countryFlagClass((item as any).code)" style="margin-right: 4px;" />
|
|
||||||
{{ (item as any).name }}<span v-if="index < Math.min(selectedCountries.length, 2) - 1">, </span>
|
|
||||||
</span>
|
|
||||||
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCountries.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
|
|
||||||
<v-select
|
|
||||||
v-model="selectedContinents"
|
|
||||||
:items="availableContinents"
|
|
||||||
item-title="name"
|
|
||||||
item-value="code"
|
|
||||||
label="Continent"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<template #selection="{ item, index }">
|
|
||||||
<span v-if="index < 2" class="v-select__selection-text">
|
|
||||||
{{ (item as any).name }}<span v-if="index < Math.min(selectedContinents.length, 2) - 1">, </span>
|
|
||||||
</span>
|
|
||||||
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedContinents.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
|
|
||||||
<v-select
|
|
||||||
v-model="selectedFlightClasses"
|
|
||||||
:items="availableFlightClasses"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
label="Flight class"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<template #selection="{ item, index }">
|
|
||||||
<span v-if="index < 2" class="v-select__selection-text">
|
|
||||||
{{ (item as any).name }}<span v-if="index < Math.min(selectedFlightClasses.length, 2) - 1">, </span>
|
|
||||||
</span>
|
|
||||||
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightClasses.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FlightStatsBar :flights="pastFlights" :upcoming-flights="upcomingFlights" />
|
|
||||||
<FlightCharts :flights="pastFlights" :upcoming-flights="upcomingFlights" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Flight } from '@/Types/types'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { usePage } from '@inertiajs/vue3'
|
import { usePage } from '@inertiajs/vue3'
|
||||||
import FlightMap from '@/Components/FlightsGoneBy/FlightMap.vue'
|
|
||||||
import type { Flight, SharedProps } from '@/Types/types'
|
|
||||||
import FlightStatsBar from '@/Components/FlightsGoneBy/FlightStatsBar.vue'
|
|
||||||
import FlightCharts from '@/Components/FlightsGoneBy/FlightCharts.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flights: Flight[]
|
flights: Flight[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const page = usePage<SharedProps>()
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
const selectedYears = ref<number[]>([])
|
const selectedYears = ref<number[]>([])
|
||||||
const selectedAirlines = ref<number[]>([])
|
const selectedAirlines = ref<number[]>([])
|
||||||
const selectedCountries = ref<string[]>([])
|
const selectedCountries = ref<string[]>([])
|
||||||
const selectedContinents = ref<string[]>([])
|
const selectedContinents = ref<string[]>([])
|
||||||
const selectedFlightClasses = ref<number[]>([])
|
const selectedFlightClasses = ref<number[]>([])
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
const emit = defineEmits<{
|
||||||
|
change: [filters: {
|
||||||
|
years: number[]
|
||||||
|
airlines: number[]
|
||||||
|
countries: string[]
|
||||||
|
continents: string[]
|
||||||
|
flightClasses: number[]
|
||||||
|
}]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function emitFilters() {
|
||||||
|
emit('change', {
|
||||||
|
years: selectedYears.value,
|
||||||
|
airlines: selectedAirlines.value,
|
||||||
|
countries: selectedCountries.value,
|
||||||
|
continents: selectedContinents.value,
|
||||||
|
flightClasses: selectedFlightClasses.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = usePage()
|
||||||
const airlineLogoUrl = (id: number) =>
|
const airlineLogoUrl = (id: number) =>
|
||||||
`${page.props.logo_api_url}/airlines/logos/tail/id/${id}`
|
`${page.props.logo_api_url}/airlines/logos/tail/id/${id}`
|
||||||
|
|
||||||
const countryFlagClass = (code: string) =>
|
const countryFlagClass = (code: string) =>
|
||||||
`fi fi-${code.toLowerCase()}`
|
`fi fi-${code.toLowerCase()}`
|
||||||
|
|
||||||
// ── Available filter options ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const availableYears = computed(() => {
|
const availableYears = computed(() => {
|
||||||
const years = new Set<number>()
|
const years = new Set<number>()
|
||||||
props.flights.forEach(f => years.add(new Date(f.departure_date).getFullYear()))
|
props.flights.forEach(f => years.add(new Date(f.departure_date).getFullYear()))
|
||||||
@@ -174,9 +49,8 @@ const availableYears = computed(() => {
|
|||||||
const availableAirlines = computed((): { id: number; name: string }[] => {
|
const availableAirlines = computed((): { id: number; name: string }[] => {
|
||||||
const map = new Map<number, { id: number; name: string }>()
|
const map = new Map<number, { id: number; name: string }>()
|
||||||
props.flights.forEach(f => {
|
props.flights.forEach(f => {
|
||||||
if (f.airline?.id && f.airline?.name) {
|
if (f.airline?.id && f.airline?.name)
|
||||||
map.set(f.airline.id, { id: f.airline.id, name: f.airline.name })
|
map.set(f.airline.id, { id: f.airline.id, name: f.airline.name })
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
})
|
})
|
||||||
@@ -206,63 +80,138 @@ const availableContinents = computed((): { code: string; name: string }[] => {
|
|||||||
const availableFlightClasses = computed((): { id: number; name: string }[] => {
|
const availableFlightClasses = computed((): { id: number; name: string }[] => {
|
||||||
const map = new Map<number, { id: number; name: string }>()
|
const map = new Map<number, { id: number; name: string }>()
|
||||||
props.flights.forEach(f => {
|
props.flights.forEach(f => {
|
||||||
if (f.flight_class?.id && f.flight_class?.name) {
|
if (f.flight_class?.id && f.flight_class?.name)
|
||||||
map.set(f.flight_class.id, { id: f.flight_class.id, name: f.flight_class.name })
|
map.set(f.flight_class.id, { id: f.flight_class.id, name: f.flight_class.name })
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Filter helper ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function matchesFilters(f: Flight, date: Date): boolean {
|
|
||||||
if (selectedYears.value.length && !selectedYears.value.includes(date.getFullYear())) return false
|
|
||||||
if (selectedAirlines.value.length && !selectedAirlines.value.includes(f.airline?.id ?? -1)) return false
|
|
||||||
if (selectedCountries.value.length) {
|
|
||||||
const depCode = f.departure_airport.region?.country?.code
|
|
||||||
const arrCode = f.arrival_airport.region?.country?.code
|
|
||||||
if (!selectedCountries.value.includes(depCode ?? '') && !selectedCountries.value.includes(arrCode ?? '')) return false
|
|
||||||
}
|
|
||||||
if (selectedContinents.value.length) {
|
|
||||||
const depCode = f.departure_airport.region?.continent?.code
|
|
||||||
const arrCode = f.arrival_airport.region?.continent?.code
|
|
||||||
if (!selectedContinents.value.includes(depCode ?? '') && !selectedContinents.value.includes(arrCode ?? '')) return false
|
|
||||||
}
|
|
||||||
if (selectedFlightClasses.value.length && !selectedFlightClasses.value.includes(f.flight_class?.id ?? -1)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Filtered flights ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const pastFlights = computed(() =>
|
|
||||||
props.flights.filter(f => {
|
|
||||||
const date = new Date(f.departure_date)
|
|
||||||
return date <= now && matchesFilters(f, date)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const upcomingFlights = computed(() =>
|
|
||||||
props.flights.filter(f => {
|
|
||||||
const date = new Date(f.departure_date)
|
|
||||||
return date > now && matchesFilters(f, date)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const mappedFlights = computed(() => [
|
|
||||||
...pastFlights.value,
|
|
||||||
...upcomingFlights.value,
|
|
||||||
])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flight-filters">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedYears"
|
||||||
|
:items="availableYears"
|
||||||
|
label="Year"
|
||||||
|
multiple clearable hide-details
|
||||||
|
density="compact" variant="outlined"
|
||||||
|
@update:model-value="emitFilters"
|
||||||
|
>
|
||||||
|
<template #selection="{ item, index }">
|
||||||
|
<span v-if="index < 2" class="v-select__selection-text">
|
||||||
|
{{ item }}<span v-if="index < Math.min(selectedYears.length, 2) - 1">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedYears.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="selectedAirlines"
|
||||||
|
:items="availableAirlines"
|
||||||
|
item-title="name" item-value="id"
|
||||||
|
label="Airline"
|
||||||
|
multiple clearable hide-details
|
||||||
|
density="compact" variant="outlined"
|
||||||
|
@update:model-value="emitFilters"
|
||||||
|
>
|
||||||
|
<template #item="{ item, props: itemProps }">
|
||||||
|
<v-list-item v-bind="itemProps">
|
||||||
|
<template #prepend="{ isSelected }">
|
||||||
|
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<img
|
||||||
|
:src="airlineLogoUrl((item as any).id)"
|
||||||
|
width="32" height="32"
|
||||||
|
style="object-fit: contain; margin-right: 8px; vertical-align: middle;"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{{ (item as any).name }}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template #selection="{ item, index }">
|
||||||
|
<span v-if="index < 2" class="v-select__selection-text">
|
||||||
|
{{ (item as any).name }}<span v-if="index < Math.min(selectedAirlines.length, 2) - 1">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAirlines.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="selectedCountries"
|
||||||
|
:items="availableCountries"
|
||||||
|
item-title="name" item-value="code"
|
||||||
|
label="Country"
|
||||||
|
multiple clearable hide-details
|
||||||
|
density="compact" variant="outlined"
|
||||||
|
@update:model-value="emitFilters"
|
||||||
|
>
|
||||||
|
<template #item="{ item, props: itemProps }">
|
||||||
|
<v-list-item v-bind="itemProps">
|
||||||
|
<template #prepend="{ isSelected }">
|
||||||
|
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<span :class="countryFlagClass((item as any).code)" style="margin-right: 8px; font-size: 1.1em;" />
|
||||||
|
{{ (item as any).name }}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template #selection="{ item, index }">
|
||||||
|
<span v-if="index < 2" class="v-select__selection-text">
|
||||||
|
<span :class="countryFlagClass((item as any).code)" style="margin-right: 4px;" />
|
||||||
|
{{ (item as any).name }}<span v-if="index < Math.min(selectedCountries.length, 2) - 1">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCountries.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="selectedContinents"
|
||||||
|
:items="availableContinents"
|
||||||
|
item-title="name" item-value="code"
|
||||||
|
label="Continent"
|
||||||
|
multiple clearable hide-details
|
||||||
|
density="compact" variant="outlined"
|
||||||
|
@update:model-value="emitFilters"
|
||||||
|
>
|
||||||
|
<template #selection="{ item, index }">
|
||||||
|
<span v-if="index < 2" class="v-select__selection-text">
|
||||||
|
{{ (item as any).name }}<span v-if="index < Math.min(selectedContinents.length, 2) - 1">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedContinents.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="selectedFlightClasses"
|
||||||
|
:items="availableFlightClasses"
|
||||||
|
item-title="name" item-value="id"
|
||||||
|
label="Flight class"
|
||||||
|
multiple clearable hide-details
|
||||||
|
density="compact" variant="outlined"
|
||||||
|
@update:model-value="emitFilters"
|
||||||
|
>
|
||||||
|
<template #selection="{ item, index }">
|
||||||
|
<span v-if="index < 2" class="v-select__selection-text">
|
||||||
|
{{ (item as any).name }}<span v-if="index < Math.min(selectedFlightClasses.length, 2) - 1">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightClasses.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile-map__toolbar {
|
.flight-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-map__toolbar .v-select {
|
.flight-filters .v-select {
|
||||||
flex: 1 1 160px;
|
flex: 1 1 160px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flight-map-wrapper">
|
<div class="flight-map-wrapper">
|
||||||
<div ref="mapContainer" class="map-container" />
|
<PlaneLoader v-if="!mapReady" class="map-loader" />
|
||||||
|
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
|
||||||
<div v-if="!flights.length" class="empty-state">
|
<div v-if="!flights.length" class="empty-state">
|
||||||
<span class="mdi mdi-earth-off" />
|
<span class="mdi mdi-earth-off" />
|
||||||
<p>No flight data available</p>
|
<p>No flight data available</p>
|
||||||
@@ -15,6 +16,7 @@ import 'maplibre-gl/dist/maplibre-gl.css'
|
|||||||
import { SharedProps } from '@/Types/types'
|
import { SharedProps } from '@/Types/types'
|
||||||
import { usePage } from '@inertiajs/vue3'
|
import { usePage } from '@inertiajs/vue3'
|
||||||
import { Flight, Airport } from '@/Types/types'
|
import { Flight, Airport } from '@/Types/types'
|
||||||
|
import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
|
||||||
|
|
||||||
interface RouteFlightBucket {
|
interface RouteFlightBucket {
|
||||||
historical: Flight[]
|
historical: Flight[]
|
||||||
@@ -39,8 +41,12 @@ interface RoutesGeoJSON {
|
|||||||
future: GeoJSON.FeatureCollection<GeoJSON.LineString, RouteFeatureProperties>
|
future: GeoJSON.FeatureCollection<GeoJSON.LineString, RouteFeatureProperties>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoApiUrl = usePage<SharedProps>().props.logo_api_url
|
||||||
|
|
||||||
|
function greatCircleGeoJSON(from: LngLat, to: LngLat, steps?: number): LngLat[] {
|
||||||
|
const dist = Math.sqrt((to[0] - from[0]) ** 2 + (to[1] - from[1]) ** 2)
|
||||||
|
const s = steps ?? (dist > 60 ? 64 : dist > 20 ? 32 : 16)
|
||||||
|
|
||||||
function greatCircleGeoJSON(from: LngLat, to: LngLat, steps = 64): LngLat[] {
|
|
||||||
const toRad = (d: number): number => d * Math.PI / 180
|
const toRad = (d: number): number => d * Math.PI / 180
|
||||||
const toDeg = (r: number): number => r * 180 / Math.PI
|
const toDeg = (r: number): number => r * 180 / Math.PI
|
||||||
const lat1 = toRad(from[1]), lng1 = toRad(from[0])
|
const lat1 = toRad(from[1]), lng1 = toRad(from[0])
|
||||||
@@ -51,8 +57,8 @@ function greatCircleGeoJSON(from: LngLat, to: LngLat, steps = 64): LngLat[] {
|
|||||||
))
|
))
|
||||||
if (d === 0) return [[from[0], from[1]], [to[0], to[1]]]
|
if (d === 0) return [[from[0], from[1]], [to[0], to[1]]]
|
||||||
const points: LngLat[] = []
|
const points: LngLat[] = []
|
||||||
for (let i = 0; i <= steps; i++) {
|
for (let i = 0; i <= s; i++) {
|
||||||
const f = i / steps
|
const f = i / s
|
||||||
const A = Math.sin((1 - f) * d) / Math.sin(d)
|
const A = Math.sin((1 - f) * d) / Math.sin(d)
|
||||||
const B = Math.sin(f * d) / Math.sin(d)
|
const B = Math.sin(f * d) / Math.sin(d)
|
||||||
const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2)
|
const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2)
|
||||||
@@ -96,7 +102,6 @@ function airportPopupHTML(airport: Airport): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function routePopupHTML(historical: Flight[], future: Flight[]): string {
|
function routePopupHTML(historical: Flight[], future: Flight[]): string {
|
||||||
const logoApiUrl = usePage<SharedProps>().props.logo_api_url
|
|
||||||
interface DirectionGroup {
|
interface DirectionGroup {
|
||||||
label: string
|
label: string
|
||||||
airlines: string[]
|
airlines: string[]
|
||||||
@@ -140,6 +145,7 @@ function routePopupHTML(historical: Flight[], future: Flight[]): string {
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'FlightMap',
|
name: 'FlightMap',
|
||||||
|
components: {PlaneLoader},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
flights: {
|
flights: {
|
||||||
@@ -158,6 +164,7 @@ export default defineComponent({
|
|||||||
const TOUCH_RADIUS = 20
|
const TOUCH_RADIUS = 20
|
||||||
const airportById = new Map<number, Airport>()
|
const airportById = new Map<number, Airport>()
|
||||||
const routeFlights = new Map<string, RouteFlightBucket>()
|
const routeFlights = new Map<string, RouteFlightBucket>()
|
||||||
|
const arcCache = new Map<string, LngLat[]>()
|
||||||
let selectedAirportId: number | null = null
|
let selectedAirportId: number | null = null
|
||||||
|
|
||||||
const isTouchDevice = (): boolean => window.matchMedia('(pointer: coarse)').matches
|
const isTouchDevice = (): boolean => window.matchMedia('(pointer: coarse)').matches
|
||||||
@@ -170,22 +177,23 @@ export default defineComponent({
|
|||||||
const applyFilter = (): void => {
|
const applyFilter = (): void => {
|
||||||
if (!map || !map.isStyleLoaded()) return
|
if (!map || !map.isStyleLoaded()) return
|
||||||
const id = selectedAirportId
|
const id = selectedAirportId
|
||||||
const filter: maplibregl.FilterSpecification | null = id
|
|
||||||
|
const routeFilter: maplibregl.FilterSpecification | null = id
|
||||||
? ['any', ['==', ['get', 'depId'], id], ['==', ['get', 'arrId'], id]]
|
? ['any', ['==', ['get', 'depId'], id], ['==', ['get', 'arrId'], id]]
|
||||||
: null
|
: null
|
||||||
|
|
||||||
map.setFilter('routes-line', filter)
|
map.setFilter('routes-line', routeFilter)
|
||||||
map.setFilter('routes-hit', filter)
|
map.setFilter('routes-hit', routeFilter)
|
||||||
map.setFilter('routes-future-line', filter)
|
map.setFilter('routes-future-line', routeFilter)
|
||||||
map.setFilter('routes-future-hit', filter)
|
map.setFilter('routes-future-hit', routeFilter)
|
||||||
|
|
||||||
|
// Use setFilter on airport layers — GPU-side, no style recompilation
|
||||||
for (let i = 0; i < PULSE_PHASES; i++) {
|
for (let i = 0; i < PULSE_PHASES; i++) {
|
||||||
map.setPaintProperty(`airports-dot-${i}`, 'circle-opacity',
|
const airportFilter: maplibregl.FilterSpecification | null = id
|
||||||
id ? ['case', ['==', ['get', 'id'], id], 1, 0.25] : 1)
|
? ['==', ['get', 'id'], id]
|
||||||
map.setPaintProperty(`airports-dot-${i}`, 'circle-stroke-opacity',
|
: null
|
||||||
id ? ['case', ['==', ['get', 'id'], id], 1, 0.25] : 1)
|
map.setFilter(`airports-dot-${i}`, airportFilter)
|
||||||
map.setPaintProperty(`airports-pulse-${i}`, 'circle-stroke-opacity',
|
map.setFilter(`airports-pulse-${i}`, airportFilter)
|
||||||
id ? ['case', ['==', ['get', 'id'], id], 0.6, 0] : 0.6)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +208,18 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Arc cache helper ──────────────────────────────────────────────────
|
||||||
|
const getArc = (flight: Flight): LngLat[] => {
|
||||||
|
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
||||||
|
if (!arcCache.has(key)) {
|
||||||
|
arcCache.set(key, greatCircleGeoJSON(
|
||||||
|
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
|
||||||
|
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return arcCache.get(key)!
|
||||||
|
}
|
||||||
|
|
||||||
// ── GeoJSON builders ──────────────────────────────────────────────────
|
// ── GeoJSON builders ──────────────────────────────────────────────────
|
||||||
const buildRoutesGeoJSON = (): RoutesGeoJSON => {
|
const buildRoutesGeoJSON = (): RoutesGeoJSON => {
|
||||||
buildRouteFlights()
|
buildRouteFlights()
|
||||||
@@ -219,50 +239,44 @@ export default defineComponent({
|
|||||||
return '#a150d5'
|
return '#a150d5'
|
||||||
}
|
}
|
||||||
|
|
||||||
const historicalFeatures: GeoJSON.Feature<GeoJSON.LineString, RouteFeatureProperties>[] =
|
const seenHistorical = new Set<string>()
|
||||||
|
const historicalFeatures: GeoJSON.Feature<GeoJSON.LineString, RouteFeatureProperties>[] = []
|
||||||
props.flights
|
props.flights
|
||||||
.filter(f => new Date(f.departure_date) <= now)
|
.filter(f => new Date(f.departure_date) <= now)
|
||||||
.map((flight) => {
|
.forEach((flight) => {
|
||||||
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
||||||
|
if (seenHistorical.has(key)) return
|
||||||
|
seenHistorical.add(key)
|
||||||
const count = routeCounts.get(key) ?? 1
|
const count = routeCounts.get(key) ?? 1
|
||||||
return {
|
historicalFeatures.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
properties: {
|
properties: {
|
||||||
color: routeColor(count), routeKey: key,
|
color: routeColor(count), routeKey: key,
|
||||||
depId: flight.departure_airport.id,
|
depId: flight.departure_airport.id,
|
||||||
arrId: flight.arrival_airport.id,
|
arrId: flight.arrival_airport.id,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: { type: 'LineString', coordinates: getArc(flight) },
|
||||||
type: 'LineString',
|
})
|
||||||
coordinates: greatCircleGeoJSON(
|
|
||||||
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
|
|
||||||
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const historicalKeys = new Set(routeCounts.keys())
|
const historicalKeys = new Set(routeCounts.keys())
|
||||||
const futureFeatures: GeoJSON.Feature<GeoJSON.LineString, RouteFeatureProperties>[] =
|
const seenFuture = new Set<string>()
|
||||||
|
const futureFeatures: GeoJSON.Feature<GeoJSON.LineString, RouteFeatureProperties>[] = []
|
||||||
props.flights
|
props.flights
|
||||||
.filter((flight) => {
|
.filter((flight) => {
|
||||||
if (new Date(flight.departure_date) <= now) return false
|
if (new Date(flight.departure_date) <= now) return false
|
||||||
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
||||||
return !historicalKeys.has(key)
|
return !historicalKeys.has(key)
|
||||||
})
|
})
|
||||||
.map((flight) => {
|
.forEach((flight) => {
|
||||||
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
const key = [flight.departure_airport.id, flight.arrival_airport.id].sort().join('-')
|
||||||
return {
|
if (seenFuture.has(key)) return
|
||||||
|
seenFuture.add(key)
|
||||||
|
futureFeatures.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
properties: { routeKey: key, depId: flight.departure_airport.id, arrId: flight.arrival_airport.id },
|
properties: { routeKey: key, depId: flight.departure_airport.id, arrId: flight.arrival_airport.id },
|
||||||
geometry: {
|
geometry: { type: 'LineString', coordinates: getArc(flight) },
|
||||||
type: 'LineString',
|
})
|
||||||
coordinates: greatCircleGeoJSON(
|
|
||||||
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
|
|
||||||
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -384,9 +398,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Unified click handler — airports take priority over routes ────
|
// ── Unified click handler ─────────────────────────────────────────
|
||||||
map!.on('click', (e) => {
|
map!.on('click', (e) => {
|
||||||
// Query airports with a larger bounding box on touch for easier tapping
|
|
||||||
const airportQuery = isTouch
|
const airportQuery = isTouch
|
||||||
? [[e.point.x - TOUCH_RADIUS, e.point.y - TOUCH_RADIUS], [e.point.x + TOUCH_RADIUS, e.point.y + TOUCH_RADIUS]] as [maplibregl.PointLike, maplibregl.PointLike]
|
? [[e.point.x - TOUCH_RADIUS, e.point.y - TOUCH_RADIUS], [e.point.x + TOUCH_RADIUS, e.point.y + TOUCH_RADIUS]] as [maplibregl.PointLike, maplibregl.PointLike]
|
||||||
: e.point as maplibregl.PointLike
|
: e.point as maplibregl.PointLike
|
||||||
@@ -408,7 +421,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check routes
|
|
||||||
const routeFeatures = map!.queryRenderedFeatures(e.point, {
|
const routeFeatures = map!.queryRenderedFeatures(e.point, {
|
||||||
layers: ['routes-hit', 'routes-future-hit'],
|
layers: ['routes-hit', 'routes-future-hit'],
|
||||||
})
|
})
|
||||||
@@ -419,7 +431,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty map click — deselect everything
|
|
||||||
selectedAirportId = null
|
selectedAirportId = null
|
||||||
applyFilter()
|
applyFilter()
|
||||||
popup!.remove()
|
popup!.remove()
|
||||||
@@ -443,12 +454,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
const fitBounds = (): void => {
|
const fitBounds = (): void => {
|
||||||
if (!props.flights.length) return
|
if (!props.flights.length) return
|
||||||
const lngs = props.flights.flatMap(f => [f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg])
|
let minLng = Infinity, maxLng = -Infinity, minLat = Infinity, maxLat = -Infinity
|
||||||
const lats = props.flights.flatMap(f => [f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg])
|
props.flights.forEach(f => {
|
||||||
map!.fitBounds(
|
minLng = Math.min(minLng, f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg)
|
||||||
[[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]],
|
maxLng = Math.max(maxLng, f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg)
|
||||||
{ padding: 60, duration: 0 },
|
minLat = Math.min(minLat, f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg)
|
||||||
)
|
maxLat = Math.max(maxLat, f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg)
|
||||||
|
})
|
||||||
|
map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding: 60, duration: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Map init ──────────────────────────────────────────────────────────
|
// ── Map init ──────────────────────────────────────────────────────────
|
||||||
@@ -479,13 +492,18 @@ export default defineComponent({
|
|||||||
renderWorldCopies: true,
|
renderWorldCopies: true,
|
||||||
})
|
})
|
||||||
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
|
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
|
||||||
map.on('load', () => { addLayers(); fitBounds() })
|
map.on('load', () => {
|
||||||
|
addLayers()
|
||||||
|
fitBounds()
|
||||||
|
mapReady.value = true // ← add this
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Data updates ──────────────────────────────────────────────────────
|
// ── Data updates ──────────────────────────────────────────────────────
|
||||||
const updateData = (): void => {
|
const updateData = (): void => {
|
||||||
if (!map || !map.isStyleLoaded()) return
|
if (!map || !map.isStyleLoaded()) return
|
||||||
|
arcCache.clear()
|
||||||
airportById.clear()
|
airportById.clear()
|
||||||
props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
|
props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
|
||||||
airportById.set(dep.id, dep)
|
airportById.set(dep.id, dep)
|
||||||
@@ -508,14 +526,16 @@ export default defineComponent({
|
|||||||
initMap()
|
initMap()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.flights, updateData, { deep: true })
|
watch(() => props.flights, updateData)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (pulseFrame !== null) cancelAnimationFrame(pulseFrame)
|
if (pulseFrame !== null) cancelAnimationFrame(pulseFrame)
|
||||||
if (map) { map.remove(); map = null }
|
if (map) { map.remove(); map = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
return { mapContainer }
|
const mapReady = ref(false)
|
||||||
|
|
||||||
|
return { mapContainer, mapReady }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -525,11 +545,22 @@ export default defineComponent({
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-loader {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-hidden {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -547,6 +578,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
.empty-state .mdi { font-size: 48px; }
|
.empty-state .mdi { font-size: 48px; }
|
||||||
.empty-state p { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; margin: 0; }
|
.empty-state p { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; margin: 0; }
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -610,4 +643,7 @@ export default defineComponent({
|
|||||||
.maplibregl-ctrl-group button:hover { background: rgba(77,166,255,0.15) !important; color: #4da6ff !important; }
|
.maplibregl-ctrl-group button:hover { background: rgba(77,166,255,0.15) !important; color: #4da6ff !important; }
|
||||||
.maplibregl-ctrl-attrib { background: rgba(10,14,22,0.7) !important; color: #666 !important; }
|
.maplibregl-ctrl-attrib { background: rgba(10,14,22,0.7) !important; color: #666 !important; }
|
||||||
.maplibregl-ctrl-attrib a { color: #666 !important; }
|
.maplibregl-ctrl-attrib a { color: #666 !important; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<FlightMap :page="page.props" :flights="mappedFlights" class="profile-map__map" />
|
||||||
|
<FlightFillter :flights="flights" @change="onFiltersChange" />
|
||||||
|
<FlightStatsBar :flights="pastFlights" :upcoming-flights="upcomingFlights" />
|
||||||
|
<FlightCharts :flights="pastFlights" :upcoming-flights="upcomingFlights" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { usePage } from '@inertiajs/vue3'
|
||||||
|
import type { Flight, SharedProps } from '@/Types/types'
|
||||||
|
import FlightStatsBar from '@/Components/FlightsGoneBy/FlightStatsBar.vue'
|
||||||
|
|
||||||
|
import FlightFillter from "@/Components/FlightsGoneBy/FlightFillter.vue";
|
||||||
|
import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
|
||||||
|
import FlightCharts from "@/Components/FlightsGoneBy/FlightCharts.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
flights: Flight[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const page = usePage<SharedProps>()
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const selectedYears = ref<number[]>([])
|
||||||
|
const selectedAirlines = ref<number[]>([])
|
||||||
|
const selectedCountries = ref<string[]>([])
|
||||||
|
const selectedContinents = ref<string[]>([])
|
||||||
|
const selectedFlightClasses = ref<number[]>([])
|
||||||
|
|
||||||
|
function onFiltersChange(filters: {
|
||||||
|
years: number[]
|
||||||
|
airlines: number[]
|
||||||
|
countries: string[]
|
||||||
|
continents: string[]
|
||||||
|
flightClasses: number[]
|
||||||
|
}) {
|
||||||
|
selectedYears.value = filters.years
|
||||||
|
selectedAirlines.value = filters.airlines
|
||||||
|
selectedCountries.value = filters.countries
|
||||||
|
selectedContinents.value = filters.continents
|
||||||
|
selectedFlightClasses.value = filters.flightClasses
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilters(f: Flight, date: Date): boolean {
|
||||||
|
if (selectedYears.value.length && !selectedYears.value.includes(date.getFullYear())) return false
|
||||||
|
if (selectedAirlines.value.length && !selectedAirlines.value.includes(f.airline?.id ?? -1)) return false
|
||||||
|
if (selectedCountries.value.length) {
|
||||||
|
const depCode = f.departure_airport.region?.country?.code
|
||||||
|
const arrCode = f.arrival_airport.region?.country?.code
|
||||||
|
if (!selectedCountries.value.includes(depCode ?? '') && !selectedCountries.value.includes(arrCode ?? '')) return false
|
||||||
|
}
|
||||||
|
if (selectedContinents.value.length) {
|
||||||
|
const depCode = f.departure_airport.region?.continent?.code
|
||||||
|
const arrCode = f.arrival_airport.region?.continent?.code
|
||||||
|
if (!selectedContinents.value.includes(depCode ?? '') && !selectedContinents.value.includes(arrCode ?? '')) return false
|
||||||
|
}
|
||||||
|
if (selectedFlightClasses.value.length && !selectedFlightClasses.value.includes(f.flight_class?.id ?? -1)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filtered flights ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const pastFlights = computed(() =>
|
||||||
|
props.flights.filter(f => {
|
||||||
|
const date = new Date(f.departure_date)
|
||||||
|
return date <= now && matchesFilters(f, date)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const upcomingFlights = computed(() =>
|
||||||
|
props.flights.filter(f => {
|
||||||
|
const date = new Date(f.departure_date)
|
||||||
|
return date > now && matchesFilters(f, date)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const mappedFlights = computed(() => [
|
||||||
|
...pastFlights.value,
|
||||||
|
...upcomingFlights.value,
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
|
||||||
|
<circle class="ring-spin" cx="60" cy="60" r="54" fill="none" stroke="#ffc107" stroke-width="1" stroke-dasharray="4 6" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Static inner ring -->
|
||||||
|
<circle cx="60" cy="60" r="46" fill="none" stroke="#ffc107" stroke-width="0.5" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Pulsing dot markers on inner ring -->
|
||||||
|
<circle class="ring-pulse" cx="60" cy="14" r="2" fill="#ffc107"/>
|
||||||
|
<circle class="ring-pulse" cx="106" cy="60" r="2" fill="#ffc107" style="animation-delay: 0.5s"/>
|
||||||
|
<circle class="ring-pulse" cx="60" cy="106" r="2" fill="#ffc107" style="animation-delay: 1s"/>
|
||||||
|
<circle class="ring-pulse" cx="14" cy="60" r="2" fill="#ffc107" style="animation-delay: 1.5s"/>
|
||||||
|
<text x="60" y="63" text-anchor="middle" font-size="6" letter-spacing="2.5" fill="#ffc107" font-family="monospace" opacity="0.7">Loading</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,100% { opacity: 0.3; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
.ring-spin { animation: spin 8s linear infinite; transform-origin: 60px 60px; }
|
||||||
|
.ring-pulse { animation: pulse 2s ease-in-out infinite; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, User} from "@/Types/types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user: User
|
||||||
|
flights: Flight[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="board-header">
|
||||||
|
<div class="board-title-group">
|
||||||
|
<span class="board-eyebrow">FLIGHT HISTORY</span>
|
||||||
|
<h1 class="board-title">{{ user.name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="board-count">
|
||||||
|
<span class="count-number">{{ flights.length }}</span>
|
||||||
|
<span class="count-label">FLIGHTS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ── Header ── */
|
||||||
|
.board-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(255,193,7,0.2);
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-eyebrow {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: #ffc107;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-title {
|
||||||
|
font-family: 'Barlow Condensed', sans-serif;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f0f2f5;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-count {
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-number {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-label {
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: #556;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, User} from "@/Types/types";
|
||||||
|
import ProfileHeader from "@/Components/FlightsGoneBy/ProfileHeader.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user: User
|
||||||
|
flights: Flight[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="board-wrapper">
|
||||||
|
<ProfileHeader :user="user" :flights="flights" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.board-wrapper {
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
font-family: 'Barlow', sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ProfileView} from "@/Types/types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
activeView: string;
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:activeView': [view: ProfileView]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view-toolbar">
|
||||||
|
<button
|
||||||
|
class="view-btn"
|
||||||
|
:class="{ active: activeView === 'map' }"
|
||||||
|
@click="emit('update:activeView', 'map')"
|
||||||
|
>
|
||||||
|
<span class="view-btn-icon mdi mdi-earth"></span>
|
||||||
|
<span class="view-btn-label">MAP</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="view-btn"
|
||||||
|
:class="{ active: activeView === 'board' }"
|
||||||
|
@click="emit('update:activeView', 'board')"
|
||||||
|
>
|
||||||
|
<span class="view-btn-icon mdi mdi-table"></span>
|
||||||
|
<span class="view-btn-label">DEPARTURE BOARD</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="view-btn"
|
||||||
|
:class="{ active: activeView === 'passes' }"
|
||||||
|
@click="emit('update:activeView', 'passes')"
|
||||||
|
>
|
||||||
|
<span class="view-btn-icon mdi mdi-ticket"></span>
|
||||||
|
<span class="view-btn-label">BOARDING PASSES</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ── 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 {
|
||||||
|
text-decoration: none;
|
||||||
|
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>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
|
||||||
|
import {ChartType} from "@/Types/types";
|
||||||
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
type: ChartType
|
||||||
|
height: number | string
|
||||||
|
options: object
|
||||||
|
series: unknown[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const ready = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PlaneLoader v-if="!ready" />
|
||||||
|
<VueApexCharts
|
||||||
|
:type="type"
|
||||||
|
:height="height"
|
||||||
|
:options="options"
|
||||||
|
:series="series"
|
||||||
|
:class="{ 'chart-hidden': !ready }"
|
||||||
|
@mounted="ready = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { Flight } from "@/Types/types";
|
|
||||||
import DepartureBoard from "@/Components/FlightsGoneBy/DepartureBoard.vue";
|
|
||||||
import BoardingPasses from "@/Components/FlightsGoneBy/BoardingPasses.vue";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import ProfileMap from "@/Components/FlightsGoneBy/ProfileMap.vue";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
layout: MainLayout
|
|
||||||
})
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
user: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
flights: Flight[]
|
|
||||||
canEdit: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
type View = 'board' | 'passes' | 'map'
|
|
||||||
const activeView = ref<View>('map')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Head :title="`${user.name}'s Flights`" />
|
|
||||||
|
|
||||||
<div class="board-wrapper">
|
|
||||||
<div class="board-header">
|
|
||||||
<div class="board-title-group">
|
|
||||||
<span class="board-eyebrow">FLIGHT HISTORY</span>
|
|
||||||
<h1 class="board-title">{{ user.name }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="board-count">
|
|
||||||
<span class="count-number">{{ flights.length }}</span>
|
|
||||||
<span class="count-label">FLIGHTS</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- View switcher toolbar -->
|
|
||||||
<div class="view-toolbar">
|
|
||||||
<button
|
|
||||||
class="view-btn"
|
|
||||||
:class="{ active: activeView === 'map' }"
|
|
||||||
@click="activeView = 'map'"
|
|
||||||
>
|
|
||||||
<span class="view-btn-icon mdi mdi-earth"></span>
|
|
||||||
<span class="view-btn-label">Map</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="view-btn"
|
|
||||||
:class="{ active: activeView === 'board' }"
|
|
||||||
@click="activeView = 'board'"
|
|
||||||
>
|
|
||||||
<span class="view-btn-icon mdi mdi-table"></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 mdi mdi-ticket"></span>
|
|
||||||
<span class="view-btn-label">BOARDING PASSES</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DepartureBoard v-if="activeView === 'board'" :flights="flights" :canEdit="canEdit" />
|
|
||||||
<BoardingPasses v-else-if="activeView === 'passes'" :flights="flights" />
|
|
||||||
<ProfileMap v-else-if="activeView === 'map'" :flights="flights" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@500;700&display=swap');
|
|
||||||
|
|
||||||
/* ── Wrapper ── */
|
|
||||||
.board-wrapper {
|
|
||||||
min-height: 100dvh;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
font-family: 'Barlow', sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header ── */
|
|
||||||
.board-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-bottom: 1px solid rgba(255,193,7,0.2);
|
|
||||||
padding-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-eyebrow {
|
|
||||||
display: block;
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
color: #ffc107;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-title {
|
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
|
||||||
font-size: 2.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #f0f2f5;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
line-height: 1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-count {
|
|
||||||
text-align: right;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-number {
|
|
||||||
display: block;
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-label {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
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>
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import {Flight, User} from "@/Types/types";
|
||||||
|
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue";
|
||||||
|
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
|
||||||
|
import BoardingPasses from "@/Components/FlightsGoneBy/BoardingPasses.vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
layout: MainLayout
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user: User
|
||||||
|
flights: Flight[]
|
||||||
|
canEdit: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head :title="`${user.name}'s Flights`" />
|
||||||
|
<ProfileLayout :flights="flights" :user="user">
|
||||||
|
<ProfileViewSwitcher :user="user" active-view="passes" />
|
||||||
|
<BoardingPasses :flights="flights" :canEdit="canEdit" />
|
||||||
|
</ProfileLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import {Flight, User} from "@/Types/types";
|
||||||
|
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue";
|
||||||
|
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
|
||||||
|
import FlightMapAndCharts from "@/Components/FlightsGoneBy/FlightMapAndCharts.vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
layout: MainLayout
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user: User
|
||||||
|
flights: Flight[]
|
||||||
|
canEdit: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head :title="`${user.name}'s Flights`" />
|
||||||
|
<ProfileLayout :flights="flights" :user="user">
|
||||||
|
<ProfileViewSwitcher active-view="map" :user="user" />
|
||||||
|
<FlightMapAndCharts :flights="flights" :canEdit="canEdit" />
|
||||||
|
</ProfileLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { ref, defineAsyncComponent } from 'vue'
|
||||||
|
import {Flight, ProfileView, User} from "@/Types/types";
|
||||||
|
import { router } from '@inertiajs/vue3'
|
||||||
|
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue";
|
||||||
|
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
|
||||||
|
import DepartureBoard from "@/Components/FlightsGoneBy/DepartureBoard.vue";
|
||||||
|
import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
|
||||||
|
|
||||||
|
const FlightMapAndCharts = defineAsyncComponent(
|
||||||
|
() => import('@/Components/FlightsGoneBy/FlightMapAndCharts.vue')
|
||||||
|
)
|
||||||
|
const BoardingPasses = defineAsyncComponent(
|
||||||
|
() => import('@/Components/FlightsGoneBy/BoardingPasses.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
defineOptions({ layout: MainLayout })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: User
|
||||||
|
flights: Flight[]
|
||||||
|
canEdit: boolean
|
||||||
|
initialView?: ProfileView
|
||||||
|
}>()
|
||||||
|
|
||||||
|
|
||||||
|
const activeView = ref<ProfileView>(props.initialView ?? 'board')
|
||||||
|
|
||||||
|
const routeNames = {
|
||||||
|
map: 'profile.map',
|
||||||
|
board: 'profile.departure-board',
|
||||||
|
passes: 'profile.boarding-passes',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function switchView(view: 'map' | 'board' | 'passes') {
|
||||||
|
activeView.value = view
|
||||||
|
window.history.replaceState(
|
||||||
|
window.history.state,
|
||||||
|
'',
|
||||||
|
route(routeNames[view], { username: props.user.name })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head :title="`${user.name}'s Flights`" />
|
||||||
|
<ProfileLayout :flights="flights" :user="user">
|
||||||
|
<ProfileViewSwitcher :active-view="activeView" @update:active-view="switchView" />
|
||||||
|
<DepartureBoard v-if="activeView === 'board'" :flights="flights" :canEdit="canEdit" />
|
||||||
|
<BoardingPasses v-else-if="activeView === 'passes'" :flights="flights" :canEdit="canEdit" />
|
||||||
|
<FlightMapAndCharts v-else-if="activeView === 'map'" :flights="flights" :canEdit="canEdit" />
|
||||||
|
</ProfileLayout>
|
||||||
|
</template>
|
||||||
Vendored
+3
@@ -8,6 +8,9 @@ declare module '@vue/runtime-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProfileView = 'map' | 'board' | 'passes';
|
||||||
|
export type ChartType = "line" | "area" | "bar" | "pie" | "donut" | "radialBar" | "scatter" | "bubble" | "heatmap" | "candlestick" | "boxPlot" | "radar" | "polarArea" | "rangeBar" | "rangeArea" | "treemap" | undefined
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ Route::domain(config('app.domain'))->group(
|
|||||||
|
|
||||||
|
|
||||||
Route::get('/u/{username}', [FlightProfileController::class, 'view'])->name('profile.view');
|
Route::get('/u/{username}', [FlightProfileController::class, 'view'])->name('profile.view');
|
||||||
|
Route::get('/u/{username}/map', [FlightProfileController::class, 'map'])->name('profile.map');
|
||||||
|
Route::get('/u/{username}/departure-board', [FlightProfileController::class, 'departureBoard'])->name('profile.departure-board');
|
||||||
|
Route::get('/u/{username}/boarding-passes', [FlightProfileController::class, 'boardingPasses'])->name('profile.boarding-passes');
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user