<?php
namespace App\Livewire\Reviews;
use App\Modules\Reviews\Models\Review;
use App\Modules\Reviews\Services\ReviewService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
use Livewire\WithPagination;
/**
* Renders the full reviews section on the product detail page.
*
* Usage: <livewire:reviews.product-reviews :product-id="$product->id" />
*/
class ProductReviews extends Component
{
use WithPagination;
public int $productId;
// Write-review form fields
public int $rating = 5;
public string $title = '';
public string $body = '';
public bool $showForm = false;
public bool $submitted = false;
public ?string $formError = null;
// Filter / sort
public string $sortBy = 'recent'; // recent | helpful | rating_high | rating_low
public int $filterRating = 0; // 0 = all
protected $rules = [
'rating' => 'required|integer|min:1|max:5',
'title' => 'nullable|string|max:120',
'body' => 'nullable|string|max:2000',
];
public function mount(int $productId): void
{
$this->productId = $productId;
}
/**
* Can the current user submit a review?
* They must be logged in, have a delivered order, and not yet reviewed this product.
*/
public function getCanReviewProperty(): bool
{
if (! Auth::check()) return false;
return DB::table('order_items')
->join('orders', 'orders.id', '=', 'order_items.order_id')
->where('orders.user_id', Auth::id())
->where('order_items.product_id', $this->productId)
->where('orders.status', 'delivered')
->whereNotExists(function ($q) {
$q->from('reviews')
->where('user_id', Auth::id())
->where('product_id', $this->productId);
})
->exists();
}
public function updatedSortBy(): void
{
$this->resetPage();
}
public function updatedFilterRating(): void
{
$this->resetPage();
}
public function toggleForm(): void
{
if (! Auth::check()) {
$this->redirectRoute('login');
return;
}
$this->showForm = ! $this->showForm;
}
public function submitReview(): void
{
$this->formError = null;
$this->validate();
try {
app(ReviewService::class)->submit(Auth::user(), $this->productId, [
'rating' => $this->rating,
'title' => $this->title ?: null,
'body' => $this->body ?: null,
]);
$this->submitted = true;
$this->showForm = false;
$this->reset(['rating', 'title', 'body']);
} catch (\RuntimeException $e) {
$this->formError = $e->getMessage();
}
}
public function markHelpful(int $reviewId): void
{
if (! Auth::check()) {
$this->redirectRoute('login');
return;
}
Review::findOrFail($reviewId)->increment('helpful_count');
}
public function render()
{
$stats = app(ReviewService::class)->getRatingStats($this->productId);
$query = Review::approved()
->where('product_id', $this->productId)
->with('user:id,name');
if ($this->filterRating > 0) {
$query->where('rating', $this->filterRating);
}
$query->when($this->sortBy === 'helpful', fn ($q) => $q->orderByDesc('helpful_count'))
->when($this->sortBy === 'rating_high', fn ($q) => $q->orderByDesc('rating'))
->when($this->sortBy === 'rating_low', fn ($q) => $q->orderBy('rating'))
->when($this->sortBy === 'recent', fn ($q) => $q->orderByDesc('created_at'));
$reviews = $query->paginate(5);
return view('livewire.reviews.product-reviews', [
'stats' => $stats,
'reviews' => $reviews,
]);
}
}