From 09a9d3284d5b6d65fc38aa109bd1ee7c75ee577e Mon Sep 17 00:00:00 2001
From: Arthur Monney <monneylobe@gmail.com>
Date: Sat, 11 Jan 2025 23:36:01 +0100
Subject: [PATCH 1/2] mep | merge develop into main (#321)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris Samory <samorytakougne@gmail.com>
---
 app/Livewire/Components/User/Activities.php   | 17 +++-
 app/Livewire/Pages/Account/Profile.php        | 58 ++++++++++--
 composer.json                                 |  2 +-
 composer.lock                                 | 16 ++--
 .../views/components/forum/thread.blade.php   |  2 +-
 .../views/components/user/articles.blade.php  | 92 +++++++++----------
 .../components/user/discussions.blade.php     | 88 +++++++++---------
 .../views/components/user/threads.blade.php   | 80 ++++++++--------
 resources/views/layouts/forum.blade.php       |  4 +-
 .../livewire/components/reactions.blade.php   |  2 +-
 .../components/user/activities.blade.php      |  2 +-
 .../livewire/pages/account/profile.blade.php  |  8 +-
 .../pages/articles/single-post.blade.php      | 22 ++---
 .../livewire/pages/forum/index.blade.php      |  2 +-
 14 files changed, 218 insertions(+), 177 deletions(-)

diff --git a/app/Livewire/Components/User/Activities.php b/app/Livewire/Components/User/Activities.php
index ba82654f..9d61fd98 100644
--- a/app/Livewire/Components/User/Activities.php
+++ b/app/Livewire/Components/User/Activities.php
@@ -7,16 +7,27 @@
 use App\Models\Activity;
 use App\Models\User;
 use Illuminate\Contracts\View\View;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+use Livewire\Attributes\Computed;
 use Livewire\Component;
 
 final class Activities extends Component
 {
     public User $user;
 
+    #[Computed(persist: true)]
+    public function activities(): Collection
+    {
+        return Cache::remember(
+            key: 'activities.'.$this->user->id,
+            ttl: now()->addDays(3),
+            callback: fn () => Activity::latestFeed($this->user)
+        );
+    }
+
     public function render(): View
     {
-        return view('livewire.components.user.activities', [
-            'activities' => Activity::latestFeed($this->user),
-        ]);
+        return view('livewire.components.user.activities');
     }
 }
diff --git a/app/Livewire/Pages/Account/Profile.php b/app/Livewire/Pages/Account/Profile.php
index d7f0fdf6..2e8e4ed1 100644
--- a/app/Livewire/Pages/Account/Profile.php
+++ b/app/Livewire/Pages/Account/Profile.php
@@ -4,30 +4,68 @@
 
 namespace App\Livewire\Pages\Account;
 
+use App\Models\Article;
+use App\Models\Discussion;
+use App\Models\Thread;
 use App\Models\User;
 use Illuminate\Contracts\View\View;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+use Livewire\Attributes\Computed;
 use Livewire\Component;
 
 final class Profile extends Component
 {
     public User $user;
 
-    public function render(): View
+    #[Computed(persist: true)]
+    public function articles(): Collection
     {
-        return view('livewire.pages.account.profile', [
-            'articles' => $this->user->articles() // @phpstan-ignore-line
-                ->recent()
+        return Cache::remember(
+            key: 'articles.'.$this->user->id,
+            ttl: now()->addDays(3),
+            callback: fn () => Article::with('media', 'tags')
+                ->select('id', 'title', 'slug', 'body', 'published_at')
+                ->whereBelongsTo($this->user)
                 ->published()
+                ->recent()
                 ->limit(5)
-                ->get(),
-            'threads' => $this->user->threads()
+                ->get()
+        );
+    }
+
+    #[Computed(persist: true)]
+    public function threads(): Collection
+    {
+        return Cache::remember(
+            key: 'threads.'.$this->user->id,
+            ttl: now()->addDays(3),
+            callback: fn () => Thread::with('channels')
+                ->withCount('replies')
+                ->whereBelongsTo($this->user)
                 ->orderByDesc('created_at')
                 ->limit(5)
-                ->get(),
-            'discussions' => $this->user->discussions()
+                ->get()
+        );
+    }
+
+    #[Computed(persist: true)]
+    public function discussions(): Collection
+    {
+        return Cache::remember(
+            key: 'discussions.'.$this->user->id,
+            ttl: now()->addDays(3),
+            callback: fn () => Discussion::with('tags')
+                ->withCount('replies', 'reactions')
+                ->whereBelongsTo($this->user)
                 ->limit(5)
-                ->get(),
-        ])
+                ->get()
+        );
+    }
+
+    public function render(): View
+    {
+        return view('livewire.pages.account.profile')
             ->title($this->user->username.' ( '.$this->user->name.')');
     }
 }
diff --git a/composer.json b/composer.json
index 58b7630f..630d4607 100644
--- a/composer.json
+++ b/composer.json
@@ -48,7 +48,7 @@
     "symfony/http-client": "^7.1.8",
     "symfony/mailgun-mailer": "^7.1",
     "torchlight/torchlight-laravel": "^0.6",
-    "vormkracht10/filament-mails": "^1.0",
+    "vormkracht10/filament-mails": "^2.0",
     "wire-elements/modal": "^2.0",
     "wire-elements/spotlight": "^2.0",
     "yarri/link-finder": "^2.7.10",
diff --git a/composer.lock b/composer.lock
index 7cb3da9c..1f31d4df 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "ed0e156f18a55dfdfc7fc7922799cd9a",
+    "content-hash": "4c716d8363d3d8066535ca12060d6e28",
     "packages": [
         {
             "name": "abraham/twitteroauth",
@@ -13554,16 +13554,16 @@
         },
         {
             "name": "vormkracht10/filament-mails",
-            "version": "v1.0.5",
+            "version": "v2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vormkracht10/filament-mails.git",
-                "reference": "ab13d2b90daad93943248f7888808f78a2ab6d09"
+                "reference": "c2c72b77e303d594c97c6a9b1c58dfec8b61d931"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vormkracht10/filament-mails/zipball/ab13d2b90daad93943248f7888808f78a2ab6d09",
-                "reference": "ab13d2b90daad93943248f7888808f78a2ab6d09",
+                "url": "https://api.github.com/repos/vormkracht10/filament-mails/zipball/c2c72b77e303d594c97c6a9b1c58dfec8b61d931",
+                "reference": "c2c72b77e303d594c97c6a9b1c58dfec8b61d931",
                 "shasum": ""
             },
             "require": {
@@ -13629,7 +13629,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-12-27T09:22:16+00:00"
+            "time": "2025-01-03T13:17:15+00:00"
         },
         {
             "name": "vormkracht10/laravel-mails",
@@ -17374,7 +17374,7 @@
     ],
     "aliases": [],
     "minimum-stability": "dev",
-    "stability-flags": {},
+    "stability-flags": [],
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
@@ -17382,6 +17382,6 @@
         "ext-fileinfo": "*",
         "ext-json": "*"
     },
-    "platform-dev": {},
+    "platform-dev": [],
     "plugin-api-version": "2.6.0"
 }
diff --git a/resources/views/components/forum/thread.blade.php b/resources/views/components/forum/thread.blade.php
index ffeaa4b7..e8db17e0 100644
--- a/resources/views/components/forum/thread.blade.php
+++ b/resources/views/components/forum/thread.blade.php
@@ -6,7 +6,7 @@
     <x-forum.thread-channels :thread="$thread" />
 
     <div class="flex items-center gap-4">
-        <h2 id="question-title-{{ $thread->id }}" class="truncate text-xl font-medium text-gray-900 dark:text-white lg:text-xl">
+        <h2 id="question-title-{{ $thread->id }}" class="truncate text-lg font-heading/7 font-semibold text-gray-900 dark:text-white lg:text-xl">
             <x-link :href="route('forum.show', $thread)" class="hover:underline">
                 {{ $thread->subject() }}
             </x-link>
diff --git a/resources/views/components/user/articles.blade.php b/resources/views/components/user/articles.blade.php
index ecdc6078..9c3eb6cd 100644
--- a/resources/views/components/user/articles.blade.php
+++ b/resources/views/components/user/articles.blade.php
@@ -3,56 +3,54 @@
     'articles',
 ])
 
-<div>
-    @if ($articles->isNotEmpty())
-        <div class="-mt-6 divide-y divide-gray-200 dark:divide-white/20">
-            @foreach ($articles as $article)
-                <div class="py-6">
-                    <x-articles.card :article="$article" is-summary />
-                </div>
-            @endforeach
-        </div>
-    @else
-        <x-empty-state>
+@if ($articles->isNotEmpty())
+    <div class="-mt-6 divide-y divide-gray-200 dark:divide-white/20">
+        @foreach ($articles as $article)
+            <div class="py-6">
+                <x-articles.card :article="$article" is-summary />
+            </div>
+        @endforeach
+    </div>
+@else
+    <x-empty-state>
+        <div class="relative">
+            <div class="absolute scale-90 shadow inset-x-0 -top-4 flex items-start -z-10 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+            </div>
             <div class="relative">
-                <div class="absolute scale-90 shadow inset-x-0 -top-4 flex items-start -z-10 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                </div>
-                <div class="relative">
-                    <div class="absolute -inset-1 bg-gradient-to-r from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
-                    <div class="relative flex items-start z-20 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                        <div class="w-1/4 h-20 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-lg dark:bg-gray-800"></div>
-                        <div class="w-3/4 pt-2 space-y-2">
-                            <div class="grid grid-cols-3 gap-2 w-2/3">
-                                <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
-                                <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
-                                <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
-                            </div>
-                            <div class="h-4 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                            <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                <div class="absolute -inset-1 bg-gradient-to-r from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
+                <div class="relative flex items-start z-20 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+                    <div class="w-1/4 h-20 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-lg dark:bg-gray-800"></div>
+                    <div class="w-3/4 pt-2 space-y-2">
+                        <div class="grid grid-cols-3 gap-2 w-2/3">
+                            <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
+                            <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
+                            <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
                         </div>
+                        <div class="h-4 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                        <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
                     </div>
                 </div>
-                <div class="absolute scale-[0.8] inset-x-0 -z-10 shadow -bottom-3 flex items-start bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                </div>
             </div>
-            <div class="mt-8 space-y-4">
-                <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
-                    <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
-                    {{ __('pages/account.activities.empty_articles') }}
-                </p>
-
-                @if ($user->isLoggedInUser())
-                    @can('create', \App\Models\Article::class)
-                        <x-buttons.primary
-                            type="button"
-                            onclick="Livewire.dispatch('openPanel', { component: 'components.slideovers.article-form' })"
-                        >
-                            <x-untitledui-file-06 class="size-5" aria-hidden="true" />
-                            {{ __('global.launch_modal.article_action') }}
-                        </x-buttons.primary>
-                    @endcan
-                @endif
+            <div class="absolute scale-[0.8] inset-x-0 -z-10 shadow -bottom-3 flex items-start bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
             </div>
-        </x-empty-state>
-    @endif
-</div>
+        </div>
+        <div class="mt-8 space-y-4">
+            <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
+                <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
+                {{ __('pages/account.activities.empty_articles') }}
+            </p>
+
+            @if ($user->isLoggedInUser())
+                @can('create', \App\Models\Article::class)
+                    <x-buttons.primary
+                        type="button"
+                        onclick="Livewire.dispatch('openPanel', { component: 'components.slideovers.article-form' })"
+                    >
+                        <x-untitledui-file-06 class="size-5" aria-hidden="true" />
+                        {{ __('global.launch_modal.article_action') }}
+                    </x-buttons.primary>
+                @endcan
+            @endif
+        </div>
+    </x-empty-state>
+@endif
diff --git a/resources/views/components/user/discussions.blade.php b/resources/views/components/user/discussions.blade.php
index 4be58ccb..713e4f27 100644
--- a/resources/views/components/user/discussions.blade.php
+++ b/resources/views/components/user/discussions.blade.php
@@ -3,58 +3,56 @@
     'discussions',
 ])
 
-<div>
-    @if ($discussions->isNotEmpty())
-        <div class="relative -mt-6 divide-y divide-gray-200 dark:divide-white/20">
-            @foreach ($discussions as $discussion)
-                <x-discussions.overview :discussion="$discussion" />
-            @endforeach
-        </div>
-    @else
-        <x-empty-state>
+@if ($discussions->isNotEmpty())
+    <div class="relative -mt-6 divide-y divide-gray-200 dark:divide-white/20">
+        @foreach ($discussions as $discussion)
+            <x-discussions.overview :discussion="$discussion" />
+        @endforeach
+    </div>
+@else
+    <x-empty-state>
+        <div class="relative">
             <div class="relative">
-                <div class="relative">
-                    <div class="absolute -inset-1 bg-gradient-to-r rotate-3 w-72 from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
-                    <div class="relative z-20 bg-white gap-3 rotate-3 p-3 w-72 rounded-lg shadow ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                        <div class="space-y-2">
-                            <div class="grid grid-cols-3 gap-2 w-1/3">
-                                <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
-                                <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
-                            </div>
-                            <div class="h-4 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                            <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                        </div>
-                    </div>
-                </div>
-                <div class="absolute transform scale-[0.8] bottom-10 right-2 shadow w-40 -rotate-6 z-20 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+                <div class="absolute -inset-1 bg-gradient-to-r rotate-3 w-72 from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
+                <div class="relative z-20 bg-white gap-3 rotate-3 p-3 w-72 rounded-lg shadow ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
                     <div class="space-y-2">
-                        <div class="grid grid-cols-3 gap-2 w-2/3">
+                        <div class="grid grid-cols-3 gap-2 w-1/3">
                             <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
                             <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
                         </div>
-                        <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                        <div class="h-4 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
                         <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
                     </div>
                 </div>
             </div>
-            <div class="mt-8 space-y-4">
-                <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
-                    <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
-                    {{ __('pages/account.activities.empty_discussions') }}
-                </p>
-
-                @if ($user->isLoggedInUser())
-                    @can('create', \App\Models\Discussion::class)
-                        <x-buttons.primary
-                            type="button"
-                            onclick="Livewire.dispatch('openPanel', { component: 'components.slideovers.thread-form' })"
-                        >
-                            <x-untitledui-message-text-square class="size-5" aria-hidden="true" />
-                            {{ __('global.launch_modal.discussion_action') }}
-                        </x-buttons.primary>
-                    @endcan
-                @endif
+            <div class="absolute transform scale-[0.8] bottom-10 right-2 shadow w-40 -rotate-6 z-20 bg-white gap-3 p-3 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+                <div class="space-y-2">
+                    <div class="grid grid-cols-3 gap-2 w-2/3">
+                        <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
+                        <div class="h-3 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded dark:bg-gray-800"></div>
+                    </div>
+                    <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                    <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                </div>
             </div>
-        </x-empty-state>
-    @endif
-</div>
+        </div>
+        <div class="mt-8 space-y-4">
+            <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
+                <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
+                {{ __('pages/account.activities.empty_discussions') }}
+            </p>
+
+            @if ($user->isLoggedInUser())
+                @can('create', \App\Models\Discussion::class)
+                    <x-buttons.primary
+                        type="button"
+                        onclick="Livewire.dispatch('openPanel', { component: 'components.slideovers.thread-form' })"
+                    >
+                        <x-untitledui-message-text-square class="size-5" aria-hidden="true" />
+                        {{ __('global.launch_modal.discussion_action') }}
+                    </x-buttons.primary>
+                @endcan
+            @endif
+        </div>
+    </x-empty-state>
+@endif
diff --git a/resources/views/components/user/threads.blade.php b/resources/views/components/user/threads.blade.php
index f7516e0d..52e6137a 100644
--- a/resources/views/components/user/threads.blade.php
+++ b/resources/views/components/user/threads.blade.php
@@ -3,51 +3,49 @@
     'threads',
 ])
 
-<div>
-    @if ($threads->isNotEmpty())
-        <div class="space-y-6 sm:space-y-5">
-            @foreach ($threads as $thread)
-                <x-forum.thread :thread="$thread" />
-            @endforeach
-        </div>
-    @else
-        <x-empty-state>
+@if ($threads->isNotEmpty())
+    <div class="space-y-4">
+        @foreach ($threads as $thread)
+            <x-forum.thread :thread="$thread" wire:key="{{ $thread->slug }}" />
+        @endforeach
+    </div>
+@else
+    <x-empty-state>
+        <div class="relative">
             <div class="relative">
-                <div class="relative">
-                    <div class="absolute -inset-1 w-72 mx-auto bg-gradient-to-r from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
-                    <div class="relative flex items-start w-72 mx-auto shadow z-20 bg-white gap-3 p-4 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                        <div class="size-10 bg-gray-50 border border-gray-200 ring-2 ring-offset-2 ring-offset-gray-50 ring-gray-200/50 dark:ring-offset-gray-900 dark:border-white/20 dark:ring-white/20 rounded-full dark:bg-gray-800"></div>
-                        <div class="w-3/4 space-y-2">
-                            <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                            <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                        </div>
+                <div class="absolute -inset-1 w-72 mx-auto bg-gradient-to-r from-flag-green to-flag-red rounded-lg blur opacity-25"></div>
+                <div class="relative flex items-start w-72 mx-auto shadow z-20 bg-white gap-3 p-4 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+                    <div class="size-10 bg-gray-50 border border-gray-200 ring-2 ring-offset-2 ring-offset-gray-50 ring-gray-200/50 dark:ring-offset-gray-900 dark:border-white/20 dark:ring-white/20 rounded-full dark:bg-gray-800"></div>
+                    <div class="w-3/4 space-y-2">
+                        <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                        <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
                     </div>
                 </div>
-                <div class="absolute z-1 inset-x-0 -top-10 transform scale-90">
-                    <div class="relative flex items-start w-72 mx-auto shadow z-20 bg-white gap-3 p-4 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
-                        <div class="size-10 bg-gray-50 border border-gray-200 ring-2 ring-offset-2 ring-offset-gray-50 ring-gray-200/50 dark:ring-offset-gray-900 dark:border-white/20 dark:ring-white/20 rounded-full dark:bg-gray-800"></div>
-                        <div class="w-3/4 space-y-2">
-                            <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                            <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
-                        </div>
+            </div>
+            <div class="absolute z-1 inset-x-0 -top-10 transform scale-90">
+                <div class="relative flex items-start w-72 mx-auto shadow z-20 bg-white gap-3 p-4 rounded-lg ring-1 ring-gray-200/50 dark:bg-gray-900 dark:ring-white/10">
+                    <div class="size-10 bg-gray-50 border border-gray-200 ring-2 ring-offset-2 ring-offset-gray-50 ring-gray-200/50 dark:ring-offset-gray-900 dark:border-white/20 dark:ring-white/20 rounded-full dark:bg-gray-800"></div>
+                    <div class="w-3/4 space-y-2">
+                        <div class="h-2 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
+                        <div class="h-6 bg-gray-50 ring-1 ring-gray-200/50 dark:ring-white/20 rounded-md dark:bg-gray-800"></div>
                     </div>
                 </div>
             </div>
-            <div class="mt-8 space-y-4">
-                <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
-                    <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
-                    {{ __('pages/account.activities.empty_threads') }}
-                </p>
+        </div>
+        <div class="mt-8 space-y-4">
+            <p class="mt-8 text-base text-gray-500 dark:text-gray-400">
+                <span class="font-medium text-gray-700 dark:text-gray-300">{{ $user->name }}</span>
+                {{ __('pages/account.activities.empty_threads') }}
+            </p>
 
-                @if ($user->isLoggedInUser())
-                    @can('create', \App\Models\Thread::class)
-                        <x-buttons.primary type="button">
-                            <x-untitledui-message-text-square class="size-5" aria-hidden="true" />
-                            {{ __('global.launch_modal.discussion_action') }}
-                        </x-buttons.primary>
-                    @endcan
-                @endif
-            </div>
-        </x-empty-state>
-    @endif
-</div>
+            @if ($user->isLoggedInUser())
+                @can('create', \App\Models\Thread::class)
+                    <x-buttons.primary type="button">
+                        <x-untitledui-message-text-square class="size-5" aria-hidden="true" />
+                        {{ __('global.launch_modal.discussion_action') }}
+                    </x-buttons.primary>
+                @endcan
+            @endif
+        </div>
+    </x-empty-state>
+@endif
diff --git a/resources/views/layouts/forum.blade.php b/resources/views/layouts/forum.blade.php
index d2d07c38..2eee8d7c 100644
--- a/resources/views/layouts/forum.blade.php
+++ b/resources/views/layouts/forum.blade.php
@@ -41,7 +41,7 @@
                         <x-nav.forum-link
                             :href="route('forum.index', ['popular' => true])"
                             :active="request()->getUri() === route('forum.index', ['popular' => true])"
-                            icon="untitledui-heart"
+                            icon="phosphor-heart-duotone"
                         >
                             {{ __('pages/forum.navigation.popular') }}
                         </x-nav.forum-link>
@@ -62,7 +62,7 @@
                         <x-nav.forum-link
                             :href="route('forum.index', ['no-replies' => true])"
                             :active="request()->getUri() === route('forum.index', ['no-replies' => true])"
-                            icon="untitledui-message-x-square"
+                            icon="phosphor-chat-teardrop-slash-duotone"
                         >
                             {{ __('pages/forum.navigation.no_reply') }}
                         </x-nav.forum-link>
diff --git a/resources/views/livewire/components/reactions.blade.php b/resources/views/livewire/components/reactions.blade.php
index 44e8c142..65d69bdc 100644
--- a/resources/views/livewire/components/reactions.blade.php
+++ b/resources/views/livewire/components/reactions.blade.php
@@ -26,7 +26,7 @@ class="size-5 text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:gr
             @click="showReactions = ! showReactions"
             @class([
                 'group relative flex items-center h-8 cursor-pointer items-center justify-between',
-                'rounded-lg bg-white px-3 py-2 shadow hover:bg-gray-50' => $withBackground,
+                'rounded-lg bg-white px-3 py-2 shadow hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700' => $withBackground,
             ])
         >
             @if($direction === 'vertical')
diff --git a/resources/views/livewire/components/user/activities.blade.php b/resources/views/livewire/components/user/activities.blade.php
index bb9b3801..427fd0be 100644
--- a/resources/views/livewire/components/user/activities.blade.php
+++ b/resources/views/livewire/components/user/activities.blade.php
@@ -3,7 +3,7 @@
         {{ __('pages/account.activities.latest_of', ['name' => $user->name]) }}
     </h3>
     <ul role="list" class="mt-6 -mb-8">
-        @forelse($activities as $activity)
+        @forelse($this->activities as $activity)
             @if($activity->subject)
                 <li>
                     <x-dynamic-component
diff --git a/resources/views/livewire/pages/account/profile.blade.php b/resources/views/livewire/pages/account/profile.blade.php
index ec9cb5fa..64685d9f 100644
--- a/resources/views/livewire/pages/account/profile.blade.php
+++ b/resources/views/livewire/pages/account/profile.blade.php
@@ -137,16 +137,16 @@ class="relative z-20 inline-flex h-8 w-full cursor-pointer items-center justify-
     </div>
 
     <x-container class="py-10 lg:pb-14">
-        <div class="grid gap-8 lg:grid-cols-3 lg:gap-x-16">
+        <div class="lg:grid lg:grid-cols-3 lg:gap-x-16">
             <div class="lg:col-span-2">
                 <div :id="$id(tabId + '-content')" x-show="tabContentActive($el)">
-                    <x-user.articles :user="$user" :articles="$articles" />
+                    <x-user.articles :user="$user" :articles="$this->articles" />
                 </div>
                 <div x-cloak :id="$id(tabId + '-content')" x-show="tabContentActive($el)">
-                    <x-user.discussions :user="$user" :discussions="$discussions" />
+                    <x-user.discussions :user="$user" :discussions="$this->discussions" />
                 </div>
                 <div x-cloak :id="$id(tabId + '-content')" x-show="tabContentActive($el)">
-                    <x-user.threads :user="$user" :threads="$threads" />
+                    <x-user.threads :user="$user" :threads="$this->threads" />
                 </div>
             </div>
             <aside class="hidden lg:block">
diff --git a/resources/views/livewire/pages/articles/single-post.blade.php b/resources/views/livewire/pages/articles/single-post.blade.php
index 33d02816..53e53e94 100644
--- a/resources/views/livewire/pages/articles/single-post.blade.php
+++ b/resources/views/livewire/pages/articles/single-post.blade.php
@@ -83,7 +83,8 @@ class="size-6 ring-1 ring-offset-2 ring-gray-200 ring-offset-gray-50 dark:ring-w
                                 <span class="absolute inset-0"></span>
                                 {{ $article->user->name }}
                             </x-link>
-                            @if($article->isPublished())
+
+                            @if ($article->isPublished())
                                 <div class="w-[1px] h-4 border-l border-gray-200 dark:border-white/20"></div>
                                 <time class="capitalize text-gray-500 dark:text-gray-400" datetime="{{ $article->published_at->format('Y-m-d') }}">
                                     {{ $article->published_at->isoFormat('LL') }}
@@ -114,21 +115,18 @@ class="object-cover size-full"
                         class="prose prose-green text-gray-500 dark:text-gray-400 dark:prose-invert lg:max-w-none"
                         :content="$article->body"
                     />
-
-                    <div class="relative inline-flex mt-5 space-x-4">
-                        <livewire:components.reactions
-                            wire:key="{{ $article->id }}"
-                            :model="$article"
-                            :with-place-holder="false"
-                            :with-background="false"
-                        />
-                    </div>
                 </div>
                 <div class="hidden lg:block">
                     <x-sticky-content class="space-y-10">
-                        @if($article->isPublished())
+                        @if ($article->isPublished())
                             <div>
-                                <p class="text-sm font-medium text-gray-500 dark:text-gray-400">
+                                <div class="relative inline-flex mt-5 space-x-4">
+                                    <livewire:components.reactions
+                                        wire:key="{{ $article->id }}"
+                                        :model="$article"
+                                    />
+                                </div>
+                                <p class="mt-10 text-sm font-medium text-gray-500 dark:text-gray-400">
                                     {{ __('pages/article.share_article') }}
                                 </p>
                                 <div class="mt-4 flex flex-wrap gap-2">
diff --git a/resources/views/livewire/pages/forum/index.blade.php b/resources/views/livewire/pages/forum/index.blade.php
index cfb902fe..01111f58 100644
--- a/resources/views/livewire/pages/forum/index.blade.php
+++ b/resources/views/livewire/pages/forum/index.blade.php
@@ -46,7 +46,7 @@ class="!pl-8"
     <div class="mt-6 lg:mb-12">
         <div class="space-y-4">
             @foreach ($threads as $thread)
-                <x-forum.thread :thread="$thread" />
+                <x-forum.thread :thread="$thread" wire:key="{{ $thread->slug }}" />
             @endforeach
         </div>
 

