diff --git a/.env.example b/.env.example index b42f89d..e07cdb6 100644 --- a/.env.example +++ b/.env.example @@ -59,14 +59,14 @@ REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 -MAIL_MAILER=log -MAIL_SCHEME=null -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" +MAIL_MAILER=mailgun +MAIL_FROM_ADDRESS="test@yourddomain.com" MAIL_FROM_NAME="${APP_NAME}" +LOCAL_DISK_ROOT= +TEST_EMAIL="" +MAILGUN_DOMAIN= +MAILGUN_SECRET= +MAILGUN_ENDPOINT=api.mailgun.net AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/app/Console/Commands/RecalculateAchivements.php b/app/Console/Commands/RecalculateAchievements.php similarity index 82% rename from app/Console/Commands/RecalculateAchivements.php rename to app/Console/Commands/RecalculateAchievements.php index 2e1a873..19f9eae 100644 --- a/app/Console/Commands/RecalculateAchivements.php +++ b/app/Console/Commands/RecalculateAchievements.php @@ -9,11 +9,8 @@ use Illuminate\Console\Command; #[Signature('app:recalculate-achivements')] #[Description('Recalculate achievements for all users')] -class RecalculateAchivements extends Command +class RecalculateAchievements extends Command { - /** - * Execute the console command. - */ public function handle(): void { $users = User::all(); diff --git a/app/Console/Commands/UpdateAlliances.php b/app/Console/Commands/UpdateAlliances.php index e2ad99e..7ff46da 100644 --- a/app/Console/Commands/UpdateAlliances.php +++ b/app/Console/Commands/UpdateAlliances.php @@ -37,6 +37,8 @@ class UpdateAlliances extends Command Airline::whereInternalName($change['airline'])->update(['alliance' => $allianceId]); } + + Alliance::recalculateChallengeThresholds(); } } diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 83bc82c..6b69680 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -190,6 +190,7 @@ class UserProfileController extends Controller $aircraftFamilies = match($achievement->internal_name){ 'aircraft.all_boeing_7x7' => Aircraft::BOEING_FAMILIES, 'aircraft.all_airbus_a3xx' => Aircraft::AIRBUS_FAMILIES, + 'aircraft.all_boeing_737' => Aircraft::BOEING_737_VARIANTS, default => [], }; diff --git a/app/Models/Aircraft.php b/app/Models/Aircraft.php index 6fd4048..07f4035 100644 --- a/app/Models/Aircraft.php +++ b/app/Models/Aircraft.php @@ -40,6 +40,22 @@ class Aircraft extends Model '787' => ['B788', 'B789', 'B78X'], ]; + public const array BOEING_737_VARIANTS = [ + '737-100' => ['B731'], + '737-200' => ['B732'], + '737-300' => ['B733'], + '737-400' => ['B734'], + '737-500' => ['B735'], + '737-600' => ['B736'], + '737-700' => ['B737', 'E737'], + '737-800' => ['B738', 'P8'], + '737-900' => ['B739'], + '737 Max 7' => ['B37M'], + '737 Max 8' => ['B38M'], + '737 Max 9' => ['B39M'], + '737 Max 10' => ['B3XM'] + ]; + public const array AIRBUS_FAMILIES = [ 'A300' => ['A30B', 'A300', 'A306'], 'A310' => ['A310', 'A312', 'A313'], diff --git a/app/Models/Alliance.php b/app/Models/Alliance.php index b3416e8..7d91bf9 100644 --- a/app/Models/Alliance.php +++ b/app/Models/Alliance.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Alliance extends Model { + protected $table = 'alliances'; protected $fillable = [ @@ -18,8 +19,25 @@ class Alliance extends Model 'internal_name' => 'string', ]; + const ALLIANCE_CHALLENGES = [ + 'star_alliance' => 'airlines_alliances.all_star_alliance', + 'oneworld' => 'airlines_alliances.all_oneworld', + 'vanilla_alliance' => 'airlines_alliances.all_vanilla_alliance', + 'skyteam' => 'airlines_alliances.all_skyteam', + ]; + public function airlines(): HasMany { return $this->hasMany(Airline::class); } + + public static function recalculateChallengeThresholds() : void { + $alliances = Alliance::all(); + foreach ($alliances as $alliance) { + $airlineCount = $alliance->airlines()->count(); + $challenge = self::ALLIANCE_CHALLENGES[$alliance->internal_name]; + Achievement::where('internal_name', $challenge)->update(['threshold' => $airlineCount]); + } + + } } diff --git a/app/Models/User.php b/app/Models/User.php index beb4950..3dde6cf 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,11 +13,14 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use App\Traits\HasAchievements; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Laravel\Sanctum\HasApiTokens; use NotificationChannels\WebPush\HasPushSubscriptions; use Spatie\Permission\Traits\HasRoles; +use App\Notifications\ResetPasswordNotification; + #[Fillable(['name', 'email', 'password', 'distance_unit', 'settings'])] #[Hidden(['password', 'remember_token'])] @@ -35,6 +38,22 @@ class User extends Authenticatable protected $appends = ['resolved_settings']; + public function calculateAchievementsAndClearCache(): void + { + $this->clearCache(); + $this->calculateAchievements(); + } + + public function clearCache(): void + { + Cache::forget("user_flights_{$this->id}"); + + $this + ->followers() + ->get() + ->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->id}")); + } + public function achievements(): HasMany { return $this->hasMany(UserAchievement::class); @@ -83,6 +102,11 @@ class User extends Authenticatable }); } + public function sendPasswordResetNotification($token): void + { + $this->notify(new ResetPasswordNotification($token)); + } + public function resolveRouteBinding($value, $field = null): ?User { return $this->where('name', 'ilike', $value)->firstOrFail(); @@ -129,8 +153,8 @@ class User extends Authenticatable $today = now('UTC')->toDateString(); return match ($filter) { - 'departed' => $collection->filter(fn($f) => $f->departure_date <= $today)->values(), - 'upcoming' => $collection->filter(fn($f) => $f->departure_date > $today)->values(), + 'departed' => $collection->filter(fn($f) => Carbon::parse($f->departure_date)->toDateString() <= $today)->values(), + 'upcoming' => $collection->filter(fn($f) => Carbon::parse($f->departure_date)->toDateString() > $today)->values(), default => $collection, }; } diff --git a/app/Notifications/ResetPasswordNotification.php b/app/Notifications/ResetPasswordNotification.php new file mode 100644 index 0000000..fd82b38 --- /dev/null +++ b/app/Notifications/ResetPasswordNotification.php @@ -0,0 +1,41 @@ +token = $token; + } + + public function via($notifiable): array + { + return ['mail']; + } + + public function toMail($notifiable) + { + $url = url(route('password.reset', [ + 'token' => $this->token, + 'email' => $notifiable->getEmailForPasswordReset(), + ], false)); + + return (new MailMessage) + ->theme('flightsgoneby') + ->subject(Lang::get('Reset your password')) + ->line(Lang::get('You are receiving this email because we received a password reset request for your account.')) + ->action(Lang::get('Reset Password'), $url) + ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')])) + ->line(Lang::get('If you did not request a password reset, no further action is required.')); + } +} diff --git a/app/Observers/FlightObserver.php b/app/Observers/FlightObserver.php index b107eb2..6b07f28 100644 --- a/app/Observers/FlightObserver.php +++ b/app/Observers/FlightObserver.php @@ -11,25 +11,12 @@ use Illuminate\Support\Facades\Cache; class FlightObserver { - protected function clearCache(UserFlight $flight): void - { - Cache::forget("user_flights_{$flight->user->id}"); - - $flight - ->user - ->followers() - ->get() - ->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->id}")); - } - - /** * Recalculate after a flight is created. */ public function created(UserFlight $flight): void { - $flight->user->calculateAchievements(); - $this->clearCache($flight); + $flight->user->calculateAchievementsAndClearCache(); new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::forCreation($flight)); } @@ -41,8 +28,7 @@ class FlightObserver public function updated(UserFlight $flight): void { \Log::info('Observer fired for flight ' . $flight->id); - $flight->user->calculateAchievements(); - $this->clearCache($flight); + $flight->user->calculateAchievementsAndClearCache(); } /** @@ -51,9 +37,7 @@ class FlightObserver */ public function deleted(UserFlight $flight): void { - $flight->user->calculateAchievements(); - $this->clearCache($flight); - + $flight->user->calculateAchievementsAndClearCache(); if ($flight->departure_date->isFuture()) { new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Cancelled); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1ea6b4c..9ec3f9f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,6 +13,7 @@ use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Http\Request; use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Support\Facades\Mail; class AppServiceProvider extends ServiceProvider { @@ -29,6 +30,9 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + if(!app()->isProduction()){ + Mail::alwaysTo(config('app.test_email')); + } Vite::prefetch(concurrency: 3); UserFlight::observe(FlightObserver::class); Airline::observe(AirlineObserver::class); diff --git a/app/Services/Achievements/Checkers/AircraftChecker.php b/app/Services/Achievements/Checkers/AircraftChecker.php index e0c7fa6..99bd939 100644 --- a/app/Services/Achievements/Checkers/AircraftChecker.php +++ b/app/Services/Achievements/Checkers/AircraftChecker.php @@ -99,9 +99,9 @@ class AircraftChecker extends BaseChecker $flownBoeingFamilies = collect(Aircraft::BOEING_FAMILIES) ->filter(fn($designators) => - $flightsWithAircraft->contains( - fn(UserFlight $f) => in_array($f->aircraft->designator, $designators) - ) + $flightsWithAircraft->contains( + fn(UserFlight $f) => in_array($f->aircraft->designator, $designators) + ) ) ->count(); @@ -109,14 +109,24 @@ class AircraftChecker extends BaseChecker // --- Airbus A3xx families --- - $flownAirbusFamilie = collect(Aircraft::AIRBUS_FAMILIES) + $flownAirbusFamilies = collect(Aircraft::AIRBUS_FAMILIES) ->filter(fn($designators) => - $flightsWithAircraft->contains( - fn(UserFlight $f) => in_array($f->aircraft->designator, $designators) - ) + $flightsWithAircraft->contains( + fn(UserFlight $f) => in_array($f->aircraft->designator, $designators) + ) ) ->count(); - $this->awardProgress($flownAirbusFamilie, 'aircraft.all_airbus_a3xx'); + $this->awardProgress($flownAirbusFamilies, 'aircraft.all_airbus_a3xx'); + + $flown737Variants = collect(Aircraft::BOEING_737_VARIANTS) + ->filter(fn($designators) => + $flightsWithAircraft->contains( + fn(UserFlight $f) => in_array($f->aircraft->designator, $designators) + ) + ) + ->count(); + $this->awardProgress($flown737Variants, 'aircraft.all_boeing_737'); + } } diff --git a/composer.json b/composer.json index 8debc92..a785cda 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,8 @@ "laravel/sanctum": "^4.0", "laravel/tinker": "^3.0", "spatie/laravel-permission": "^8.0", + "symfony/http-client": "^8.1", + "symfony/mailgun-mailer": "^8.1", "tightenco/ziggy": "^2.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index fe230ad..92febfa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eb16a3b778bbeaad9a49b64820d911ba", + "content-hash": "6fb94af09ede5fe0164f292fa2c328bd", "packages": [ { "name": "brick/math", @@ -4475,87 +4475,6 @@ ], "time": "2024-06-11T12:45:25+00:00" }, - { - "name": "simplepie/simplepie", - "version": "1.9.0", - "source": { - "type": "git", - "url": "https://github.com/simplepie/simplepie.git", - "reference": "76cccb1b2c5dcaf44f304c925ab30c0f48643992" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/simplepie/simplepie/zipball/76cccb1b2c5dcaf44f304c925ab30c0f48643992", - "reference": "76cccb1b2c5dcaf44f304c925ab30c0f48643992", - "shasum": "" - }, - "require": { - "ext-pcre": "*", - "ext-xml": "*", - "ext-xmlreader": "*", - "php": ">=7.2.0" - }, - "require-dev": { - "donatj/mock-webserver": "^2.7", - "friendsofphp/php-cs-fixer": "^2.19 || ^3.8", - "mf2/mf2": "^0.5.0", - "phpstan/phpstan": "~1.12.2", - "phpunit/phpunit": "^8 || ^9 || ^10", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/simple-cache": "^1 || ^2 || ^3" - }, - "suggest": { - "ext-curl": "", - "ext-iconv": "", - "ext-intl": "", - "ext-mbstring": "", - "mf2/mf2": "Microformat module that allows for parsing HTML for microformats" - }, - "type": "library", - "autoload": { - "psr-0": { - "SimplePie": "library" - }, - "psr-4": { - "SimplePie\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ryan Parman", - "homepage": "http://ryanparman.com/", - "role": "Creator, alumnus developer" - }, - { - "name": "Sam Sneddon", - "homepage": "https://gsnedders.com/", - "role": "Alumnus developer" - }, - { - "name": "Ryan McCue", - "email": "me@ryanmccue.info", - "homepage": "http://ryanmccue.info/", - "role": "Developer" - } - ], - "description": "A simple Atom/RSS parsing library for PHP", - "homepage": "http://simplepie.org/", - "keywords": [ - "atom", - "feeds", - "rss" - ], - "support": { - "issues": "https://github.com/simplepie/simplepie/issues", - "source": "https://github.com/simplepie/simplepie/tree/1.9.0" - }, - "time": "2025-09-12T06:34:27+00:00" - }, { "name": "spatie/laravel-package-tools", "version": "1.93.1", @@ -5492,6 +5411,185 @@ ], "time": "2026-01-29T09:41:02+00:00" }, + { + "name": "symfony/http-client", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "d41e9778eed03c64b095532c6d414e92e3c520d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/d41e9778eed03c64b095532c6d414e92e3c520d9", + "reference": "d41e9778eed03c64b095532c6d414e92e3c520d9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/http-client-contracts": "^3.7", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<3", + "php-http/discovery": "<1.15" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/guzzle": "^7.10", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T12:55:20+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, { "name": "symfony/http-foundation", "version": "v8.0.7", @@ -5756,6 +5854,76 @@ ], "time": "2026-02-25T16:59:43+00:00" }, + { + "name": "symfony/mailgun-mailer", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailgun-mailer.git", + "reference": "d0567991cb97cbc2f5dd6f9cc7898af69abc9ba3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/d0567991cb97cbc2f5dd6f9cc7898af69abc9ba3", + "reference": "d0567991cb97cbc2f5dd6f9cc7898af69abc9ba3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/mailer": "^7.4|^8.0" + }, + "require-dev": { + "symfony/http-client": "^7.4|^8.0", + "symfony/webhook": "^7.4|^8.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Mailgun\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mailgun Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailgun-mailer/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-09T10:54:51+00:00" + }, { "name": "symfony/mime", "version": "v8.0.7", diff --git a/config/app.php b/config/app.php index 0f79282..b0b2ca8 100644 --- a/config/app.php +++ b/config/app.php @@ -41,7 +41,7 @@ return [ 'debug' => (bool) env('APP_DEBUG', false), 'verify_ssl' => env('VERIFY_SSL', true), - + 'test_email' => env('TEST_EMAIL', ''), /* |-------------------------------------------------------------------------- | Application URL diff --git a/config/mail.php b/config/mail.php index e32e88d..5b6f0c6 100644 --- a/config/mail.php +++ b/config/mail.php @@ -96,7 +96,9 @@ return [ ], 'retry_after' => 60, ], - + 'mailgun' => [ + 'transport' => 'mailgun', + ], ], /* diff --git a/config/services.php b/config/services.php index 6a90eb8..25c5f5e 100644 --- a/config/services.php +++ b/config/services.php @@ -34,5 +34,11 @@ return [ 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], + 'mailgun' => [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], ]; diff --git a/database/migrations/2026_07_07_041555_all737_families.php b/database/migrations/2026_07_07_041555_all737_families.php new file mode 100644 index 0000000..1223305 --- /dev/null +++ b/database/migrations/2026_07_07_041555_all737_families.php @@ -0,0 +1,64 @@ + 'B731', + 'manufacturer_code' => 'BOEING', + 'model_full_name' => '737-100', + 'aircraft_description' => 'LandPlane', + 'engine_type' => 'Jet', + 'engine_count' => 2, + 'wtc' => 'M', + 'preferred' => false, + ]); + + $modelNames = ['737-8', '737-9', '737-10', '737-7']; + Aircraft::whereIn('model_full_name', $modelNames)->delete(); + + $impossible = AchievementDifficulty::where('internal_name', 'impossible')->first(); + $aircraftCategory = AchievementCategory::where('internal_name', 'aircraft')->first(); + + Achievement::create([ + 'internal_name' => 'aircraft.all_boeing_737', + 'name' => 'Rarest of the Commoners', + 'short_description' => 'Fly Every Variant of the Boeing 737', + 'icon' => 'standard_achievement.png', + 'achievement_category_id' => $aircraftCategory->id, + 'achievement_difficulty_id' => $impossible->id, + 'progressive' => true, + 'threshold' => 13, + 'sort_order' => 7, + 'has_page' => true, + 'long_description' => '' + ]); + + foreach(User::all() as $user){ + $user->calculateAchievements(); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/public/img/logos/header_logo_color.png b/public/img/logos/header_logo_color.png new file mode 100644 index 0000000..53b3ac8 Binary files /dev/null and b/public/img/logos/header_logo_color.png differ diff --git a/resources/js/Components/FlightsGoneBy/UserFlightContextMenu.vue b/resources/js/Components/FlightsGoneBy/UserFlightContextMenu.vue index 15af63e..937e890 100644 --- a/resources/js/Components/FlightsGoneBy/UserFlightContextMenu.vue +++ b/resources/js/Components/FlightsGoneBy/UserFlightContextMenu.vue @@ -27,14 +27,14 @@ const showDeleteDialog = computed({ function copyFlightToClipboard(flight: Flight) { const dep = `${flight.departure_airport.display_code} (${flight.departure_airport.municipality})` const arr = `${flight.arrival_airport.display_code} (${flight.arrival_airport.municipality})` - const times = `${flight.departure_time_display} → ${flight.arrival_time_display}` const dayDiff = flight.arrival_day_difference !== 0 ? `(${flight.arrival_day_difference > 0 ? '+' : ''}${flight.arrival_day_difference})` : '' + const times = `${flight.departure_time_display} → ${flight.arrival_time_display} ${dayDiff}` const airline = flight.airline?.display_name const aircraft = flight.aircraft?.display_name - const reg = flight.aircraft_registration ? `(${flight.aircraft_registration})` : '' - const text = `${airline} | ${dep} → ${arr} ${dayDiff} | ${times} · ${flight.departure_date_display} | ${aircraft} ${reg}` + const reg = flight.aircraft_registration ? `· (${flight.aircraft_registration})` : '' + const text = `${flight.departure_date_display} | ${flight.flight_number} | ${airline} | ${dep} → ${arr} | ${times} | ${aircraft} ${reg}` copyToClipboard(text) } diff --git a/resources/js/Pages/Auth/ForgotPassword.vue b/resources/js/Pages/Auth/ForgotPassword.vue index 72a1311..be699f6 100644 --- a/resources/js/Pages/Auth/ForgotPassword.vue +++ b/resources/js/Pages/Auth/ForgotPassword.vue @@ -23,7 +23,7 @@ const submit = () => { {{ status }} - + diff --git a/resources/js/Pages/Profile/Achievements/aircraft.all_boeing_737.vue b/resources/js/Pages/Profile/Achievements/aircraft.all_boeing_737.vue new file mode 100644 index 0000000..f358dff --- /dev/null +++ b/resources/js/Pages/Profile/Achievements/aircraft.all_boeing_737.vue @@ -0,0 +1,137 @@ + + + + + + + diff --git a/resources/views/emails/reset-password.blade.php b/resources/views/emails/reset-password.blade.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/resources/views/emails/reset-password.blade.php @@ -0,0 +1 @@ + 'primary', + 'align' => 'center', +]) + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..c13e31a --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,12 @@ +@props(['url']) + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..be6f520 --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,12 @@ +@props(['url']) + + + + FlightsGoneBy + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..037efe3 --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,58 @@ + + + +{{ config('app.name') }} + + + + + +{!! $head ?? '' !!} + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..a16bace --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ + +{{-- Header --}} + + +{{ config('app.name') }} + + + +{{-- Body --}} +{!! $slot !!} + +{{-- Subcopy --}} +@isset($subcopy) + + +{!! $subcopy !!} + + +@endisset + +{{-- Footer --}} + + +© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }} + + + diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..d6ee6f0 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,297 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #52525b; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: start; +} + +a { + color: #18181b; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #18181b; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: start; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: start; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #18181b; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + margin-top: 15px; + margin-bottom: 10px; + max-height: 75px; + width: 75px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + border-bottom: 1px solid #fafafa; + border-top: 1px solid #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e4e4e7; + border-radius: 4px; + border-width: 1px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); + margin: 0 auto; + padding: 0; + width: 570px; +} + +.inner-body a { + word-break: break-all; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e4e4e7; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #a1a1aa; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #a1a1aa; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #e4e4e7; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #52525b; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + float: unset; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #18181b; + border-bottom: 8px solid #18181b; + border-left: 18px solid #18181b; + border-right: 18px solid #18181b; + border-top: 8px solid #18181b; +} + +.button-green, +.button-success { + background-color: #16a34a; + border-bottom: 8px solid #16a34a; + border-left: 18px solid #16a34a; + border-right: 18px solid #16a34a; + border-top: 8px solid #16a34a; +} + +.button-red, +.button-error { + background-color: #dc2626; + border-bottom: 8px solid #dc2626; + border-left: 18px solid #dc2626; + border-right: 18px solid #dc2626; + border-top: 8px solid #dc2626; +} + +/* Panels */ + +.panel { + border-left: #18181b solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #fafafa; + color: #52525b; + padding: 16px; +} + +.panel-content p { + color: #52525b; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/resources/views/vendor/mail/html/themes/flightsgoneby.css b/resources/views/vendor/mail/html/themes/flightsgoneby.css new file mode 100644 index 0000000..c237f00 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/flightsgoneby.css @@ -0,0 +1,52 @@ +/* Body */ +body { + background-color: #f4f6fb; + color: #4a5568; +} + +/* Header */ +.header { + background-color: rgb(0, 12, 41); + padding: 32px 24px; + text-align: center; + border-radius: 8px 8px 0 0; +} + +/* Content wrapper */ +.wrapper { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-top: none; + border-radius: 0 0 8px 8px; +} + +.content-cell { + padding: 32px; +} + +/* Button — outlined so it reads on light or dark backgrounds */ +.button-blue, +.button-primary { + background-color: transparent; + border: 2px solid #38bdf8; + border-radius: 6px; + color: #0c4a6e !important; + display: inline-block; + font-weight: 600; + padding: 12px 24px; + text-decoration: none; +} + +/* Footer */ +.footer { + background-color: rgb(0, 12, 41); + color: #94a3b8; + padding: 24px; + border-radius: 8px; + margin-top: 24px; +} + +.footer p { + color: #94a3b8; + font-size: 12px; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..ec58e83 --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..80bce21 --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('app.name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }}