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,502 @@
'use client'
import { useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'sonner'
import { Loader2, Package, Building, Phone, Mail, MessageCircle } from 'lucide-react'
const inquirySchema = z.object({
// Company Information
companyName: z.string().min(2, 'Company name must be at least 2 characters'),
contactPerson: z.string().min(2, 'Contact person name must be at least 2 characters'),
designation: z.string().min(2, 'Designation is required'),
email: z.string().email('Please enter a valid email address'),
phone: z.string().min(10, 'Please enter a valid phone number'),
// Business Details
businessType: z.string().min(1, 'Please select business type'),
gstNumber: z.string().optional(),
address: z.string().min(10, 'Please provide complete address'),
// Product Requirements
quantityRequired: z.string().min(1, 'Quantity is required'),
quantityUnit: z.string().default('tons'),
deliveryLocation: z.string().min(2, 'Delivery location is required'),
expectedDeliveryDate: z.string().optional(),
// Additional Information
message: z.string().min(10, 'Please provide detailed requirements (minimum 10 characters)'),
hearAboutUs: z.string().optional(),
// Terms
agreedToTerms: z.boolean().refine(val => val === true, {
message: 'You must agree to the terms and conditions'
})
})
type InquiryFormData = z.infer<typeof inquirySchema>
interface B2BInquiryFormProps {
isOpen: boolean
onOpenChange: (open: boolean) => void
product?: {
id: string
name: string
category: { name: string }
price: number
weight?: string
}
}
export default function B2BInquiryForm({ isOpen, onOpenChange, product }: B2BInquiryFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const form = useForm<InquiryFormData>({
resolver: zodResolver(inquirySchema),
defaultValues: {
companyName: '',
contactPerson: '',
designation: '',
email: '',
phone: '',
businessType: '',
gstNumber: '',
address: '',
quantityRequired: '',
quantityUnit: 'tons',
deliveryLocation: '',
expectedDeliveryDate: '',
message: product ? `I am interested in bulk procurement of ${product.name}. Please provide detailed quotation including pricing, minimum order quantity, and delivery terms.` : '',
hearAboutUs: '',
agreedToTerms: false
}
})
const onSubmit = async (data: InquiryFormData) => {
setIsSubmitting(true)
try {
const formData = {
...data,
productId: product?.id,
productName: product?.name,
productCategory: product?.category?.name,
productPrice: product?.price,
submissionType: 'b2b_inquiry',
submittedAt: new Date().toISOString()
}
const response = await fetch('/api/inquiries', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
if (!response.ok) {
throw new Error('Failed to submit inquiry')
}
const result = await response.json()
toast.success('Inquiry submitted successfully!', {
description: 'Our team will contact you within 24 hours with a detailed quotation.'
})
form.reset()
onOpenChange(false)
} catch (error) {
console.error('Error submitting inquiry:', error)
toast.error('Failed to submit inquiry', {
description: 'Please try again or contact us directly.'
})
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
<Package className="h-6 w-6 text-emerald-600" />
B2B Inquiry Form
</DialogTitle>
<DialogDescription>
{product ? (
<>Requesting quote for: <span className="font-semibold text-emerald-600">{product.name}</span></>
) : (
'Fill out this form to get a detailed quotation for bulk orders'
)}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Company Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<Building className="h-5 w-5" />
Company Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="companyName"
render={({ field }) => (
<FormItem>
<FormLabel>Company Name *</FormLabel>
<FormControl>
<Input placeholder="Enter your company name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="businessType"
render={({ field }) => (
<FormItem>
<FormLabel>Business Type *</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select business type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="retailer">Retailer</SelectItem>
<SelectItem value="wholesaler">Wholesaler</SelectItem>
<SelectItem value="distributor">Distributor</SelectItem>
<SelectItem value="restaurant">Restaurant/Hotel</SelectItem>
<SelectItem value="caterer">Catering Service</SelectItem>
<SelectItem value="food_processor">Food Processor</SelectItem>
<SelectItem value="export">Export Business</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="contactPerson"
render={({ field }) => (
<FormItem>
<FormLabel>Contact Person *</FormLabel>
<FormControl>
<Input placeholder="Full name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="designation"
render={({ field }) => (
<FormItem>
<FormLabel>Designation *</FormLabel>
<FormControl>
<Input placeholder="e.g., Procurement Manager" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
<Mail className="h-4 w-4" />
Email Address *
</FormLabel>
<FormControl>
<Input type="email" placeholder="business@company.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
<Phone className="h-4 w-4" />
Phone Number *
</FormLabel>
<FormControl>
<Input type="tel" placeholder="+91 98765 43210" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="gstNumber"
render={({ field }) => (
<FormItem>
<FormLabel>GST Number (Optional)</FormLabel>
<FormControl>
<Input placeholder="Enter GST number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Complete Business Address *</FormLabel>
<FormControl>
<Textarea
placeholder="Enter complete address including city, state, and pincode"
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Product Requirements Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<Package className="h-5 w-5" />
Product Requirements
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="quantityRequired"
render={({ field }) => (
<FormItem>
<FormLabel>Quantity Required *</FormLabel>
<FormControl>
<Input type="number" placeholder="e.g., 10" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="quantityUnit"
render={({ field }) => (
<FormItem>
<FormLabel>Unit</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tons">Tons</SelectItem>
<SelectItem value="kg">Kilograms</SelectItem>
<SelectItem value="quintal">Quintal</SelectItem>
<SelectItem value="bags">Bags</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expectedDeliveryDate"
render={({ field }) => (
<FormItem>
<FormLabel>Expected Delivery Date</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="deliveryLocation"
render={({ field }) => (
<FormItem>
<FormLabel>Delivery Location *</FormLabel>
<FormControl>
<Input placeholder="City, State where delivery is required" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Additional Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<MessageCircle className="h-5 w-5" />
Additional Information
</h3>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Detailed Requirements *</FormLabel>
<FormControl>
<Textarea
placeholder="Please provide specific requirements including quality specifications, packaging preferences, payment terms, or any other details that would help us provide an accurate quotation."
className="min-h-[120px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hearAboutUs"
render={({ field }) => (
<FormItem>
<FormLabel>How did you hear about us? (Optional)</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="google_search">Google Search</SelectItem>
<SelectItem value="social_media">Social Media</SelectItem>
<SelectItem value="referral">Referral</SelectItem>
<SelectItem value="trade_show">Trade Show</SelectItem>
<SelectItem value="advertisement">Advertisement</SelectItem>
<SelectItem value="existing_customer">Existing Customer</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Terms and Conditions */}
<div className="space-y-4">
<FormField
control={form.control}
name="agreedToTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-sm">
I agree to the terms and conditions and authorize Padmaaja Rasooi to contact me regarding this inquiry. *
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
</div>
{/* Submit Button */}
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-emerald-600 hover:bg-emerald-700"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : (
'Submit Inquiry'
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,195 @@
'use client'
import { useState, useEffect } from 'react'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { ShoppingCart, X, Plus, Minus } from 'lucide-react'
import { cartManager } from '@/lib/cart'
import Link from 'next/link'
import Image from 'next/image'
import { toast } from 'sonner'
interface CartItem {
id: string
name: string
price: number
quantity: number
image: string | null
}
interface CartSidebarProps {
children?: React.ReactNode
}
export default function CartSidebar({ children }: CartSidebarProps) {
const [cart, setCart] = useState<CartItem[]>([])
const [isOpen, setIsOpen] = useState(false)
const loadCart = () => {
const cartItems = cartManager.getCart()
setCart(cartItems)
}
useEffect(() => {
loadCart()
const handleCartUpdate = () => loadCart()
window.addEventListener('cartUpdated', handleCartUpdate)
return () => {
window.removeEventListener('cartUpdated', handleCartUpdate)
}
}, [])
const updateQuantity = (productId: string, newQuantity: number) => {
cartManager.updateQuantity(productId, newQuantity)
loadCart()
}
const removeItem = (productId: string) => {
cartManager.removeFromCart(productId)
toast.success('Item removed from cart')
loadCart()
}
const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0)
const cartTotal = cartManager.getTotalPrice()
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
{children || (
<Button variant="ghost" size="sm" className="relative">
<ShoppingCart className="h-5 w-5" />
{itemCount > 0 && (
<Badge
className="absolute -top-[5px] -right-[5px] h-5 w-5 flex items-center justify-center p-0 bg-red-500 text-white !text-xs"
>
{itemCount > 99 ? '99+' : itemCount}
</Badge>
)}
</Button>
)}
</SheetTrigger>
<SheetContent className="w-full sm:max-w-lg">
<SheetHeader>
<SheetTitle className="flex items-center">
<ShoppingCart className="h-5 w-5 mr-2" />
Shopping Cart ({itemCount})
</SheetTitle>
<SheetDescription>
Manage your cart items
</SheetDescription>
</SheetHeader>
<div className="mt-6 flex-1 overflow-y-auto">
{cart.length === 0 ? (
<div className="text-center py-12">
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">Your cart is empty</p>
<Button
className="mt-4"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/products">Continue Shopping</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{cart.map((item) => (
<div key={item.id} className="flex items-start space-x-3 p-3 border rounded-lg">
<Image
src={item.image || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.name}
width={60}
height={60}
className="rounded-lg object-cover flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm line-clamp-2">{item.name}</h4>
<div className="flex items-center space-x-1 mt-1">
<span className="text-sm font-bold text-gray-900">
{(item.price || 0).toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between mt-2">
<div className="flex items-center space-x-1">
<Button
variant="outline"
size="sm"
className="h-6 w-6 p-0"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
<Minus className="h-3 w-3" />
</Button>
<span className="text-sm font-medium w-8 text-center">
{item.quantity || 0}
</span>
<Button
variant="outline"
size="sm"
className="h-6 w-6 p-0"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={() => removeItem(item.id)}
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
</div>
))}
<Separator />
<div className="space-y-2 p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between font-bold">
<span>Total</span>
<span>{cartTotal.toFixed(2)}</span>
</div>
</div>
<div className="space-y-2">
<Button
className="w-full"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/checkout">Checkout</Link>
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/cart">View Cart</Link>
</Button>
</div>
</div>
)}
</div>
</SheetContent>
</Sheet>
)
}

View File

@@ -0,0 +1,463 @@
'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'
import { isFeatureEnabled } from '@/lib/business-config'
import B2BInquiryForm from '@/components/shop/B2BInquiryForm'
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 [isInquiryFormOpen, setIsInquiryFormOpen] = 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)
}
const handleRequestQuote = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setIsInquiryFormOpen(true)
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="h-full hover:shadow-lg transition-all duration-300 group overflow-hidden cursor-pointer hover:scale-[1.02]"
onClick={handleCardClick}
>
<div className="relative overflow-hidden">
<OptimizedImage
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
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={50}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
/>
{/* 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>
) : (
<>
</>
)}
{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>
</div>
<CardHeader className="pb-2 p-3 sm:pb-2">
<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">
{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">
{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">
{/* B2C Feature - Add to Cart Button (Disabled for B2B mode) */}
{isFeatureEnabled('cart') ? (
<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"
onClick={handleRequestQuote}
className="flex-1 text-xs sm:text-sm h-8 sm:h-9 bg-emerald-600 hover:bg-emerald-700"
>
<Eye className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">Get Quote</span>
<span className="sm:hidden">Quote</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">
{/* B2C Feature - Add to Cart Button (Disabled for B2B mode) */}
{isFeatureEnabled('cart') ? (
<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
onClick={() => {
setIsQuickViewOpen(false)
setIsInquiryFormOpen(true)
}}
className="w-full bg-emerald-600 hover:bg-emerald-700"
size="lg"
>
<Eye className="h-4 w-4 mr-2" />
Request Quote
</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>
{/* B2B Inquiry Form */}
<B2BInquiryForm
isOpen={isInquiryFormOpen}
onOpenChange={setIsInquiryFormOpen}
product={{
id: product.id,
name: product.name,
category: product.category,
price: product.price,
weight: product.weight || undefined
}}
/>
</motion.div>
)
}

View File

@@ -0,0 +1,102 @@
'use client'
import { useState } from 'react'
import ProductCard from '@/components/shop/ProductCard'
import { Product } from '@/types'
import { Button } from '@/components/ui/button'
interface ProductGridProps {
products: Product[]
categoryFilter?: string
initialLoadCount?: number
loadMoreCount?: number
}
export default function ProductGrid({
products,
categoryFilter,
initialLoadCount = 8,
loadMoreCount = 4
}: ProductGridProps) {
const [displayCount, setDisplayCount] = useState(initialLoadCount)
const filteredProducts = categoryFilter
? products.filter(product =>
product.category.name.toLowerCase().includes(categoryFilter.toLowerCase()) ||
product.name.toLowerCase().includes(categoryFilter.toLowerCase())
)
: products
const displayedProducts = filteredProducts.slice(0, displayCount)
const hasMoreProducts = displayCount < filteredProducts.length
const remainingProducts = filteredProducts.length - displayCount
const loadMore = () => {
setDisplayCount(prev => Math.min(prev + loadMoreCount, filteredProducts.length))
}
const showLess = () => {
setDisplayCount(initialLoadCount)
}
if (filteredProducts.length === 0) {
return (
<div className="text-center py-8 sm:py-12 px-4">
<p className="text-gray-500 text-base sm:text-lg">No products found in this category.</p>
<p className="text-gray-400 text-sm mt-2">Check back soon for new products!</p>
</div>
)
}
return (
<div className="space-y-8">
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 md:gap-4 gap-8">
{displayedProducts.map((product, index) => (
<ProductCard
key={product.id}
product={product}
index={index}
/>
))}
</div>
{/* Load More / Show Less Controls */}
{filteredProducts.length > initialLoadCount && (
<div className="flex flex-col items-center gap-4 pt-4">
{/* Products Counter */}
<div className="text-center">
<p className="text-sm text-gray-600">
Showing <span className="font-semibold">{displayedProducts.length}</span> of{' '}
<span className="font-semibold">{filteredProducts.length}</span> products
</p>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
{hasMoreProducts && (
<Button
onClick={loadMore}
variant="outline"
className="px-6 py-2 rounded-lg border-gray-300 hover:border-gray-400 transition-colors"
>
Load {Math.min(loadMoreCount, remainingProducts)} More
{remainingProducts > loadMoreCount && ` (${remainingProducts} remaining)`}
</Button>
)}
{displayCount > initialLoadCount && (
<Button
onClick={showLess}
variant="ghost"
className="px-6 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
Show Less
</Button>
)}
</div>
</div>
)}
</div>
)
}