From 24f11cf4d0b1c5c684b9c0a470809cc3db560795 Mon Sep 17 00:00:00 2001
From: Arthur Monney <monneylobe@gmail.com>
Date: Tue, 8 Apr 2025 12:47:28 +0200
Subject: [PATCH 2/2] MEP > merge develop into main (#355)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Endaman Stevy <stevyabessolo@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Stevy Endaman <endamanstevy@gmail.com>
---
 .github/workflows/dependabot-auto-merge.yml   |    2 +-
 app/Actions/Article/CreateArticleAction.php   |   22 +-
 app/Actions/Article/DeclineArticleAction.php  |   31 +
 app/Actions/Article/UpdateArticleAction.php   |    4 +
 app/Data/ArticleData.php                      |    2 +
 app/Filament/Resources/ArticleResource.php    |   26 +-
 .../ArticleResource/Pages/ListArticles.php    |   10 +
 app/Models/Article.php                        |    2 +
 .../ArticleDeclinedNotification.php           |   41 +
 app/Providers/EventServiceProvider.php        |    7 -
 composer.json                                 |    1 +
 composer.lock                                 | 1248 +++++++++++++++--
 config/app-modules.php                        |  105 ++
 ...613_add_reason_field_to_articles_table.php |   17 +
 lang/en/emails/article.php                    |   20 +
 lang/fr/emails/article.php                    |   20 +
 .../views/emails/article_declined.blade.php   |   28 +
 stubs/app-modules/.gitkeep                    |    0
 stubs/app-modules/app/Plugin.php              |   26 +
 stubs/app-modules/app/ServiceProvider.php     |   23 +
 stubs/app-modules/composer-stub.json          |   26 +
 .../Feature/Filament/ArticleResourceTest.php  |   18 +-
 22 files changed, 1569 insertions(+), 110 deletions(-)
 create mode 100644 app/Actions/Article/DeclineArticleAction.php
 create mode 100644 app/Notifications/ArticleDeclinedNotification.php
 create mode 100644 config/app-modules.php
 create mode 100644 database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php
 create mode 100644 lang/en/emails/article.php
 create mode 100644 lang/fr/emails/article.php
 create mode 100644 resources/views/emails/article_declined.blade.php
 create mode 100644 stubs/app-modules/.gitkeep
 create mode 100644 stubs/app-modules/app/Plugin.php
 create mode 100644 stubs/app-modules/app/ServiceProvider.php
 create mode 100644 stubs/app-modules/composer-stub.json

diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
index 8627586e..88fc5943 100644
--- a/.github/workflows/dependabot-auto-merge.yml
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -13,7 +13,7 @@ jobs:
 
       - name: Dependabot metadata
         id: metadata
-        uses: dependabot/fetch-metadata@v2.2.0
+        uses: dependabot/fetch-metadata@v2.3.0
         with:
           github-token: "${{ secrets.GITHUB_TOKEN }}"
       - name: Auto-merge Dependabot PRs for semver-minor updates
diff --git a/app/Actions/Article/CreateArticleAction.php b/app/Actions/Article/CreateArticleAction.php
index e2457168..3c442ef5 100644
--- a/app/Actions/Article/CreateArticleAction.php
+++ b/app/Actions/Article/CreateArticleAction.php
@@ -27,6 +27,25 @@ public function execute(ArticleData $articleData): Article
             );
         }
 
+        $user = Auth::user();
+
+        if ($user->isAdmin() || $user->isModerator()) {
+            $articleData->published_at = new Carbon(
+                time: today(),
+                timezone: config('app.timezone')
+            );
+
+            $articleData->submitted_at = new Carbon(
+                time: $articleData->submitted_at,
+                timezone: config('app.timezone')
+            );
+
+            $articleData->approved_at = new Carbon(
+                time: today(),
+                timezone: config('app.timezone')
+            );
+        }
+
         // @phpstan-ignore-next-line
         return Article::query()->create([
             'title' => $articleData->title,
@@ -34,8 +53,9 @@ public function execute(ArticleData $articleData): Article
             'body' => $articleData->body,
             'published_at' => $articleData->published_at,
             'submitted_at' => $articleData->submitted_at,
+            'approved_at' => $articleData->approved_at,
             'canonical_url' => $articleData->canonical_url,
-            'user_id' => Auth::id(),
+            'user_id' => $user->id,
         ]);
     }
 }
diff --git a/app/Actions/Article/DeclineArticleAction.php b/app/Actions/Article/DeclineArticleAction.php
new file mode 100644
index 00000000..12d75316
--- /dev/null
+++ b/app/Actions/Article/DeclineArticleAction.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Actions\Article;
+
+use App\Models\Article;
+use App\Notifications\ArticleDeclinedNotification;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+
+final class DeclineArticleAction
+{
+    public function execute(string $reason, Article $article): Article
+    {
+        return DB::transaction(function () use ($reason, $article) {
+
+            $article->update([
+                'declined_at' => Carbon::now(),
+                'reason' => $reason,
+                'submitted_at' => null,
+            ]);
+
+            $article->user->notify(new ArticleDeclinedNotification($article));
+
+            $article->refresh();
+
+            return $article;
+        });
+    }
+}
diff --git a/app/Actions/Article/UpdateArticleAction.php b/app/Actions/Article/UpdateArticleAction.php
index ddb508de..e645bf42 100644
--- a/app/Actions/Article/UpdateArticleAction.php
+++ b/app/Actions/Article/UpdateArticleAction.php
@@ -31,6 +31,10 @@ public function execute(ArticleData $articleData, Article $article): Article
             );
         }
 
+        if ($articleData->declined_at) {
+            $articleData->declined_at = null;
+        }
+
         $article->update($articleData->toArray());
 
         $article->refresh();
diff --git a/app/Data/ArticleData.php b/app/Data/ArticleData.php
index fca6e491..9620e2ba 100644
--- a/app/Data/ArticleData.php
+++ b/app/Data/ArticleData.php
@@ -17,5 +17,7 @@ public function __construct(
         public ?string $canonical_url = null,
         public ?Carbon $published_at = null,
         public ?Carbon $submitted_at = null,
+        public ?Carbon $declined_at = null,
+        public ?Carbon $approved_at = null,
     ) {}
 }
diff --git a/app/Filament/Resources/ArticleResource.php b/app/Filament/Resources/ArticleResource.php
index a462240a..83e9e2a0 100644
--- a/app/Filament/Resources/ArticleResource.php
+++ b/app/Filament/Resources/ArticleResource.php
@@ -5,10 +5,13 @@
 namespace App\Filament\Resources;
 
 use App\Actions\Article\ApprovedArticleAction;
+use App\Actions\Article\DeclineArticleAction;
 use App\Filament\Resources\ArticleResource\Pages;
 use App\Models\Article;
 use Awcodes\FilamentBadgeableColumn\Components\Badge;
 use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn;
+use Filament\Forms\Components\Textarea;
+use Filament\Notifications\Notification;
 use Filament\Resources\Resource;
 use Filament\Support\Enums\MaxWidth;
 use Filament\Tables;
@@ -99,7 +102,6 @@ public static function table(Table $table): Table
 
                         return '';
                     })
