Updated logo API
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\UserAction;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FeedController extends Controller
|
||||
{
|
||||
public function view()
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$followeeIds = $user->following()->pluck('followee_id');
|
||||
|
||||
$feed = UserAction::whereIn('user_id', $followeeIds)
|
||||
->whereNotIn('type', ['flight_deleted'])
|
||||
->with([
|
||||
'user',
|
||||
])
|
||||
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
return Inertia::render('Feed', [
|
||||
'user' => $user,
|
||||
'feed' => $feed,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,9 @@ class FlightController extends Controller
|
||||
$airlines = $code
|
||||
? Airline::where($codeColumn, $code)
|
||||
->get()
|
||||
->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name])
|
||||
->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name, 'logo_url' => $airline->logo_url])
|
||||
->values()
|
||||
->toArray()
|
||||
: collect()->toArray();
|
||||
return response()->json([
|
||||
'airline_options' => $airlines,
|
||||
@@ -74,27 +76,23 @@ class FlightController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
private function recordChanges(UserFlight $flight): void
|
||||
private function recordChanges(UserFlight $flight, array $dirty, array $original, array $updated): void
|
||||
{
|
||||
$dirty = $flight->getDirty();
|
||||
|
||||
if (empty($dirty)) {
|
||||
return;
|
||||
$changes = [];
|
||||
foreach ($dirty as $field => $newValue) {
|
||||
$changes[] = $this->formatChange($field, $flight->getOriginal($field), $newValue);
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
foreach ($dirty as $field => $newValue) {
|
||||
$original = $flight->getOriginal($field);
|
||||
$actions[] = [
|
||||
UserAction::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'user_flight_id' => $flight->id,
|
||||
'message' => $this->formatChange($field, $original, $newValue),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
UserAction::insert($actions);
|
||||
'data' => [
|
||||
'changes' => $changes,
|
||||
'original' => $original,
|
||||
'updated' => $updated,
|
||||
],
|
||||
'type' => 'flight_updated',
|
||||
]);
|
||||
}
|
||||
|
||||
private array $labelCache = [];
|
||||
@@ -129,13 +127,17 @@ class FlightController extends Controller
|
||||
return $this->labelCache[$cacheKey] = $label;
|
||||
}
|
||||
|
||||
private function formatChange(string $field, mixed $from, mixed $to): string
|
||||
private function formatChange(string $field, mixed $from, mixed $to): array
|
||||
{
|
||||
$label = str($field)->replace('_id', '')->replace('_', ' ')->title();
|
||||
$fromLabel = $this->resolveLabel($field, $from);
|
||||
$toLabel = $this->resolveLabel($field, $to);
|
||||
|
||||
return "{$label} changed from {$fromLabel} to {$toLabel}";
|
||||
return [
|
||||
'field' => $field,
|
||||
'from' => $fromLabel,
|
||||
'to' => $toLabel,
|
||||
];
|
||||
}
|
||||
|
||||
private function flightPayload(array $validated): array
|
||||
@@ -168,9 +170,35 @@ class FlightController extends Controller
|
||||
|
||||
$newFlight = auth()->user()->flights()->create($this->flightPayload($validated));
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $newFlight->user_id,
|
||||
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_logged',
|
||||
'data' => [
|
||||
'flight' => $this->flightSnapshot($newFlight->id),
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]);
|
||||
}
|
||||
|
||||
private function flightSnapshot(int $id): array
|
||||
{
|
||||
return UserFlight::with([
|
||||
'departureAirport',
|
||||
'departureAirport.region.country',
|
||||
'arrivalAirport',
|
||||
'arrivalAirport.region.country',
|
||||
'aircraft',
|
||||
'airline',
|
||||
'airline.country',
|
||||
'flightClass',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'crewType',
|
||||
])->find($id)->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function update(Request $request, UserFlight $flight)
|
||||
{
|
||||
$this->authorize('update', $flight);
|
||||
@@ -178,12 +206,46 @@ class FlightController extends Controller
|
||||
$validated = $request->validate($this->rules());
|
||||
|
||||
$flight->fill($this->flightPayload($validated));
|
||||
$this->recordChanges($flight);
|
||||
|
||||
if (!$flight->isDirty()) {
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||
}
|
||||
|
||||
$dirty = $flight->getDirty();
|
||||
$original = $this->flightSnapshot($flight->id);
|
||||
|
||||
$flight->save();
|
||||
|
||||
$updated = $this->flightSnapshot($flight->id);
|
||||
$this->recordChanges($flight, $dirty, $original, $updated);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||
}
|
||||
|
||||
public function delete(UserFlight $flight)
|
||||
{
|
||||
$this->authorize('delete', $flight);
|
||||
|
||||
$snapshot = $this->flightSnapshot($flight->id);
|
||||
|
||||
if(now()->utc()->isBefore($flight->departure_date)){
|
||||
$action = 'flight_deleted';
|
||||
} else {
|
||||
$action = 'flight_cancelled';
|
||||
}
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'type' => $action,
|
||||
'data' => [
|
||||
'flight' => $snapshot,
|
||||
]
|
||||
]);
|
||||
|
||||
$flight->delete();
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name]);
|
||||
}
|
||||
|
||||
public function staticData() : array {
|
||||
return [
|
||||
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
|
||||
|
||||
@@ -9,34 +9,5 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
/** @mixin UserFlight */
|
||||
class UserFlightResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
$departureTz = $this->departureAirport->timezone;
|
||||
$arrivalTz = $this->arrivalAirport->timezone;
|
||||
$duration = $this->departure_date->diffInMinutes($this->arrival_date);
|
||||
$hours = intdiv($duration, 60);
|
||||
$minutes = $duration % 60;
|
||||
$durationDisplay = $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
||||
|
||||
$departureLocal = $this->departure_date->copy()->setTimezone($departureTz);
|
||||
$arrivalLocal = $this->arrival_date?->copy()->setTimezone($arrivalTz);
|
||||
|
||||
$distance = $this->calculateGreatCircleDistance();
|
||||
|
||||
$dayDifference = (int) abs(Carbon::parse($arrivalLocal->toDateString())
|
||||
->diffInDays(Carbon::parse($departureLocal->toDateString())));
|
||||
|
||||
|
||||
return [
|
||||
...$this->resource->toArray(),
|
||||
'departure_date_display' => $departureLocal->format('j M Y'),
|
||||
'departure_time_display' => $departureLocal->format('g:iA'),
|
||||
'arrival_date_display' => $arrivalLocal?->format('j M Y'),
|
||||
'arrival_time_display' => $arrivalLocal?->format('g:iA'),
|
||||
'arrival_day_difference' => $dayDifference,
|
||||
'duration' => $duration,
|
||||
'duration_display' => $durationDisplay,
|
||||
'distance' => $distance,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,46 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserAction extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'user_flight_id',
|
||||
'message',
|
||||
'type',
|
||||
'data'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_flight_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'message' => 'string',
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'display_type',
|
||||
];
|
||||
|
||||
protected function displayType(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => match ($this->type) {
|
||||
'flight_booked' => 'Flight Booked',
|
||||
'flight_cancelled' => 'Flight Cancelled',
|
||||
'flight_updated' => 'Flight Updated',
|
||||
'flight_imported' => 'Flight Imported',
|
||||
'flight_logged' => 'Flight Logged',
|
||||
'flight_deleted' => 'Flight Deleted',
|
||||
default => 'Unknown Action'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -34,6 +36,17 @@ class UserFlight extends Model
|
||||
'auto_update' => 'boolean',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'departure_date_display',
|
||||
'departure_time_display',
|
||||
'arrival_date_display',
|
||||
'arrival_time_display',
|
||||
'arrival_day_difference',
|
||||
'duration',
|
||||
'duration_display',
|
||||
'distance',
|
||||
];
|
||||
|
||||
public function calculateGreatCircleDistance(): float{
|
||||
$earthRadiusKm = 6371;
|
||||
[$depLat, $depLong] = [$this->departureAirport->latitude_deg, $this->departureAirport->longitude_deg];
|
||||
@@ -52,6 +65,89 @@ class UserFlight extends Model
|
||||
return $earthRadiusKm * $c;
|
||||
}
|
||||
|
||||
protected function departureDateDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date
|
||||
->toMutable()
|
||||
->setTimezone($this->departureAirport->timezone)
|
||||
->format('j M Y')
|
||||
);
|
||||
}
|
||||
|
||||
protected function departureTimeDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date
|
||||
->toMutable()
|
||||
->setTimezone($this->departureAirport->timezone)
|
||||
->format('g:iA')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalDateDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->arrival_date
|
||||
?->copy()
|
||||
->setTimezone($this->arrivalAirport->timezone)
|
||||
->format('j M Y')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalTimeDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->arrival_date
|
||||
?->copy()
|
||||
->setTimezone($this->arrivalAirport->timezone)
|
||||
->format('g:iA')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalDayDifference(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!$this->arrival_date) return 0;
|
||||
|
||||
$departureLocal = $this->departure_date->copy()->setTimezone($this->departureAirport->timezone);
|
||||
$arrivalLocal = $this->arrival_date->copy()->setTimezone($this->arrivalAirport->timezone);
|
||||
|
||||
return (int) abs(
|
||||
Carbon::parse($arrivalLocal->toDateString())
|
||||
->diffInDays(Carbon::parse($departureLocal->toDateString()))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function duration(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date->diffInMinutes($this->arrival_date)
|
||||
);
|
||||
}
|
||||
|
||||
protected function durationDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$hours = intdiv($this->duration, 60);
|
||||
$minutes = $this->duration % 60;
|
||||
|
||||
return $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function distance(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->calculateGreatCircleDistance()
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@@ -45,7 +45,7 @@ class UserFlightPolicy
|
||||
*/
|
||||
public function delete(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
return $user->id === $userFlight->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Airport;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_actions', function (Blueprint $table) {
|
||||
$table->dropColumn('message');
|
||||
$table->string('type')->after('user_flight_id');
|
||||
$table->dropColumn('user_flight_id');
|
||||
$table->json('data')->after('type');
|
||||
});
|
||||
|
||||
Airport::whereMunicipality('Fayetteville/Springdale/Rogers')->update(['municipality' => 'Fayetteville']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_actions', function (Blueprint $table) {
|
||||
$table->dropColumn(['type', 'data']);
|
||||
$table->text('message')->after('user_flight_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Http\Controllers\Api\AirlineApiController;
|
||||
use App\Http\Controllers\Api\UserApiController;
|
||||
use App\Http\Controllers\FeedController;
|
||||
use App\Http\Controllers\FlightController;
|
||||
use App\Http\Controllers\FlightImportController;
|
||||
use App\Http\Controllers\FlightProfileController;
|
||||
@@ -44,6 +45,7 @@ Route::domain(config('app.domain'))->group(
|
||||
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
||||
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
||||
Route::put('/flights/{flight}', [FlightController::class, 'update'])->name('flights.update');
|
||||
Route::delete('/flights/{flight}', [FlightController::class, 'delete'])->name('flights.delete');
|
||||
|
||||
Route::get('/reconcile', function () {
|
||||
$flight = new FlightImportController()->reconcile(request());
|
||||
@@ -63,9 +65,8 @@ Route::domain(config('app.domain'))->group(
|
||||
|
||||
Route::post('/u/{user}/follow', [UserController::class, 'follow'])->name('profile.follow');
|
||||
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
Route::get('/feed', [FeedController::class, 'view'])->name('feed');
|
||||
|
||||
});
|
||||
|
||||
Route::post('/import/save', [FlightImportController::class, 'save'])->name('import.save');
|
||||
|
||||
Reference in New Issue
Block a user