UNPKG

ajinkya-mhetre-mern

Version:

A MERN starter with frontend and backend folders

633 lines (577 loc) 28.9 kB
// pages/Home.jsx import React, { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { ShoppingCart, User, LogOut, Search, Filter, X, Plus, Minus, MapPin, Calendar, Award, Phone, Mail } from 'lucide-react' const baseUrl = import.meta.env.VITE_API_URL; const Home = () => { const { user, isAuthenticated, logout , token } = useAuth() const [products, setProducts] = useState([]) const [loading, setLoading] = useState(true) const [searchTerm, setSearchTerm] = useState('') const [selectedCategory, setSelectedCategory] = useState('all') const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 }) const [currentPage, setCurrentPage] = useState(1) const [orderLoading, setOrderLoading] = useState(false) const [selectedProduct, setSelectedProduct] = useState(null) const [showProductModal, setShowProductModal] = useState(false) const [productLoading, setProductLoading] = useState(false) const [quantity, setQuantity] = useState(1) const [showShippingForm, setShowShippingForm] = useState(false) const [shippingAddress, setShippingAddress] = useState({ street: '', city: '', state: '', zipCode: '' }) // Fetch products from API const fetchProducts = async (page = 1) => { try { setLoading(true) const response = await fetch(`${baseUrl}/customer/products?page=${page}&limit=12`) const data = await response.json() if (data.success) { setProducts(data.products) setPagination(data.pagination) } else { console.error('Failed to fetch products') } } catch (error) { console.error('Error fetching products:', error) } finally { setLoading(false) } } useEffect(() => { fetchProducts(currentPage) }, [currentPage]) const categories = ['all', 'vegetable', 'fruit', 'grain'] const filteredProducts = products.filter(product => { const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) const matchesCategory = selectedCategory === 'all' || product.category === selectedCategory return matchesSearch && matchesCategory }) // Fetch product details const fetchProductDetails = async (productId) => { try { setProductLoading(true) const response = await fetch(`${baseUrl}/customer/products/${productId}`) const data = await response.json() if (data.success) { setSelectedProduct(data.product) setShowProductModal(true) setQuantity(1) setShowShippingForm(false) setShippingAddress({ street: '', city: '', state: '', zipCode: '' }) } else { alert('Failed to load product details') } } catch (error) { console.error('Error fetching product details:', error) alert('Error loading product details') } finally { setProductLoading(false) } } const placeOrder = async () => { if (!isAuthenticated()) { alert('Please login to place an order') return } if (user.role !== 'customer') { alert('Only customers can place orders') return } if (!shippingAddress.street || !shippingAddress.city || !shippingAddress.state || !shippingAddress.zipCode) { alert('Please fill in all shipping address fields') return } try { setOrderLoading(true) const orderData = { items: [ { productId: selectedProduct._id, quantity: quantity } ], shippingAddress: shippingAddress } const response = await fetch(`${baseUrl}/customer/orders`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(orderData) }) const data = await response.json() if (!response.ok) { throw new Error(data.message || 'Failed to place order') } if (data.success) { alert(`${data.message}\nOrder Total: ₹${data.order.totalAmount}\nStatus: ${data.order.status}`) setShowProductModal(false) setSelectedProduct(null) // Refresh products to update quantities fetchProducts(currentPage) } else { alert(data.message || 'Failed to place order. Please try again.') } } catch (error) { console.error('Error placing order:', error) alert('Error placing order. Please try again.') } finally { setOrderLoading(false) } } const handlePageChange = (newPage) => { setCurrentPage(newPage) } const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-IN') } const isProductExpired = (expiryDate) => { return new Date(expiryDate) < new Date() } return ( <div className="min-h-screen bg-gray-50"> {/* Hero Section */} <section className="bg-green-600 text-white py-16"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="text-center"> <h2 className="text-4xl font-bold mb-4">Fresh Farm Products Delivered</h2> <p className="text-xl mb-8">Connecting farmers directly with consumers for the freshest produce</p> </div> </div> </section> {/* Search and Filter Section */} <section className="py-8 bg-white border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex flex-col md:flex-row gap-4 items-center justify-between"> <div className="relative flex-1 max-w-md"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" /> <input type="text" placeholder="Search products..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" /> </div> <div className="flex items-center space-x-2"> <Filter className="h-5 w-5 text-gray-500" /> <select value={selectedCategory} onChange={(e) => setSelectedCategory(e.target.value)} className="border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-green-500" > {categories.map(category => ( <option key={category} value={category}> {category.charAt(0).toUpperCase() + category.slice(1)} </option> ))} </select> </div> </div> </div> </section> {/* Products Section */} <section className="py-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between items-center mb-8"> <h3 className="text-2xl font-bold text-gray-900">Available Products</h3> <div className="text-sm text-gray-600"> Showing {filteredProducts.length} of {pagination.total} products </div> </div> {loading ? ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {[...Array(6)].map((_, index) => ( <div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse"> <div className="h-48 bg-gray-300"></div> <div className="p-4"> <div className="h-4 bg-gray-300 rounded mb-2"></div> <div className="h-4 bg-gray-300 rounded w-2/3 mb-2"></div> <div className="h-4 bg-gray-300 rounded w-1/2"></div> </div> </div> ))} </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {filteredProducts.map(product => { const isExpired = isProductExpired(product.expiryDate) const isOutOfStock = product.availableQuantity === 0 const isAvailable = !isExpired && !isOutOfStock && product.isActive return ( <div key={product._id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"> <div className="h-48 bg-gray-200 flex items-center justify-center overflow-hidden"> {product.images && product.images.length > 0 ? ( <img src={product.images[0].url} alt={product.name} className="w-full h-full object-cover" onError={(e) => { e.target.style.display = 'none' e.target.nextSibling.style.display = 'flex' }} /> ) : null} <div className="w-full h-full flex items-center justify-center text-gray-500" style={{display: product.images && product.images.length > 0 ? 'none' : 'flex'}}> Product Image </div> </div> <div className="p-4"> <div className="flex justify-between items-start mb-2"> <h4 className="text-lg font-semibold text-gray-900 cursor-pointer hover:text-green-600" onClick={() => fetchProductDetails(product._id)} > {product.name} </h4> <span className={`px-2 py-1 rounded-full text-xs ${ isAvailable ? 'bg-green-100 text-green-800' : isExpired ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800' }`}> {isExpired ? 'Expired' : isOutOfStock ? 'Out of Stock' : 'Available'} </span> </div> <p className="text-gray-600 text-sm mb-2"> by {product.farmer.name} - {product.farmer.farmDetails.farmName} </p> <p className="text-gray-700 text-sm mb-2 line-clamp-2"> {product.description} </p> <div className="flex justify-between text-sm text-gray-600 mb-2"> <span>Available: {product.availableQuantity} {product.unit}</span> <span>Expires: {formatDate(product.expiryDate)}</span> </div> <div className="flex justify-between items-center"> <span className="text-xl font-bold text-green-600"> ₹{product.price}/{product.unit} </span> <div className="flex gap-2"> <button onClick={() => fetchProductDetails(product._id)} className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-2 rounded-lg text-sm font-medium" > View Details </button> {!isAuthenticated() ? ( <Link to="/login" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium" > Login to Order </Link> ) : user.role === 'customer' ? ( <button onClick={() => fetchProductDetails(product._id)} disabled={!isAvailable || orderLoading} className={`flex items-center space-x-1 px-4 py-2 rounded-lg text-sm font-medium ${ isAvailable && !orderLoading ? 'bg-green-600 hover:bg-green-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed' }`} > <ShoppingCart className="h-4 w-4" /> <span>Order Now</span> </button> ) : ( <span className="text-sm text-gray-500 px-4 py-2"> Customer only </span> )} </div> </div> </div> </div> ) })} </div> )} {filteredProducts.length === 0 && !loading && ( <div className="text-center py-12"> <p className="text-gray-500 text-lg">No products found matching your criteria.</p> </div> )} {/* Pagination */} {pagination.pages > 1 && ( <div className="flex justify-center items-center space-x-2 mt-8"> <button onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1} className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> {[...Array(pagination.pages)].map((_, index) => ( <button key={index + 1} onClick={() => handlePageChange(index + 1)} className={`px-3 py-2 text-sm font-medium rounded-md ${ currentPage === index + 1 ? 'text-white bg-green-600 border border-green-600' : 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50' }`} > {index + 1} </button> ))} <button onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === pagination.pages} className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> )} </div> </section> {/* Product Detail Modal */} {showProductModal && selectedProduct && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto"> {/* Modal Header */} <div className="flex justify-between items-center p-6 border-b"> <h2 className="text-2xl font-bold text-gray-900">{selectedProduct.name}</h2> <button onClick={() => setShowProductModal(false)} className="text-gray-400 hover:text-gray-600" > <X className="h-6 w-6" /> </button> </div> <div className="p-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> {/* Product Images */} <div> <div className="mb-4"> {selectedProduct.images && selectedProduct.images.length > 0 ? ( <img src={selectedProduct.images[0].url} alt={selectedProduct.name} className="w-full h-80 object-cover rounded-lg" /> ) : ( <div className="w-full h-80 bg-gray-200 rounded-lg flex items-center justify-center"> <span className="text-gray-500">No Image Available</span> </div> )} </div> {/* Additional Images */} {selectedProduct.images && selectedProduct.images.length > 1 && ( <div className="grid grid-cols-4 gap-2"> {selectedProduct.images.slice(1).map((image, index) => ( <img key={index} src={image.url} alt={`${selectedProduct.name} ${index + 2}`} className="w-full h-16 object-cover rounded cursor-pointer hover:opacity-80" /> ))} </div> )} </div> {/* Product Details */} <div> <div className="mb-4"> <span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${ selectedProduct.isActive && selectedProduct.availableQuantity > 0 && new Date(selectedProduct.expiryDate) > new Date() ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }`}> {selectedProduct.isActive && selectedProduct.availableQuantity > 0 && new Date(selectedProduct.expiryDate) > new Date() ? 'Available' : 'Not Available' } </span> </div> <div className="mb-4"> <span className="text-3xl font-bold text-green-600"> ₹{selectedProduct.price} </span> <span className="text-gray-600 ml-2">per {selectedProduct.unit}</span> </div> <div className="mb-6"> <h3 className="text-lg font-semibold text-gray-900 mb-2">Description</h3> <p className="text-gray-700">{selectedProduct.description}</p> </div> {/* Product Info */} <div className="grid grid-cols-2 gap-4 mb-6"> <div> <span className="text-sm text-gray-600">Category</span> <p className="font-medium capitalize">{selectedProduct.category}</p> </div> <div> <span className="text-sm text-gray-600">Available Quantity</span> <p className="font-medium">{selectedProduct.availableQuantity} {selectedProduct.unit}</p> </div> <div> <span className="text-sm text-gray-600">Harvest Date</span> <p className="font-medium">{formatDate(selectedProduct.harvestDate)}</p> </div> <div> <span className="text-sm text-gray-600">Expiry Date</span> <p className="font-medium">{formatDate(selectedProduct.expiryDate)}</p> </div> </div> {/* Farmer Details */} <div className="bg-gray-50 p-4 rounded-lg mb-6"> <h3 className="text-lg font-semibold text-gray-900 mb-3">Farmer Details</h3> <div className="space-y-2"> <div className="flex items-center"> <User className="h-4 w-4 text-gray-500 mr-2" /> <span className="font-medium">{selectedProduct.farmer.name}</span> </div> <div className="flex items-center"> <MapPin className="h-4 w-4 text-gray-500 mr-2" /> <span>{selectedProduct.farmer.farmDetails.farmName}</span> </div> <div className="flex items-center"> <MapPin className="h-4 w-4 text-gray-500 mr-2" /> <span>{selectedProduct.farmer.farmDetails.farmLocation}</span> </div> <div className="flex items-center"> <span className="text-sm text-gray-600 mr-2">Size:</span> <span>{selectedProduct.farmer.farmDetails.farmSize}</span> </div> {selectedProduct.farmer.farmDetails.certifications && ( <div className="flex items-start"> <Award className="h-4 w-4 text-gray-500 mr-2 mt-1" /> <div> {selectedProduct.farmer.farmDetails.certifications.map((cert, index) => ( <span key={index} className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mr-1 mb-1"> {cert} </span> ))} </div> </div> )} </div> </div> {/* Order Section */} {isAuthenticated() && user.role === 'customer' && ( <div className="border-t pt-6"> {!showShippingForm ? ( <div> <div className="flex items-center mb-4"> <span className="text-sm font-medium text-gray-700 mr-4">Quantity:</span> <div className="flex items-center border rounded-lg"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))} className="p-2 hover:bg-gray-100" > <Minus className="h-4 w-4" /> </button> <span className="px-4 py-2 border-l border-r">{quantity}</span> <button onClick={() => setQuantity(Math.min(selectedProduct.availableQuantity, quantity + 1))} className="p-2 hover:bg-gray-100" > <Plus className="h-4 w-4" /> </button> </div> </div> <div className="mb-4"> <span className="text-lg font-semibold"> Total: ₹{selectedProduct.price * quantity} </span> </div> <button onClick={() => setShowShippingForm(true)} disabled={selectedProduct.availableQuantity === 0 || new Date(selectedProduct.expiryDate) < new Date()} className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-300 text-white py-3 px-4 rounded-lg font-medium" > Proceed to Order </button> </div> ) : ( <div> <h3 className="text-lg font-semibold mb-4">Shipping Address</h3> <div className="space-y-4"> <input type="text" placeholder="Street Address" value={shippingAddress.street} onChange={(e) => setShippingAddress({...shippingAddress, street: e.target.value})} className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" /> <div className="grid grid-cols-2 gap-4"> <input type="text" placeholder="City" value={shippingAddress.city} onChange={(e) => setShippingAddress({...shippingAddress, city: e.target.value})} className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" /> <input type="text" placeholder="State" value={shippingAddress.state} onChange={(e) => setShippingAddress({...shippingAddress, state: e.target.value})} className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" /> </div> <input type="text" placeholder="ZIP Code" value={shippingAddress.zipCode} onChange={(e) => setShippingAddress({...shippingAddress, zipCode: e.target.value})} className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" /> </div> <div className="flex gap-4 mt-6"> <button onClick={() => setShowShippingForm(false)} className="flex-1 bg-gray-300 hover:bg-gray-400 text-gray-700 py-3 px-4 rounded-lg font-medium" > Back </button> <button onClick={placeOrder} disabled={orderLoading} className="flex-1 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white py-3 px-4 rounded-lg font-medium" > {orderLoading ? 'Placing Order...' : `Place Order - ₹${selectedProduct.price * quantity}`} </button> </div> </div> )} </div> )} {!isAuthenticated() && ( <div className="border-t pt-6"> <Link to="/login" className="w-full block text-center bg-blue-600 hover:bg-blue-700 text-white py-3 px-4 rounded-lg font-medium" > Login to Order </Link> </div> )} {isAuthenticated() && user.role !== 'customer' && ( <div className="border-t pt-6"> <p className="text-center text-gray-500">Only customers can place orders</p> </div> )} </div> </div> </div> </div> </div> )} </div> ) } export default Home