-                    ->searchable()
                     ->sortable(),
             ])
             ->actions([
@@ -123,15 +125,27 @@ public static function table(Table $table): Table
                         ->label('Décliner')
                         ->icon('heroicon-s-x-mark')
                         ->color('warning')
-                        ->modalHeading(__('Voulez vous décliner cet article'))
-                        ->successNotificationTitle(__('Opération effectuée avec succès'))
+                        ->form([
+                            Textarea::make('reason')
+                                ->label(__('Raison du refus'))
+                                ->maxLength(255)
+                                ->required(),
+                        ])
+                        ->modalHeading('Décliner l\'article')
+                        ->modalDescription('Veuillez fournir une raison détaillée pour le refus de cet article. L\'auteur recevra cette explication.')
+                        ->successNotificationTitle('Article décliné avec succès')
                         ->requiresConfirmation()
                         ->modalIcon('heroicon-s-x-mark')
-                        ->action(function ($record): void {
+                        ->action(function (array $data, Article $record): void {
                             Gate::authorize('decline', $record);
 
-                            $record->declined_at = now();
-                            $record->save();
+                            app(DeclineArticleAction::class)->execute($data['reason'], $record);
+
+                            Notification::make()
+                                ->title('Article décliné')
+                                ->body('L\'auteur a été notifié de la raison du refus.')
+                                ->success()
+                                ->send();
                         }),
                     Tables\Actions\Action::make('show')
                         ->icon('untitledui-eye')
diff --git a/app/Filament/Resources/ArticleResource/Pages/ListArticles.php b/app/Filament/Resources/ArticleResource/Pages/ListArticles.php
index a793cae9..f8be7594 100644
--- a/app/Filament/Resources/ArticleResource/Pages/ListArticles.php
+++ b/app/Filament/Resources/ArticleResource/Pages/ListArticles.php
@@ -7,6 +7,7 @@
 use App\Filament\Resources\ArticleResource;
 use App\Models\Article;
 use Closure;
+use Filament\Resources\Components\Tab;
 use Filament\Resources\Pages\ListRecords;
 
 final class ListArticles extends ListRecords
@@ -17,4 +18,13 @@ public function isTableRecordSelectable(): Closure
     {
         return fn (Article $record): bool => $record->isNotPublished();
     }
+
+    public function getTabs(): array
+    {
+        return [
+            'En attente' => Tab::make()->query(fn ($query) => $query->awaitingApproval()),
+            'Apprové' => Tab::make()->query(fn ($query) => $query->published()),
+            'Décliné' => Tab::make()->query(fn ($query) => $query->declined()),
+        ];
+    }
 }
diff --git a/app/Models/Article.php b/app/Models/Article.php
index 32c697b6..8616bc63 100644
--- a/app/Models/Article.php
+++ b/app/Models/Article.php
@@ -32,6 +32,7 @@
  * @property bool $is_pinned
  * @property int $is_sponsored
  * @property string | null $canonical_url
+ * @property string | null $reason
  * @property int | null $tweet_id
  * @property int $user_id
  * @property string | null $locale
@@ -63,6 +64,7 @@ final class Article extends Model implements HasMedia, ReactableInterface, Sitem
         'body',
         'slug',
         'canonical_url',
+        'reason',
         'show_toc',
         'is_pinned',
         'user_id',
diff --git a/app/Notifications/ArticleDeclinedNotification.php b/app/Notifications/ArticleDeclinedNotification.php
new file mode 100644
index 00000000..d6148720
--- /dev/null
+++ b/app/Notifications/ArticleDeclinedNotification.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Notifications;
+
+use App\Models\Article;
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Notification;
+
+final class ArticleDeclinedNotification extends Notification
+{
+    use Queueable;
+
+    public function __construct(public Article $article) {}
+
+    public function via(mixed $notifiable): array
+    {
+        return ['mail', 'database'];
+    }
+
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        return (new MailMessage)
+            ->subject(__('emails/article.article_declined.subject'))
+            ->markdown('emails.article_declined', ['article' => $this->article]);
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function toArray(): array
+    {
+        return [
+            'article' => $this->article,
+            'owner' => $this->article->user->name,
+            'email' => $this->article->user->email,
+        ];
+    }
+}
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index e7e51e32..1430832a 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -9,18 +9,15 @@
 use App\Events\CommentWasAdded;
 use App\Events\ReplyWasCreated;
 use App\Events\SponsoringPaymentInitialize;
-use App\Events\ThreadWasCreated;
 use App\Events\UserBannedEvent;
 use App\Events\UserUnbannedEvent;
 // use App\Listeners\SendCompanyEmailVerificationNotification;
 use App\Listeners\NotifyMentionedUsers;
-use App\Listeners\PostNewThreadNotification;
 use App\Listeners\SendBanNotificationListener;
 use App\Listeners\SendNewArticleNotification;
 use App\Listeners\SendNewCommentNotification;
 // use App\Listeners\SendWelcomeCompanyNotification;
 use App\Listeners\SendNewReplyNotification;
-use App\Listeners\SendNewThreadNotification;
 use App\Listeners\SendPaymentNotification;
 use App\Listeners\SendUnbanNotificationListener;
 use App\Listeners\SendWelcomeMailNotification;
@@ -42,10 +39,6 @@ final class EventServiceProvider extends ServiceProvider
             SendNewReplyNotification::class,
             NotifyMentionedUsers::class,
         ],
-        ThreadWasCreated::class => [
-            SendNewThreadNotification::class,
-            PostNewThreadNotification::class,
-        ],
         ArticleWasSubmittedForApproval::class => [
             SendNewArticleNotification::class,
         ],
diff --git a/composer.json b/composer.json
index 630d4607..6f7932fa 100644
--- a/composer.json
+++ b/composer.json
@@ -23,6 +23,7 @@
     "gehrisandro/tailwind-merge-laravel": "^1.2",
     "graham-campbell/markdown": "^15.2",
     "guzzlehttp/guzzle": "^7.7.0",
+    "internachi/modular": "^2.3",
     "jenssegers/agent": "^2.6.4",
     "laravel-notification-channels/telegram": "^5.0",
     "laravel-notification-channels/twitter": "^8.1",
diff --git a/composer.lock b/composer.lock
index 1f31d4df..2ddd4e9d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "4c716d8363d3d8066535ca12060d6e28",
+    "content-hash": "9e2c8137faec35e0ddbd84e450feab24",
     "packages": [
         {
             "name": "abraham/twitteroauth",
@@ -1578,6 +1578,341 @@
             ],
             "time": "2025-01-08T16:17:16+00:00"
         },
+        {
+            "name": "composer/class-map-generator",
+            "version": "1.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/class-map-generator.git",
+                "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/class-map-generator/zipball/134b705ddb0025d397d8318a75825fe3c9d1da34",
+                "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34",
+                "shasum": ""
+            },
+            "require": {
+                "composer/pcre": "^2.1 || ^3.1",
+                "php": "^7.2 || ^8.0",
+                "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.12 || ^2",
+                "phpstan/phpstan-deprecation-rules": "^1 || ^2",
+                "phpstan/phpstan-phpunit": "^1 || ^2",
+                "phpstan/phpstan-strict-rules": "^1.1 || ^2",
+                "phpunit/phpunit": "^8",
+                "symfony/filesystem": "^5.4 || ^6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\ClassMapGenerator\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "https://seld.be"
+                }
+            ],
+            "description": "Utilities to scan PHP code and generate class maps.",
+            "keywords": [
+                "classmap"
+            ],
+            "support": {
+                "issues": "https://github.com/composer/class-map-generator/issues",
+                "source": "https://github.com/composer/class-map-generator/tree/1.6.1"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-03-24T13:50:44+00:00"
+        },
+        {
+            "name": "composer/composer",
+            "version": "2.8.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/composer.git",
+                "reference": "937c775a644bd7d2c3dfbb352747488463a6e673"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/composer/zipball/937c775a644bd7d2c3dfbb352747488463a6e673",
+                "reference": "937c775a644bd7d2c3dfbb352747488463a6e673",
+                "shasum": ""
+            },
+            "require": {
+                "composer/ca-bundle": "^1.5",
+                "composer/class-map-generator": "^1.4.0",
+                "composer/metadata-minifier": "^1.0",
+                "composer/pcre": "^2.2 || ^3.2",
+                "composer/semver": "^3.3",
+                "composer/spdx-licenses": "^1.5.7",
+                "composer/xdebug-handler": "^2.0.2 || ^3.0.3",
+                "justinrainbow/json-schema": "^5.3",
+                "php": "^7.2.5 || ^8.0",
+                "psr/log": "^1.0 || ^2.0 || ^3.0",
+                "react/promise": "^2.11 || ^3.2",
+                "seld/jsonlint": "^1.4",
+                "seld/phar-utils": "^1.2",
+                "seld/signal-handler": "^2.0",
+                "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3",
+                "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3",
+                "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3",
+                "symfony/polyfill-php73": "^1.24",
+                "symfony/polyfill-php80": "^1.24",
+                "symfony/polyfill-php81": "^1.24",
+                "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.11.8",
+                "phpstan/phpstan-deprecation-rules": "^1.2.0",
+                "phpstan/phpstan-phpunit": "^1.4.0",
+                "phpstan/phpstan-strict-rules": "^1.6.0",
+                "phpstan/phpstan-symfony": "^1.4.0",
+                "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1"
+            },
+            "suggest": {
+                "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+                "ext-zip": "Enabling the zip extension allows you to unzip archives",
+                "ext-zlib": "Allow gzip compression of HTTP requests"
+            },
+            "bin": [
+                "bin/composer"
+            ],
+            "type": "library",
+            "extra": {
+                "phpstan": {
+                    "includes": [
+                        "phpstan/rules.neon"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-main": "2.8-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\": "src/Composer/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "naderman@naderman.de",
+                    "homepage": "https://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "https://seld.be"
+                }
+            ],
+            "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
+            "homepage": "https://getcomposer.org/",
+            "keywords": [
+                "autoload",
+                "dependency",
+                "package"
+            ],
+            "support": {
+                "irc": "ircs://irc.libera.chat:6697/composer",
+                "issues": "https://github.com/composer/composer/issues",
+                "security": "https://github.com/composer/composer/security/policy",
+                "source": "https://github.com/composer/composer/tree/2.8.6"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-02-25T12:03:50+00:00"
+        },
+        {
+            "name": "composer/metadata-minifier",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/metadata-minifier.git",
+                "reference": "c549d23829536f0d0e984aaabbf02af91f443207"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207",
+                "reference": "c549d23829536f0d0e984aaabbf02af91f443207",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^2",
+                "phpstan/phpstan": "^0.12.55",
+                "symfony/phpunit-bridge": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\MetadataMinifier\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "Small utility library that handles metadata minification and expansion.",
+            "keywords": [
+                "composer",
+                "compression"
+            ],
+            "support": {
+                "issues": "https://github.com/composer/metadata-minifier/issues",
+                "source": "https://github.com/composer/metadata-minifier/tree/1.0.0"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-04-07T13:37:33+00:00"
+        },
+        {
+            "name": "composer/pcre",
+            "version": "3.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/pcre.git",
+                "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+                "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "conflict": {
+                "phpstan/phpstan": "<1.11.10"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.12 || ^2",
+                "phpstan/phpstan-strict-rules": "^1 || ^2",
+                "phpunit/phpunit": "^8 || ^9"
+            },
+            "type": "library",
+            "extra": {
+                "phpstan": {
+                    "includes": [
+                        "extension.neon"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Pcre\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+            "keywords": [
+                "PCRE",
+                "preg",
+                "regex",
+                "regular expression"
+            ],
+            "support": {
+                "issues": "https://github.com/composer/pcre/issues",
+                "source": "https://github.com/composer/pcre/tree/3.3.2"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-11-12T16:29:46+00:00"
+        },
         {
             "name": "composer/semver",
             "version": "3.4.3",
@@ -1659,6 +1994,152 @@
             ],
             "time": "2024-09-19T14:15:21+00:00"
         },
+        {
+            "name": "composer/spdx-licenses",
+            "version": "1.5.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/spdx-licenses.git",
+                "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
+                "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.55",
+                "symfony/phpunit-bridge": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Spdx\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "naderman@naderman.de",
+                    "homepage": "http://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                },
+                {
+                    "name": "Rob Bast",
+                    "email": "rob.bast@gmail.com",
+                    "homepage": "http://robbast.nl"
+                }
+            ],
+            "description": "SPDX licenses list and validation library.",
+            "keywords": [
+                "license",
+                "spdx",
+                "validator"
+            ],
+            "support": {
+                "irc": "ircs://irc.libera.chat:6697/composer",
+                "issues": "https://github.com/composer/spdx-licenses/issues",
+                "source": "https://github.com/composer/spdx-licenses/tree/1.5.8"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-11-20T07:44:33+00:00"
+        },
+        {
+            "name": "composer/xdebug-handler",
+            "version": "3.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/xdebug-handler.git",
+                "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+                "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+                "shasum": ""
+            },
+            "require": {
+                "composer/pcre": "^1 || ^2 || ^3",
+                "php": "^7.2.5 || ^8.0",
+                "psr/log": "^1 || ^2 || ^3"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.0",
+                "phpstan/phpstan-strict-rules": "^1.1",
+                "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Composer\\XdebugHandler\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "John Stevenson",
+                    "email": "john-stevenson@blueyonder.co.uk"
+                }
+            ],
+            "description": "Restarts a process without Xdebug.",
+            "keywords": [
+                "Xdebug",
+                "performance"
+            ],
+            "support": {
+                "irc": "ircs://irc.libera.chat:6697/composer",
+                "issues": "https://github.com/composer/xdebug-handler/issues",
+                "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-05-06T16:37:16+00:00"
+        },
         {
             "name": "cyrildewit/eloquent-viewable",
             "version": "v7.0.3",
@@ -4030,6 +4511,76 @@
             ],
             "time": "2023-12-03T19:50:20+00:00"
         },
+        {
+            "name": "internachi/modular",
+            "version": "2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/InterNACHI/modular.git",
+                "reference": "e7ff4074001d3df50d9ce877385c55d88e254484"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/InterNACHI/modular/zipball/e7ff4074001d3df50d9ce877385c55d88e254484",
+                "reference": "e7ff4074001d3df50d9ce877385c55d88e254484",
+                "shasum": ""
+            },
+            "require": {
+                "composer/composer": "^2.1",
+                "ext-dom": "*",
+                "ext-simplexml": "*",
+                "illuminate/support": "^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
+                "php": ">=8.0"
+            },
+            "require-dev": {
+                "ext-json": "*",
+                "friendsofphp/php-cs-fixer": "^3.14",
+                "livewire/livewire": "^2.5|^3.0",
+                "mockery/mockery": "^1.5",
+                "orchestra/testbench": "^7.52|^8.33|^9.11|^10.0|dev-master|dev-main",
+                "phpunit/phpunit": "^9.5|^10.5|^11.5"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "aliases": {
+                        "Modules": "InterNACHI\\Modular\\Support\\Facades\\Modules"
+                    },
+                    "providers": [
+                        "InterNACHI\\Modular\\Support\\ModularServiceProvider",
+                        "InterNACHI\\Modular\\Support\\ModularizedCommandsServiceProvider",
+                        "InterNACHI\\Modular\\Support\\ModularEventServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "InterNACHI\\Modular\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Chris Morrell",
+                    "homepage": "http://www.cmorrell.com"
+                }
+            ],
+            "description": "Modularize your Laravel apps",
+            "keywords": [
+                "laravel",
+                "modular",
+                "module",
+                "modules"
+            ],
+            "support": {
+                "issues": "https://github.com/InterNACHI/modular/issues",
+                "source": "https://github.com/InterNACHI/modular/tree/2.3.0"
+            },
+            "time": "2025-02-28T22:15:39+00:00"
+        },
         {
             "name": "jaybizzle/crawler-detect",
             "version": "v1.3.0",
@@ -4183,7 +4734,76 @@
             },
             "autoload": {
                 "psr-4": {
-                    "Jenssegers\\Agent\\": "src/"
+                    "Jenssegers\\Agent\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jens Segers",
+                    "homepage": "https://jenssegers.com"
+                }
+            ],
+            "description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
+            "homepage": "https://github.com/jenssegers/agent",
+            "keywords": [
+                "Agent",
+                "browser",
+                "desktop",
+                "laravel",
+                "mobile",
+                "platform",
+                "user agent",
+                "useragent"
+            ],
+            "support": {
+                "issues": "https://github.com/jenssegers/agent/issues",
+                "source": "https://github.com/jenssegers/agent/tree/v2.6.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/jenssegers",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/jenssegers/agent",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-06-13T08:05:20+00:00"
+        },
+        {
+            "name": "justinrainbow/json-schema",
+            "version": "5.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jsonrainbow/json-schema.git",
+                "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8",
+                "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+                "json-schema/json-schema-test-suite": "1.2.0",
+                "phpunit/phpunit": "^4.8.35"
+            },
+            "bin": [
+                "bin/validate-json"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JsonSchema\\": "src/JsonSchema/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -4192,37 +4812,33 @@
             ],
             "authors": [
                 {
-                    "name": "Jens Segers",
-                    "homepage": "https://jenssegers.com"
+                    "name": "Bruno Prieto Reis",
+                    "email": "bruno.p.reis@gmail.com"
+                },
+                {
+                    "name": "Justin Rainbow",
+                    "email": "justin.rainbow@gmail.com"
+                },
+                {
+                    "name": "Igor Wiedler",
+                    "email": "igor@wiedler.ch"
+                },
+                {
+                    "name": "Robert Schönthal",
+                    "email": "seroscho@googlemail.com"
                 }
             ],
-            "description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
-            "homepage": "https://github.com/jenssegers/agent",
+            "description": "A library to validate a json schema.",
+            "homepage": "https://github.com/justinrainbow/json-schema",
             "keywords": [
-                "Agent",
-                "browser",
-                "desktop",
-                "laravel",
-                "mobile",
-                "platform",
-                "user agent",
-                "useragent"
+                "json",
+                "schema"
             ],
             "support": {
-                "issues": "https://github.com/jenssegers/agent/issues",
-                "source": "https://github.com/jenssegers/agent/tree/v2.6.4"
+                "issues": "https://github.com/jsonrainbow/json-schema/issues",
+                "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0"
             },
-            "funding": [
-                {
-                    "url": "https://github.com/jenssegers",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/jenssegers/agent",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-06-13T08:05:20+00:00"
+            "time": "2024-07-06T21:00:26+00:00"
         },
         {
             "name": "kelunik/certificate",
@@ -8951,6 +9567,79 @@
             ],
             "time": "2024-04-27T21:32:50+00:00"
         },
+        {
+            "name": "react/promise",
+            "version": "v3.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/reactphp/promise.git",
+                "reference": "8a164643313c71354582dc850b42b33fa12a4b63"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
+                "reference": "8a164643313c71354582dc850b42b33fa12a4b63",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "1.10.39 || 1.4.10",
+                "phpunit/phpunit": "^9.6 || ^7.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "React\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jan Sorgalla",
+                    "email": "jsorgalla@gmail.com",
+                    "homepage": "https://sorgalla.com/"
+                },
+                {
+                    "name": "Christian Lück",
+                    "email": "christian@clue.engineering",
+                    "homepage": "https://clue.engineering/"
+                },
+                {
+                    "name": "Cees-Jan Kiewiet",
+                    "email": "reactphp@ceesjankiewiet.nl",
+                    "homepage": "https://wyrihaximus.net/"
+                },
+                {
+                    "name": "Chris Boden",
+                    "email": "cboden@gmail.com",
+                    "homepage": "https://cboden.dev/"
+                }
+            ],
+            "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+            "keywords": [
+                "promise",
+                "promises"
+            ],
+            "support": {
+                "issues": "https://github.com/reactphp/promise/issues",
+                "source": "https://github.com/reactphp/promise/tree/v3.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://opencollective.com/reactphp",
+                    "type": "open_collective"
+                }
+            ],
+            "time": "2024-05-24T10:39:05+00:00"
+        },
         {
             "name": "revolt/event-loop",
             "version": "v1.0.6",
@@ -9007,68 +9696,246 @@
                     "email": "me@kelunik.com"
                 }
             ],
