feat: hunt resource and personalised mails
This commit is contained in:
parent
33f87bfa10
commit
932d9a38a4
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nouveau participant sur {{ $hunt->title }}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #f4f7fb; color: #172033; font-family: Arial, Helvetica, sans-serif;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: #f4f7fb; padding: 32px 16px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 560px; overflow: hidden; background: #ffffff; border: 1px solid #e6ebf2; border-radius: 12px;">
|
||||
<tr>
|
||||
<td style="padding: 32px 32px 16px;">
|
||||
<p style="margin: 0 0 8px; color: #657086; font-size: 14px;">{{ config('app.name') }}</p>
|
||||
<h1 style="margin: 0; color: #101828; font-size: 28px; line-height: 1.2;">Quelqu'un vient de rejoindre ta hunt</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 32px 24px;">
|
||||
<p style="margin: 0 0 16px; color: #344054; font-size: 16px; line-height: 1.6;">
|
||||
Bonjour {{ $notifiable->username }},
|
||||
</p>
|
||||
<p style="margin: 0 0 24px; color: #344054; font-size: 16px; line-height: 1.6;">
|
||||
<strong>{{ $participant->username }}</strong> participe maintenant à ta hunt <strong>{{ $hunt->title }}</strong>.
|
||||
</p>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 0 0 24px; background: #f8fafc; border: 1px solid #e6ebf2; border-radius: 10px;">
|
||||
<tr>
|
||||
<td style="padding: 18px 20px;">
|
||||
<p style="margin: 0 0 6px; color: #667085; font-size: 13px; font-weight: 700; text-transform: uppercase;">Hunt</p>
|
||||
<p style="margin: 0 0 16px; color: #101828; font-size: 18px; font-weight: 700; line-height: 1.35;">{{ $hunt->title }}</p>
|
||||
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="width: 50%; padding: 0 12px 0 0;">
|
||||
<p style="margin: 0 0 4px; color: #667085; font-size: 13px;">Ville</p>
|
||||
<p style="margin: 0; color: #344054; font-size: 15px; font-weight: 700;">{{ $hunt->city }}</p>
|
||||
</td>
|
||||
<td style="width: 50%; padding: 0 0 0 12px;">
|
||||
<p style="margin: 0 0 4px; color: #667085; font-size: 13px;">Participants</p>
|
||||
<p style="margin: 0; color: #344054; font-size: 15px; font-weight: 700;">{{ $participantsCount }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin: 0 0 16px; color: #344054; font-size: 16px; line-height: 1.6;">
|
||||
Garde un oeil sur l'activité de cette hunt pour suivre sa progression et préparer la suite de l'expérience.
|
||||
</p>
|
||||
<p style="margin: 0;">
|
||||
<a href="{{ config('app.url') }}" style="display: inline-block; padding: 14px 22px; background: #16a34a; border-radius: 8px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
||||
Ouvrir {{ config('app.name') }}
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 20px 32px 32px; border-top: 1px solid #edf1f7;">
|
||||
<p style="margin: 0; color: #98a2b3; font-size: 13px; line-height: 1.5;">
|
||||
Cet email est envoyé parce que tu es le créateur de la hunt {{ $hunt->title }}.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Filament\Resources\Hunts\Pages\EditHunts;
|
||||
use App\Filament\Resources\Hunts\RelationManagers\StepsRelationManager;
|
||||
use App\Models\Hunts;
|
||||
use App\Models\User;
|
||||
use Dotswan\MapPicker\Fields\Map;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('uses an image upload and map picker when creating hunt steps', function () {
|
||||
$admin = User::factory()->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,
|
||||
);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue