first commit
This commit is contained in:
224
components/reviews/ReviewCard.tsx
Normal file
224
components/reviews/ReviewCard.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Star, ThumbsUp, Flag, MoreHorizontal, Edit, Trash2, Verified } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import Image from 'next/image'
|
||||
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
|
||||
user: {
|
||||
id: string
|
||||
name: string
|
||||
image?: string
|
||||
}
|
||||
_count?: {
|
||||
helpfulVotedBy: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ReviewCardProps {
|
||||
review: Review
|
||||
onEdit?: (review: Review) => void
|
||||
onDelete?: (reviewId: string) => void
|
||||
onHelpfulVote?: (reviewId: string) => void
|
||||
onReport?: (reviewId: string) => void
|
||||
userHasVoted?: boolean
|
||||
}
|
||||
|
||||
export default function ReviewCard({
|
||||
review,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onHelpfulVote,
|
||||
onReport,
|
||||
userHasVoted = false
|
||||
}: ReviewCardProps) {
|
||||
const { data: session } = useSession()
|
||||
const [isVoting, setIsVoting] = useState(false)
|
||||
const [hasVoted, setHasVoted] = useState(userHasVoted)
|
||||
const [voteCount, setVoteCount] = useState(review.helpfulVotes)
|
||||
|
||||
const isOwner = session?.user?.id === review.user.id
|
||||
|
||||
const handleHelpfulVote = async () => {
|
||||
if (!session) {
|
||||
toast.error('Please sign in to vote on reviews')
|
||||
return
|
||||
}
|
||||
|
||||
setIsVoting(true)
|
||||
try {
|
||||
const response = await fetch(`/api/reviews/${review.id}/helpful`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
setHasVoted(data.voted)
|
||||
setVoteCount(prev => data.voted ? prev + 1 : prev - 1)
|
||||
toast.success(data.message)
|
||||
onHelpfulVote?.(review.id)
|
||||
} else {
|
||||
toast.error(data.error || 'Failed to vote')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to vote on review')
|
||||
} finally {
|
||||
setIsVoting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReport = () => {
|
||||
if (!session) {
|
||||
toast.error('Please sign in to report reviews')
|
||||
return
|
||||
}
|
||||
onReport?.(review.id)
|
||||
}
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return Array.from({ length: 5 }, (_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={`h-4 w-4 ${
|
||||
index < rating
|
||||
? 'text-yellow-400 fill-current'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={review.user.image} alt={review.user.name} />
|
||||
<AvatarFallback>
|
||||
{review.user.name?.split(' ').map(n => n[0]).join('') || 'U'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-semibold">{review.user.name}</span>
|
||||
{review.isVerified && (
|
||||
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||
<Verified className="h-3 w-3 mr-1" />
|
||||
Verified Purchase
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<div className="flex items-center space-x-1">
|
||||
{renderStars(review.rating)}
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>{formatDistanceToNow(new Date(review.createdAt), { addSuffix: true })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{isOwner && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit?.(review)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit Review
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete?.(review.id)}
|
||||
className="text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Review
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
{review.title && (
|
||||
<h4 className="font-semibold mb-2">{review.title}</h4>
|
||||
)}
|
||||
|
||||
{review.comment && (
|
||||
<p className="text-gray-700 mb-3 leading-relaxed">{review.comment}</p>
|
||||
)}
|
||||
|
||||
{review.images.length > 0 && (
|
||||
<div className="flex space-x-2 mb-3 overflow-x-auto">
|
||||
{review.images.map((image, index) => (
|
||||
<div key={index} className="relative w-20 h-20 flex-shrink-0">
|
||||
<Image
|
||||
src={image}
|
||||
alt={`Review image ${index + 1}`}
|
||||
fill
|
||||
className="object-cover rounded-md"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleHelpfulVote}
|
||||
disabled={isVoting}
|
||||
className={`${hasVoted ? 'text-green-600' : 'text-gray-600'}`}
|
||||
>
|
||||
<ThumbsUp className={`h-4 w-4 mr-1 ${hasVoted ? 'fill-current' : ''}`} />
|
||||
Helpful ({voteCount})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReport}
|
||||
className="text-gray-600 hover:text-red-600"
|
||||
>
|
||||
<Flag className="h-4 w-4 mr-1" />
|
||||
Report
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user