-            "description": "Rock-solid event loop for concurrent PHP applications.",
+            "description": "Rock-solid event loop for concurrent PHP applications.",
+            "keywords": [
+                "async",
+                "asynchronous",
+                "concurrency",
+                "event",
+                "event-loop",
+                "non-blocking",
+                "scheduler"
+            ],
+            "support": {
+                "issues": "https://github.com/revoltphp/event-loop/issues",
+                "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6"
+            },
+            "time": "2023-11-30T05:34:44+00:00"
+        },
+        {
+            "name": "ryangjchandler/blade-capture-directive",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ryangjchandler/blade-capture-directive.git",
+                "reference": "cb6f58663d97f17bece176295240b740835e14f1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/cb6f58663d97f17bece176295240b740835e14f1",
+                "reference": "cb6f58663d97f17bece176295240b740835e14f1",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/contracts": "^10.0|^11.0",
+                "php": "^8.1",
+                "spatie/laravel-package-tools": "^1.9.2"
+            },
+            "require-dev": {
+                "nunomaduro/collision": "^7.0|^8.0",
+                "nunomaduro/larastan": "^2.0",
+                "orchestra/testbench": "^8.0|^9.0",
+                "pestphp/pest": "^2.0",
+                "pestphp/pest-plugin-laravel": "^2.0",
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan-deprecation-rules": "^1.0",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpunit/phpunit": "^10.0",
+                "spatie/laravel-ray": "^1.26"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "aliases": {
+                        "BladeCaptureDirective": "RyanChandler\\BladeCaptureDirective\\Facades\\BladeCaptureDirective"
+                    },
+                    "providers": [
+                        "RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "RyanChandler\\BladeCaptureDirective\\": "src",
+                    "RyanChandler\\BladeCaptureDirective\\Database\\Factories\\": "database/factories"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ryan Chandler",
+                    "email": "support@ryangjchandler.co.uk",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Create inline partials in your Blade templates with ease.",
+            "homepage": "https://github.com/ryangjchandler/blade-capture-directive",
+            "keywords": [
+                "blade-capture-directive",
+                "laravel",
+                "ryangjchandler"
+            ],
+            "support": {
+                "issues": "https://github.com/ryangjchandler/blade-capture-directive/issues",
+                "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.0.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ryangjchandler",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-02-26T18:08:49+00:00"
+        },
+        {
+            "name": "seld/jsonlint",
+            "version": "1.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/jsonlint.git",
+                "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2",
+                "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.11",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13"
+            },
+            "bin": [
+                "bin/jsonlint"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Seld\\JsonLint\\": "src/Seld/JsonLint/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "https://seld.be"
+                }
+            ],
+            "description": "JSON Linter",
+            "keywords": [
+                "json",
+                "linter",
+                "parser",
+                "validator"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/jsonlint/issues",
+                "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Seldaek",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-07-11T14:55:45+00:00"
+        },
+        {
+            "name": "seld/phar-utils",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/phar-utils.git",
+                "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c",
+                "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Seld\\PharUtils\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be"
+                }
+            ],
+            "description": "PHAR file format utilities, for when PHP phars you up",
             "keywords": [
-                "async",
-                "asynchronous",
-                "concurrency",
-                "event",
-                "event-loop",
-                "non-blocking",
-                "scheduler"
+                "phar"
             ],
             "support": {
-                "issues": "https://github.com/revoltphp/event-loop/issues",
-                "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6"
+                "issues": "https://github.com/Seldaek/phar-utils/issues",
+                "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1"
             },
