Skip to content

Commit 16d0e6b

Browse files
simonhampclaude
andcommitted
Add promoted Wall of Love submissions to homepage feedback section
Allow approved submissions to be promoted to homepage with optional testimonial text override. Adds masonry grid feedback section below partners, Filament promote/unpromote actions, and edit form controls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 35fd572 commit 16d0e6b

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

app/Filament/Resources/WallOfLoveSubmissionResource.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,48 @@ public static function form(Form $form): Form
4949
->maxLength(1000)
5050
->rows(4),
5151
]),
52+
53+
Forms\Components\Section::make('Status & Promotion')
54+
->schema([
55+
Forms\Components\Toggle::make('is_approved')
56+
->label('Approved')
57+
->helperText('Approved submissions appear on the Wall of Love.')
58+
->formatStateUsing(fn (?WallOfLoveSubmission $record) => $record?->isApproved() ?? false)
59+
->dehydrated(false)
60+
->afterStateUpdated(function (bool $state, ?WallOfLoveSubmission $record) {
61+
if (! $record) {
62+
return;
63+
}
64+
65+
if ($state) {
66+
$record->update([
67+
'approved_at' => now(),
68+
'approved_by' => auth()->id(),
69+
]);
70+
} else {
71+
$record->update([
72+
'approved_at' => null,
73+
'approved_by' => null,
74+
'promoted' => false,
75+
'promoted_testimonial' => null,
76+
]);
77+
}
78+
})
79+
->live(),
80+
81+
Forms\Components\Toggle::make('promoted')
82+
->label('Promoted to Homepage')
83+
->helperText('Promoted submissions appear in the feedback section on the homepage.')
84+
->visible(fn (Forms\Get $get) => $get('is_approved'))
85+
->live(),
86+
87+
Forms\Components\Textarea::make('promoted_testimonial')
88+
->label('Promoted Testimonial (optional override)')
89+
->helperText('Leave empty to use the original testimonial, or enter a clipped version for the homepage.')
90+
->rows(4)
91+
->visible(fn (Forms\Get $get) => $get('is_approved') && $get('promoted')),
92+
])
93+
->columns(1),
5294
]);
5395
}
5496

@@ -84,6 +126,15 @@ public static function table(Table $table): Table
84126
->falseColor('warning')
85127
->sortable(),
86128

129+
Tables\Columns\IconColumn::make('promoted')
130+
->label('Promoted')
131+
->boolean()
132+
->trueIcon('heroicon-o-star')
133+
->falseIcon('heroicon-o-star')
134+
->trueColor('warning')
135+
->falseColor('gray')
136+
->sortable(),
137+
87138
Tables\Columns\TextColumn::make('approvedBy.name')
88139
->label('Approved By')
89140
->toggleable(),
@@ -104,6 +155,12 @@ public static function table(Table $table): Table
104155
true: fn (Builder $query) => $query->whereNotNull('approved_at'),
105156
false: fn (Builder $query) => $query->whereNull('approved_at'),
106157
),
158+
159+
Tables\Filters\TernaryFilter::make('promoted')
160+
->label('Promoted')
161+
->placeholder('All')
162+
->trueLabel('Promoted')
163+
->falseLabel('Not Promoted'),
107164
])
108165
->actions([
109166
Tables\Actions\Action::make('approve')
@@ -130,6 +187,33 @@ public static function table(Table $table): Table
130187
->modalHeading('Unapprove Submission')
131188
->modalDescription('Are you sure you want to unapprove this submission?'),
132189

190+
Tables\Actions\Action::make('promote')
191+
->icon('heroicon-o-star')
192+
->color('warning')
193+
->visible(fn (WallOfLoveSubmission $record) => $record->isApproved() && ! $record->isPromoted())
194+
->form([
195+
Forms\Components\Textarea::make('promoted_testimonial')
196+
->label('Testimonial Text (optional override)')
197+
->helperText('Leave empty to use the original testimonial, or enter a clipped version.')
198+
->rows(4)
199+
->default(fn (WallOfLoveSubmission $record) => $record->testimonial),
200+
])
201+
->action(fn (WallOfLoveSubmission $record, array $data) => $record->update([
202+
'promoted' => true,
203+
'promoted_testimonial' => $data['promoted_testimonial'] !== $record->testimonial ? $data['promoted_testimonial'] : null,
204+
]))
205+
->modalHeading('Promote to Homepage')
206+
->modalDescription('This will display this testimonial in the feedback section on the homepage.'),
207+
208+
Tables\Actions\Action::make('unpromote')
209+
->icon('heroicon-o-x-mark')
210+
->color('gray')
211+
->visible(fn (WallOfLoveSubmission $record) => $record->isPromoted())
212+
->action(fn (WallOfLoveSubmission $record) => $record->update(['promoted' => false]))
213+
->requiresConfirmation()
214+
->modalHeading('Remove from Homepage')
215+
->modalDescription('This will remove this testimonial from the homepage feedback section.'),
216+
133217
Tables\Actions\EditAction::make(),
134218
Tables\Actions\DeleteAction::make(),
135219
])

app/Models/WallOfLoveSubmission.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Models;
44

5+
use Illuminate\Database\Eloquent\Builder;
56
use Illuminate\Database\Eloquent\Factories\HasFactory;
67
use Illuminate\Database\Eloquent\Model;
78
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -19,10 +20,13 @@ class WallOfLoveSubmission extends Model
1920
'testimonial',
2021
'approved_at',
2122
'approved_by',
23+
'promoted',
24+
'promoted_testimonial',
2225
];
2326

2427
protected $casts = [
2528
'approved_at' => 'datetime',
29+
'promoted' => 'boolean',
2630
];
2731

