From 932d9a38a44cf0e968cfcecd29def83a0f958fd4 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Wed, 29 Apr 2026 15:03:44 +0200 Subject: [PATCH] feat: hunt resource and personalised mails --- .../RelationManagers/StepsRelationManager.php | 82 +++++++++++++++++-- .../HuntParticipantJoinedNotification.php | 9 +- .../views/mail/hunts/joined-hunt.blade.php | 70 ++++++++++++++++ .../Filament/HuntStepsRelationManagerTest.php | 58 +++++++++++++ .../HuntParticipationNotificationTest.php | 40 +++++++++ 5 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 resources/views/mail/hunts/joined-hunt.blade.php create mode 100644 tests/Feature/Filament/HuntStepsRelationManagerTest.php diff --git a/app/Filament/Resources/Hunts/RelationManagers/StepsRelationManager.php b/app/Filament/Resources/Hunts/RelationManagers/StepsRelationManager.php index 14d212a..44bcf6d 100644 --- a/app/Filament/Resources/Hunts/RelationManagers/StepsRelationManager.php +++ b/app/Filament/Resources/Hunts/RelationManagers/StepsRelationManager.php @@ -2,11 +2,14 @@ namespace App\Filament\Resources\Hunts\RelationManagers; +use Dotswan\MapPicker\Fields\Map; use Filament\Actions\BulkActionGroup; use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Resources\RelationManagers\RelationManager; @@ -20,6 +23,14 @@ class StepsRelationManager extends RelationManager public function form(Schema $schema): Schema { + $ownerRecord = $this->getOwnerRecord(); + $defaultLocation = ($ownerRecord->latitude !== null && $ownerRecord->longitude !== null) + ? [ + 'lat' => (float) $ownerRecord->latitude, + 'lng' => (float) $ownerRecord->longitude, + ] + : null; + return $schema ->components([ TextInput::make('step_number') @@ -31,14 +42,16 @@ class StepsRelationManager extends RelationManager Textarea::make('description'), Textarea::make('hint_text') ->label('Hint'), - TextInput::make('hint_media_url') - ->url() - ->label('Hint Media URL'), - TextInput::make('latitude') - ->numeric() + FileUpload::make('hint_media_url') + ->label('Hint image') + ->image() + ->disk('public') + ->directory('hunt-steps'), + Hidden::make('latitude') + ->default($defaultLocation['lat'] ?? null) ->required(), - TextInput::make('longitude') - ->numeric() + Hidden::make('longitude') + ->default($defaultLocation['lng'] ?? null) ->required(), TextInput::make('radius_m') ->numeric() @@ -47,6 +60,58 @@ class StepsRelationManager extends RelationManager TextInput::make('xp') ->numeric() ->default(10), + Map::make('location') + ->label('Location') + ->columnSpanFull() + ->default($defaultLocation) + ->defaultLocation( + latitude: $defaultLocation['lat'] ?? 40.4168, + longitude: $defaultLocation['lng'] ?? -3.7038, + ) + ->draggable(true) + ->clickable(true) + ->zoom(15) + ->minZoom(0) + ->maxZoom(28) + ->tilesUrl('https://tile.openstreetmap.org/{z}/{x}/{y}.png') + ->detectRetina(true) + ->showFullscreenControl(false) + ->showZoomControl(true) + ->showMyLocationButton(true) + ->boundaries(true, 49.5, -11, 61, 2) + ->rangeSelectField('radius_m') + ->extraStyles([ + 'border-radius: 10px', + ]) + ->afterStateUpdated(function ($set, ?array $state): void { + if ($state === null) { + return; + } + + $set('latitude', $state['lat']); + $set('longitude', $state['lng']); + }) + ->afterStateHydrated(function (?array $state, $record, $set) use ($defaultLocation): void { + if (($state['lat'] ?? null) !== null && ($state['lng'] ?? null) !== null) { + return; + } + + $latitude = $record?->latitude ?? ($defaultLocation['lat'] ?? null); + $longitude = $record?->longitude ?? ($defaultLocation['lng'] ?? null); + + if ($latitude === null || $longitude === null) { + return; + } + + $location = [ + 'lat' => (float) $latitude, + 'lng' => (float) $longitude, + ]; + + $set('location', $location); + $set('latitude', $location['lat']); + $set('longitude', $location['lng']); + }), ]); } @@ -56,6 +121,9 @@ class StepsRelationManager extends RelationManager ->recordTitleAttribute('title') ->columns([ Tables\Columns\TextColumn::make('step_number')->sortable(), + Tables\Columns\ImageColumn::make('hint_media_url') + ->label('Hint image') + ->disk('public'), Tables\Columns\TextColumn::make('title'), Tables\Columns\TextColumn::make('latitude'), Tables\Columns\TextColumn::make('longitude'), diff --git a/app/Notifications/HuntParticipantJoinedNotification.php b/app/Notifications/HuntParticipantJoinedNotification.php index 585773c..c05cce4 100644 --- a/app/Notifications/HuntParticipantJoinedNotification.php +++ b/app/Notifications/HuntParticipantJoinedNotification.php @@ -30,8 +30,11 @@ class HuntParticipantJoinedNotification extends Notification implements ShouldQu { return (new MailMessage) ->subject('Nouveau participant sur '.$this->hunt->title) - ->greeting('Bonjour '.$notifiable->username) - ->line($this->participant->username.' participe maintenant à votre chasse "'.$this->hunt->title.'".') - ->line('Vous pouvez suivre les participants depuis votre espace organisateur.'); + ->view('mail.hunts.joined-hunt', [ + 'notifiable' => $notifiable, + 'hunt' => $this->hunt, + 'participant' => $this->participant, + 'participantsCount' => $this->hunt->participants()->count(), + ]); } } diff --git a/resources/views/mail/hunts/joined-hunt.blade.php b/resources/views/mail/hunts/joined-hunt.blade.php new file mode 100644 index 0000000..a66801c --- /dev/null +++ b/resources/views/mail/hunts/joined-hunt.blade.php @@ -0,0 +1,70 @@ + + + + + + Nouveau participant sur {{ $hunt->title }} + + + + + + +
+ + + + + + + + + + +
+

{{ config('app.name') }}

+

Quelqu'un vient de rejoindre ta hunt

+
+

+ Bonjour {{ $notifiable->username }}, +

+

+ {{ $participant->username }} participe maintenant à ta hunt {{ $hunt->title }}. +

+ + + + +
+

Hunt

+

{{ $hunt->title }}

+ + + + + + +
+

Ville

+

{{ $hunt->city }}

+
+

Participants

+

{{ $participantsCount }}

+
+
+

+ Garde un oeil sur l'activité de cette hunt pour suivre sa progression et préparer la suite de l'expérience. +

+

+ + Ouvrir {{ config('app.name') }} + +

+
+

+ Cet email est envoyé parce que tu es le créateur de la hunt {{ $hunt->title }}. +

+
+
+ + diff --git a/tests/Feature/Filament/HuntStepsRelationManagerTest.php b/tests/Feature/Filament/HuntStepsRelationManagerTest.php new file mode 100644 index 0000000..40ffb8a --- /dev/null +++ b/tests/Feature/Filament/HuntStepsRelationManagerTest.php @@ -0,0 +1,58 @@ +create([ + 'role' => UserRole::ADMIN, + ]); + + $hunt = Hunts::withoutSyncingToSearch(fn () => Hunts::factory()->create([ + 'creator_id' => $admin->id, + 'latitude' => 44.837789, + 'longitude' => -0.57918, + ])); + + $this->actingAs($admin); + + Livewire::test(StepsRelationManager::class, [ + 'ownerRecord' => $hunt, + 'pageClass' => EditHunts::class, + ]) + ->mountTableAction('create') + ->assertTableActionDataSet([ + 'latitude' => 44.837789, + 'longitude' => -0.57918, + 'location' => [ + 'lat' => 44.837789, + 'lng' => -0.57918, + ], + ]) + ->assertFormFieldExists( + 'hint_media_url', + fn ($field): bool => $field instanceof FileUpload, + ) + ->assertFormFieldExists( + 'latitude', + fn ($field): bool => $field instanceof Hidden, + ) + ->assertFormFieldExists( + 'longitude', + fn ($field): bool => $field instanceof Hidden, + ) + ->assertFormFieldExists( + 'location', + fn ($field): bool => $field instanceof Map, + ); +}); diff --git a/tests/Feature/HuntParticipationNotificationTest.php b/tests/Feature/HuntParticipationNotificationTest.php index d3236a5..2fd656d 100644 --- a/tests/Feature/HuntParticipationNotificationTest.php +++ b/tests/Feature/HuntParticipationNotificationTest.php @@ -61,6 +61,46 @@ it('does not notify the hunt creator when the user already participates', functi Notification::assertNotSentTo($creator, HuntParticipantJoinedNotification::class); }); +it('uses the personalized joined hunt email view', function () { + $creator = User::factory()->create([ + 'username' => 'creator-test', + ]); + $participant = User::factory()->create([ + 'username' => 'participant-test', + ]); + $hunt = Hunts::withoutSyncingToSearch(fn () => Hunts::factory()->create([ + 'creator_id' => $creator->id, + 'title' => 'La quête des lanternes', + 'city' => 'Lyon', + ])); + + $hunt->participants()->attach($participant->id, [ + 'current_step_number' => 0, + 'status' => 'in_progress', + ]); + + $notification = new HuntParticipantJoinedNotification($hunt, $participant); + $mailMessage = $notification->toMail($creator); + + expect($mailMessage->subject)->toBe('Nouveau participant sur La quête des lanternes') + ->and($mailMessage->view)->toBe('mail.hunts.joined-hunt') + ->and($mailMessage->viewData) + ->toMatchArray([ + 'notifiable' => $creator, + 'hunt' => $hunt, + 'participant' => $participant, + 'participantsCount' => 1, + ]); + + $renderedView = view($mailMessage->view, $mailMessage->viewData)->render(); + + expect($renderedView) + ->toContain('Bonjour creator-test') + ->toContain('participant-test') + ->toContain('La quête des lanternes') + ->toContain('Lyon'); +}); + it('queues email verification notifications', function () { expect(new VerifyEmailNotification)->toBeInstanceOf(ShouldQueue::class); });