From de183995b685404b365b48d0e3a453d254e67efe Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 25 Apr 2026 22:57:18 +1000 Subject: [PATCH] Updated logo API --- app/Http/Controllers/FeedController.php | 31 +++ app/Http/Controllers/FlightController.php | 104 ++++++++-- app/Http/Resources/UserFlightResource.php | 29 --- app/Models/UserAction.php | 35 +++- app/Models/UserFlight.php | 96 +++++++++ app/Policies/UserFlightPolicy.php | 2 +- ...04_24_143954_update_user_actions_table.php | 29 +++ resources/css/app.css | 3 +- .../FlightsGoneBy/AirlineSearchBox.vue | 2 +- .../Components/FlightsGoneBy/BoardingPass.vue | 12 ++ .../FlightsGoneBy/DepartureBoard.vue | 36 +++- .../FlightsGoneBy/Feed/FeedItem.vue | 185 ++++++++++++++++++ .../Feed/FieldChanges/AircraftFieldChange.vue | 34 ++++ .../Feed/FieldChanges/AirlineFieldChange.vue | 41 ++++ .../Feed/FieldChanges/AirportFieldChange.vue | 43 ++++ .../Feed/FieldChanges/DateFieldChange.vue | 27 +++ .../FieldChanges/FlightClassFieldChange.vue | 27 +++ .../Feed/FieldChanges/GenericFieldChange.vue | 100 ++++++++++ .../Feed/FlightBookedFeedItem.vue | 18 ++ .../Feed/FlightCancelledFeedItem.vue | 49 +++++ .../Feed/FlightUpdatedFeedItem.vue | 129 ++++++++++++ .../FlightsGoneBy/ProfileViewSwitcher.vue | 1 + resources/js/Pages/AddFlight.vue | 3 +- resources/js/Pages/Feed.vue | 75 +++++++ resources/js/Types/types.d.ts | 34 +++- routes/web.php | 7 +- 26 files changed, 1088 insertions(+), 64 deletions(-) create mode 100644 app/Http/Controllers/FeedController.php create mode 100644 database/migrations/2026_04_24_143954_update_user_actions_table.php create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FeedItem.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue create mode 100644 resources/js/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue create mode 100644 resources/js/Pages/Feed.vue diff --git a/app/Http/Controllers/FeedController.php b/app/Http/Controllers/FeedController.php new file mode 100644 index 0000000..989615b --- /dev/null +++ b/app/Http/Controllers/FeedController.php @@ -0,0 +1,31 @@ +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, + ]); + } +} diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index 58fe7cc..82d67f7 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -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; - } - - $actions = []; + $changes = []; foreach ($dirty as $field => $newValue) { - $original = $flight->getOriginal($field); - $actions[] = [ - 'user_id' => $flight->user_id, - 'user_flight_id' => $flight->id, - 'message' => $this->formatChange($field, $original, $newValue), - 'created_at' => now(), - 'updated_at' => now(), - ]; + $changes[] = $this->formatChange($field, $flight->getOriginal($field), $newValue); } - UserAction::insert($actions); + UserAction::create([ + 'user_id' => $flight->user_id, + 'user_flight_id' => $flight->id, + '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(), diff --git a/app/Http/Resources/UserFlightResource.php b/app/Http/Resources/UserFlightResource.php index 20cdc3a..2a022ee 100644 --- a/app/Http/Resources/UserFlightResource.php +++ b/app/Http/Resources/UserFlightResource.php @@ -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, - ]; - } } diff --git a/app/Models/UserAction.php b/app/Models/UserAction.php index b037e3f..e4ed715 100644 --- a/app/Models/UserAction.php +++ b/app/Models/UserAction.php @@ -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'); + } + } diff --git a/app/Models/UserFlight.php b/app/Models/UserFlight.php index e9e3b2a..d07f94d 100644 --- a/app/Models/UserFlight.php +++ b/app/Models/UserFlight.php @@ -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); diff --git a/app/Policies/UserFlightPolicy.php b/app/Policies/UserFlightPolicy.php index 91eacb4..a6b5e98 100644 --- a/app/Policies/UserFlightPolicy.php +++ b/app/Policies/UserFlightPolicy.php @@ -45,7 +45,7 @@ class UserFlightPolicy */ public function delete(User $user, UserFlight $userFlight): bool { - return false; + return $user->id === $userFlight->user_id; } /** diff --git a/database/migrations/2026_04_24_143954_update_user_actions_table.php b/database/migrations/2026_04_24_143954_update_user_actions_table.php new file mode 100644 index 0000000..13572d4 --- /dev/null +++ b/database/migrations/2026_04_24_143954_update_user_actions_table.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 6c1185f..e3d5e05 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -67,7 +67,8 @@ } -a { +a, a:visited { + color: var(--text); cursor: pointer; text-decoration: none; } diff --git a/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue b/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue index e38eab1..712c79e 100644 --- a/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue +++ b/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue @@ -9,7 +9,6 @@ const props = defineProps<{ errorMessages?: string[] | string }>() -const page = usePage().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}`" /> diff --git a/resources/js/Components/FlightsGoneBy/BoardingPass.vue b/resources/js/Components/FlightsGoneBy/BoardingPass.vue index a8549eb..05b0610 100644 --- a/resources/js/Components/FlightsGoneBy/BoardingPass.vue +++ b/resources/js/Components/FlightsGoneBy/BoardingPass.vue @@ -78,6 +78,18 @@ defineProps<{ diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue new file mode 100644 index 0000000..fab5879 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue @@ -0,0 +1,34 @@ + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue new file mode 100644 index 0000000..ca276fd --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue @@ -0,0 +1,41 @@ + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue new file mode 100644 index 0000000..f03a404 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue @@ -0,0 +1,43 @@ + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue new file mode 100644 index 0000000..d397b46 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue new file mode 100644 index 0000000..ac1c460 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue @@ -0,0 +1,27 @@ + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue new file mode 100644 index 0000000..c25c245 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue b/resources/js/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue new file mode 100644 index 0000000..9eaa681 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue b/resources/js/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue new file mode 100644 index 0000000..fd67000 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue b/resources/js/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue new file mode 100644 index 0000000..6b99e02 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/ProfileViewSwitcher.vue b/resources/js/Components/FlightsGoneBy/ProfileViewSwitcher.vue index 516422a..401c6c2 100644 --- a/resources/js/Components/FlightsGoneBy/ProfileViewSwitcher.vue +++ b/resources/js/Components/FlightsGoneBy/ProfileViewSwitcher.vue @@ -12,6 +12,7 @@ const emit = defineEmits<{