feature(users can comment on deals)
This commit is contained in:
parent
aa7e2f245f
commit
a06fac4fef
@ -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
|
||||
|
||||
44
app/Http/Controllers/CommentController.php
Normal file
44
app/Http/Controllers/CommentController.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CommentRequest;
|
||||
use App\Models\Comment;
|
||||
use App\Models\Deal;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
public function index(Deal $deal)
|
||||
{
|
||||
$comments = $deal->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();
|
||||
}
|
||||
}
|
||||
@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
20
app/Http/Requests/CommentRequest.php
Normal file
20
app/Http/Requests/CommentRequest.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CommentRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'text' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ public function rules(): array
|
||||
'message' => 'required|string|min:10|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return parent::getRedirectUrl().'#contact';
|
||||
|
||||
25
app/Models/Comment.php
Normal file
25
app/Models/Comment.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Comment extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'text',
|
||||
'deal_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public function deal(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Deal::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -152,4 +152,9 @@ public function reports(): HasMany
|
||||
{
|
||||
return $this->hasMany(Report::class);
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Deal;
|
||||
use App\Models\User;
|
||||
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::create('comments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('text');
|
||||
$table->foreignIdFor(Deal::class);
|
||||
$table->foreignIdFor(User::class);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('comments');
|
||||
}
|
||||
};
|
||||
22
resources/js/comments.js
Normal file
22
resources/js/comments.js
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
@props(['broker' => ''])
|
||||
<div class="p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl">
|
||||
<div {{$attributes->merge(['class' => "p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl"])}}>
|
||||
<p class="font-bold mb-2">Broker Contact</p>
|
||||
<div class="text-accent-600 space-y-1">
|
||||
<p data-is-loading="false" class="broker-name">{{$broker->name ?? ''}}</p>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
@props(['comments' => []])
|
||||
<div data-is-loading="true" class="comments-container mt-2 space-y-2 max-h-40 overflow-y-scroll data-[is-loading=true]:h-10">
|
||||
@forelse($comments as $comment)
|
||||
<x-dashboard.user.deal-comment.item :comment="$comment"/>
|
||||
@empty
|
||||
<div class="rounded-lg bg-white border border-gray-200 py-2 px-3">
|
||||
<p class="text-center text-accent-600">No comments posted !</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
@props(['comment'])
|
||||
<div class="rounded-lg bg-white border border-gray-200 py-2 px-3">
|
||||
<p class="font-bold text-accent-600">{{$comment->user->name}}</p>
|
||||
<p>{{$comment->text}}</p>
|
||||
</div>
|
||||
@ -1,19 +1,21 @@
|
||||
<x-ui.modal id="deal-modal" class="deal-identifier w-11/12 md:w-10/12">
|
||||
<x-ui.modal id="deal-modal" class="deal-identifier w-11/12 md:w-10/12 overflow-scroll">
|
||||
<form class="flex justify-between items-start mb-4" method="dialog">
|
||||
<p class="text-xl font-bold">Deal Details</p>
|
||||
<button type="submit" class="">
|
||||
<x-heroicon-o-x-mark class="w-4"/>
|
||||
</button>
|
||||
</form>
|
||||
<div class="grid md:grid-cols-12 gap-4 items-stretch">
|
||||
<div data-is-loading="true" class="md:col-span-8 h-0 min-h-full data-[is-loading=true]:h-60">
|
||||
<div class="grid md:grid-cols-12 gap-4 items-stretch mb-4">
|
||||
<div data-is-loading="true" class="md:col-span-8 h-40 md:h-0 min-h-full data-[is-loading=true]:h-60">
|
||||
<div class="rounded-lg bg-gray-200 h-full">
|
||||
<img src="" alt=" "
|
||||
class="deal-image h-full w-full object-cover rounded-lg border-none">
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-4 flex flex-col gap-y-4">
|
||||
<x-ui.button-sm data-is-loading="true" class="w-fit deal-category data-[is-loading=true]:h-6 data-[is-loading=true]:w-15" variant="neutral"/>
|
||||
<x-ui.button-sm data-is-loading="true"
|
||||
class="w-fit deal-category data-[is-loading=true]:h-6 data-[is-loading=true]:w-15"
|
||||
variant="neutral"/>
|
||||
<p data-is-loading="true" class="deal-title font-bold text-lg data-[is-loading=true]:h-3 "></p>
|
||||
<p data-is-loading="true" class="deal-description text-sm text-accent-600 wrap-break-word"></p>
|
||||
<div class="flex items-center justify-between">
|
||||
@ -23,15 +25,26 @@ class="deal-image h-full w-full object-cover rounded-lg border-none">
|
||||
</div>
|
||||
<x-dashboard.user.deal-stats/>
|
||||
</div>
|
||||
<x-ui.button variant="neutral" data-is-loading="false" class="hidden deal-link space-x-2 items-center justify-center">
|
||||
<x-ui.button variant="neutral" data-is-loading="false"
|
||||
class="hidden deal-link space-x-2 items-center justify-center">
|
||||
<div class="flex space-x-2">
|
||||
<p>View Deal</p>
|
||||
<x-heroicon-o-arrow-top-right-on-square class="w-5 ml-1"/>
|
||||
</div>
|
||||
</x-ui.button>
|
||||
<x-dashboard.user.broker-contact/>
|
||||
<x-dashboard.user.broker-contact id="comments"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="p-2 text-sm bg-gray-50 border-gray-200 border rounded-xl">
|
||||
<p class="font-bold mb-2">Comments</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-ui.input class="flex-1" name="comment" placeholder="Add a comment..."/>
|
||||
<x-ui.button id="commentSubmitBtn" variant="ghost" icon="paper-airplane">
|
||||
<p class="hidden md:block">Comment</p>
|
||||
</x-ui.button>
|
||||
</div>
|
||||
<x-dashboard.user.deal-comment/>
|
||||
</div>
|
||||
</x-ui.modal>
|
||||
|
||||
@ -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 @@
|
||||
</div>
|
||||
</a>
|
||||
@else
|
||||
<button {{$attributes->merge(['class' => "font-medium hover:opacity-80 active:scale-80 transition-all ease-in-out duration-300 $variantClass", 'type'=>'submit'])}}>
|
||||
<div class="flex justify-center items-center space-x-2">
|
||||
<button {{$attributes->merge(['class' => "font-medium active:scale-80 transition-all ease-in-out duration-300 $variantClass", 'type'=>'submit'])}}>
|
||||
<div class="flex justify-center items-center md:space-x-2">
|
||||
@if($icon !=='')
|
||||
@svg("heroicon-o-$icon", 'w-5 h-5')
|
||||
@endif
|
||||
@if(filled($slot))
|
||||
<p>{{$slot}}</p>
|
||||
{{$slot}}
|
||||
@endif
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\CommentController;
|
||||
use App\Http\Controllers\Interaction\InteractionController;
|
||||
use App\Http\Controllers\Interaction\ReportController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@ -12,3 +13,5 @@
|
||||
Route::post('/report/{deal}', [ReportController::class, 'store'])->name('report-deal');
|
||||
Route::delete('/report/{deal}', [ReportController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::apiResource('deals.comments', CommentController::class)->except(['update', 'edit', 'destroy']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user