Skip to content

Commit 265b002

Browse files
πŸ’ Add wall of love page for EAP members (#207)
* ✨ Add Wall of Love page and early adopter card component * πŸ’„ Update early adopter card styles and add featured attribute for specific adopters * ✨ Enhance early adopter card animations and improve layout responsiveness * ♿️ Refactor early adopter card structure to use semantic HTML elements and improve accessibility * πŸ’„ Update early adopter card image positioning and loading placeholder styles * Manage the wall of love * Update page * Code style --------- Co-authored-by: Simon Hamp <simon.hamp@me.com>
1 parent 0f197cd commit 265b002

20 files changed

+910
-14
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource\Pages;
6+
use App\Models\WallOfLoveSubmission;
7+
use Filament\Forms;
8+
use Filament\Forms\Form;
9+
use Filament\Resources\Resource;
10+
use Filament\Tables;
11+
use Filament\Tables\Table;
12+
use Illuminate\Database\Eloquent\Builder;
13+
14+
class WallOfLoveSubmissionResource extends Resource
15+
{
16+
protected static ?string $model = WallOfLoveSubmission::class;
17+
18+
protected static ?string $navigationIcon = 'heroicon-o-heart';
19+
20+
protected static ?string $navigationLabel = 'Wall of Love';
21+
22+
protected static ?string $pluralModelLabel = 'Wall of Love Submissions';
23+
24+
public static function form(Form $form): Form
25+
{
26+
return $form
27+
->schema([
28+
Forms\Components\Section::make('Submission Details')
29+
->schema([
30+
Forms\Components\TextInput::make('name')
31+
->required()
32+
->maxLength(255),
33+
34+
Forms\Components\TextInput::make('company')
35+
->maxLength(255),
36+
37+
Forms\Components\FileUpload::make('photo_path')
38+
->label('Photo')
39+
->image()
40+
->disk('public')
41+
->directory('wall-of-love-photos'),
42+
43+
Forms\Components\TextInput::make('url')
44+
->label('Website/Social URL')
45+
->url()
46+
->maxLength(255),
47+
48+
Forms\Components\Textarea::make('testimonial')
49+
->maxLength(1000)
50+
->rows(4),
51+
]),
52+
53+
Forms\Components\Section::make('Review Information')
54+
->schema([
55+
Forms\Components\Select::make('user_id')
56+
->relationship('user', 'name')
57+
->required()
58+
->disabled(),
59+
60+
Forms\Components\DateTimePicker::make('approved_at')
61+
->label('Approved At'),
62+
63+
Forms\Components\Select::make('approved_by')
64+
->relationship('approvedBy', 'name')
65+
->label('Approved By'),
66+
67+
Forms\Components\Placeholder::make('created_at')
68+
->label('Submitted At')
69+
->content(fn (WallOfLoveSubmission $record): ?string => $record->created_at?->diffForHumans()),
70+
]),
71+
]);
72+
}
73+
74+
public static function table(Table $table): Table
75+
{
76+
return $table
77+
->columns([
78+
Tables\Columns\TextColumn::make('name')
79+
->searchable()
80+
->sortable(),
81+
82+
Tables\Columns\TextColumn::make('company')
83+
->searchable()
84+
->toggleable(),
85+
86+
Tables\Columns\TextColumn::make('user.name')
87+
->label('Submitted By')
88+
->searchable()
89+
->sortable(),
90+
91+
Tables\Columns\ImageColumn::make('photo_path')
92+
->label('Photo')
93+
->disk('public')
94+
->height(40)
95+
->toggleable(),
96+
97+
Tables\Columns\IconColumn::make('approved_at')
98+
->label('Status')
99+
->boolean()
100+
->trueIcon('heroicon-o-check-circle')
101+
->falseIcon('heroicon-o-clock')
102+
->trueColor('success')
103+
->falseColor('warning')
104+
->sortable(),
105+
106+
Tables\Columns\TextColumn::make('approvedBy.name')
107+
->label('Approved By')
108+
->toggleable(),
109+
110+
Tables\Columns\TextColumn::make('created_at')
111+
->label('Submitted')
112+
->dateTime()
113+
->sortable()
114+
->toggleable(),
115+
])
116+
->filters([
117+
Tables\Filters\TernaryFilter::make('approved_at')
118+
->label('Status')
119+
->placeholder('All submissions')
120+
->trueLabel('Approved')
121+
->falseLabel('Pending')
122+
->queries(
123+
true: fn (Builder $query) => $query->whereNotNull('approved_at'),
124+
false: fn (Builder $query) => $query->whereNull('approved_at'),
125+
),
126+
])
127+
->actions([
128+
Tables\Actions\Action::make('approve')
129+
->icon('heroicon-o-check')
130+
->color('success')
131+
->visible(fn (WallOfLoveSubmission $record) => $record->isPending())
132+
->action(fn (WallOfLoveSubmission $record) => $record->update([
133+
'approved_at' => now(),
134+
'approved_by' => auth()->id(),
135+
]))
136+
->requiresConfirmation()
137+
->modalHeading('Approve Submission')
138+
->modalDescription('Are you sure you want to approve this submission for the Wall of Love?'),
139+
140+
Tables\Actions\Action::make('unapprove')
141+
->icon('heroicon-o-x-mark')
142+
->color('warning')
143+
->visible(fn (WallOfLoveSubmission $record) => $record->isApproved())
144+
->action(fn (WallOfLoveSubmission $record) => $record->update([
145+
'approved_at' => null,
146+
'approved_by' => null,
147+
]))
148+
->requiresConfirmation()
149+
->modalHeading('Unapprove Submission')
150+
->modalDescription('Are you sure you want to unapprove this submission?'),
151+
152+
Tables\Actions\EditAction::make(),
153+
Tables\Actions\DeleteAction::make(),
154+
])
155+
->bulkActions([
156+
Tables\Actions\BulkActionGroup::make([
157+
Tables\Actions\BulkAction::make('approve')
158+
->icon('heroicon-o-check')
159+
->color('success')
160+
->action(function ($records) {
161+
$records->each(fn (WallOfLoveSubmission $record) => $record->update([
162+
'approved_at' => now(),
163+
'approved_by' => auth()->id(),
164+
]));
165+
})
166+
->requiresConfirmation()
167+
->modalHeading('Approve Selected Submissions')
168+
->modalDescription('Are you sure you want to approve all selected submissions?'),
169+
170+
Tables\Actions\DeleteBulkAction::make(),
171+
]),
172+
])
173+
->defaultSort('created_at', 'desc');
174+
}
175+
176+
public static function getRelations(): array
177+
{
178+
return [
179+
//
180+
];
181+
}
182+
183+
public static function getPages(): array
184+
{
185+
return [
186+
'index' => Pages\ListWallOfLoveSubmissions::route('/'),
187+
// 'create' => Pages\CreateWallOfLoveSubmission::route('/create'),
188+
'edit' => Pages\EditWallOfLoveSubmission::route('/{record}/edit'),
189+
];
190+
}
191+
}
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\WallOfLoveSubmissionResource\Pages;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource;
6+
use Filament\Actions;
7+
use Filament\Resources\Pages\EditRecord;
8+
9+
class EditWallOfLoveSubmission extends EditRecord
10+
{
11+
protected static string $resource = WallOfLoveSubmissionResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
Actions\DeleteAction::make(),
17+
];
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\WallOfLoveSubmissionResource\Pages;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource;
6+
use Filament\Resources\Pages\ListRecords;
7+
8+
class ListWallOfLoveSubmissions extends ListRecords
9+
{
10+
protected static string $resource = WallOfLoveSubmissionResource::class;
11+
12+
protected function getHeaderActions(): array
13+
{
14+
return [
15+
];
16+
}
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
class WallOfLoveSubmissionController extends Controller
6+
{
7+
public function create()
8+
{
9+
// Check if user is eligible (has early adopter license)
10+
$hasEarlyAdopterLicense = auth()->user()
11+
->licenses()
12+
->where('created_at', '<', '2025-06-01')
13+
->exists();
14+
15+
if (! $hasEarlyAdopterLicense) {
16+
abort(404);
17+
}
18+
19+
// Check if user already has a submission
20+
$hasExistingSubmission = auth()->user()->wallOfLoveSubmissions()->exists();
21+
22+
if ($hasExistingSubmission) {
23+
return redirect()->route('customer.licenses')->with('info', 'You have already submitted your story to the Wall of Love.');
24+
}
25+
26+
return view('customer.wall-of-love.create');
27+
}
28+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use Livewire\Component;
6+
7+
class WallOfLoveBanner extends Component
8+
{
9+
public function dismissBanner(): void
10+
{
11+
cache()->put('wall_of_love_dismissed_'.auth()->id(), true, now()->addWeek());
12+
$this->dispatch('banner-dismissed');
13+
}
14+
15+
public function shouldShowBanner(): bool
16+
{
17+
// Check if user has early adopter licenses (before June 1st, 2025)
18+
$hasEarlyAdopterLicenses = auth()->user()->licenses()->where('created_at', '<', '2025-06-01')->exists();
19+
20+
// Check if user already submitted
21+
$hasExistingSubmission = auth()->user()->wallOfLoveSubmissions()->exists();
22+
23+
// Check if banner was dismissed
24+
$hasDismissedBanner = cache()->has('wall_of_love_dismissed_'.auth()->id());
25+
26+
return $hasEarlyAdopterLicenses && ! $hasExistingSubmission && ! $hasDismissedBanner;
27+
}
28+
29+
public function render()
30+
{
31+
return view('livewire.wall-of-love-banner');
32+
}
33+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use App\Models\WallOfLoveSubmission;
6+
use Livewire\Component;
7+
use Livewire\Features\SupportFileUploads\WithFileUploads;
8+
9+
class WallOfLoveSubmissionForm extends Component
10+
{
11+
use WithFileUploads;
12+
13+
public $name = '';
14+
15+
public $company = '';
16+
17+
public $photo;
18+
19+
public $url = '';
20+
21+
public $testimonial = '';
22+
23+
protected $rules = [
24+
'name' => 'required|string|max:255',
25+
'company' => 'nullable|string|max:255',
26+
'photo' => 'nullable|image|max:2048', // 2MB max
27+
'url' => 'nullable|url|max:255',
28+
'testimonial' => 'nullable|string|max:1000',
29+
];
30+
31+
public function mount()
32+
{
33+
// Pre-fill name if user has a name
34+
$this->name = auth()->user()->name ?? '';
35+
}
36+
37+
public function submit()
38+
{
39+
$this->validate();
40+
41+
$photoPath = null;
42+
if ($this->photo) {
43+
$photoPath = $this->photo->store('wall-of-love-photos', 'public');
44+
}
45+
46+
WallOfLoveSubmission::create([
47+
'user_id' => auth()->id(),
48+
'name' => $this->name,
49+
'company' => $this->company ?: null,
50+
'photo_path' => $photoPath,
51+
'url' => $this->url ?: null,
52+
'testimonial' => $this->testimonial ?: null,
53+
]);
54+
55+
return redirect()->route('customer.licenses')->with('success', 'Thank you! Your submission has been received and is awaiting review.');
56+
}
57+
58+
public function render()
59+
{
60+
return view('livewire.wall-of-love-submission-form');
61+
}
62+
}

β€Žapp/Models/License.phpβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function canCreateSubLicense(): bool
118118

119119
public function isLegacy(): bool
120120
{
121-
return !$this->subscription_item_id
121+
return ! $this->subscription_item_id
122122
&& $this->created_at->lt(Carbon::create(2025, 5, 8));
123123
}
124124

β€Žapp/Models/User.phpβ€Ž

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Illuminate\Support\Collection;
1313
use Laravel\Cashier\Billable;
1414
use Laravel\Sanctum\HasApiTokens;
15-
use Stripe\Customer;
1615

1716
class User extends Authenticatable implements FilamentUser
1817
{
@@ -48,6 +47,14 @@ public function licenses(): HasMany
4847
return $this->hasMany(License::class);
4948
}
5049

50+
/**
51+
* @return HasMany<WallOfLoveSubmission>
52+
*/
53+
public function wallOfLoveSubmissions(): HasMany
54+
{
55+
return $this->hasMany(WallOfLoveSubmission::class);
56+
}
57+
5158
public function getFirstNameAttribute(): ?string
5259
{
5360
if (empty($this->name)) {

0 commit comments

Comments
Β (0)