From a06fac4fef7dc1a10462c84b6deaf391bb33907d Mon Sep 17 00:00:00 2001 From: kusowl Date: Wed, 4 Feb 2026 16:50:20 +0530 Subject: [PATCH] feature(users can comment on deals) --- .../Controllers/Admin/ReportController.php | 2 +- .../Auth/PasswordResetController.php | 4 +- app/Http/Controllers/CommentController.php | 44 +++++++++++++++++++ app/Http/Controllers/ContactController.php | 2 + app/Http/Requests/CommentRequest.php | 20 +++++++++ app/Http/Requests/ContactRequest.php | 3 +- app/Models/Comment.php | 25 +++++++++++ app/Models/Deal.php | 5 +++ app/Models/User.php | 5 +++ app/Notifications/NewContactNotification.php | 8 ++-- .../ReportRejectedNotificationToUser.php | 9 ++-- .../ReportResolvedNotificationToBroker.php | 3 +- .../ReportResolvedNotificationToUser.php | 7 ++- ...026_02_04_051922_create_comments_table.php | 26 +++++++++++ resources/js/comments.js | 22 ++++++++++ resources/js/deal-view-modal.js | 30 +++++++++++-- .../dashboard/user/broker-contact.blade.php | 2 +- .../user/deal-comment/index.blade.php | 11 +++++ .../user/deal-comment/item.blade.php | 5 +++ .../dashboard/user/deal-modal.blade.php | 25 ++++++++--- .../views/components/ui/button.blade.php | 12 ++--- routes/api/interactions.php | 3 ++ 22 files changed, 236 insertions(+), 37 deletions(-) create mode 100644 app/Http/Controllers/CommentController.php create mode 100644 app/Http/Requests/CommentRequest.php create mode 100644 app/Models/Comment.php create mode 100644 database/migrations/2026_02_04_051922_create_comments_table.php create mode 100644 resources/js/comments.js create mode 100644 resources/views/components/dashboard/user/deal-comment/index.blade.php create mode 100644 resources/views/components/dashboard/user/deal-comment/item.blade.php diff --git a/app/Http/Controllers/Admin/ReportController.php b/app/Http/Controllers/Admin/ReportController.php index 6ad4427..2542156 100644 --- a/app/Http/Controllers/Admin/ReportController.php +++ b/app/Http/Controllers/Admin/ReportController.php @@ -5,8 +5,8 @@ use App\Enums\ReportStatus; use App\Http\Controllers\Controller; use App\Models\Report; -use App\Notifications\ReportResolvedNotificationToBroker; use App\Notifications\ReportRejectedNotificationToUser; +use App\Notifications\ReportResolvedNotificationToBroker; use Illuminate\Support\Facades\DB; class ReportController extends Controller diff --git a/app/Http/Controllers/Auth/PasswordResetController.php b/app/Http/Controllers/Auth/PasswordResetController.php index 1d1fe1f..61b8563 100644 --- a/app/Http/Controllers/Auth/PasswordResetController.php +++ b/app/Http/Controllers/Auth/PasswordResetController.php @@ -46,7 +46,7 @@ public function verify(Request $request, VerifyOTPAction $otpAction) $data = $request->validate(['otp' => 'required|string:min:5:max:6']); try { $isVerified = $otpAction->execute($data); - if (!$isVerified) { + if (! $isVerified) { return back()->with('error', 'Invalid OTP'); } @@ -67,7 +67,7 @@ public function update(Request $request) 'password' => 'required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols(), ]); $user = User::find(Session::get('otp_user_id')); - if (!$user) { + if (! $user) { return back()->with('error', 'Session Expired'); } $user->update(['password' => $data['password']]); diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php new file mode 100644 index 0000000..11bf6bc --- /dev/null +++ b/app/Http/Controllers/CommentController.php @@ -0,0 +1,44 @@ +comments()->with('user')->latest()->get(); + $html = view('components.dashboard.user.deal-comment.index', compact('comments'))->render(); + + return response()->json(['html' => $html]); + } + + public function store(Deal $deal, CommentRequest $request) + { + $data = $request->validated(); + $data['user_id'] = $request->user()->id; + $data['deal_id'] = $deal->id; + + Comment::create($data); + + return response()->json(['message' => 'Comment created successfully.'], 201); + } + + public function show(Comment $comment) {} + + public function update(CommentRequest $request, Comment $comment) + { + $comment->update($request->validated()); + + } + + public function destroy(Comment $comment) + { + $comment->delete(); + + return response()->json(); + } +} diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 32939b4..0e8f829 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -15,9 +15,11 @@ public function __invoke(ContactRequest $request) $data = $request->validated(); $admin = Admin::first(); $admin->user->notify(new NewContactNotification($data['name'], $data['email'], $data['message'])); + return back()->with('success', 'Your message has been sent successfully.'); } catch (\Throwable $e) { \Log::error('Error sending contact message', [$e->getMessage()]); + return back()->with('error', 'Something went wrong.'); } } diff --git a/app/Http/Requests/CommentRequest.php b/app/Http/Requests/CommentRequest.php new file mode 100644 index 0000000..08b7f68 --- /dev/null +++ b/app/Http/Requests/CommentRequest.php @@ -0,0 +1,20 @@ + ['required', 'string', 'max:255'], + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/app/Http/Requests/ContactRequest.php b/app/Http/Requests/ContactRequest.php index 749558e..00e2335 100644 --- a/app/Http/Requests/ContactRequest.php +++ b/app/Http/Requests/ContactRequest.php @@ -27,8 +27,9 @@ public function rules(): array 'message' => 'required|string|min:10|max:255', ]; } + protected function getRedirectUrl(): string { - return parent::getRedirectUrl() . '#contact'; + return parent::getRedirectUrl().'#contact'; } } diff --git a/app/Models/Comment.php b/app/Models/Comment.php new file mode 100644 index 0000000..3770640 --- /dev/null +++ b/app/Models/Comment.php @@ -0,0 +1,25 @@ +belongsTo(Deal::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Deal.php b/app/Models/Deal.php index 424ab55..411adc7 100644 --- a/app/Models/Deal.php +++ b/app/Models/Deal.php @@ -76,6 +76,11 @@ public function reports(): BelongsToMany return $this->belongsToMany(Report::class); } + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } + /** * Scope a query to only include active deals */ diff --git a/app/Models/User.php b/app/Models/User.php index 814e40c..fcedc85 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -152,4 +152,9 @@ public function reports(): HasMany { return $this->hasMany(Report::class); } + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } } diff --git a/app/Notifications/NewContactNotification.php b/app/Notifications/NewContactNotification.php index 78def0e..ff94d47 100644 --- a/app/Notifications/NewContactNotification.php +++ b/app/Notifications/NewContactNotification.php @@ -15,8 +15,7 @@ public function __construct( private readonly string $customerName, private readonly string $customerEmail, private readonly string $customerMessage - ) { - } + ) {} public function via($notifiable): array { @@ -31,9 +30,8 @@ public function toMail($notifiable): MailMessage ->line('You have received a new message from your contact form:') ->line("**Name:** {$this->customerName}") ->line("**Email:** {$this->customerEmail}") - ->line("**Message:**") + ->line('**Message:**') ->line($this->customerMessage) - ->action('Reply via Email', 'mailto:'.$this->customerEmail);; + ->action('Reply via Email', 'mailto:'.$this->customerEmail); } - } diff --git a/app/Notifications/ReportRejectedNotificationToUser.php b/app/Notifications/ReportRejectedNotificationToUser.php index fa77a36..081f19c 100644 --- a/app/Notifications/ReportRejectedNotificationToUser.php +++ b/app/Notifications/ReportRejectedNotificationToUser.php @@ -13,8 +13,7 @@ class ReportRejectedNotificationToUser extends Notification implements ShouldQue public function __construct( private readonly string $dealTitle, - ) { - } + ) {} public function via($notifiable): array { @@ -27,9 +26,9 @@ public function toMail($notifiable): MailMessage return (new MailMessage) ->subject('Update on Your Recent Report: '.$this->dealTitle) ->greeting('Hello!') - ->line("Thank you for helping us maintain the integrity of our marketplace.") + ->line('Thank you for helping us maintain the integrity of our marketplace.') ->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.") - ->line("Based on our moderation policy, we have rejected your report.") + ->line('Based on our moderation policy, we have rejected your report.') ->action('View Marketplace', route('explore')) ->line('Your feedback helps make our community a safer place for everyone.'); } @@ -38,7 +37,7 @@ public function toArray($notifiable): array { return [ 'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found', - 'deal_title' => $this->dealTitle + 'deal_title' => $this->dealTitle, ]; } } diff --git a/app/Notifications/ReportResolvedNotificationToBroker.php b/app/Notifications/ReportResolvedNotificationToBroker.php index ccae8d1..204dc18 100644 --- a/app/Notifications/ReportResolvedNotificationToBroker.php +++ b/app/Notifications/ReportResolvedNotificationToBroker.php @@ -14,8 +14,7 @@ class ReportResolvedNotificationToBroker extends Notification implements ShouldQ public function __construct( private readonly string $dealTitle, private readonly bool $isContentRemoved - ) { - } + ) {} public function via($notifiable): array { diff --git a/app/Notifications/ReportResolvedNotificationToUser.php b/app/Notifications/ReportResolvedNotificationToUser.php index 5db7871..a4fcb08 100644 --- a/app/Notifications/ReportResolvedNotificationToUser.php +++ b/app/Notifications/ReportResolvedNotificationToUser.php @@ -14,8 +14,7 @@ class ReportResolvedNotificationToUser extends Notification implements ShouldQue public function __construct( private readonly string $dealTitle, private readonly bool $isContentRemoved - ) { - } + ) {} public function via($notifiable): array { @@ -31,7 +30,7 @@ public function toMail($notifiable): MailMessage return (new MailMessage) ->subject('Update on Your Recent Report: '.$this->dealTitle) ->greeting('Hello!') - ->line("Thank you for helping us maintain the integrity of our marketplace.") + ->line('Thank you for helping us maintain the integrity of our marketplace.') ->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.") ->line("Based on our moderation policy, the content {$outcome}") ->action('View Marketplace', route('explore')) @@ -42,7 +41,7 @@ public function toArray($notifiable): array { return [ 'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found', - 'deal_title' => $this->dealTitle + 'deal_title' => $this->dealTitle, ]; } } diff --git a/database/migrations/2026_02_04_051922_create_comments_table.php b/database/migrations/2026_02_04_051922_create_comments_table.php new file mode 100644 index 0000000..cb919cb --- /dev/null +++ b/database/migrations/2026_02_04_051922_create_comments_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('text'); + $table->foreignIdFor(Deal::class); + $table->foreignIdFor(User::class); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('comments'); + } +}; diff --git a/resources/js/comments.js b/resources/js/comments.js new file mode 100644 index 0000000..05c94e2 --- /dev/null +++ b/resources/js/comments.js @@ -0,0 +1,22 @@ +export const postComment = async (dealId, text) => { + try { + let data = { + deal_id: dealId, + text: text + }; + + const response = await axios.post(`/api/deals/${dealId}/comments`, data) + console.log(response) + } catch (e) { + console.error(e) + } +} + +export const getComments = async (dealId) => { + try { + const response = await axios.get(`/api/deals/${dealId}/comments`) + return response.data.html + } catch (e) { + console.error(e) + } +} diff --git a/resources/js/deal-view-modal.js b/resources/js/deal-view-modal.js index b6e23df..61ec118 100644 --- a/resources/js/deal-view-modal.js +++ b/resources/js/deal-view-modal.js @@ -2,6 +2,7 @@ import {showToast} from "@/toast.js"; import {closeModal, showModal} from "@/modal.js"; import {redirect} from "./interaction.js"; import {toggleShimmer} from "./shimmer.js"; +import {getComments, postComment} from "./comments.js"; export async function showDealModal(dealId) { if (!dealId) { @@ -11,6 +12,7 @@ export async function showDealModal(dealId) { const dealModal = document.getElementById('deal-modal'); + showModal('deal-modal'); toggleShimmer(false, dealModal); @@ -19,6 +21,8 @@ export async function showDealModal(dealId) { const response = await axios.get('/api/deals/' + dealId); setDealDetails(response.data); + await setComments(dealId, dealModal); + toggleShimmer(true, dealModal); dealModal.dataset.dealId = dealId; @@ -37,10 +41,7 @@ function setDealDetails(dealDetails) { const deal = dealDetails.data const { - id, title, description, link, - image, category, broker, - totalRedirection, totalLikes, - isLiked, isFavorite + id, title, description, link, image, category, broker, totalRedirection, totalLikes, isLiked, isFavorite } = deal; const dealModal = document.getElementById('deal-modal'); @@ -89,6 +90,13 @@ function setDealDetails(dealDetails) { } } +async function setComments(dealId, dealModal) { + const commentsContainer = dealModal.querySelector('.comments-container'); + toggleShimmer(false, commentsContainer); + commentsContainer.innerHTML = await getComments(dealId); + toggleShimmer(true, commentsContainer); +} + window.addEventListener('DOMContentLoaded', () => { const dealCards = document.querySelectorAll('.deal-card'); if (dealCards) { @@ -104,4 +112,18 @@ window.addEventListener('DOMContentLoaded', () => { }); }) } + const commentBtn = document.getElementById('commentSubmitBtn'); + + if (commentBtn) { + commentBtn.addEventListener('click', async () => { + const dealModal = document.getElementById('deal-modal'); + const dealId = dealModal.dataset.dealId; + const textInput = dealModal.querySelector('input[name="comment"]'); + let text = textInput.value; + textInput.value = ''; + + await postComment(dealId, text); + await setComments(dealId, dealModal); + }) + } }); diff --git a/resources/views/components/dashboard/user/broker-contact.blade.php b/resources/views/components/dashboard/user/broker-contact.blade.php index 9bda292..33c8fd9 100644 --- a/resources/views/components/dashboard/user/broker-contact.blade.php +++ b/resources/views/components/dashboard/user/broker-contact.blade.php @@ -1,5 +1,5 @@ @props(['broker' => '']) -
+
merge(['class' => "p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl"])}}>

