Skip to content

Commit 8ee0542

Browse files
committed
Leads
1 parent 3544f31 commit 8ee0542

File tree

21 files changed

+918
-6
lines changed

21 files changed

+918
-6
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\LeadResource\Pages;
6+
use App\Models\Lead;
7+
use Filament\Infolists;
8+
use Filament\Infolists\Infolist;
9+
use Filament\Resources\Resource;
10+
use Filament\Tables;
11+
use Filament\Tables\Table;
12+
13+
class LeadResource extends Resource
14+
{
15+
protected static ?string $model = Lead::class;
16+
17+
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
18+
19+
protected static ?string $navigationLabel = 'Leads';
20+
21+
protected static ?string $pluralModelLabel = 'Leads';
22+
23+
public static function canCreate(): bool
24+
{
25+
return false;
26+
}
27+
28+
public static function infolist(Infolist $infolist): Infolist
29+
{
30+
return $infolist
31+
->schema([
32+
Infolists\Components\Section::make('Contact Information')
33+
->schema([
34+
Infolists\Components\TextEntry::make('name')
35+
->label('Name'),
36+
Infolists\Components\TextEntry::make('email')
37+
->label('Email')
38+
->copyable(),
39+
Infolists\Components\TextEntry::make('company')
40+
->label('Company'),
41+
])
42+
->columns(3),
43+
44+
Infolists\Components\Section::make('Project Details')
45+
->schema([
46+
Infolists\Components\TextEntry::make('budget_label')
47+
->label('Budget'),
48+
Infolists\Components\TextEntry::make('description')
49+
->label('App Description')
50+
->columnSpanFull(),
51+
]),
52+
53+
Infolists\Components\Section::make('Metadata')
54+
->schema([
55+
Infolists\Components\TextEntry::make('ip_address')
56+
->label('IP Address'),
57+
Infolists\Components\TextEntry::make('created_at')
58+
->label('Submitted')
59+
->dateTime(),
60+
])
61+
->columns(2)
62+
->collapsed(),
63+
]);
64+
}
65+
66+
public static function table(Table $table): Table
67+
{
68+
return $table
69+
->columns([
70+
Tables\Columns\TextColumn::make('name')
71+
->searchable()
72+
->sortable(),
73+
74+
Tables\Columns\TextColumn::make('email')
75+
->searchable()
76+
->sortable(),
77+
78+
Tables\Columns\TextColumn::make('company')
79+
->searchable()
80+
->sortable(),
81+
82+
Tables\Columns\TextColumn::make('budget_label')
83+
->label('Budget')
84+
->sortable(query: function ($query, string $direction) {
85+
return $query->orderBy('budget', $direction);
86+
}),
87+
88+
Tables\Columns\TextColumn::make('created_at')
89+
->label('Submitted')
90+
->dateTime()
91+
->sortable(),
92+
])
93+
->filters([
94+
Tables\Filters\SelectFilter::make('budget')
95+
->options(Lead::BUDGETS),
96+
])
97+
->actions([
98+
Tables\Actions\ViewAction::make(),
99+
])
100+
->bulkActions([
101+
Tables\Actions\BulkActionGroup::make([
102+
Tables\Actions\DeleteBulkAction::make(),
103+
]),
104+
])
105+
->defaultSort('created_at', 'desc');
106+
}
107+
108+
public static function getRelations(): array
109+
{
110+
return [
111+
//
112+
];
113+
}
114+
115+
public static function getPages(): array
116+
{
117+
return [
118+
'index' => Pages\ListLeads::route('/'),
119+
'view' => Pages\ViewLead::route('/{record}'),
120+
];
121+
}
122+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\LeadResource\Pages;
4+
5+
use App\Filament\Resources\LeadResource;
6+
use Filament\Actions;
7+
use Filament\Resources\Pages\ListRecords;
8+
9+
class ListLeads extends ListRecords
10+
{
11+
protected static string $resource = LeadResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
Actions\CreateAction::make(),
17+
];
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\LeadResource\Pages;
4+
5+
use App\Filament\Resources\LeadResource;
6+
use Filament\Actions;
7+
use Filament\Resources\Pages\ViewRecord;
8+
9+
class ViewLead extends ViewRecord
10+
{
11+
protected static string $resource = LeadResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
Actions\EditAction::make(),
17+
];
18+
}
19+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use App\Models\Lead;
6+
use App\Notifications\LeadReceived;
7+
use App\Notifications\NewLeadSubmitted;
8+
use App\Rules\Turnstile;
9+
use Illuminate\Support\Facades\Notification;
10+
use Illuminate\Support\Facades\RateLimiter;
11+
use Livewire\Component;
12+
13+
class LeadSubmissionForm extends Component
14+
{
15+
public string $name = '';
16+
17+
public string $email = '';
18+
19+
public string $company = '';
20+
21+
public string $description = '';
22+
23+
public string $budget = '';
24+
25+
public string $turnstileToken = '';
26+
27+
public bool $submitted = false;
28+
29+
protected function rules(): array
30+
{
31+
$rules = [
32+
'name' => ['required', 'string', 'max:255'],
33+
'email' => ['required', 'email', 'max:255'],
34+
'company' => ['required', 'string', 'max:255'],
35+
'description' => ['required', 'string', 'max:5000'],
36+
'budget' => ['required', 'string', 'in:'.implode(',', array_keys(Lead::BUDGETS))],
37+
];
38+
39+
if (config('services.turnstile.secret_key')) {
40+
$rules['turnstileToken'] = ['required', new Turnstile];
41+
}
42+
43+
return $rules;
44+
}
45+
46+
public function messages(): array
47+
{
48+
return [
49+
'budget.in' => 'Please select a budget range.',
50+
'turnstileToken.required' => 'Please complete the security check.',
51+
];
52+
}
53+
54+
public function submit(): void
55+
{
56+
$key = 'leads:'.request()->ip();
57+
58+
if (RateLimiter::tooManyAttempts($key, 5)) {
59+
$seconds = RateLimiter::availableIn($key);
60+
$this->addError('form', "Too many submissions. Please try again in {$seconds} seconds.");
61+
62+
return;
63+
}
64+
65+
$this->validate();
66+
67+
RateLimiter::hit($key, 60);
68+
69+
$lead = Lead::create([
70+
'name' => $this->name,
71+
'email' => $this->email,
72+
'company' => $this->company,
73+
'description' => $this->description,
74+
'budget' => $this->budget,
75+
'ip_address' => request()->ip(),
76+
]);
77+
78+
$lead->notify(new LeadReceived);
79+
80+
Notification::route('mail', '[email protected]')
81+
->notify(new NewLeadSubmitted($lead));
82+
83+
$this->submitted = true;
84+
}
85+
86+
public function render()
87+
{
88+
return view('livewire.lead-submission-form', [
89+
'budgets' => Lead::BUDGETS,
90+
]);
91+
}
92+
}