-            "time": "2023-11-30T05:34:44+00:00"
+            "time": "2022-08-31T10:31:18+00:00"
         },
         {
-            "name": "ryangjchandler/blade-capture-directive",
-            "version": "v1.0.0",
+            "name": "seld/signal-handler",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
-                "url": "https://github.com/ryangjchandler/blade-capture-directive.git",
-                "reference": "cb6f58663d97f17bece176295240b740835e14f1"
+                "url": "https://github.com/Seldaek/signal-handler.git",
+                "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/cb6f58663d97f17bece176295240b740835e14f1",
-                "reference": "cb6f58663d97f17bece176295240b740835e14f1",
+                "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98",
+                "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98",
                 "shasum": ""
             },
             "require": {
-                "illuminate/contracts": "^10.0|^11.0",
-                "php": "^8.1",
-                "spatie/laravel-package-tools": "^1.9.2"
+                "php": ">=7.2.0"
             },
             "require-dev": {
-                "nunomaduro/collision": "^7.0|^8.0",
-                "nunomaduro/larastan": "^2.0",
-                "orchestra/testbench": "^8.0|^9.0",
-                "pestphp/pest": "^2.0",
-                "pestphp/pest-plugin-laravel": "^2.0",
-                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1",
                 "phpstan/phpstan-deprecation-rules": "^1.0",
-                "phpstan/phpstan-phpunit": "^1.0",
-                "phpunit/phpunit": "^10.0",
-                "spatie/laravel-ray": "^1.26"
+                "phpstan/phpstan-phpunit": "^1",
+                "phpstan/phpstan-strict-rules": "^1.3",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.23",
+                "psr/log": "^1 || ^2 || ^3"
             },
             "type": "library",
             "extra": {
-                "laravel": {
-                    "aliases": {
-                        "BladeCaptureDirective": "RyanChandler\\BladeCaptureDirective\\Facades\\BladeCaptureDirective"
-                    },
-                    "providers": [
-                        "RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider"
-                    ]
+                "branch-alias": {
+                    "dev-main": "2.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "RyanChandler\\BladeCaptureDirective\\": "src",
-                    "RyanChandler\\BladeCaptureDirective\\Database\\Factories\\": "database/factories"
+                    "Seld\\Signal\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -9077,29 +9944,24 @@
             ],
             "authors": [
                 {
-                    "name": "Ryan Chandler",
-                    "email": "support@ryangjchandler.co.uk",
-                    "role": "Developer"
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
                 }
             ],
-            "description": "Create inline partials in your Blade templates with ease.",
-            "homepage": "https://github.com/ryangjchandler/blade-capture-directive",
+            "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development",
             "keywords": [
-                "blade-capture-directive",
-                "laravel",
-                "ryangjchandler"
+                "posix",
+                "sigint",
+                "signal",
+                "sigterm",
+                "unix"
             ],
             "support": {
-                "issues": "https://github.com/ryangjchandler/blade-capture-directive/issues",
-                "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.0.0"
+                "issues": "https://github.com/Seldaek/signal-handler/issues",
+                "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2"
             },
-            "funding": [
-                {
-                    "url": "https://github.com/ryangjchandler",
-                    "type": "github"
-                }
-            ],
-            "time": "2024-02-26T18:08:49+00:00"
+            "time": "2023-09-03T09:24:00+00:00"
         },
         {
             "name": "sentry/sentry",
@@ -11066,6 +11928,72 @@
             ],
             "time": "2024-09-25T14:20:29+00:00"
         },
+        {
+            "name": "symfony/filesystem",
+            "version": "v7.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/filesystem.git",
+                "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
+                "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-mbstring": "~1.8"
+            },
+            "require-dev": {
+                "symfony/process": "^6.4|^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Filesystem\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides basic utilities for the filesystem",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/filesystem/tree/v7.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-10-25T15:15:23+00:00"
+        },
         {
             "name": "symfony/finder",
             "version": "v7.2.2",
@@ -12265,6 +13193,82 @@
             ],
             "time": "2024-09-09T11:45:10+00:00"
         },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.31.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
+                "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-09-09T11:45:10+00:00"
+        },
         {
             "name": "symfony/polyfill-php80",
             "version": "v1.31.0",
@@ -12345,6 +13349,82 @@
             ],
             "time": "2024-09-09T11:45:10+00:00"
         },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.31.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-09-09T11:45:10+00:00"
+        },
         {
             "name": "symfony/polyfill-php83",
             "version": "v1.31.0",
@@ -17374,7 +18454,7 @@
     ],
     "aliases": [],
     "minimum-stability": "dev",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
@@ -17382,6 +18462,6 @@
         "ext-fileinfo": "*",
         "ext-json": "*"
     },
-    "platform-dev": [],
+    "platform-dev": {},
     "plugin-api-version": "2.6.0"
 }
