Updated logo API

This commit is contained in:
2026-04-25 22:57:18 +10:00
parent 678096b463
commit de183995b6
26 changed files with 1088 additions and 64 deletions
@@ -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>