Files
padmaja/components/reviews/ReviewsList.tsx
2026-01-17 14:17:42 +05:30

448 lines
14 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Star, Filter, Plus } from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import ReviewCard from './ReviewCard'
import ReviewForm from './ReviewForm'
import { toast } from 'sonner'
import { useSession } from 'next-auth/react'
interface Review {
id: string
rating: number
title?: string
comment?: string
images: string[]
isVerified: boolean
helpfulVotes: number
createdAt: string
userHasVoted?: boolean // Track if current user has voted this review as helpful
user: {
id: string
name: string
image?: string
}
_count?: {
helpfulVotedBy: number
}
}
interface ReviewsListProps {
productId: string
productName: string
averageRating?: number
totalReviews?: number
ratingBreakdown?: { [key: number]: number }
disableImageUpload?: boolean
}
export default function ReviewsList({
productId,
productName,
averageRating = 0,
totalReviews = 0,
ratingBreakdown = {},
disableImageUpload = false
}: ReviewsListProps) {
const { data: session } = useSession()
const [reviews, setReviews] = useState<Review[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [ratingFilter, setRatingFilter] = useState<string>('all')
const [showForm, setShowForm] = useState(false)
const [editingReview, setEditingReview] = useState<Review | null>(null)
const [userReview, setUserReview] = useState<Review | null>(null)
const [allReviews, setAllReviews] = useState<Review[]>([])
const [calculatedStats, setCalculatedStats] = useState({
averageRating: 0,
totalReviews: 0,
ratingBreakdown: {} as { [key: number]: number }
})
const scrollToReviews = () => {
const reviewsSection = document.getElementById('reviews-section')
if (reviewsSection) {
reviewsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
// If there are no reviews, automatically show the form
if (calculatedStats.totalReviews === 0) {
setTimeout(() => setShowForm(true), 500)
}
}
}
const calculateStats = useCallback((allReviewsData: Review[]) => {
const total = allReviewsData.length
if (total === 0) {
setCalculatedStats({
averageRating: 0,
totalReviews: 0,
ratingBreakdown: {}
})
return
}
const sum = allReviewsData.reduce((acc, review) => acc + review.rating, 0)
const avg = sum / total
const breakdown: { [key: number]: number } = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }
allReviewsData.forEach(review => {
breakdown[review.rating] = (breakdown[review.rating] || 0) + 1
})
setCalculatedStats({
averageRating: avg,
totalReviews: total,
ratingBreakdown: breakdown
})
}, [])
const fetchAllReviewsForStats = useCallback(async () => {
try {
const params = new URLSearchParams({
productId,
limit: '1000' // Get all reviews for stats
})
const response = await fetch(`/api/reviews?${params}`)
const data = await response.json()
if (response.ok) {
setAllReviews(data.reviews)
calculateStats(data.reviews)
}
} catch (error) {
console.error('Failed to fetch all reviews for stats:', error)
}
}, [productId, calculateStats])
const fetchReviews = useCallback(async (resetList = false) => {
try {
setLoading(true)
const params = new URLSearchParams({
productId,
page: resetList ? '1' : page.toString(),
limit: '10'
})
if (ratingFilter !== 'all') {
params.append('rating', ratingFilter)
}
const response = await fetch(`/api/reviews?${params}`)
const data = await response.json()
if (response.ok) {
if (resetList) {
setReviews(data.reviews)
setPage(1)
} else {
setReviews(prev => [...prev, ...data.reviews])
}
setHasMore(data.pagination.page < data.pagination.pages)
// Check if current user has a review
if (session?.user) {
const userReviewInList = data.reviews.find(
(review: Review) => review.user.id === session.user.id
)
setUserReview(userReviewInList || null)
}
// If this is the first page, also fetch all reviews for stats
if (resetList) {
fetchAllReviewsForStats()
}
} else {
toast.error('Failed to load reviews')
}
} catch (error) {
toast.error('Failed to load reviews')
} finally {
setLoading(false)
}
}, [productId, page, ratingFilter, session?.user, fetchAllReviewsForStats])
useEffect(() => {
fetchReviews(true)
}, [fetchReviews])
const loadMore = () => {
setPage(prev => prev + 1)
fetchReviews()
}
const handleReviewSubmit = (newReview: Review) => {
setShowForm(false)
setEditingReview(null)
setUserReview(newReview)
fetchReviews(true) // Refresh the list
}
const handleEdit = (review: Review) => {
setEditingReview(review)
setShowForm(true)
}
const handleDelete = async (reviewId: string) => {
if (!confirm('Are you sure you want to delete this review?')) return
try {
const response = await fetch(`/api/reviews/${reviewId}`, {
method: 'DELETE'
})
if (response.ok) {
toast.success('Review deleted successfully')
setReviews(prev => prev.filter(review => review.id !== reviewId))
setUserReview(null)
} else {
const data = await response.json()
toast.error(data.error || 'Failed to delete review')
}
} catch (error) {
toast.error('Failed to delete review')
}
}
const handleHelpfulVote = async (reviewId: string) => {
if (!session?.user) {
toast.error('Please sign in to vote')
return
}
try {
const response = await fetch(`/api/reviews/${reviewId}/helpful`, {
method: 'POST'
})
if (response.ok) {
const data = await response.json()
// Update the review in the list with new vote status and count
setReviews(prev => prev.map(review =>
review.id === reviewId
? {
...review,
userHasVoted: data.hasVoted,
helpfulVotes: data.totalVotes,
_count: {
...review._count,
helpfulVotedBy: data.totalVotes
}
}
: review
))
toast.success(data.hasVoted ? 'Marked as helpful' : 'Removed helpful vote')
} else {
const data = await response.json()
toast.error(data.error || 'Failed to vote')
}
} catch (error) {
toast.error('Failed to vote')
}
}
const renderRatingBreakdown = () => {
if (!calculatedStats.totalReviews) return null
return (
<div className="space-y-2">
{[5, 4, 3, 2, 1].map(rating => {
const count = calculatedStats.ratingBreakdown[rating] || 0
const percentage = calculatedStats.totalReviews > 0 ? (count / calculatedStats.totalReviews) * 100 : 0
return (
<div key={rating} className="flex items-center space-x-2 text-sm">
<div className="flex items-center space-x-1 w-16">
<span>{rating}</span>
<Star className="h-3 w-3 text-yellow-400 fill-current" />
</div>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-yellow-400 h-2 rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="w-8 text-right">{count}</span>
</div>
)
})}
</div>
)
}
return (
<div id="reviews-section" className="space-y-6">
{/* Reviews Summary - Only show if there are reviews */}
{calculatedStats.totalReviews > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Customer Reviews</span>
{session && !userReview && !showForm && (
<Button onClick={scrollToReviews}>
<Plus className="h-4 w-4 mr-2" />
Write Review
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Overall Rating */}
<div className="text-center">
<div className="text-4xl font-bold mb-2">{calculatedStats.averageRating.toFixed(1)}</div>
<div className="flex items-center justify-center mb-2">
{Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className={`h-5 w-5 ${
index < Math.round(calculatedStats.averageRating)
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))}
</div>
<p className="text-gray-600">{calculatedStats.totalReviews} reviews</p>
</div>
{/* Rating Breakdown */}
<div>
<h4 className="font-semibold mb-3">Rating Breakdown</h4>
{renderRatingBreakdown()}
</div>
</div>
</CardContent>
</Card>
)}
{/* No Reviews State - Show when there are no reviews */}
{calculatedStats.totalReviews === 0 && !loading && (
<Card>
<CardContent className="text-center py-12">
<div className="space-y-4">
<div className="flex items-center justify-center">
{Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className="h-8 w-8 text-gray-300"
/>
))}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">No reviews yet</h3>
<p className="text-gray-600 mb-4">Be the first to review this product</p>
{session ? (
<Button onClick={() => setShowForm(true)} className="bg-emerald-600 hover:bg-emerald-700">
<Plus className="h-4 w-4 mr-2" />
Write First Review
</Button>
) : (
<p className="text-sm text-gray-500">Sign in to write a review</p>
)}
</div>
</div>
</CardContent>
</Card>
)}
{/* Review Form */}
{showForm && (
<ReviewForm
productId={productId}
productName={productName}
existingReview={editingReview || undefined}
disableImageUpload={disableImageUpload}
onSubmit={handleReviewSubmit}
onCancel={() => {
setShowForm(false)
setEditingReview(null)
}}
/>
)}
{/* Filters and Reviews List */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">All Reviews ({calculatedStats.totalReviews})</h3>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-500" />
<Select value={ratingFilter} onValueChange={setRatingFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Ratings</SelectItem>
<SelectItem value="5">5 Stars</SelectItem>
<SelectItem value="4">4 Stars</SelectItem>
<SelectItem value="3">3 Stars</SelectItem>
<SelectItem value="2">2 Stars</SelectItem>
<SelectItem value="1">1 Star</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Reviews */}
{loading && reviews.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600">Loading reviews...</p>
</div>
) : reviews.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">No reviews yet</p>
{session && !userReview && (
<Button onClick={() => setShowForm(true)}>
Be the first to review
</Button>
)}
</div>
) : (
<div>
{reviews.map((review) => (
<ReviewCard
key={review.id}
review={review}
onEdit={handleEdit}
onDelete={handleDelete}
onHelpfulVote={handleHelpfulVote}
userHasVoted={review.userHasVoted || false}
/>
))}
{hasMore && (
<div className="text-center pt-4">
<Button
variant="outline"
onClick={loadMore}
disabled={loading}
>
{loading ? 'Loading...' : 'Load More Reviews'}
</Button>
</div>
)}
</div>
)}
</div>
</div>
)
}