2832
public function user(): BelongsTo
@@ -44,4 +48,27 @@ public function isPending(): bool
4448
{
4549
return $this->approved_at === null;
4650
}
51+
52+
public function isPromoted(): bool
53+
{
54+
return $this->promoted;
55+
}
56+
57+
/**
58+
* @param Builder<WallOfLoveSubmission> $query
59+
* @return Builder<WallOfLoveSubmission>
60+
*/
61+
public function scopeApproved(Builder $query): Builder
62+
{
63+
return $query->whereNotNull('approved_at');
64+
}
65+
66+
/**
67+
* @param Builder<WallOfLoveSubmission> $query
68+
* @return Builder<WallOfLoveSubmission>
69+
*/
70+
public function scopePromoted(Builder $query): Builder
71+
{
72+
return $query->where('promoted', true);
73+
}
4774
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('wall_of_love_submissions', function (Blueprint $table) {
15+
$table->boolean('promoted')->default(false)->after('approved_by');
16+
});
17+
}
18+
19+
/**
20+
* Reverse the migrations.
21+
*/
22+
public function down(): void
23+
{
24+
Schema::table('wall_of_love_submissions', function (Blueprint $table) {
25+
$table->dropColumn('promoted');
26+
});
27+
}
28+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('wall_of_love_submissions', function (Blueprint $table) {
15+
$table->text('promoted_testimonial')->nullable()->after('promoted');
16+
});
17+
}
18+
19+
/**
20+
* Reverse the migrations.
21+
*/
22+
public function down(): void
23+
{
24+
Schema::table('wall_of_love_submissions', function (Blueprint $table) {
25+
$table->dropColumn('promoted_testimonial');
26+
});
27+
}
28+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
@php
2+
$submissions = \App\Models\WallOfLoveSubmission::query()
3+
->approved()
4+
->promoted()
5+
->latest()
6+
->take(4)
7+
->get();
8+
@endphp
9+
10+
@if ($submissions->count() > 0)
11+
<section class="mt-12" aria-labelledby="feedback-title">
12+
<div class="text-center">
13+
<h2
14+
id="feedback-title"
15+
x-init="
16+
() => {
17+
motion.inView($el, () => {
18+
gsap.fromTo(
19+
$el,
20+
{ autoAlpha: 0, y: 10 },
21+
{ autoAlpha: 1, y: 0, duration: 0.5, ease: 'power2.out' },
22+
)
23+
})
24+
}
25+
"
26+
class="text-lg font-semibold text-gray-900 dark:text-white"
27+
>
28+
What developers are saying
29+
</h2>
30+
<p
31+
x-init="
32+
() => {
33+
motion.inView($el, () => {
34+
gsap.fromTo(
35+
$el,
36+
{ autoAlpha: 0, y: 10 },
37+
{ autoAlpha: 1, y: 0, duration: 0.5, delay: 0.1, ease: 'power2.out' },
38+
)
39+
})
40+
}
41+
"
42+
class="mt-1 text-sm text-gray-500 dark:text-gray-400"
43+
>
44+
From the <a href="{{ route('wall-of-love') }}" class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">Wall of Love</a>
45+
</p>
46+
</div>
47+
48+
<div class="mt-6 columns-1 gap-4 sm:columns-2 lg:columns-4">
49+
@foreach ($submissions as $submission)
50+
<div
51+
x-init="
52+
() => {
53+
motion.inView($el, () => {
54+
gsap.fromTo(
55+
$el,
56+
{ autoAlpha: 0, y: 15 },
57+
{ autoAlpha: 1, y: 0, duration: 0.4, delay: {{ $loop->index * 0.08 }}, ease: 'power2.out' },
58+
)
59+
})
60+
}
61+
"
62+
class="mb-4 break-inside-avoid rounded-xl border border-gray-200/60 bg-white p-4 dark:border-slate-700/60 dark:bg-slate-800/50"
63+
>
64+
{{-- Quote --}}
65+
<blockquote class="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
66+
"{{ $submission->promoted_testimonial ?? $submission->testimonial }}"
67+
</blockquote>
68+
69+
{{-- Author --}}
70+
<div class="mt-3 flex items-center gap-2.5">
71+
@if ($submission->photo_path)
72+
<img
73+
src="{{ Storage::disk('public')->url($submission->photo_path) }}"
74+
alt="{{ $submission->name }}"
75+
class="size-7 rounded-full object-cover"
76+
loading="lazy"
77+
/>
78+
@else
79+
<div class="grid size-7 place-items-center rounded-full bg-gradient-to-br from-indigo-400 to-purple-500 text-xs font-medium text-white">
80+
{{ substr($submission->name, 0, 1) }}
81+
</div>
82+
@endif
83+
<div class="min-w-0 flex-1">
84+
<div class="truncate text-xs font-medium text-gray-900 dark:text-white">
85+
@if ($submission->url)
86+
<a
87+
href="{{ $submission->url }}"
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
class="hover:text-indigo-600 dark:hover:text-indigo-400"
91+
>
92+
{{ $submission->name }}
93+
</a>
94+
@else
95+
{{ $submission->name }}
96+
@endif
97+
</div>
98+
@if ($submission->company)
99+
<div class="truncate text-xs text-gray-500 dark:text-gray-400">
100+
{{ $submission->company }}
101+
</div>
102+
@endif
103+
</div>
104+
</div>
105+
</div>
106+
@endforeach
107+
</div>
108+
</section>
109+
@endif

resources/views/welcome.blade.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010

1111
{{-- Partners --}}
1212
<x-home.partners />
13+
14+
{{-- Feedback --}}
15+
<x-home.feedback />
1316
</x-layout>

0 commit comments

Comments
 (0)