Skip to content

Commit 21b2942

Browse files
committed
Add forum post reaction
1 parent 1e1ab26 commit 21b2942

12 files changed

Lines changed: 360 additions & 19 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
public function up(): void
10+
{
11+
Schema::create('forum_post_reactions', function (Blueprint $table) {
12+
$table->id();
13+
14+
// Add tenant relationship if tenancy is enabled
15+
if (config('filament-forum.tenancy.enabled')) {
16+
$tenantModel = config('filament-forum.tenancy.model');
17+
$table->foreignIdFor($tenantModel)
18+
->constrained()
19+
->cascadeOnDelete();
20+
}
21+
22+
$table->foreignId('forum_post_id')->constrained()->cascadeOnDelete();
23+
$table->morphs('reactor');
24+
25+
if (config('database.default') === 'mysql') {
26+
$table->string('type', 50)->collation('utf8mb4_bin');
27+
} else {
28+
$table->string('type', 50);
29+
}
30+
31+
$table->timestamps();
32+
33+
$table->unique(['forum_post_id', 'reactor_type', 'reactor_id', 'type'], 'unique_post_reaction');
34+
$table->index(['forum_post_id', 'type']);
35+
});
36+
}
37+
38+
public function down(): void
39+
{
40+
Schema::dropIfExists('forum_post_reactions');
41+
}
42+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@php
2+
$record = $getRecord();
3+
@endphp
4+
5+
<div class="fi-in-entry-wrp">
6+
@livewire('tapp.filament-forum.forum-post-reactions', [
7+
'post' => $record,
8+
], key('post-reactions-' . $record->id))
9+
</div>

resources/views/filament/tables/components/forum-post-card-column.blade.php

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,33 +68,40 @@ class="w-6 h-6 text-primary-500"
6868
</div>
6969
</a>
7070

71-
{{-- Footer with stats and comment authors --}}
71+
{{-- Footer with stats, reactions and comment authors --}}
7272
<div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-100">
7373
<div class="text-sm text-gray-500">
7474
@php
7575
$viewsCount = $record->views_count ?? 0;
7676
$replies = $record->comments()->count();
7777
@endphp
78-
78+
7979
{{ trans_choice('filament-forum::filament-forum.forum-post.views', $viewsCount, ['value' => $viewsCount]) }} &bull; {{ trans_choice('filament-forum::filament-forum.forum-post.replies', $replies, ['value' => $replies]) }}
8080
</div>
8181