Broker Contact

{{$broker->name ?? ''}}

diff --git a/resources/views/components/dashboard/user/deal-comment/index.blade.php b/resources/views/components/dashboard/user/deal-comment/index.blade.php new file mode 100644 index 0000000..8abe3b9 --- /dev/null +++ b/resources/views/components/dashboard/user/deal-comment/index.blade.php @@ -0,0 +1,11 @@ +@props(['comments' => []]) +
+ @forelse($comments as $comment) + + @empty +
+

No comments posted !

+
+ @endforelse +
+ diff --git a/resources/views/components/dashboard/user/deal-comment/item.blade.php b/resources/views/components/dashboard/user/deal-comment/item.blade.php new file mode 100644 index 0000000..8def81e --- /dev/null +++ b/resources/views/components/dashboard/user/deal-comment/item.blade.php @@ -0,0 +1,5 @@ +@props(['comment']) +
+

{{$comment->user->name}}

+

{{$comment->text}}

+
diff --git a/resources/views/components/dashboard/user/deal-modal.blade.php b/resources/views/components/dashboard/user/deal-modal.blade.php index 4a6afe7..4b1a26e 100644 --- a/resources/views/components/dashboard/user/deal-modal.blade.php +++ b/resources/views/components/dashboard/user/deal-modal.blade.php @@ -1,19 +1,21 @@ - +

