ajinkya-mhetre-mern
Version:
A MERN starter with frontend and backend folders
588 lines (550 loc) • 25.4 kB
JSX
import React, { useState, useEffect } from 'react';
import { Plus, Edit3, Save, X, Upload, Eye, Package, Trash2 } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import Stats from '../../components/Framers/Stats';
const baseUrl = import.meta.env.VITE_API_URL;
function FarmersAddProducts() {
const { token } = useAuth();
const [products, setProducts] = useState([]);
const [showAddForm, setShowAddForm] = useState(false);
const [editingProduct, setEditingProduct] = useState(null);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('view');
// Form state
const [formData, setFormData] = useState({
name: '',
description: '',
category: 'fruit',
price: '',
unit: 'kg',
availableQuantity: '',
harvestDate: '',
expiryDate: '',
images: []
});
// Edit form state
const [editData, setEditData] = useState({
price: '',
availableQuantity: '',
description: ''
});
const categories = ['fruit', 'vegetable', 'grain', 'dairy', 'meat', 'herbs'];
const units = ['kg', 'dozen', 'piece', 'bunch', 'liter'];
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
try {
setLoading(true);
const response = await fetch(`${baseUrl}/farmer/products`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
setProducts(data.products);
}
} catch (error) {
console.error('Error fetching products:', error);
setProducts([]); // Set empty array on error
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleEditInputChange = (e) => {
const { name, value } = e.target;
setEditData(prev => ({
...prev,
[name]: value
}));
};
const handleImageUpload = (e) => {
const files = Array.from(e.target.files);
setFormData(prev => ({
...prev,
images: files
}));
};
const handleSubmit = async () => {
try {
setLoading(true);
// Create FormData for file upload
const submitData = new FormData();
Object.keys(formData).forEach(key => {
if (key === 'images') {
formData.images.forEach(file => {
submitData.append('images', file);
});
} else {
submitData.append(key, formData[key]);
}
});
const response = await fetch(`${baseUrl}/farmer/products`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: submitData
});
const data = await response.json();
if (data.success) {
fetchProducts(); // Refresh the list
resetForm();
setActiveTab('view');
}
} catch (error) {
console.error('Error adding product:', error);
} finally {
setLoading(false);
}
};
const handleUpdate = async (productId) => {
try {
setLoading(true);
const response = await fetch(`${baseUrl}/farmer/products/${productId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(editData)
});
const data = await response.json();
if (data.success) {
fetchProducts(); // Refresh the list
setEditingProduct(null);
setEditData({ price: '', availableQuantity: '', description: '' });
}
} catch (error) {
console.error('Error updating product:', error);
} finally {
setLoading(false);
}
};
const startEdit = (product) => {
setEditingProduct(product._id);
setEditData({
price: product.price,
availableQuantity: product.availableQuantity,
description: product.description
});
};
const cancelEdit = () => {
setEditingProduct(null);
setEditData({ price: '', availableQuantity: '', description: '' });
};
const resetForm = () => {
setFormData({
name: '',
description: '',
category: 'fruit',
price: '',
unit: 'kg',
availableQuantity: '',
harvestDate: '',
expiryDate: '',
images: []
});
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString();
};
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
<Stats />
{/* Header */}
<div className="border-b border-gray-200 p-6">
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<Package className="text-green-600" />
Products Management
</h1>
<p className="text-gray-600 mt-2">Manage your farm products, pricing, and inventory</p>
</div>
{/* Tabs */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-6">
<button
onClick={() => setActiveTab('view')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'view'
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Eye className="inline mr-2" size={16} />
View Products
</button>
<button
onClick={() => setActiveTab('add')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'add'
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Plus className="inline mr-2" size={16} />
Add Product
</button>
</nav>
</div>
{/* Content */}
<div className="p-6">
{activeTab === 'view' && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-900">Your Products</h2>
<div className="text-sm text-gray-600">
Total Products: {products.length}
</div>
</div>
{loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading products...</p>
</div>
) : products.length === 0 ? (
<div className="text-center py-12">
<Package className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No products</h3>
<p className="mt-1 text-sm text-gray-500">Get started by adding your first product.</p>
<div className="mt-6">
<button
onClick={() => setActiveTab('add')}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
>
<Plus className="mr-2" size={16} />
Add Product
</button>
</div>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Quantity
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Harvest Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Expiry Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{products.map((product) => (
<tr key={product._id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
{product.images && product.images.length > 0 ? (
<img
className="h-10 w-10 rounded-full object-cover"
src={product.images[0].url}
alt={product.name}
/>
) : (
<div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
<Package size={16} className="text-gray-400" />
</div>
)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{product.name}</div>
<div className="text-sm text-gray-500 max-w-xs truncate">
{editingProduct === product._id ? (
<input
type="text"
name="description"
value={editData.description}
onChange={handleEditInputChange}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
) : (
product.description
)}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800 capitalize">
{product.category}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{editingProduct === product._id ? (
<div className="flex items-center">
<span className="mr-1">₹</span>
<input
type="number"
name="price"
value={editData.price}
onChange={handleEditInputChange}
className="w-20 border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
<span className="ml-1 text-gray-500">/{product.unit}</span>
</div>
) : (
`₹${product.price}/${product.unit}`
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{editingProduct === product._id ? (
<div className="flex items-center">
<input
type="number"
name="availableQuantity"
value={editData.availableQuantity}
onChange={handleEditInputChange}
className="w-20 border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
<span className="ml-1 text-gray-500">{product.unit}</span>
</div>
) : (
`${product.availableQuantity} ${product.unit}`
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatDate(product.harvestDate)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatDate(product.expiryDate)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col space-y-1">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
product.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{product.isActive ? 'Active' : 'Inactive'}
</span>
{product.availableQuantity === 0 && (
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-orange-100 text-orange-800">
Out of Stock
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{editingProduct === product._id ? (
<div className="flex space-x-2">
<button
onClick={() => handleUpdate(product._id)}
disabled={loading}
className="text-green-600 hover:text-green-900 disabled:opacity-50"
>
<Save size={16} />
</button>
<button
onClick={cancelEdit}
className="text-gray-600 hover:text-gray-900"
>
<X size={16} />
</button>
</div>
) : (
<button
onClick={() => startEdit(product)}
className="text-blue-600 hover:text-blue-900"
>
<Edit3 size={16} />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{activeTab === 'add' && (
<div className="max-w-2xl mx-auto">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Add New Product</h2>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Product Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="e.g., Fresh Tomatoes"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Category</label>
<select
name="category"
value={formData.category}
onChange={handleInputChange}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
{categories.map(category => (
<option key={category} value={category}>
{category.charAt(0).toUpperCase() + category.slice(1)}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
<textarea
name="description"
value={formData.description}
onChange={handleInputChange}
rows={3}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="Describe your product..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Price (₹)</label>
<input
type="number"
name="price"
value={formData.price}
onChange={handleInputChange}
min="0"
step="0.01"
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Unit</label>
<select
name="unit"
value={formData.unit}
onChange={handleInputChange}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
{units.map(unit => (
<option key={unit} value={unit}>{unit}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Available Quantity</label>
<input
type="number"
name="availableQuantity"
value={formData.availableQuantity}
onChange={handleInputChange}
min="0"
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Harvest Date</label>
<input
type="date"
name="harvestDate"
value={formData.harvestDate}
onChange={handleInputChange}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Expiry Date</label>
<input
type="date"
name="expiryDate"
value={formData.expiryDate}
onChange={handleInputChange}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Product Images</label>
<div className="border-2 border-dashed border-gray-300 rounded-md p-6 text-center hover:border-green-500 transition-colors">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<label htmlFor="images" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Upload product images
</span>
<input
id="images"
name="images"
type="file"
multiple
accept="image/*"
onChange={handleImageUpload}
className="sr-only"
/>
</label>
<p className="mt-1 text-xs text-gray-500">PNG, JPG, GIF up to 10MB each</p>
</div>
</div>
{formData.images.length > 0 && (
<div className="mt-2 text-sm text-gray-600">
{formData.images.length} file(s) selected
</div>
)}
</div>
<div className="flex gap-4 pt-6">
<button
onClick={handleSubmit}
disabled={loading}
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
<Plus size={16} />
)}
Add Product
</button>
<button
onClick={resetForm}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
>
Reset
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
export default FarmersAddProducts;