82-
{{-- Comment Authors --}}
83-
<div class="flex items-center -space-x-2">
84-
@foreach($record->getCommentAuthors() as $author)
85-
<img
86-
src="{{ $author->profile_photo ?? 'https://ui-avatars.com/api/?name=' . urlencode($author->name) }}"
87-
alt="{{ $author->name }}"
88-
class="w-8 h-8 rounded-full border-2 border-white"
89-
>
90-
@endforeach
91-
@if($record->getCountDistinctUsersWhoCommented() > 3)
92-
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 border-2 border-white">
93-
<span class="text-xs text-gray-500 font-medium">
94-
+{{ $record->getCountDistinctUsersWhoCommented() - 3 }}
95-
</span>
96-
</div>
97-
@endif
82+
<div class="flex items-center gap-3">
83+
{{-- Post Reactions --}}
84+
<div>
85+
@livewire('tapp.filament-forum.forum-post-reactions', ['post' => $record], key('post-reactions-' . $record->id))
86+
</div>
87+
88+
{{-- Comment Authors --}}
89+
<div class="flex items-center -space-x-2">
90+
@foreach($record->getCommentAuthors() as $author)
91+
<img
92+
src="{{ $author->profile_photo ?? 'https://ui-avatars.com/api/?name=' . urlencode($author->name) }}"
93+
alt="{{ $author->name }}"
94+
class="w-8 h-8 rounded-full border-2 border-white"
95+
>
96+
@endforeach
97+
@if($record->getCountDistinctUsersWhoCommented() > 3)
98+
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 border-2 border-white">
99+
<span class="text-xs text-gray-500 font-medium">
100+
+{{ $record->getCountDistinctUsersWhoCommented() - 3 }}
101+
</span>
102+
</div>
103+
@endif
104+
</div>
98105
</div>
99106
</div>
100107
</div>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<div class="flex items-center gap-1">
2+
{{-- Existing Reactions Display (clickable to toggle) --}}
3+
@foreach($reactionCounts as $type => $count)
4+
@if($count > 0)
5+
<button
6+
wire:click="toggleReaction('{{ $type }}')"
7+
@class([
8+
'flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-sm transition-colors border',
9+
'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700' => in_array($type, $userReactions),
10+
'hover:bg-gray-100 text-gray-600 border-gray-200 dark:hover:bg-gray-700 dark:text-gray-400 dark:border-gray-600' => !in_array($type, $userReactions),
11+
])
12+
@auth
13+
x-tooltip.raw="{{ $availableReactions[$type] ?? $type }}"
14+
@else
15+
disabled
16+
x-tooltip.raw="{{ __('filament-forum::filament-forum.comments.login-to-react') }}"
17+
@endauth
18+
>
19+
<span>{{ $type }}</span>
20+
<span class="text-xs font-medium">{{ $count }}</span>
21+
</button>
22+
@endif
23+
@endforeach
24+
25+
{{-- Smiley icon to open reaction picker --}}
26+
<div class="relative" x-data="{ open: @entangle('showReactionPicker') }" x-on:click.outside="open = false">
27+
@auth
28+
<button
29+
wire:click="toggleReactionPicker"
30+
class="flex items-center justify-center w-7 h-7 rounded-full transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
31+
x-tooltip.raw="{{ __('filament-forum::filament-forum.comments.add-reaction') }}"
32+
>
33+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
34+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
35+
</svg>
36+
</button>
37+
38+
{{-- Reaction Picker --}}
39+
<div x-show="open"
40+
x-transition:enter="transition ease-out duration-100"
41+
x-transition:enter-start="transform opacity-0 scale-95"
42+
x-transition:enter-end="transform opacity-100 scale-100"
43+
x-transition:leave="transition ease-in duration-75"
44+
x-transition:leave-start="transform opacity-100 scale-100"
45+
x-transition:leave-end="transform opacity-0 scale-95"
46+
class="absolute bottom-full right-0 mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg p-2 z-10">
47+
<div class="flex space-x-1">
48+
@foreach($availableReactions as $emoji => $label)
49+
<button
50+
wire:click="toggleReaction('{{ $emoji }}')"
51+
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
52+
x-tooltip.raw="{{ $label }}"
53+
>
54+
<span class="text-lg">{{ $emoji }}</span>
55+
</button>
56+
@endforeach
57+
</div>
58+
</div>
59+
@else
60+
<button
61+
disabled
62+
class="flex items-center justify-center w-7 h-7 rounded-full text-gray-300 cursor-not-allowed dark:text-gray-600"
63+
x-tooltip.raw="{{ __('filament-forum::filament-forum.comments.login-to-react') }}"
64+
>
65+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
66+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
67+
</svg>
68+
</button>
69+
@endauth
70+
</div>
71+
</div>

src/Events/PostWasReacted.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Tapp\FilamentForum\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Foundation\Events\Dispatchable;
7+
use Illuminate\Queue\SerializesModels;
8+
use Tapp\FilamentForum\Models\ForumPost;
9+
use Tapp\FilamentForum\Models\ForumPostReaction;
10+
11+
class PostWasReacted
12+
{
13+
use Dispatchable;
14+
use InteractsWithSockets;
15+
use SerializesModels;
16+
17+
/**
18+
* Create a new event instance.
19+
*/
20+
public function __construct(
21+
public $reactor,
22+
public ForumPost $post,
23+
public ForumPostReaction $reaction,
24+
) {}
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Tapp\FilamentForum\Filament\Infolists\Components;
4+
5+
use Filament\Infolists\Components\Entry;
6+
7+
class ForumPostReactionsEntry extends Entry
8+
{
9+
protected string $view = 'filament-forum::filament.infolists.components.forum-post-reactions-entry';
10+
}

src/Filament/Resources/ForumPosts/Schemas/ForumPostInfolist.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Filament\Schemas\Components\Section;
1010
use Filament\Schemas\Schema;
1111
use Tapp\FilamentForum\Filament\Infolists\Components\ForumCommentsEntry;
12+
use Tapp\FilamentForum\Filament\Infolists\Components\ForumPostReactionsEntry;
1213
use Tapp\FilamentForum\Models\ForumPost;
1314

1415
class ForumPostInfolist
@@ -65,6 +66,9 @@ public static function configure(Schema $schema): Schema
6566
->columnSpanFull(),
6667
]),
6768

