Updated logo API
This commit is contained in:
@@ -67,7 +67,8 @@
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
a, a:visited {
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const props = defineProps<{
|
||||
errorMessages?: string[] | string
|
||||
}>()
|
||||
|
||||
const page = usePage<SharedProps>().props
|
||||
|
||||
const model = defineModel<{ value: number, title: string, logo_url: string } | null>()
|
||||
|
||||
@@ -58,6 +57,7 @@ const searchAirlines = async (query: string) => {
|
||||
style="padding: 0.25em"
|
||||
width="40"
|
||||
height="40"
|
||||
:alt="`${model.title}`"
|
||||
:src="`${model.logo_url}`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -78,6 +78,18 @@ defineProps<{
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.feed-boarding-pass{
|
||||
max-width: 600px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.boarding-pass{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pass-stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -11,6 +11,7 @@ import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||
import {FlightStats} from "@/Composables/useFlightStats";
|
||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||
import CrewTooltip from "@/Components/FlightsGoneBy/CrewTooltip.vue";
|
||||
import {Link, router} from "@inertiajs/vue3";
|
||||
|
||||
const props = defineProps<{
|
||||
flightStats: FlightStats
|
||||
@@ -22,6 +23,8 @@ function editRoute(id: number) {
|
||||
return route('flights.edit', { flight: id })
|
||||
}
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
const ITEMS_PER_PAGE = 25
|
||||
|
||||
const headers = [
|
||||
@@ -35,6 +38,7 @@ const headers = [
|
||||
{ title: 'DURATION', key: 'duration', sortable: true },
|
||||
{ title: 'DISTANCE', key: 'distance', sortable: true },
|
||||
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
||||
{ title: 'REG', key: 'aircraft_registration', sortable: true },
|
||||
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
@@ -64,6 +68,8 @@ const customKeySort = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const deleting = ref(false)
|
||||
const sortBy = ref<DataTableSortItem[]>([])
|
||||
const currentPage = ref(1)
|
||||
|
||||
@@ -236,6 +242,12 @@ watch(
|
||||
</AircraftToolTip>
|
||||
</td>
|
||||
|
||||
<td class="v-data-table__td ">
|
||||
<span class="mono-tag">
|
||||
{{(item as Flight).aircraft_registration}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="v-data-table__td ">
|
||||
<span class="class-cell">
|
||||
<CrewTooltip v-if="(item as Flight).flight_reason?.name == 'Crew'" :crew-type="(item as Flight).crew_type!">
|
||||
@@ -266,8 +278,30 @@ watch(
|
||||
title="Edit"
|
||||
:href="editRoute((item as Flight).id)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-trash-can-outline"
|
||||
title="Delete"
|
||||
@click="showDeleteDialog = true"
|
||||
/>
|
||||
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-dialog 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
|
||||
color="error"
|
||||
:loading="deleting"
|
||||
@click="deleting = true; router.delete(route('flights.delete', { flight: (item as Flight).id }), { onFinish: () => deleting = false })"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
@@ -277,7 +311,7 @@ watch(
|
||||
|
||||
<template #no-data>
|
||||
<div class="no-data">
|
||||
<span>NO FLIGHTS ON RECORD</span>
|
||||
<span>NO FLIGHTS DEPARTING</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
UserAction,
|
||||
UserActionFlightBookedData,
|
||||
UserActionFlightCancelledData,
|
||||
UserActionFlightUpdatedData
|
||||
} from "@/Types/types";
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
import FlightBookedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue";
|
||||
import {computed} from "vue";
|
||||
import {Link} from "@inertiajs/vue3";
|
||||
import FlightUpdatedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue";
|
||||
import FlightCancelledFeedItem from "@/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
action: UserAction
|
||||
}>()
|
||||
|
||||
const flight = computed(() =>{
|
||||
if (props.action.type === 'flight_booked' || props.action.type === 'flight_logged'){
|
||||
return (props.action.data as UserActionFlightBookedData).flight
|
||||
} else if (props.action.type === 'flight_updated'){
|
||||
return (props.action.data as UserActionFlightUpdatedData).updated
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const badgeVariant = computed(() => {
|
||||
switch (props.action.type) {
|
||||
case 'flight_booked': return 'generic'
|
||||
case 'flight_logged': return 'generic'
|
||||
case 'flight_updated': return 'economy'
|
||||
case 'flight_cancelled': return 'crew'
|
||||
default: return 'economy'
|
||||
}
|
||||
})
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime()
|
||||
const mins = Math.floor(diff / 60000)
|
||||
if (mins < 1) return 'just now'
|
||||
if (mins < 60) return `${mins}m ago`
|
||||
const hrs = Math.floor(mins / 60)
|
||||
if (hrs < 24) return `${hrs}h ago`
|
||||
return `${Math.floor(hrs / 24)}d ago`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="feed-item glass glass-border">
|
||||
<div class="card-top">
|
||||
<div class="avatar">
|
||||
<Link :href="route('profile.view', { user: action.user?.name })">
|
||||
{{ action.user?.name?.charAt(0).toUpperCase() ?? '?' }}
|
||||
</Link>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="name">
|
||||
<Link :href="route('profile.view', { user: action.user?.name })">
|
||||
{{ action.user?.name ?? 'Unknown' }}
|
||||
</Link>
|
||||
</span>
|
||||
<span class="time">{{ timeAgo(action.created_at) }}</span>
|
||||
</div>
|
||||
<span class="type-badge">
|
||||
<InlineBadge :variant="badgeVariant">{{ action.display_type }}</InlineBadge>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<FlightBookedFeedItem v-if="action.type == 'flight_booked' || action.type == 'flight_logged'" :flight="(action.data as UserActionFlightBookedData).flight" ></FlightBookedFeedItem>
|
||||
<FlightUpdatedFeedItem v-if="action.type == 'flight_updated'" :data="(action.data as UserActionFlightUpdatedData)" ></FlightUpdatedFeedItem>
|
||||
<FlightCancelledFeedItem v-if="action.type == 'flight_cancelled'" :data="(action.data as UserActionFlightCancelledData)" flight=""></FlightCancelledFeedItem>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<Link v-if="flight" :href="`/u/${action.user?.name}/departure-board/${flight?.id}`" class="view-flight-link">
|
||||
View Flight
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M13 6l6 6-6 6"/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feed-item {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px solid rgba(99, 102, 241, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #818cf8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.changes {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.3rem 1rem;
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.changes dt {
|
||||
color: #9ca3af;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.changes dd {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.view-flight-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #818cf8;
|
||||
text-decoration: none;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.25);
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.view-flight-link:hover {
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
border-color: rgba(99, 102, 241, 0.45);
|
||||
}
|
||||
|
||||
.view-flight-link svg {
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionChange} from "@/Types/types";
|
||||
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||
import {computed} from "vue";
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
original: Flight
|
||||
updated: Flight
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GenericFieldChange label="Aircraft">
|
||||
<template #from>
|
||||
<AircraftToolTip v-if="original.aircraft" :aircraft="original.aircraft">
|
||||
{{ original.aircraft.display_name}}
|
||||
</AircraftToolTip>
|
||||
<span v-else>None</span>
|
||||
</template>
|
||||
<template #to>
|
||||
<AircraftToolTip v-if="updated.aircraft" :aircraft="updated.aircraft">
|
||||
{{ updated.aircraft.display_name}}
|
||||
</AircraftToolTip>
|
||||
<span v-else>None</span>
|
||||
</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionChange} from "@/Types/types";
|
||||
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||
import {computed} from "vue";
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
original: Flight
|
||||
updated: Flight
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GenericFieldChange label="Airline">
|
||||
<template #from>
|
||||
<span class="airline">
|
||||
<AirlineLogo :airline="original.airline" />
|
||||
{{ original.airline?.display_name ?? 'None' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #to>
|
||||
<span class="airline">
|
||||
<AirlineLogo :airline="updated.airline" />
|
||||
{{ updated.airline?.display_name ?? 'None' }}
|
||||
</span>
|
||||
</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
<style scoped>
|
||||
.airline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.from .airline {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionChange} from "@/Types/types";
|
||||
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||
import {computed} from "vue";
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
change: UserActionChange
|
||||
label: string
|
||||
original: Flight
|
||||
updated: Flight
|
||||
}>()
|
||||
|
||||
const originalAirport = computed(() =>
|
||||
props.change.field === 'departure_airport_id'
|
||||
? props.original.departure_airport
|
||||
: props.original.arrival_airport
|
||||
)
|
||||
|
||||
const updatedAirport = computed(() =>
|
||||
props.change.field === 'departure_airport_id'
|
||||
? props.updated.departure_airport
|
||||
: props.updated.arrival_airport
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GenericFieldChange :label="label">
|
||||
<template #from>
|
||||
<AirportToolTip :airport="originalAirport">
|
||||
{{ originalAirport.municipality }} ({{originalAirport.display_code}})
|
||||
</AirportToolTip>
|
||||
</template>
|
||||
<template #to>
|
||||
<AirportToolTip :airport="updatedAirport">
|
||||
{{ updatedAirport.municipality }} ({{updatedAirport.display_code}})
|
||||
</AirportToolTip>
|
||||
</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionChange} from "@/Types/types";
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
change: UserActionChange
|
||||
label: string
|
||||
original: Flight
|
||||
updated: Flight
|
||||
}>()
|
||||
|
||||
const displayField = computed(() =>
|
||||
props.change.field === 'departure_date' ? 'departure_date_display' : 'arrival_date_display'
|
||||
)
|
||||
|
||||
const timeField = computed(() =>
|
||||
props.change.field === 'departure_date' ? 'departure_time_display' : 'arrival_time_display'
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GenericFieldChange :label="label">
|
||||
<template #from>{{ (original as any)[displayField] }} at {{ (original as any)[timeField] }}</template>
|
||||
<template #to>{{ (updated as any)[displayField] }} at {{ (updated as any)[timeField] }}</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionChange} from "@/Types/types";
|
||||
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
original: Flight
|
||||
updated: Flight
|
||||
}>()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GenericFieldChange label="Flight Class">
|
||||
<template #from>
|
||||
<span style="display:inline-flex"><FlightClassBadge style="text-decoration:line-through" :flight="original"/></span>
|
||||
</template>
|
||||
<template #to>
|
||||
<span style="display:inline-flex"><FlightClassBadge :flight="updated"/></span>
|
||||
</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field-change">
|
||||
<span class="field-label">{{ label }}</span>
|
||||
<div class="field-values">
|
||||
<span class="from">
|
||||
<slot name="from" />
|
||||
</span>
|
||||
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M13 6l6 6-6 6"/>
|
||||
</svg>
|
||||
<span class="to">
|
||||
<slot name="to" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.field-change {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.field-change:nth-child(odd) {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.field-values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.from {
|
||||
color: #6b7280;
|
||||
text-decoration: line-through;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
color: #374151;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.to {
|
||||
color: #f9fafb;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.field-change {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.field-values {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1rem 1fr;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.to {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight} from "@/Types/types";
|
||||
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
flight: Flight
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flight-booked">
|
||||
<BoardingPass :class="`feed-boarding-pass`" :flight="flight" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionFlightCancelledData} from "@/Types/types";
|
||||
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
data: UserActionFlightCancelledData
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flight-booked">
|
||||
<div class="cancelled-flight">
|
||||
<span v-if="data.flight.flight_number" class="flight-summary">
|
||||
<AirlineLogo :airline="data.flight.airline" />
|
||||
<span>Flight <strong>{{ data.flight.flight_number }}</strong> on {{ data.flight.departure_date_display }} at {{ data.flight.departure_time_display }}</span>
|
||||
</span>
|
||||
<span v-else class="flight-summary">
|
||||
<AirlineLogo :airline="data.flight.airline" />
|
||||
<span>Flight from {{ data.flight.departure_airport.municipality }} ({{ data.flight.departure_airport.display_code }}) → {{ data.flight.arrival_airport.municipality }} ({{ data.flight.arrival_airport.display_code }}) on {{ data.flight.departure_date_display }} at {{ data.flight.departure_time_display }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.cancelled-flight {
|
||||
padding: 0.75rem 1.25rem;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.flight-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.flight-summary strong {
|
||||
color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import {Flight, UserActionFlightUpdatedData} from "@/Types/types";
|
||||
import AirportFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue";
|
||||
import AircraftFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue";
|
||||
import AirlineFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue";
|
||||
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||
import DateFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue";
|
||||
import FlightClassFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue";
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
data: UserActionFlightUpdatedData
|
||||
}>()
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
flight_number: 'Flight Number',
|
||||
departure_date: 'Departure Date',
|
||||
arrival_date: 'Arrival Date',
|
||||
departure_airport_id: 'Departure Airport',
|
||||
arrival_airport_id: 'Arrival Airport',
|
||||
airline_id: 'Airline',
|
||||
aircraft_id: 'Aircraft',
|
||||
aircraft_registration: 'Aircraft Registration',
|
||||
flight_class_id: 'Flight Class',
|
||||
seat_number: 'Seat Number',
|
||||
seat_type_id: 'Seat Type',
|
||||
flight_reason_id: 'Flight Reason',
|
||||
crew_type_id: 'Crew Type',
|
||||
note: 'Note',
|
||||
auto_update: 'Auto Update',
|
||||
}
|
||||
|
||||
const relationMap: Record<string, { relation: string, display: string }> = {
|
||||
flight_reason_id: { relation: 'flight_reason', display: 'name' },
|
||||
seat_type_id: { relation: 'seat_type', display: 'name' },
|
||||
flight_class_id: { relation: 'flight_class', display: 'name' },
|
||||
crew_type_id: { relation: 'crew_type', display: 'name' },
|
||||
}
|
||||
|
||||
function resolveValue(flight: Flight, field: string): string {
|
||||
const mapping = relationMap[field]
|
||||
if (mapping) {
|
||||
return (flight as any)[mapping.relation]?.[mapping.display] ?? 'None'
|
||||
}
|
||||
return (flight as any)[field] ?? 'None'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="updated-flight">
|
||||
<span v-if="data.updated.flight_number" class="flight-summary">
|
||||
<AirlineLogo :airline="data.updated.airline" />
|
||||
<span>Flight <strong>{{ data.updated.flight_number }}</strong> on {{ data.updated.departure_date_display }} at {{ data.updated.departure_time_display }}</span>
|
||||
</span>
|
||||
<span v-else class="flight-summary">
|
||||
<AirlineLogo :airline="data.updated.airline" />
|
||||
<span>Flight from {{ data.updated.departure_airport.municipality }} ({{ data.updated.departure_airport.display_code }}) → {{ data.updated.arrival_airport.municipality }} ({{ data.updated.arrival_airport.display_code }}) on {{ data.updated.departure_date_display }} at {{ data.updated.departure_time_display }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="changes">
|
||||
|
||||
<template v-for="change in data.changes" :key="change.field">
|
||||
<AirportFieldChange
|
||||
v-if="change.field === 'departure_airport_id' || change.field === 'arrival_airport_id'"
|
||||
:change="change"
|
||||
:label="change.field === 'departure_airport_id' ? 'Departure Airport' : 'Arrival Airport'"
|
||||
:original="data.original"
|
||||
:updated="data.updated"
|
||||
/>
|
||||
<AircraftFieldChange
|
||||
v-else-if="change.field === 'aircraft_id'"
|
||||
:original="data.original"
|
||||
:updated="data.updated"
|
||||
/>
|
||||
<AirlineFieldChange
|
||||
v-else-if="change.field === 'airline_id'"
|
||||
:original="data.original"
|
||||
:updated="data.updated"
|
||||
/>
|
||||
<DateFieldChange
|
||||
v-else-if="change.field === 'departure_date' || change.field === 'arrival_date'"
|
||||
:change="change"
|
||||
:label="change.field === 'departure_date' ? 'Departure Date' : 'Arrival Date'"
|
||||
:original="data.original"
|
||||
:updated="data.updated"
|
||||
/>
|
||||
<FlightClassFieldChange
|
||||
v-else-if="change.field === 'flight_class_id'"
|
||||
:original="data.original"
|
||||
:updated="data.updated"
|
||||
/>
|
||||
<GenericFieldChange
|
||||
v-else
|
||||
:label="fieldLabels[change.field] ?? change.field"
|
||||
>
|
||||
<template #from>{{ resolveValue(data.original, change.field) }}</template>
|
||||
<template #to>{{ resolveValue(data.updated, change.field) }}</template>
|
||||
</GenericFieldChange>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.changes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.updated-flight {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.flight-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.flight-summary strong {
|
||||
color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ const emit = defineEmits<{
|
||||
|
||||
<template>
|
||||
<div class="view-toolbar">
|
||||
|
||||
<button
|
||||
class="view-btn"
|
||||
:class="{ active: activeView === 'map' }"
|
||||
|
||||
@@ -46,7 +46,7 @@ const lookupError = ref<string | null>(null)
|
||||
const lookupComplete = ref(true)
|
||||
|
||||
interface LookupResult {
|
||||
airline_options: { value: number; title: string }[]
|
||||
airline_options: { value: number; title: string, logo_url: string }[]
|
||||
from_options: { value: number; title: string; country_code: string }[]
|
||||
to_options: { value: number; title: string; country_code: string }[]
|
||||
aircraft_options: { value: number; title: string }[]
|
||||
@@ -72,7 +72,6 @@ async function lookupFlight() {
|
||||
}
|
||||
lookupResult.value = data
|
||||
lookupComplete.value = true
|
||||
|
||||
if (data.airline_options?.length) {
|
||||
airlineOptionsData.value = data.airline_options
|
||||
if (!form.airline) form.airline = data.airline_options[0]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import {User, UserAction} from "@/Types/types";
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
import FeedItem from "@/Components/FlightsGoneBy/Feed/FeedItem.vue";
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
feed: UserAction[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Feed" />
|
||||
|
||||
<div class="feed-page">
|
||||
<header class="feed-header">
|
||||
<h1>Feed</h1>
|
||||
<span class="feed-count">{{ feed.length }} updates</span>
|
||||
</header>
|
||||
|
||||
<div v-if="feed.length === 0" class="empty">
|
||||
<p>Nothing here yet — follow someone to see their flight updates!</p>
|
||||
</div>
|
||||
|
||||
<div class="feed-list">
|
||||
<FeedItem v-for="action in feed" :key="action.id" :action="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feed-page {
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.feed-page {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feed-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feed-count {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty p {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.feed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
Vendored
+33
-1
@@ -8,7 +8,7 @@ declare module '@vue/runtime-core' {
|
||||
}
|
||||
}
|
||||
|
||||
export type ProfileView = 'map' | 'board' | 'passes';
|
||||
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 type BadgeVariant = 'first' | 'business' | 'premium' | 'economy' | 'private' | 'unspecified' | 'generic' | 'general_aviation' | 'crew'
|
||||
|
||||
@@ -19,6 +19,38 @@ export interface User {
|
||||
email_verified_at: string | null
|
||||
}
|
||||
|
||||
export type UserActionType = "flight_cancelled" | "flight_booked" | "flight_updated" | "flight_logged" | "flight_deleted"
|
||||
export type UserActionDataKey = "field" | "from" | "to"
|
||||
|
||||
export type UserActionFlightBookedData = {
|
||||
flight: Flight
|
||||
}
|
||||
|
||||
export type UserActionFlightCancelledData = {
|
||||
flight: Flight
|
||||
}
|
||||
|
||||
export type UserActionFlightUpdatedData = {
|
||||
changes: UserActionChange[],
|
||||
original: Flight,
|
||||
updated: Flight
|
||||
}
|
||||
|
||||
export type UserActionChange = {field: string, from: string, to: string}
|
||||
export type UserActionData = UserActionFlightBookedData | UserActionFlightCancelledData | UserActionFlightUpdatedData
|
||||
|
||||
export interface UserAction {
|
||||
id: number
|
||||
user_id: number
|
||||
type: UserActionType
|
||||
data: UserActionData
|
||||
created_at: string
|
||||
updated_at: string
|
||||
user: User
|
||||
display_type: string
|
||||
user_flight: Flight
|
||||
}
|
||||
|
||||
export type SharedProps = import('@inertiajs/core').PageProps & {
|
||||
auth: {
|
||||
user: User | null
|
||||
|
||||
Reference in New Issue
Block a user