From 9044500f9bda1e920ed72386d892db03c150909a Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 25 Aug 2026 23:18:24 +0600 Subject: [PATCH 1/4] feat(blog): send email notifications on reaction milestones --- app/Http/Controllers/BlogController.php | 13 +++++ app/Mail/BlogReactionMilestoneMail.php | 70 +++++++++++++++++++++++++ tests/Feature/BlogTest.php | 36 +++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 app/Mail/BlogReactionMilestoneMail.php diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index efac56a..c7bcb33 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Mail\BlogCommentNotificationMail; +use App\Mail\BlogReactionMilestoneMail; use App\Models\Blog; use App\Models\BlogComment; use Illuminate\Http\Request; @@ -97,6 +98,18 @@ public function toggleReaction(Blog $blog) $existing->delete(); } else { $blog->reactions()->create(['user_id' => $user->id]); + + $reactionsCount = $blog->reactions()->count(); + $milestones = [1, 10, 25, 50, 100, 250, 500, 1000]; + $isMilestone = in_array($reactionsCount, $milestones, true) || ($reactionsCount > 1000 && $reactionsCount % 500 === 0); + + if ($isMilestone) { + $blog->loadMissing('user:id,name,email,receive_emails'); + + if ($blog->user && $blog->user_id !== $user->id && $blog->user->email && $blog->user->receive_emails !== false) { + Mail::to($blog->user->email)->queue(new BlogReactionMilestoneMail($blog, $user, $reactionsCount)); + } + } } return back(); diff --git a/app/Mail/BlogReactionMilestoneMail.php b/app/Mail/BlogReactionMilestoneMail.php new file mode 100644 index 0000000..3607109 --- /dev/null +++ b/app/Mail/BlogReactionMilestoneMail.php @@ -0,0 +1,70 @@ +milestoneCount === 1 + ? 'Your blog received its first love reaction! ❤️' + : "🎉 Milestone: {$this->milestoneCount} people loved your blog post!"; + + return new Envelope( + subject: $subject, + ); + } + + public function content(): Content + { + $appUrl = config('app.url', 'https://hscstack.site'); + $blogUrl = rtrim($appUrl, '/')."/blogs/{$this->blog->slug}"; + $reactorName = htmlspecialchars($this->reactor->name, ENT_QUOTES, 'UTF-8'); + $blogTitle = htmlspecialchars($this->blog->title, ENT_QUOTES, 'UTF-8'); + + if ($this->milestoneCount === 1) { + $headline = 'Congratulations! Your blog post received its very first love reaction ❤️'; + $message = "

{$reactorName} just loved your article \"{$blogTitle}\".

" + .'

Readers are appreciating your work! Keep sharing knowledge with the community.

'; + } else { + $headline = "🎉 Exciting News! {$this->milestoneCount} Love Reactions Milestone reached!"; + $message = "

Your article \"{$blogTitle}\" just hit {$this->milestoneCount} love reactions, with the latest from {$reactorName}!

" + .'

Thank you for creating content that resonates with our students and readers.

'; + } + + $mailContent = "

{$headline}

" + .$message + .'

' + ."" + .'View Blog Post →' + .'' + .'

'; + + return new Content( + view: 'emails.bulk_announcement', + with: [ + 'mailSubject' => $this->milestoneCount === 1 ? 'First Reaction on Your Blog' : "{$this->milestoneCount} Reactions Milestone", + 'mailContent' => $mailContent, + 'recipientName' => $this->blog->user?->name, + 'imageUrl' => null, + ], + ); + } +} diff --git a/tests/Feature/BlogTest.php b/tests/Feature/BlogTest.php index 6dd715d..c1ab0a7 100644 --- a/tests/Feature/BlogTest.php +++ b/tests/Feature/BlogTest.php @@ -1,6 +1,7 @@ create(['email' => 'author@example.com', 'receive_emails' => true]); + $blog = Blog::factory()->create([ + 'user_id' => $author->id, + 'title' => 'Milestone Post', + 'is_published' => true, + ]); + + $firstUser = User::factory()->create(['name' => 'First Lover']); + $secondUser = User::factory()->create(['name' => 'Second Lover']); + + // 1st reaction (milestone: 1) -> queues email + $this->actingAs($firstUser)->post("/blogs/{$blog->slug}/react"); + Mail::assertQueued(BlogReactionMilestoneMail::class, 1); + + // 2nd reaction (not milestone) -> does not queue new email + $this->actingAs($secondUser)->post("/blogs/{$blog->slug}/react"); + Mail::assertQueued(BlogReactionMilestoneMail::class, 1); +}); + +test('email is not sent if author reacts to own blog', function () { + Mail::fake(); + + $author = User::factory()->create(['email' => 'author@example.com', 'receive_emails' => true]); + $blog = Blog::factory()->create([ + 'user_id' => $author->id, + 'is_published' => true, + ]); + + $this->actingAs($author)->post("/blogs/{$blog->slug}/react"); + Mail::assertNothingQueued(); +}); From f23e623e394500aa39f6f37e8dd47568eacadbcd Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 25 Aug 2026 23:21:09 +0600 Subject: [PATCH 2/4] refactor(mail): consolidate blog notification emails into unified BlogNotificationMail --- app/Http/Controllers/BlogController.php | 7 +- app/Mail/BlogCommentNotificationMail.php | 60 --------------- app/Mail/BlogNotificationMail.php | 96 ++++++++++++++++++++++++ app/Mail/BlogReactionMilestoneMail.php | 70 ----------------- tests/Feature/BlogTest.php | 9 +-- 5 files changed, 103 insertions(+), 139 deletions(-) delete mode 100644 app/Mail/BlogCommentNotificationMail.php create mode 100644 app/Mail/BlogNotificationMail.php delete mode 100644 app/Mail/BlogReactionMilestoneMail.php diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index c7bcb33..e97160e 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -2,8 +2,7 @@ namespace App\Http\Controllers; -use App\Mail\BlogCommentNotificationMail; -use App\Mail\BlogReactionMilestoneMail; +use App\Mail\BlogNotificationMail; use App\Models\Blog; use App\Models\BlogComment; use Illuminate\Http\Request; @@ -107,7 +106,7 @@ public function toggleReaction(Blog $blog) $blog->loadMissing('user:id,name,email,receive_emails'); if ($blog->user && $blog->user_id !== $user->id && $blog->user->email && $blog->user->receive_emails !== false) { - Mail::to($blog->user->email)->queue(new BlogReactionMilestoneMail($blog, $user, $reactionsCount)); + Mail::to($blog->user->email)->queue(BlogNotificationMail::forReactionMilestone($blog, $user, $reactionsCount)); } } } @@ -136,7 +135,7 @@ public function storeComment(Request $request, Blog $blog) $blog->loadMissing('user:id,name,email,receive_emails'); if ($blog->user && $blog->user_id !== $userId && $blog->user->email && $blog->user->receive_emails !== false) { - Mail::to($blog->user->email)->queue(new BlogCommentNotificationMail($blog, $comment)); + Mail::to($blog->user->email)->queue(BlogNotificationMail::forComment($blog, $comment)); } return back()->with('success', 'Comment posted successfully.'); diff --git a/app/Mail/BlogCommentNotificationMail.php b/app/Mail/BlogCommentNotificationMail.php deleted file mode 100644 index 705dcd9..0000000 --- a/app/Mail/BlogCommentNotificationMail.php +++ /dev/null @@ -1,60 +0,0 @@ -comment->user?->name ?? 'Someone'; - - return new Envelope( - subject: "New comment on your blog: \"{$this->blog->title}\"", - ); - } - - public function content(): Content - { - $appUrl = config('app.url', 'https://hscstack.site'); - $blogUrl = rtrim($appUrl, '/')."/blogs/{$this->blog->slug}#comments"; - $commenterName = htmlspecialchars($this->comment->user?->name ?? 'A reader', ENT_QUOTES, 'UTF-8'); - $commentContent = nl2br(htmlspecialchars($this->comment->content, ENT_QUOTES, 'UTF-8')); - $blogTitle = htmlspecialchars($this->blog->title, ENT_QUOTES, 'UTF-8'); - - $mailContent = "

{$commenterName} just commented on your blog post \"{$blogTitle}\":

" - .'
' - ."

{$commentContent}

" - .'
' - .'

' - ."" - .'View & Reply to Comment →' - .'' - .'

'; - - return new Content( - view: 'emails.bulk_announcement', - with: [ - 'mailSubject' => "New comment on \"{$this->blog->title}\"", - 'mailContent' => $mailContent, - 'recipientName' => $this->blog->user?->name, - 'imageUrl' => null, - ], - ); - } -} diff --git a/app/Mail/BlogNotificationMail.php b/app/Mail/BlogNotificationMail.php new file mode 100644 index 0000000..62da325 --- /dev/null +++ b/app/Mail/BlogNotificationMail.php @@ -0,0 +1,96 @@ +slug}#comments"; + $commenterName = htmlspecialchars($comment->user?->name ?? 'A reader', ENT_QUOTES, 'UTF-8'); + $commentContent = nl2br(htmlspecialchars($comment->content, ENT_QUOTES, 'UTF-8')); + $blogTitle = htmlspecialchars($blog->title, ENT_QUOTES, 'UTF-8'); + + $mailSubject = "New comment on \"{$blog->title}\""; + $mailContent = "

{$commenterName} just commented on your blog post \"{$blogTitle}\":

" + .'
' + ."

{$commentContent}

" + .'
' + .'

' + ."" + .'View & Reply to Comment →' + .'' + .'

'; + + return new self($mailSubject, $mailContent, $blog->user?->name); + } + + public static function forReactionMilestone(Blog $blog, User $reactor, int $milestoneCount): self + { + $appUrl = config('app.url', 'https://hscstack.site'); + $blogUrl = rtrim($appUrl, '/')."/blogs/{$blog->slug}"; + $reactorName = htmlspecialchars($reactor->name, ENT_QUOTES, 'UTF-8'); + $blogTitle = htmlspecialchars($blog->title, ENT_QUOTES, 'UTF-8'); + + if ($milestoneCount === 1) { + $mailSubject = 'Your blog received its first love reaction! ❤️'; + $headline = 'Congratulations! Your blog post received its very first love reaction ❤️'; + $message = "

{$reactorName} just loved your article \"{$blogTitle}\".

" + .'

Readers are appreciating your work! Keep sharing knowledge with the community.

'; + } else { + $mailSubject = "🎉 Milestone: {$milestoneCount} people loved your blog post!"; + $headline = "🎉 Exciting News! {$milestoneCount} Love Reactions Milestone reached!"; + $message = "

Your article \"{$blogTitle}\" just hit {$milestoneCount} love reactions, with the latest from {$reactorName}!

" + .'

Thank you for creating content that resonates with our students and readers.

'; + } + + $mailContent = "

{$headline}

" + .$message + .'

' + ."" + .'View Blog Post →' + .'' + .'

'; + + return new self($mailSubject, $mailContent, $blog->user?->name); + } + + public function envelope(): Envelope + { + return new Envelope( + subject: $this->mailSubject, + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.bulk_announcement', + with: [ + 'mailSubject' => $this->mailSubject, + 'mailContent' => $this->mailContent, + 'recipientName' => $this->recipientName, + 'imageUrl' => null, + ], + ); + } +} diff --git a/app/Mail/BlogReactionMilestoneMail.php b/app/Mail/BlogReactionMilestoneMail.php deleted file mode 100644 index 3607109..0000000 --- a/app/Mail/BlogReactionMilestoneMail.php +++ /dev/null @@ -1,70 +0,0 @@ -milestoneCount === 1 - ? 'Your blog received its first love reaction! ❤️' - : "🎉 Milestone: {$this->milestoneCount} people loved your blog post!"; - - return new Envelope( - subject: $subject, - ); - } - - public function content(): Content - { - $appUrl = config('app.url', 'https://hscstack.site'); - $blogUrl = rtrim($appUrl, '/')."/blogs/{$this->blog->slug}"; - $reactorName = htmlspecialchars($this->reactor->name, ENT_QUOTES, 'UTF-8'); - $blogTitle = htmlspecialchars($this->blog->title, ENT_QUOTES, 'UTF-8'); - - if ($this->milestoneCount === 1) { - $headline = 'Congratulations! Your blog post received its very first love reaction ❤️'; - $message = "

{$reactorName} just loved your article \"{$blogTitle}\".

" - .'

Readers are appreciating your work! Keep sharing knowledge with the community.

'; - } else { - $headline = "🎉 Exciting News! {$this->milestoneCount} Love Reactions Milestone reached!"; - $message = "

Your article \"{$blogTitle}\" just hit {$this->milestoneCount} love reactions, with the latest from {$reactorName}!

" - .'

Thank you for creating content that resonates with our students and readers.

'; - } - - $mailContent = "

{$headline}

" - .$message - .'

' - ."" - .'View Blog Post →' - .'' - .'

'; - - return new Content( - view: 'emails.bulk_announcement', - with: [ - 'mailSubject' => $this->milestoneCount === 1 ? 'First Reaction on Your Blog' : "{$this->milestoneCount} Reactions Milestone", - 'mailContent' => $mailContent, - 'recipientName' => $this->blog->user?->name, - 'imageUrl' => null, - ], - ); - } -} diff --git a/tests/Feature/BlogTest.php b/tests/Feature/BlogTest.php index c1ab0a7..95f68c7 100644 --- a/tests/Feature/BlogTest.php +++ b/tests/Feature/BlogTest.php @@ -1,7 +1,6 @@ assertSessionHas('success'); - Mail::assertQueued(BlogCommentNotificationMail::class, function ($mail) use ($author) { + Mail::assertQueued(BlogNotificationMail::class, function ($mail) use ($author) { return $mail->hasTo($author->email); }); }); @@ -198,11 +197,11 @@ // 1st reaction (milestone: 1) -> queues email $this->actingAs($firstUser)->post("/blogs/{$blog->slug}/react"); - Mail::assertQueued(BlogReactionMilestoneMail::class, 1); + Mail::assertQueued(BlogNotificationMail::class, 1); // 2nd reaction (not milestone) -> does not queue new email $this->actingAs($secondUser)->post("/blogs/{$blog->slug}/react"); - Mail::assertQueued(BlogReactionMilestoneMail::class, 1); + Mail::assertQueued(BlogNotificationMail::class, 1); }); test('email is not sent if author reacts to own blog', function () { From e5375f73bbb5cbc2f89d111a20e988f85a4e96bd Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 25 Aug 2026 23:24:00 +0600 Subject: [PATCH 3/4] fix(ui): ensure comment delete button is always visible on mobile --- resources/js/pages/Blog/Show.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/pages/Blog/Show.vue b/resources/js/pages/Blog/Show.vue index 00348aa..511d19f 100644 --- a/resources/js/pages/Blog/Show.vue +++ b/resources/js/pages/Blog/Show.vue @@ -636,7 +636,7 @@ const goBack = () => { " @click="deleteComment(comment.id)" title="Delete comment" - class="cursor-pointer rounded-lg p-1.5 text-slate-400 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-rose-50 hover:text-rose-600 dark:text-gray-500 dark:hover:bg-rose-950/40 dark:hover:text-rose-400" + class="cursor-pointer rounded-lg p-1.5 text-slate-400 opacity-100 transition-opacity hover:bg-rose-50 hover:text-rose-600 sm:opacity-0 sm:group-hover:opacity-100 dark:text-gray-500 dark:hover:bg-rose-950/40 dark:hover:text-rose-400" > From d06fc1cd93260c23ffee493faefd5a1955727b76 Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 25 Aug 2026 23:26:04 +0600 Subject: [PATCH 4/4] style(blog): redesign write for us banner into a sleek minimal callout --- resources/js/pages/Blog/Show.vue | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/resources/js/pages/Blog/Show.vue b/resources/js/pages/Blog/Show.vue index 511d19f..10513fb 100644 --- a/resources/js/pages/Blog/Show.vue +++ b/resources/js/pages/Blog/Show.vue @@ -445,26 +445,29 @@ const goBack = () => { - -
+
-
-

- Want to write a blog on this site? -

-

- Share your thoughts, stories, and expertise with our - community. -

+
+ Want to write a blog here? + +
- Join us + Join us + -
+