443 lines
17 KiB
TypeScript
443 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
import { ShoppingCart, Star, Eye, X } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import OptimizedImage from '@/components/ui/OptimizedImage'
|
|
import { motion } from 'framer-motion'
|
|
import { cartManager } from '@/lib/cart'
|
|
import { toast } from 'sonner'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Product } from '@/types'
|
|
import { useState, useEffect } from 'react'
|
|
|
|
interface ProductReviewStats {
|
|
averageRating: number
|
|
totalReviews: number
|
|
}
|
|
|
|
interface ProductCardProps {
|
|
product: Product
|
|
index: number
|
|
}
|
|
|
|
export default function ProductCard({ product, index }: ProductCardProps) {
|
|
const router = useRouter()
|
|
const [isQuickViewOpen, setIsQuickViewOpen] = useState(false)
|
|
const [reviewStats, setReviewStats] = useState<ProductReviewStats>({
|
|
averageRating: 0,
|
|
totalReviews: 0
|
|
})
|
|
|
|
// Fetch review statistics for the product
|
|
useEffect(() => {
|
|
const fetchReviewStats = async () => {
|
|
try {
|
|
const response = await fetch(`/api/reviews?productId=${product.id}&limit=1000`)
|
|
const data = await response.json()
|
|
|
|
if (response.ok && data.reviews) {
|
|
const reviews = data.reviews
|
|
const totalReviews = reviews.length
|
|
|
|
if (totalReviews > 0) {
|
|
const sum = reviews.reduce((acc: number, review: any) => acc + review.rating, 0)
|
|
const averageRating = sum / totalReviews
|
|
|
|
setReviewStats({
|
|
averageRating: Math.round(averageRating * 10) / 10, // Round to 1 decimal
|
|
totalReviews
|
|
})
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch review stats:', error)
|
|
}
|
|
}
|
|
|
|
fetchReviewStats()
|
|
}, [product.id])
|
|
|
|
const getDiscountedPrice = (price: number, discount: number) => {
|
|
return price - (price * discount / 100)
|
|
}
|
|
|
|
// Helper function to calculate per kg price
|
|
const getPerKgPrice = (price: number, weight: string | null, discount: number = 0) => {
|
|
if (!weight) return null
|
|
|
|
// Extract numeric value from weight string (e.g., "1kg", "500g", "2.5 kg")
|
|
const weightMatch = weight.toLowerCase().match(/(\d+(?:\.\d+)?)\s*(kg|g|gram|kilos?)/i)
|
|
if (!weightMatch) return null
|
|
|
|
const value = parseFloat(weightMatch[1])
|
|
const unit = weightMatch[2].toLowerCase()
|
|
|
|
// Convert to kg
|
|
let weightInKg = value
|
|
if (unit.startsWith('g')) {
|
|
weightInKg = value / 1000
|
|
}
|
|
|
|
const finalPrice = discount > 0 ? getDiscountedPrice(price, discount) : price
|
|
return Math.round(finalPrice / weightInKg)
|
|
}
|
|
|
|
const handleAddToCart = (e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
console.log('Add to cart clicked for:', product.name) // Debug log
|
|
|
|
if (product.stock === 0) {
|
|
toast.error('Product is out of stock')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const success = cartManager.addToCart(product, 1)
|
|
if (success) {
|
|
console.log('Item added successfully') // Debug log
|
|
toast.success(`${product.name} added to cart!`)
|
|
} else {
|
|
toast.error('Not enough stock available')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding to cart:', error)
|
|
toast.error('Failed to add to cart')
|
|
}
|
|
}
|
|
|
|
const handleCardClick = () => {
|
|
router.push(`/products/${product.slug}`)
|
|
}
|
|
|
|
const handleViewDetails = (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
setIsQuickViewOpen(true)
|
|
}
|
|
|
|
return (
|
|
<article
|
|
itemScope
|
|
itemType="https://schema.org/Product"
|
|
role="article"
|
|
aria-labelledby={`product-title-${product.id}`}
|
|
aria-describedby={`product-description-${product.id}`}
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
className="h-full"
|
|
>
|
|
<Card
|
|
className="h-full hover:shadow-lg transition-all duration-300 group overflow-hidden cursor-pointer hover:scale-[1.02]"
|
|
onClick={handleCardClick}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
handleCardClick()
|
|
}
|
|
}}
|
|
aria-label={`View details for ${product.name}`}
|
|
>
|
|
<header className="relative overflow-hidden">
|
|
<div itemProp="image">
|
|
<OptimizedImage
|
|
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
|
|
alt={`${product.name} - Premium rice product from Padmaaja Rasooi`}
|
|
width={400}
|
|
height={300}
|
|
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-300"
|
|
priority={index < 4} // Prioritize first 4 products
|
|
quality={85}
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
|
|
/>
|
|
</div>
|
|
|
|
{/* Per kg price badge - Top Left */}
|
|
{getPerKgPrice(product.price, product.weight, product.discount) && (
|
|
<Badge className="absolute top-2 left-2 sm:top-3 sm:left-3 bg-emerald-500 hover:bg-emerald-600 text-white font-semibold shadow-lg text-base">
|
|
₹{getPerKgPrice(product.price, product.weight, product.discount)}/kg
|
|
</Badge>
|
|
)}
|
|
|
|
{/* Discount badge - Top Left, below per kg badge if both exist */}
|
|
{product.discount > 0 && (
|
|
<Badge className={`absolute ${getPerKgPrice(product.price, product.weight, product.discount) ? 'top-8 sm:top-10' : 'top-2 sm:top-3'} left-2 sm:left-3 bg-red-500 hover:bg-red-600 text-white font-semibold shadow-lg text-xs`}>
|
|
{product.discount}% OFF
|
|
</Badge>
|
|
)}
|
|
|
|
{/* Dynamic Rating Display */}
|
|
{reviewStats.totalReviews > 0 ? (
|
|
<div className="absolute top-2 right-2 sm:top-3 sm:right-3 flex items-center bg-white/90 backdrop-blur-sm rounded-full px-2 py-1 shadow-lg">
|
|
<Star className="h-3 w-3 text-yellow-400 fill-current" />
|
|
<span className="text-xs ml-1 font-medium">{reviewStats.averageRating}</span>
|
|
<span className="text-xs ml-1 text-gray-500">({reviewStats.totalReviews})</span>
|
|
</div>
|
|
) : (
|
|
<div className="absolute top-2 right-2 sm:top-3 sm:right-3 flex items-center bg-white/90 backdrop-blur-sm rounded-full px-2 py-1 shadow-lg">
|
|
<Star className="h-3 w-3 text-gray-300" />
|
|
<span className="text-xs ml-1 text-gray-400">No reviews</span>
|
|
</div>
|
|
)}
|
|
{product.stock === 0 && (
|
|
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
|
<Badge variant="destructive" className="text-xs sm:text-sm">Out of Stock</Badge>
|
|
</div>
|
|
)}
|
|
<Badge className="absolute bottom-2 right-2 sm:bottom-3 sm:right-3 bg-white/90 text-slate-700 border border-slate-200 text-xs font-medium shadow-lg backdrop-blur-sm">
|
|
{product.category.name}
|
|
</Badge>
|
|
</header>
|
|
|
|
<CardHeader className="pb-2 p-3 sm:pb-2" role="contentinfo">
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<CardTitle
|
|
className="text-base sm:text-lg font-semibold truncate group-hover:text-blue-600 transition-colors flex-1 leading-tight cursor-help"
|
|
id={`product-title-${product.id}`}
|
|
itemProp="name"
|
|
>
|
|
{product.name}
|
|
</CardTitle>
|
|
</TooltipTrigger>
|
|
<TooltipContent className="max-w-xs">
|
|
<p>{product.name}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<CardDescription
|
|
className="line-clamp-2 md:text-xs text-sm text-gray-600 leading-relaxed"
|
|
id={`product-description-${product.id}`}
|
|
itemProp="description"
|
|
>
|
|
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-0 p-3 sm:p-6 sm:pt-0 space-y-3 sm:space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
{product.discount > 0 ? (
|
|
<>
|
|
<span className="text-base sm:text-lg font-bold text-green-600">
|
|
₹{getDiscountedPrice(product.price, product.discount).toFixed(2)}
|
|
</span>
|
|
<span className="text-xs sm:text-sm text-gray-500 line-through">
|
|
₹{product.price.toFixed(2)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-base sm:text-lg font-bold text-gray-900">
|
|
₹{product.price.toFixed(2)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* <span className="text-xs sm:text-sm text-gray-500">
|
|
Stock: <span className={product.stock > 0 ? 'text-green-600' : 'text-red-600'}>{product.stock}</span>
|
|
</span> */}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={handleAddToCart}
|
|
disabled={product.stock === 0}
|
|
className="flex-1 text-xs sm:text-sm h-8 sm:h-9"
|
|
>
|
|
<ShoppingCart className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
|
|
<span className="hidden sm:inline">{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}</span>
|
|
<span className="sm:hidden">{product.stock === 0 ? 'Out' : 'Add'}</span>
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleViewDetails}
|
|
className="px-2 sm:px-3 h-8 sm:h-9"
|
|
>
|
|
<Eye className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick View Dialog */}
|
|
<Dialog open={isQuickViewOpen} onOpenChange={setIsQuickViewOpen}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-bold">{product.name}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Product Image */}
|
|
<div className="space-y-4">
|
|
<div className="relative aspect-square overflow-hidden rounded-lg border">
|
|
<OptimizedImage
|
|
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
|
|
alt={product.name}
|
|
width={400}
|
|
height={400}
|
|
className="w-full h-full object-contain"
|
|
quality={80}
|
|
/>
|
|
{product.discount > 0 && (
|
|
<Badge className="absolute top-4 left-4 bg-red-500 hover:bg-red-600 text-white font-semibold">
|
|
{product.discount}% OFF
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Additional Images if available */}
|
|
{product.images.length > 1 && (
|
|
<div className="grid grid-cols-4 gap-2">
|
|
{product.images.slice(1, 5).map((image, idx) => (
|
|
<div key={idx} className="aspect-square rounded border overflow-hidden">
|
|
<OptimizedImage
|
|
src={image}
|
|
alt={`${product.name} ${idx + 2}`}
|
|
width={100}
|
|
height={100}
|
|
className="w-full h-full object-contain"
|
|
quality={60}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Product Details */}
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">Product Details</h3>
|
|
<p className="text-gray-600 leading-relaxed">
|
|
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Category and Brand */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Category</span>
|
|
<p className="font-semibold">{product.category?.name}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Brand</span>
|
|
<p className="font-semibold">{product.brand || 'Padmaaja Rasooi'}</p>
|
|
</div>
|
|
{product.weight && (
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Weight</span>
|
|
<p className="font-semibold">{product.weight}</p>
|
|
</div>
|
|
)}
|
|
{product.origin && (
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Origin</span>
|
|
<p className="font-semibold">{product.origin}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pricing */}
|
|
<div className="border-t pt-4">
|
|
<div className="flex items-center space-x-3 mb-4">
|
|
{product.discount > 0 ? (
|
|
<>
|
|
<span className="text-2xl font-bold text-green-600">
|
|
₹{getDiscountedPrice(product.price, product.discount).toFixed(2)}
|
|
</span>
|
|
<span className="text-lg text-gray-500 line-through">
|
|
₹{product.price.toFixed(2)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-2xl font-bold text-gray-900">
|
|
₹{product.price.toFixed(2)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Per kg price in quick view */}
|
|
{getPerKgPrice(product.price, product.weight, product.discount) && (
|
|
<div className="mb-4 text-sm text-gray-600">
|
|
<span className="font-medium">Per kg: </span>
|
|
₹{getPerKgPrice(product.price, product.weight, product.discount)}/kg
|
|
</div>
|
|
)}
|
|
|
|
{/* Stock Status */}
|
|
<div className="mb-4">
|
|
<span className="text-sm font-medium text-gray-500">Availability: </span>
|
|
<span className={`font-semibold ${product.stock > 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
{product.stock > 0 ? `In Stock (${product.stock} available)` : 'Out of Stock'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="space-y-3">
|
|
<Button
|
|
onClick={handleAddToCart}
|
|
disabled={product.stock === 0}
|
|
className="w-full"
|
|
size="lg"
|
|
>
|
|
<ShoppingCart className="h-4 w-4 mr-2" />
|
|
{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setIsQuickViewOpen(false)
|
|
router.push(`/products/${product.slug}`)
|
|
}}
|
|
className="w-full"
|
|
size="lg"
|
|
>
|
|
View Full Details
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dynamic Rating in Quick View */}
|
|
<div className="flex items-center space-x-2 pt-4 border-t">
|
|
<div className="flex items-center">
|
|
{[...Array(5)].map((_, i) => (
|
|
<Star
|
|
key={i}
|
|
className={`h-4 w-4 ${
|
|
reviewStats.totalReviews > 0 && i < Math.round(reviewStats.averageRating)
|
|
? 'text-yellow-400 fill-current'
|
|
: 'text-gray-300'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="text-sm text-gray-600">
|
|
{reviewStats.totalReviews > 0
|
|
? `${reviewStats.averageRating} (${reviewStats.totalReviews} reviews)`
|
|
: 'No reviews yet'
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</motion.div>
|
|
</article>
|
|
)
|
|
}
|
|
|