Deal Details

-
- +
+

Comments

+
+ + + + +
+ +
diff --git a/resources/views/components/ui/button.blade.php b/resources/views/components/ui/button.blade.php index a39b000..86e775d 100644 --- a/resources/views/components/ui/button.blade.php +++ b/resources/views/components/ui/button.blade.php @@ -1,9 +1,9 @@ @props(['variant' => '', 'icon' => '', 'link' => '', 'external' => false, 'round' => false]) @php $variants = [ - 'neutral' => 'bg-primary-600 text-white', - 'red' => 'bg-red-500 text-white', - 'ghost' => 'bg-gray-200 text-gray-900' + 'neutral' => 'bg-primary-600 text-white hover:bg-gray-200 hover:text-gray-900', + 'red' => 'bg-red-500 text-white hover:bg-red-400 hover:text-red-800', + 'ghost' => 'bg-gray-200 text-gray-900 hover:bg-primary-600 hover:text-white' ]; $variantClass = $variants[$variant] ?? ''; @@ -29,13 +29,13 @@
@else - diff --git a/routes/api/interactions.php b/routes/api/interactions.php index 468be40..54bc6c7 100644 --- a/routes/api/interactions.php +++ b/routes/api/interactions.php @@ -1,5 +1,6 @@ name('report-deal'); Route::delete('/report/{deal}', [ReportController::class, 'destroy']); }); + +Route::apiResource('deals.comments', CommentController::class)->except(['update', 'edit', 'destroy']);