Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/Http/Controllers/BlogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Http\Controllers;

use App\Mail\BlogCommentNotificationMail;
use App\Mail\BlogNotificationMail;
use App\Models\Blog;
use App\Models\BlogComment;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -97,6 +97,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(BlogNotificationMail::forReactionMilestone($blog, $user, $reactionsCount));
}
}
}

return back();
Expand All @@ -123,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.');
Expand Down
60 changes: 0 additions & 60 deletions app/Mail/BlogCommentNotificationMail.php

This file was deleted.

96 changes: 96 additions & 0 deletions app/Mail/BlogNotificationMail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace App\Mail;

use App\Models\Blog;
use App\Models\BlogComment;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class BlogNotificationMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;

public function __construct(
public string $mailSubject,
public string $mailContent,
public ?string $recipientName = null,
) {}

public static function forComment(Blog $blog, BlogComment $comment): self
{
$appUrl = config('app.url', 'https://hscstack.site');
$blogUrl = rtrim($appUrl, '/')."/blogs/{$blog->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 = "<p><strong>{$commenterName}</strong> just commented on your blog post <a href=\"{$blogUrl}\" target=\"_blank\"><strong>\"{$blogTitle}\"</strong></a>:</p>"
.'<blockquote style="border-left: 4px solid #4f46e5; background-color: #f8fafc; padding: 14px 18px; margin: 18px 0; border-radius: 0 8px 8px 0; font-style: normal; color: #334155;">'
."<p style=\"margin: 0; font-size: 14px; line-height: 1.6;\">{$commentContent}</p>"
.'</blockquote>'
.'<p style="margin-top: 24px;">'
."<a href=\"{$blogUrl}\" target=\"_blank\" style=\"display: inline-block; background-color: #4f46e5; color: #ffffff; padding: 10px 20px; font-weight: 600; text-decoration: none; border-radius: 10px; font-size: 13px;\">"
.'View & Reply to Comment &rarr;'
.'</a>'
.'</p>';

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 = "<p><strong>{$reactorName}</strong> just loved your article <a href=\"{$blogUrl}\" target=\"_blank\"><strong>\"{$blogTitle}\"</strong></a>.</p>"
.'<p>Readers are appreciating your work! Keep sharing knowledge with the community.</p>';
} else {
$mailSubject = "🎉 Milestone: {$milestoneCount} people loved your blog post!";
$headline = "🎉 Exciting News! {$milestoneCount} Love Reactions Milestone reached!";
$message = "<p>Your article <a href=\"{$blogUrl}\" target=\"_blank\"><strong>\"{$blogTitle}\"</strong></a> just hit <strong>{$milestoneCount} love reactions</strong>, with the latest from <strong>{$reactorName}</strong>!</p>"
.'<p>Thank you for creating content that resonates with our students and readers.</p>';
}

$mailContent = "<p style=\"font-size: 16px; font-weight: 700; color: #4f46e5;\">{$headline}</p>"
.$message
.'<p style="margin-top: 24px;">'
."<a href=\"{$blogUrl}\" target=\"_blank\" style=\"display: inline-block; background-color: #4f46e5; color: #ffffff; padding: 10px 20px; font-weight: 600; text-decoration: none; border-radius: 10px; font-size: 13px;\">"
.'View Blog Post &rarr;'
.'</a>'
.'</p>';

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,
],
);
}
}
33 changes: 18 additions & 15 deletions resources/js/pages/Blog/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -445,26 +445,29 @@ const goBack = () => {
</div>
</article>

<!-- "Write for us" CTA Banner -->
<section
class="mt-12 rounded-2xl border border-indigo-100 bg-indigo-50/50 p-6 sm:flex sm:items-center sm:justify-between dark:border-indigo-500/30 dark:bg-indigo-500/10"
<!-- "Write for us" Minimal Callout -->
<div
class="mt-8 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-indigo-100/80 bg-indigo-50/40 px-4 py-3 text-xs text-slate-600 dark:border-indigo-950/60 dark:bg-indigo-950/20 dark:text-gray-400"
>
<div class="mb-4 sm:mb-0">
<h3 class="text-lg font-bold text-slate-900 dark:text-gray-100">
Want to write a blog on this site?
</h3>
<p class="mt-1 text-sm text-slate-600 dark:text-gray-400">
Share your thoughts, stories, and expertise with our
community.
</p>
<div class="flex items-center gap-2">
<span class="font-semibold text-slate-900 dark:text-gray-200"
>Want to write a blog here?</span
>
<span class="hidden text-slate-400 sm:inline dark:text-gray-500"
>•</span
>
<span class="hidden sm:inline"
>Share your thoughts with the community.</span
>
</div>
<Link
href="/join"
class="inline-flex items-center justify-center rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
class="inline-flex items-center gap-1 font-semibold text-indigo-600 hover:underline dark:text-indigo-400"
>
Join us
<span>Join us</span>
<span aria-hidden="true">&rarr;</span>
</Link>
</section>
</div>

<!-- Comments Section -->
<section
Expand Down Expand Up @@ -636,7 +639,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"
>
<Trash2 class="h-4 w-4" />
</button>
Expand Down
39 changes: 37 additions & 2 deletions tests/Feature/BlogTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use App\Mail\BlogCommentNotificationMail;
use App\Mail\BlogNotificationMail;
use App\Models\Blog;
use App\Models\BlogComment;
use App\Models\User;
Expand Down Expand Up @@ -159,7 +159,7 @@
])
->assertSessionHas('success');

Mail::assertQueued(BlogCommentNotificationMail::class, function ($mail) use ($author) {
Mail::assertQueued(BlogNotificationMail::class, function ($mail) use ($author) {
return $mail->hasTo($author->email);
});
});
Expand All @@ -181,3 +181,38 @@

Mail::assertNothingQueued();
});

test('email is queued to author on milestone reactions', function () {
Mail::fake();

$author = User::factory()->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(BlogNotificationMail::class, 1);

// 2nd reaction (not milestone) -> does not queue new email
$this->actingAs($secondUser)->post("/blogs/{$blog->slug}/react");
Mail::assertQueued(BlogNotificationMail::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();
});