diff --git a/app/Http/Controllers/AchievementController.php b/app/Http/Controllers/AchievementController.php
deleted file mode 100644
index 2904358..0000000
--- a/app/Http/Controllers/AchievementController.php
+++ /dev/null
@@ -1,115 +0,0 @@
-get()
- ->groupBy(fn(Achievement $a) => $a->category->name)
- ->map(fn($group) => $group->sortBy('sort_order')->values());
-
- $userAchievements = $user->achievements()
- ->with('achievement')
- ->select(['achievement_id', 'progress'])
- ->orderBy('achievement_id')
- ->get()
- ->keyBy('achievement_id');
-
- $unlockedByCategory = $achievements->map(fn($group) =>
- $group->filter(fn($a) => $userAchievements->get($a->id)?->unlocked)->count()
- );
-
- $unlockedCount = $userAchievements->filter(fn($ua) => $ua->unlocked)->count();
-
- return Inertia::render('UserAchievements', [
- 'user' => $user,
- 'canEdit' => auth()->id() === $user->id,
- 'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
- 'achievements' => $achievements,
- 'userAchievements' => $userAchievements,
- 'loggedInUser' => auth()->user(),
- 'unlockedCount' => $unlockedCount,
- 'unlockedByCategory' => $unlockedByCategory,
- 'totalAchievements' => $achievements->flatten()->count(),
- ]);
- }
-
- function getRegionsByCountryCode(string $countryCode)
- {
- return Country::whereCode($countryCode)
- ->first()
- ->regions()
- ->orderBy('name')
- ->get()
- ->toArray();
- }
-
- public function specific(User $user, Achievement $achievement)
- {
- $regions = match($achievement->internal_name){
- 'fun_challenges.australian_states' => $this->getRegionsByCountryCode('AU'),
- 'fun_challenges.chinese_provinces' => $this->getRegionsByCountryCode('CN'),
- 'fun_challenges.canadian_provinces' => $this->getRegionsByCountryCode('CA'),
- 'fun_challenges.brazilian_states' => $this->getRegionsByCountryCode('BR'),
- 'fun_challenges.us_states' => $this->getRegionsByCountryCode('US'),
- default => [],
- };
-
- $allianceInternalName = match($achievement->internal_name){
- 'airlines_alliances.all_star_alliance' => 'star_alliance',
- 'airlines_alliances.all_oneworld' => 'oneworld',
- 'airlines_alliances.all_skyteam' => 'skyteam',
- 'airlines_alliances.all_vanilla_alliance' => 'vanilla_alliance',
- default => null,
- };
-
- $continents = match($achievement->internal_name){
- 'countries_continents.all_continent_pairs_one_way', 'countries_continents.all_continent_pairs_both_ways' => Continent::all()->toArray(),
- default => [],
- };
-
- $alliance = null;
- $airlines = [];
-
- if ($allianceInternalName) {
- $alliance = Alliance::where('internal_name', $allianceInternalName)
- ->with('airlines')
- ->firstOrFail();
- $airlines = $alliance->airlines()->with('country')->orderBy('name')->get();
- }
-
- $aircraftFamilies = match($achievement->internal_name){
- 'aircraft.all_boeing_7x7' => Aircraft::BOEING_FAMILIES,
- 'aircraft.all_airbus_a3xx' => Aircraft::AIRBUS_FAMILIES,
- default => [],
- };
-
- return Inertia::render('Profile/UserAchievement', [
- 'user' => $user,
- 'achievement' => $achievement,
- 'loggedInUser' => auth()->user(),
- 'userAchievement' => $user->achievements()->where('achievement_id', $achievement->id)->first(),
- 'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
- 'flight_api_url' => FlightProfileController::getUserFlightApiURL($user),
- 'regions' => $regions,
- 'alliance' => $alliance,
- 'airlines' => $airlines,
- 'continents' => $continents,
- 'aircraft_families' => $aircraftFamilies,
- 'achievementCount' => $user->unlockedAchievements()->count(),
- ]);
- }
-
-}
diff --git a/app/Http/Controllers/Api/UserApiController.php b/app/Http/Controllers/Api/UserApiController.php
index 349dadf..6cbffbe 100644
--- a/app/Http/Controllers/Api/UserApiController.php
+++ b/app/Http/Controllers/Api/UserApiController.php
@@ -50,14 +50,4 @@ class UserApiController extends ApiController
]);
}
- public function flights(string $username, Request $request): JsonResponse
- {
- $user = User::where('name', 'ilike', $username)->first();
-
- if (!$user) {
- return response()->json(['message' => 'User not found'], 404);
- }
-
- return response()->json($user->FlightController()->flights($request));
- }
}
diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php
deleted file mode 100644
index ea07a43..0000000
--- a/app/Http/Controllers/FlightProfileController.php
+++ /dev/null
@@ -1,100 +0,0 @@
-check()) {
- $user = auth()->user();
- $defaultPage = $user->resolved_settings['default_login_page'];
-
- $route = match ($defaultPage) {
- 'feed_first' => $user->following()->count() > 0 ? 'feed' : 'profile.view',
- 'feed' => 'feed',
- 'profile' => 'profile.view',
- 'dashboard' => 'dashboard',
- };
-
- $args = $route == 'profile.view' ? $user->name : null;
-
- return redirect()->route($route, $args);
- }
- return redirect()->route('login');
- }
-
- public static function getUserFlightApiURL(User $user){
- return '/data/user/'.$user->name.'/flights';
- }
-
- public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
- return [
- 'user' => $user,
- 'canEdit' => (auth()->check() && auth()->id() === $user->id) || (auth()->check() && auth()->user()->hasRole('admin')),
- 'initialView' => $view,
- 'selectedFlightId' => $selectedFlightId,
- 'flight_api_url' => self::getUserFlightApiURL($user),
- 'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
- 'flightCount' => $user->departedFlights()->count(),
- ];
- }
-
- public function departureBoard(User $user, ?UserFlight $flight = null){
- $profileData = $this->profileData($user, 'board', $flight?->id);
- return Inertia::render('UserProfile', $profileData);
- }
-
- public function map(User $user){
- $profileData = $this->profileData($user, 'map');
- return Inertia::render('UserProfile', $profileData);
- }
-
- public function boardingPasses(User $user){
- $profileData = $this->profileData($user, 'passes');
- return Inertia::render('UserProfile', $profileData);
- }
-
- public function view(User $user)
- {
- $loggedInUser = auth()->user();
-
- $isPrivate = $user->resolved_settings['private_profile'];
-
- if ($isPrivate && $user->id !== $loggedInUser?->id) {
- if (!$loggedInUser || !$loggedInUser->isFollowing($user)) {
- abort(404);
- }
- }
-
- $defaultView = $loggedInUser ? $loggedInUser->resolved_settings['default_profile_view'] : 'map';
-
- return match($defaultView) {
- 'boarding-passes' => $this->boardingPasses($user),
- 'map' => $this->map($user),
- 'departure-board' => $this->departureBoard($user),
- 'achievements' => redirect()->route('profile.achievements', $user->name),
- };
-
- }
-
- public function flight(User $user, UserFlight $userFlight)
- {
- if($userFlight->user_id !== $user->id){
- abort(404);
- }
-
- return Inertia::render('UserFlight', [
- 'flightCount' => $user->departedFlights()->count(),
- 'flight' => $userFlight->snapshot($userFlight->id),
- 'canEdit' => auth()->check() && auth()->id() === $user->id,
- 'user' => $user,
- 'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
- ]);
- }
-}
diff --git a/app/Http/Controllers/FollowerController.php b/app/Http/Controllers/FollowerController.php
new file mode 100644
index 0000000..0e59ccf
--- /dev/null
+++ b/app/Http/Controllers/FollowerController.php
@@ -0,0 +1,66 @@
+where('followee_id', auth()->id())
+ ->orderBy('verified') // unverified first
+ ->get()
+ ->map(fn (Followee $f) => [
+ 'user' => $f->user,
+ 'verified' => $f->verified,
+ ]);
+
+ return response()->json($followers);
+ }
+
+ public function approve(User $follower): JsonResponse
+ {
+ $followee = Followee::where('user_id', $follower->id)
+ ->where('followee_id', auth()->id())
+ ->pending()
+ ->firstOrFail();
+
+ $followee->update(['verified' => true]);
+
+ Notification::create([
+ 'user_id' => $follower->id,
+ 'title' => 'Follow request accepted',
+ 'body' => auth()->user()->name . ' accepted your follow request.',
+ 'is_achievement' => false,
+ 'url' => '/u/' . auth()->user()->name,
+ ]);
+
+ return response()->json(['status' => 'approved']);
+ }
+
+ public function deny(User $follower): JsonResponse
+ {
+ Followee::where('user_id', $follower->id)
+ ->where('followee_id', auth()->id())
+ ->pending()
+ ->delete();
+
+ return response()->json(['status' => 'denied']);
+ }
+
+ public function remove(User $follower): JsonResponse
+ {
+ Followee::where('user_id', $follower->id)
+ ->where('followee_id', auth()->id())
+ ->verified()
+ ->delete();
+
+ return response()->json(['status' => 'removed']);
+ }
+}
diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php
index c5d8540..97e2dff 100644
--- a/app/Http/Controllers/SettingsController.php
+++ b/app/Http/Controllers/SettingsController.php
@@ -26,4 +26,15 @@ class SettingsController extends Controller
$request->user()->updateSettings($validated['settings']);
return response()->json(['message' => 'Settings saved.']);
}
+
+ public function updateSingle(Request $request, string $key)
+ {
+ $validated = $request->validate([
+ 'value' => ['required'],
+ ]);
+
+ $request->user()->updateSetting($key, $validated['value']);
+
+ return response()->json(['message' => 'Setting saved.']);
+ }
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 80bd64b..351568c 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -9,38 +9,77 @@ use App\Settings\SettingsRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Gate;
use Inertia\Inertia;
class UserController extends Controller
{
public function follow(User $user): JsonResponse
{
+ abort_if($user->id === auth()->id(), 403);
+
$existing = Followee::where('user_id', auth()->id())
->where('followee_id', $user->id)
->first();
if ($existing) {
$existing->delete();
- return response()->json(['following' => false]);
+ return response()->json(['status' => 'none']);
}
+ $canView = Gate::allows('viewProfileData', $user);
+
Followee::create([
'user_id' => auth()->id(),
'followee_id' => $user->id,
+ 'verified' => $canView,
]);
Notification::create([
- 'user_id' => $user->id,
- 'title' => 'New follower',
- 'body' => auth()->user()->name . ' is now following you.',
+ 'user_id' => $user->id,
+ 'title' => $canView ? 'New follower' : 'Follow request',
+ 'body' => $canView
+ ? auth()->user()->name . ' is now following you.'
+ : auth()->user()->name . ' wants to follow you.',
'is_achievement' => false,
- 'url' => '/u/'. auth()->user()->name,
+ 'url' => $canView ? '/u/' . auth()->user()->name : '/follow-requests',
]);
- return response()->json(['following' => true]);
+ return response()->json(['status' => $canView ? 'following' : 'requested']);
}
- public function settings(){
+ public function approveRequest(User $follower): JsonResponse
+ {
+ $followee = Followee::where('user_id', $follower->id)
+ ->where('followee_id', auth()->id())
+ ->pending()
+ ->firstOrFail();
+
+ $followee->update(['verified' => true]);
+
+ Notification::create([
+ 'user_id' => $follower->id,
+ 'title' => 'Follow request accepted',
+ 'body' => auth()->user()->name . ' accepted your follow request.',
+ 'is_achievement' => false,
+ 'url' => '/u/' . auth()->user()->name,
+ ]);
+
+ return response()->json(['approved' => true]);
+ }
+
+ public function denyRequest(User $follower): JsonResponse
+ {
+ Followee::where('user_id', $follower->id)
+ ->where('followee_id', auth()->id())
+ ->pending()
+ ->delete();
+
+ return response()->json(['denied' => true]);
+ }
+
+ public function settings(?string $category = null){
+ $allowedTabs = ['general', 'followers'];
$user = auth()->user();
$current = array_merge(SettingsRegistry::defaults(), $user->settings ?? []);
$fields = array_map(fn($field) => array_merge($field, [
@@ -50,6 +89,7 @@ class UserController extends Controller
return Inertia::render('UserSettings', [
'fields' => $fields,
'categories' => SettingsRegistry::categories(),
+ 'defaultTab' => in_array($category, $allowedTabs, true) ? $category : 'general',
]);
}
}
diff --git a/app/Http/Controllers/UserFlightController.php b/app/Http/Controllers/UserFlightController.php
index 70f96dc..b4ecb02 100644
--- a/app/Http/Controllers/UserFlightController.php
+++ b/app/Http/Controllers/UserFlightController.php
@@ -5,34 +5,54 @@ namespace App\Http\Controllers;
use App\Models\User;
use App\Models\UserFlight;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Gate;
class UserFlightController extends Controller
{
- protected User $user;
-
- function __construct(User $user){
- $this->user = $user;
+ public function viewableFlights(User $user, ?Request $request = null)
+ {
+ if (Gate::denies('viewProfileData', $user)) {
+ return response()->json([]);
+ }
+ return $this->flights($user, $request);
}
- public function flights(?Request $request = null)
+ public function flights(User $user, ?Request $request = null)
{
- return UserFlight::where('user_id', $this->user->id)
- ->with([
- 'departureAirport.region.country',
- 'departureAirport.region.continent',
- 'arrivalAirport.region.country',
- 'arrivalAirport.region.continent',
- 'airline.country',
- 'airline.alliance',
- 'aircraft',
- 'seatType',
- 'flightReason',
- 'flightClass',
- 'crewType'
- ])
- ->when($request?->boolean('departed_only'), fn($q) => $q->where('departure_date', '<=', now('UTC')))
- ->orderBy('departure_date', 'desc')
- ->get();
+ $key = "user_flights_{$user->id}";
+
+ $json = Cache::remember($key, now()->addDays(30), function () use ($user) {
+ return UserFlight::where('user_id', $user->id)
+ ->with([
+ 'departureAirport.region.country',
+ 'departureAirport.region.continent',
+ 'arrivalAirport.region.country',
+ 'arrivalAirport.region.continent',
+ 'airline.country',
+ 'airline.alliance',
+ 'aircraft',
+ 'seatType',
+ 'flightReason',
+ 'flightClass',
+ 'crewType'
+ ])
+ ->orderBy('departure_date', 'desc')
+ ->get()
+ ->values()
+ ->toJson();
+ });
+
+ if ($request?->boolean('departed_only')) {
+ $filtered = collect(json_decode($json))
+ ->filter(fn($f) => $f->departure_date <= now('UTC')->toDateString())
+ ->values()
+ ->toJson();
+
+ return response($filtered, 200)->header('Content-Type', 'application/json');
+ }
+
+ return response($json, 200)->header('Content-Type', 'application/json');
}
}
diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php
new file mode 100644
index 0000000..f5d8489
--- /dev/null
+++ b/app/Http/Controllers/UserProfileController.php
@@ -0,0 +1,194 @@
+check()) {
+ $user = auth()->user();
+ $defaultPage = $user->resolved_settings['default_login_page'];
+
+ $route = match ($defaultPage) {
+ 'feed_first' => $user->following()->count() > 0 ? 'feed' : 'profile.view',
+ 'feed' => 'feed',
+ 'profile' => 'profile.view',
+ 'dashboard' => 'dashboard',
+ };
+
+ $args = $route == 'profile.view' ? $user->name : null;
+
+ return redirect()->route($route, $args);
+ }
+ return redirect()->route('login');
+ }
+
+ public static function getUserFlightApiURL(User $user){
+ return '/data/user/'.$user->name.'/flights';
+ }
+
+ public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
+ return [
+ 'user' => $user,
+ 'canView' => Gate::allows('viewProfileData', $user),
+ 'canEdit' => auth()->check() && (auth()->id() === $user->id || auth()->user()->hasRole('admin')),
+ 'initialView' => $view,
+ 'selectedFlightId' => $selectedFlightId,
+ 'flight_api_url' => self::getUserFlightApiURL($user),
+ 'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
+ 'flightCount' => $user->departedFlights()->count(),
+ ];
+ }
+
+ public function departureBoard(User $user, ?UserFlight $flight = null){
+ $profileData = $this->profileData($user, 'board', $flight?->id);
+ return Inertia::render('UserProfile', $profileData);
+ }
+
+ public function map(User $user){
+ $profileData = $this->profileData($user, 'map');
+ return Inertia::render('UserProfile', $profileData);
+ }
+
+ public function boardingPasses(User $user){
+ $profileData = $this->profileData($user, 'passes');
+ return Inertia::render('UserProfile', $profileData);
+ }
+
+ public function view(User $user, ?string $page = null)
+ {
+ $loggedInUser = auth()->user();
+ $defaultView = $page ?: ($loggedInUser ? $loggedInUser->resolved_settings['default_profile_view'] : 'map');
+
+ return match($defaultView) {
+ 'boarding-passes' => $this->boardingPasses($user),
+ 'map' => $this->map($user),
+ 'departure-board' => $this->departureBoard($user),
+ 'achievements' => redirect()->route('profile.achievements', $user->name),
+ };
+
+ }
+
+ public function flight(User $user, UserFlight $userFlight)
+ {
+ if($userFlight->user_id !== $user->id){
+ abort(404);
+ }
+
+ return Inertia::render('UserFlight', [
+ 'flightCount' => $user->departedFlights()->count(),
+ 'flight' => $userFlight->snapshot($userFlight->id),
+ 'canEdit' => auth()->check() && auth()->id() === $user->id,
+ 'canView' => Gate::allows('viewProfileData', $user),
+ 'user' => $user,
+ 'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
+ ]);
+ }
+
+
+ public function achievements(User $user)
+ {
+ $canView = Gate::allows('viewProfileData', $user);
+
+ $achievements = Achievement::with(['category', 'difficulty'])
+ ->get()
+ ->groupBy(fn(Achievement $a) => $a->category->name)
+ ->map(fn($group) => $group->sortBy('sort_order')->values());
+
+ $userAchievements = $user->achievements()
+ ->with('achievement')
+ ->select(['achievement_id', 'progress'])
+ ->orderBy('achievement_id')
+ ->get()
+ ->keyBy('achievement_id');
+
+ $unlockedByCategory = $achievements->map(fn($group) =>
+ $group->filter(fn($a) => $userAchievements->get($a->id)?->unlocked)->count()
+ );
+
+ $unlockedCount = $userAchievements->filter(fn($ua) => $ua->unlocked)->count();
+ return Inertia::render('UserAchievements', [
+ 'canView' => $canView,
+ 'user' => $user,
+ 'canEdit' => auth()->id() === $user->id,
+ 'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
+ 'achievements' => $canView ? $achievements : [],
+ 'userAchievements' => $canView ? $userAchievements : [],
+ 'loggedInUser' => auth()->user(),
+ 'unlockedCount' => $unlockedCount,
+ 'unlockedByCategory' => $unlockedByCategory,
+ 'totalAchievements' => $achievements->flatten()->count(),
+ ]);
+ }
+
+ public function achievement(User $user, Achievement $achievement)
+ {
+ $regions = match($achievement->internal_name){
+ 'fun_challenges.australian_states' => Country::whereCode('AU')->first()->sortedRegions(),
+ 'fun_challenges.chinese_provinces' => Country::whereCode('CN')->first()->sortedRegions(),
+ 'fun_challenges.canadian_provinces' => Country::whereCode('CA')->first()->sortedRegions(),
+ 'fun_challenges.brazilian_states' => Country::whereCode('BR')->first()->sortedRegions(),
+ 'fun_challenges.us_states' => Country::whereCode('US')->first()->sortedRegions(),
+ default => [],
+ };
+
+ $allianceInternalName = match($achievement->internal_name){
+ 'airlines_alliances.all_star_alliance' => 'star_alliance',
+ 'airlines_alliances.all_oneworld' => 'oneworld',
+ 'airlines_alliances.all_skyteam' => 'skyteam',
+ 'airlines_alliances.all_vanilla_alliance' => 'vanilla_alliance',
+ default => null,
+ };
+
+ $continents = match($achievement->internal_name){
+ 'countries_continents.all_continent_pairs_one_way', 'countries_continents.all_continent_pairs_both_ways' => Continent::all()->toArray(),
+ default => [],
+ };
+
+ $alliance = null;
+ $airlines = [];
+
+ if ($allianceInternalName) {
+ $alliance = Alliance::where('internal_name', $allianceInternalName)
+ ->with('airlines')
+ ->firstOrFail();
+ $airlines = $alliance->airlines()->with('country')->orderBy('name')->get();
+ }
+
+ $aircraftFamilies = match($achievement->internal_name){
+ 'aircraft.all_boeing_7x7' => Aircraft::BOEING_FAMILIES,
+ 'aircraft.all_airbus_a3xx' => Aircraft::AIRBUS_FAMILIES,
+ default => [],
+ };
+
+ $canView = Gate::allows('viewProfileData', $user);
+
+ return Inertia::render('Profile/UserAchievement', [
+ 'user' => $user,
+ 'achievement' => $achievement,
+ 'loggedInUser' => auth()->user(),
+ 'userAchievement' => $canView ? $user->achievements()->where('achievement_id', $achievement->id)->first() : null,
+ 'followStatus' => auth()->check() ? auth()->user()->followStatus($user) : 'none',
+ 'flight_api_url' => UserProfileController::getUserFlightApiURL($user),
+ 'regions' => $regions,
+ 'alliance' => $alliance,
+ 'airlines' => $airlines,
+ 'continents' => $continents,
+ 'aircraft_families' => $aircraftFamilies,
+ 'achievementCount' => $user->unlockedAchievements()->count(),
+ 'canView' => $canView,
+ ]);
+ }
+}
diff --git a/app/Models/Country.php b/app/Models/Country.php
index 229331e..4c1ac1a 100644
--- a/app/Models/Country.php
+++ b/app/Models/Country.php
@@ -17,4 +17,13 @@ class Country extends Model
{
return $this->hasMany(Region::class);
}
+
+ function sortedRegions(): array
+ {
+ return $this
+ ->regions()
+ ->orderBy('name')
+ ->get()
+ ->toArray();
+ }
}
diff --git a/app/Models/Followee.php b/app/Models/Followee.php
index 6527d25..130fd87 100644
--- a/app/Models/Followee.php
+++ b/app/Models/Followee.php
@@ -10,6 +10,11 @@ class Followee extends Model
protected $fillable = [
'user_id',
'followee_id',
+ 'verified',
+ ];
+
+ protected $casts = [
+ 'verified' => 'boolean',
];
public function user(): BelongsTo
@@ -21,4 +26,14 @@ class Followee extends Model
{
return $this->belongsTo(User::class, 'followee_id');
}
+
+ public function scopeVerified($query)
+ {
+ return $query->where('verified', true);
+ }
+
+ public function scopePending($query)
+ {
+ return $query->where('verified', false);
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 788638a..a2b2958 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -48,6 +48,10 @@ class User extends Authenticatable
$this->update(['settings' => array_merge($current, $values)]);
}
+ function updateSetting($settingName, $value) : void{
+ $this->updateSettings([$settingName => $value]);
+ }
+
protected function resolvedSettings(): Attribute
{
return Attribute::make(
@@ -80,11 +84,6 @@ class User extends Authenticatable
return $this->where('name', 'ilike', $value)->firstOrFail();
}
- public function FlightController(): UserFlightController
- {
- return new UserFlightController($this);
- }
-
public function flights(): HasMany {
return $this->hasMany(UserFlight::class);
}
@@ -114,7 +113,21 @@ class User extends Authenticatable
public function isFollowing(User $user): bool
{
- return $this->following()->where('followee_id', $user->id)->exists();
+ return $this->following()
+ ->where('followee_id', $user->id)
+ ->verified()
+ ->exists();
+ }
+
+ public function followStatus(User $user): string
+ {
+ $followee = $this->following()->where('followee_id', $user->id)->first();
+
+ if (!$followee) {
+ return 'none';
+ }
+
+ return $followee->verified ? 'following' : 'requested';
}
public function notifications(): HasMany
diff --git a/app/Models/UserFlight.php b/app/Models/UserFlight.php
index b4cdb4d..ed8e2d5 100644
--- a/app/Models/UserFlight.php
+++ b/app/Models/UserFlight.php
@@ -48,6 +48,9 @@ class UserFlight extends Model
'duration_display',
'distance',
'livery_url',
+ 'scope',
+ 'range',
+ 'region_range'
];
public function calculateGreatCircleDistance(): float{
@@ -108,6 +111,26 @@ class UserFlight extends Model
);
}
+ protected function scope(): Attribute
+ {
+ return Attribute::make(
+ get: fn() => $this->departureAirport->region->country_id == $this->arrivalAirport->region->country_id ? 'domestic' : 'international'
+ );
+ }
+ protected function range(): Attribute
+ {
+ return Attribute::make(
+ get: fn() => $this->departureAirport->region->continent_id == $this->arrivalAirport->region->continent_id ? 'intracontinental' : 'intercontinental'
+ );
+ }
+
+ protected function regionRange(): Attribute
+ {
+ return Attribute::make(
+ get: fn() => $this->departureAirport->region_id == $this->arrivalAirport->region_id ? 'intraregional' : 'interregional'
+ );
+ }
+
protected function arrivalDayDifference(): Attribute
{
return Attribute::make(
diff --git a/app/Observers/FlightObserver.php b/app/Observers/FlightObserver.php
index 0ac6b5e..fa373e0 100644
--- a/app/Observers/FlightObserver.php
+++ b/app/Observers/FlightObserver.php
@@ -3,15 +3,23 @@
namespace App\Observers;
use App\Models\UserFlight;
+use Illuminate\Support\Facades\Cache;
class FlightObserver
{
+ protected function clearCache(UserFlight $flight): void
+ {
+ Cache::forget("user_flights_{$flight->user->id}");
+ }
+
+
/**
* Recalculate after a flight is created.
*/
public function created(UserFlight $flight): void
{
$flight->user->calculateAchievements();
+ $this->clearCache($flight);
}
/**
@@ -22,6 +30,7 @@ class FlightObserver
public function updated(UserFlight $flight): void
{
$flight->user->calculateAchievements();
+ $this->clearCache($flight);
}
/**
@@ -31,5 +40,6 @@ class FlightObserver
public function deleted(UserFlight $flight): void
{
$flight->user->calculateAchievements();
+ $this->clearCache($flight);
}
}
diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php
new file mode 100644
index 0000000..2220ebe
--- /dev/null
+++ b/app/Policies/UserPolicy.php
@@ -0,0 +1,82 @@
+id === $profileUser->id || $viewer->hasRole('admin'))) {
+ return true;
+ }
+
+
+ $isPrivate = $profileUser->resolved_settings['private_profile'] == 'private';
+
+ if (!$isPrivate) {
+ return true;
+ }
+
+ return $viewer && $viewer->isFollowing($profileUser);
+ }
+
+ /**
+ * Determine whether the user can view any models.
+ */
+ public function viewAny(User $user): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can view the model.
+ */
+ public function view(User $user, User $model): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can create models.
+ */
+ public function create(User $user): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can update the model.
+ */
+ public function update(User $user, User $model): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can delete the model.
+ */
+ public function delete(User $user, User $model): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can restore the model.
+ */
+ public function restore(User $user, User $model): bool
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can permanently delete the model.
+ */
+ public function forceDelete(User $user, User $model): bool
+ {
+ return false;
+ }
+}
diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php
index 8e186fe..24b3ea3 100644
--- a/app/Settings/SettingsRegistry.php
+++ b/app/Settings/SettingsRegistry.php
@@ -67,6 +67,20 @@ class SettingsRegistry
['value' => 'achievements', 'label' => 'Achievements'],
],
],
+ [
+ 'key' => 'show_map_legend',
+ 'type' => 'checkbox',
+ 'label' => 'Expand Map Legend By Default',
+ 'category' => 'FlightsGoneBy Settings',
+ 'default' => true,
+ ],
+ [
+ 'key' => 'hide_impossible_achievements',
+ 'type' => 'checkbox',
+ 'label' => 'Hide Impossible Achievements By Default',
+ 'category' => 'FlightsGoneBy Settings',
+ 'default' => true,
+ ],
[
'category' => 'AI Generated Content',
'key' => 'ai_liveries',
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 33a18db..c91c737 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -33,7 +33,7 @@ return Application::configure(basePath: dirname(__DIR__))
//
})
->withExceptions(function (Exceptions $exceptions): void {
- $exceptions->respond(function (Response $response, Throwable $e, Request $request) {
+ $exceptions->respond(function ($response, Throwable $e, Request $request) {
$status = $response->getStatusCode();
$errors = [
diff --git a/database/migrations/2026_06_20_083315_update_followees_table.php b/database/migrations/2026_06_20_083315_update_followees_table.php
new file mode 100644
index 0000000..6b5c24a
--- /dev/null
+++ b/database/migrations/2026_06_20_083315_update_followees_table.php
@@ -0,0 +1,26 @@
+boolean('verified')->default(true)->after('followee_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ //
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index 013aa90..6d5dc9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
"maplibre-gl": "^5.22.0",
"vue-echarts": "^8.0.1",
"vue3-apexcharts": "^1.11.1",
- "vuetify": "^4.0.5"
+ "vuetify": "^4.1.2"
},
"devDependencies": {
"@inertiajs/vue3": "^2.0.0",
@@ -5263,9 +5263,9 @@
}
},
"node_modules/vuetify": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.1.tgz",
- "integrity": "sha512-m75ZkDIBwstDhID/zPtQmh9m601G/C4wnvCmlA6hgvhD7EOwR9I4DClpxPQiNTd1toKWHCr3kD2XJqU1fh0tYA==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.2.tgz",
+ "integrity": "sha512-2SUzXt2Q71/cVmSZhU6kOmAo1ev3NZaI/ynj55TTcu6jXAc9kezoqZKY+Xm+0STnAOcCoTseF0qPSs01LjUaaA==",
"license": "MIT",
"funding": {
"type": "github",
diff --git a/package.json b/package.json
index 9a6de63..5127293 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,6 @@
"maplibre-gl": "^5.22.0",
"vue-echarts": "^8.0.1",
"vue3-apexcharts": "^1.11.1",
- "vuetify": "^4.0.5"
+ "vuetify": "^4.1.2"
}
}
diff --git a/resources/js/Components/Distance.vue b/resources/js/Components/Distance.vue
index 8801fb4..70b380a 100644
--- a/resources/js/Components/Distance.vue
+++ b/resources/js/Components/Distance.vue
@@ -1,10 +1,11 @@
+
+
+
+ {{ pendingCount }} pending request{{ pendingCount > 1 ? 's' : '' }} +
+ ++ No followers yet. +
+ +{{ category }}
+ + {{ categories[category] }} + +{{ category }}
- - {{ categories[category] }} - -