first commit
This commit is contained in:
502
components/shop/B2BInquiryForm.tsx
Normal file
502
components/shop/B2BInquiryForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
195
components/shop/CartSidebar.tsx
Normal file
195
components/shop/CartSidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
463
components/shop/ProductCard.tsx
Normal file
463
components/shop/ProductCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
102
components/shop/ProductGrid.tsx
Normal file
102
components/shop/ProductGrid.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user