diff --git a/config/app-modules.php b/config/app-modules.php
new file mode 100644
index 00000000..33a994a1
--- /dev/null
+++ b/config/app-modules.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    /*
+    |--------------------------------------------------------------------------
+    | Modules Namespace
+    |--------------------------------------------------------------------------
+    |
+    | This is the PHP namespace that your modules will be created in. For
+    | example, a module called "Helpers" will be placed in \Laravelcm\Helpers
+    | by default.
+    |
+    | It is *highly recommended* that you configure this to your organization
+    | name to make extracting modules to their own package easier (should you
+    | choose to ever do so).
+    |
+    | If you set the namespace, you should also set the vendor name to match.
+    |
+    */
+
+    'modules_namespace' => 'Laravelcm',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Composer "Vendor" Name
+    |--------------------------------------------------------------------------
+    |
+    | This is the prefix used for your composer.json file. This should be the
+    | kebab-case version of your module namespace (if left null, we will
+    | generate the kebab-case version for you).
+    |
+    */
+
+    'modules_vendor' => 'laravelcm',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Modules Directory
+    |--------------------------------------------------------------------------
+    |
+    | If you want to install modules in a custom directory, you can do so here.
+    | Keeping the default `app-modules/` directory is highly recommended,
+    | though, as it keeps your modules near the rest of your application code
+    | in an alpha-sorted directory listing.
+    |
+    */
+
+    'modules_directory' => 'app-modules',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Base Test Case
+    |--------------------------------------------------------------------------
+    |
+    | This is the base TestCase class name that auto-generated Tests should
+    | extend. By default, it assumes the default \Tests\TestCase exists.
+    |
+    */
+
+    'tests_base' => 'Tests\TestCase',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Custom Stubs
+    |--------------------------------------------------------------------------
+    |
+    | If you would like to use your own custom stubs for new modules, you can
+    | configure those here. This should be an array where the key is the path
+    | relative to the module and the value is the absolute path to the stub
+    | stub file. Destination paths and contents support placeholders. See the
+    | README.md file for more information.
+    |
+    | For example:
+    |
+    | 'stubs' => [
+    | 	'src/Providers/StubClassNamePrefixServiceProvider.php' => base_path('stubs/app-modules/ServiceProvider.php'),
+    | ],
+    */
+
+    'stubs' => [
+        'composer.json' => base_path('stubs/app-modules/composer-stub.json'),
+        'src/Providers/StubClassNamePrefixServiceProvider.php' => base_path('stubs/app-modules/app/ServiceProvider.php'),
+        'src/StubClassNamePrefixPlugin.php' => base_path('stubs/app-modules/app/Plugin.php'),
+        'src/Models/.gitkeep' => base_path('stubs/app-modules/.gitkeep'),
+        'tests/.gitkeep' => base_path('stubs/app-modules/.gitkeep'),
+        'database/factories/.gitkeep' => base_path('stubs/app-modules/.gitkeep'),
+        'database/migrations/.gitkeep' => base_path('stubs/app-modules/.gitkeep'),
+        'database/seeders/.gitkeep' => base_path('stubs/app-modules/.gitkeep'),
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Custom override of event discovery
+    |--------------------------------------------------------------------------
+    |
+    | This is a custom override of the event discovery feature. If you want to
+    | disable event discovery, set this to false. If you want to enable event
+    | discovery, set this to true. We will still check the app namespace for
+    | the presence of event discovery.
+    */
+
+    'should_discover_events' => true,
+];
diff --git a/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php b/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php
new file mode 100644
index 00000000..16b8cd48
--- /dev/null
+++ b/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('articles', static function (Blueprint $table): void {
+            $table->string('reason')->nullable()->after('canonical_url');
+        });
+    }
+};
diff --git a/lang/en/emails/article.php b/lang/en/emails/article.php
new file mode 100644
index 00000000..6c406acc
--- /dev/null
+++ b/lang/en/emails/article.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+
+    'article_declined' => [
+        'subject' => 'Your article has been declined',
+        'head' => 'Your article :title has been declined for the following reason:',
+        'recommandation_body' => 'Don\'t be discouraged! You can:',
+        'recommandation_1' => '1. Review your article and the reason for the decline',
+        'recommandation_2' => '2. Make the necessary changes',
+        'recommandation_3' => '3. Resubmit your article once the corrections are made',
+        'help' => 'Our team is here to help you improve your content.',
+        'closing' => 'Best regards,',
+        'team' => 'The Laravel Cameroon Team',
+        'button_update_article' => 'Edit my article',
+    ],
+
+];
diff --git a/lang/fr/emails/article.php b/lang/fr/emails/article.php
new file mode 100644
index 00000000..1c612c56
--- /dev/null
+++ b/lang/fr/emails/article.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+
+    'article_declined' => [
+        'subject' => 'Votre article a été décliné',
+        'head' => 'Votre article :title a été décliné pour la raison suivante :',
+        'recommandation_body' => 'Ne vous découragez pas ! Vous pouvez :',
+        'recommandation_1' => '1. Consulter votre article et la raison du refus',
+        'recommandation_2' => '2. Apporter les modifications nécessaires',
+        'recommandation_3' => '3. Re-soumettre votre article une fois les corrections effectuées',
+        'help' => 'Notre équipe est là pour vous aider à améliorer votre contenu.',
+        'closing' => 'Cordialement,',
+        'team' => 'L\'équipe Laravel Cameroun',
+        'button_update_article' => 'Modifier mon article',
+    ],
+
+];
diff --git a/resources/views/emails/article_declined.blade.php b/resources/views/emails/article_declined.blade.php
new file mode 100644
index 00000000..e62d4ea3
--- /dev/null
+++ b/resources/views/emails/article_declined.blade.php
@@ -0,0 +1,28 @@
+<x-mail::message>
+
+<x-mail::panel>
+
+{{ __('emails/article.article_declined.head',  ['title' => $article->title]) }}
+
+{{ $article->reason }}
+
+{{ __('emails/article.article_declined.recommandation_body') }}
+
+{{ __('emails/article.article_declined.recommandation_1') }}
+{{ __('emails/article.article_declined.recommandation_2') }}
+{{ __('emails/article.article_declined.recommandation_3') }}
+
+<x-mail::button :url="route('articles.show', $article)">
+{{ __('emails/article.article_declined.button_update_article') }}
+</x-mail::button>
+
+{{ __('emails/article.article_declined.help') }}
+
+</x-mail::panel>
+
+<p>
+    {{ __('emails/article.article_declined.closing') }}  <br>
+    {{ __('emails/article.article_declined.team') }}
+</p>
+
+</x-mail::message>
diff --git a/stubs/app-modules/.gitkeep b/stubs/app-modules/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/stubs/app-modules/app/Plugin.php b/stubs/app-modules/app/Plugin.php
new file mode 100644
index 00000000..fbfda925
--- /dev/null
+++ b/stubs/app-modules/app/Plugin.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Laravelcm\StubClassNamePrefix;
+
+use Filament\Contracts\Plugin;
+use Filament\Panel;
+
+final class StubComposerNamePlugin implements Plugin
+{
+    public function getId(): string
+    {
+        return 'StubModuleName';
+    }
+
+    public function register(Panel $panel): void
+    {
+        $panel->discoverResources(
+            in: __DIR__.'/Filament/Resources',
+            for: 'Laravelcm\\StubClassNamePrefix\\Filament\\Resources'
+        );
+    }
+
+    public function boot(Panel $panel): void {}
+}
diff --git a/stubs/app-modules/app/ServiceProvider.php b/stubs/app-modules/app/ServiceProvider.php
new file mode 100644
index 00000000..19b3c8e3
--- /dev/null
+++ b/stubs/app-modules/app/ServiceProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Laravelcm\StubClassNamePrefix\Providers;
+
+use Filament\Panel;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\ServiceProvider;
+use Laravelcm\StubClassNamePrefix\StubClassNamePrefixPlugin;
+
+final class StubClassNamePrefixServiceProvider extends ServiceProvider
+{
+    public function register(): void
+    {
+        Panel::configureUsing(fn (Panel $panel) => $panel->getId() !== 'admin' || $panel->plugin(new StubClassNamePrefixPlugin));
+    }
+
+    public function boot(): void
+    {
+        Relation::morphMap([]);
+    }
+}
diff --git a/stubs/app-modules/composer-stub.json b/stubs/app-modules/composer-stub.json
new file mode 100644
index 00000000..906e86eb
--- /dev/null
+++ b/stubs/app-modules/composer-stub.json
@@ -0,0 +1,26 @@
+{
+    "name": "StubComposerName",
+    "description": "Module StubComposerName for Laravel.cm",
+    "type": "library",
+    "version": "1.0",
+    "license": "proprietary",
+    "require": {
+        "filament/filament": "^3.2"
+    },
+    "autoload": {
+        "psr-4": {
+            "Laravelcm\\StubClassNamePrefix\\": "src/",
+            "Laravelcm\\StubClassNamePrefix\\Tests\\": "tests/",
+            "Laravelcm\\StubClassNamePrefix\\Database\\Factories\\": "database/factories/",
+            "Laravelcm\\StubClassNamePrefix\\Database\\Seeders\\": "database/seeders/"
+        }
+    },
+    "minimum-stability": "stable",
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Laravelcm\\StubClassNamePrefix\\Providers\\StubClassNamePrefixServiceProvider"
+            ]
+        }
+    }
+}
diff --git a/tests/Feature/Filament/ArticleResourceTest.php b/tests/Feature/Filament/ArticleResourceTest.php
index 2d02d951..326af861 100644
--- a/tests/Feature/Filament/ArticleResourceTest.php
+++ b/tests/Feature/Filament/ArticleResourceTest.php
@@ -35,11 +35,8 @@
 
         $article->refresh();
 
-        expect($article->approved_at)
-            ->not()
-            ->toBe(null)
-            ->and($article->declined_at)
-            ->toBe(null);
+        expect($article->approved_at)->toBeInstanceOf(\Carbon\Carbon::class)
+            ->and($article->declined_at)->toBeNull();
 
         Livewire::test(ArticleResource\Pages\ListArticles::class)
             ->assertTableActionHidden('approved', $article)
@@ -50,15 +47,14 @@
         $article = $this->articles->first();
 
         Livewire::test(ArticleResource\Pages\ListArticles::class)
-            ->callTableAction('declined', $article);
+            ->callTableAction('declined', $article, data: [
+                'reason' => 'Ce contenu ne respecte pas nos règles éditoriales.',
+            ]);
 
         $article->refresh();
 
-        expect($article->declined_at)
-            ->not
-            ->toBe(null)
-            ->and($article->approved_at)
-            ->toBe(null);
+        expect($article->declined_at)->toBeInstanceOf(\Carbon\Carbon::class)
+            ->and($article->approved_at)->toBeNull();
 
         Livewire::test(ArticleResource\Pages\ListArticles::class)
             ->assertTableActionHidden('approved', $article)
