ajinkya-mhetre-mern
Version:
A MERN starter with frontend and backend folders
633 lines (577 loc) • 28.9 kB
JSX
// 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