first commit

This commit is contained in:
2026-01-17 14:17:42 +05:30
commit 0f194eb9e7
328 changed files with 73544 additions and 0 deletions

View 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>
)
}