69+
ForumPostReactionsEntry::make('reactions')
70+
->hiddenLabel(),
71+
6872
Group::make()
6973
->schema([
7074
ForumCommentsEntry::make('comments')

src/Filament/Resources/ForumPosts/Tables/ForumPostsTable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static function configure(Table $table): Table
1818
return $table
1919
->recordUrl(null)
2020
->modifyQueryUsing(function (Builder $query) {
21-
return $query->with(['user', 'forum']);
21+
return $query->with(['user', 'forum', 'reactions']);
2222
})
2323
->columns([
2424
Stack::make([

src/FilamentForumServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function configurePackage(Package $package): void
3333
'create_forum_post_views_table',
3434
'create_forum_comments_table',
3535
'create_forum_comment_reactions_table',
36+
'create_forum_post_reactions_table',
3637
])
3738
->hasTranslations()
3839
->hasCommand(\Tapp\FilamentForum\Console\InstallTestsCommand::class);
@@ -43,6 +44,7 @@ public function packageBooted()
4344
// Register Livewire components
4445
Livewire::component('tapp.filament-forum.forum-comments', \Tapp\FilamentForum\Livewire\ForumComments::class);
4546
Livewire::component('tapp.filament-forum.forum-comment-reactions', \Tapp\FilamentForum\Livewire\ForumCommentReactions::class);
47+
Livewire::component('tapp.filament-forum.forum-post-reactions', \Tapp\FilamentForum\Livewire\ForumPostReactions::class);
4648

4749
// Register assets
4850
FilamentAsset::register([
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Tapp\FilamentForum\Livewire;
4+
5+
use Illuminate\Support\Facades\Auth;
6+
use Livewire\Component;
7+
use Tapp\FilamentForum\Models\ForumPost;
8+
9+
class ForumPostReactions extends Component
10+
{
11+
public ForumPost $post;
12+
13+
public array $reactionCounts = [];
14+
15+
public array $userReactions = [];
16+
17+
public bool $showReactionPicker = false;
18+
19+
public array $availableReactions = [
20+
'👍' => 'Like',
21+
'❤️' => 'Love',
22+
'😂' => 'Laugh',
23+
'😮' => 'Wow',
24+
'😢' => 'Sad',
25+
'😡' => 'Angry',
26+
];
27+
28+
public function mount(ForumPost $post): void
29+
{
30+
$this->post = $post;
31+
$this->loadReactions();
32+
}
33+
34+
public function hydrate(): void
35+
{
36+
if (empty($this->reactionCounts)) {
37+
$this->loadReactions();
38+
}
39+
}
40+
41+
public function toggleReaction(string $type): void
42+
{
43+
if (! Auth::check()) {
44+
return;
45+
}
46+
47+
$this->post->toggleReaction($type, Auth::user());
48+
$this->loadReactions();
49+
$this->showReactionPicker = false;
50+
}
51+
52+
public function toggleReactionPicker(): void
53+
{
54+
if (! Auth::check()) {
55+
return;
56+
}
57+
58+
$this->showReactionPicker = ! $this->showReactionPicker;
59+
}
60+
61+
protected function loadReactions(): void
62+
{
63+
try {
64+
$this->reactionCounts = $this->post->getReactionCounts();
65+
$this->userReactions = Auth::check()
66+
? $this->post->getUserReactions(Auth::user())->toArray()
67+
: [];
68+
} catch (\Exception $e) {
69+
$this->reactionCounts = [];
70+
$this->userReactions = [];
71+
}
72+
}
73+
74+
public function render()
75+
{
76+
return view('filament-forum::livewire.forum-post-reactions');
77+
}
78+
}

0 commit comments

Comments
 (0)