app/Models/Lead.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Notifications\Notifiable;
8+
9+
class Lead extends Model
10+
{
11+
use HasFactory;
12+
use Notifiable;
13+
14+
protected $fillable = [
15+
'name',
16+
'email',
17+
'company',
18+
'description',
19+
'budget',
20+
'ip_address',
21+
];
22+
23+
public const BUDGETS = [
24+
'less_than_5k' => 'Less than $5,000',
25+
'5k_to_10k' => '$5,000 - $10,000',
26+
'10k_to_25k' => '$10,000 - $25,000',
27+
'25k_to_50k' => '$25,000 - $50,000',
28+
'50k_plus' => '$50,000+',
29+
];
30+
31+
public function getBudgetLabelAttribute(): string
32+
{
33+
return self::BUDGETS[$this->budget] ?? $this->budget;
34+
}
35+
}

app/Notifications/LeadReceived.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Notifications\Messages\MailMessage;
8+
use Illuminate\Notifications\Notification;
9+
10+
class LeadReceived extends Notification implements ShouldQueue
11+
{
12+
use Queueable;
13+
14+
public function via(object $notifiable): array
15+
{
16+
return ['mail'];
17+
}
18+
19+
public function toMail(object $notifiable): MailMessage
20+
{
21+
return (new MailMessage)
22+
->subject('Thank you for your enquiry')
23+
->greeting("Hi {$notifiable->name},")
24+
->line('Thank you for reaching out to NativePHP about your app development project.')
25+
->line('We have received your enquiry and one of our team members will be in touch soon to discuss your requirements.')
26+
->line('In the meantime, feel free to explore our documentation or join our Discord community.')
27+
->action('Visit NativePHP', url('/'))
28+
->salutation('Best regards,<br>The NativePHP Team');
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Models\Lead;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Contracts\Queue\ShouldQueue;
8+
use Illuminate\Notifications\Messages\MailMessage;
9+
use Illuminate\Notifications\Notification;
10+
11+
class NewLeadSubmitted extends Notification implements ShouldQueue
12+
{
13+
use Queueable;
14+
15+
public function __construct(
16+
public Lead $lead
17+
) {}
18+
19+
public function via(object $notifiable): array
20+
{
21+
return ['mail'];
22+
}
23+
24+
public function toMail(object $notifiable): MailMessage
25+
{
26+
return (new MailMessage)
27+
->subject('New Build My App Enquiry: '.$this->lead->company)
28+
->replyTo($this->lead->email, $this->lead->name)
29+
->greeting('New lead received!')
30+
->line("**Name:** {$this->lead->name}")
31+
->line("**Email:** {$this->lead->email}")
32+
->line("**Company:** {$this->lead->company}")
33+
->line("**Budget:** {$this->lead->budget_label}")
34+
->line('**Project Description:**')
35+
->line($this->lead->description);
36+
}
37+
}

app/Providers/RouteServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public function boot(): void
2828
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
2929
});
3030

31+
RateLimiter::for('leads', function (Request $request) {
32+
return Limit::perMinute(5)->by($request->ip());
33+
});
34+
3135
$this->routes(function () {
3236
Route::middleware('api')
3337
->prefix('api')

0 commit comments

Comments
 (0)