ajinkya-mhetre-mern
Version:
A MERN starter with frontend and backend folders
476 lines (438 loc) • 19.3 kB
JSX
import React, { useState, useEffect } from 'react'
import { useAuth } from '../../contexts/AuthContext'
const baseUrl = import.meta.env.VITE_API_URL;
function UserOrder() {
const { token } = useAuth()
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [selectedOrder, setSelectedOrder] = useState(null)
const [cancelling, setCancelling] = useState(null)
const [currentPage, setCurrentPage] = useState(1)
const [ordersPerPage] = useState(10)
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState('all')
// Fetch all orders
const fetchOrders = async () => {
try {
setLoading(true)
const response = await fetch(`${baseUrl}/customer/orders`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
// Removed body - GET requests cannot have a body
})
if (!response.ok) {
throw new Error('Failed to fetch orders')
}
const data = await response.json()
if (data.success) {
setOrders(data.orders)
} else {
throw new Error('Failed to fetch orders')
}
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
// Fetch single order details
const fetchOrderDetails = async (orderId) => {
try {
const response = await fetch(`${baseUrl}/customer/orders/${orderId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
// Removed body - GET requests cannot have a body
})
if (!response.ok) {
throw new Error('Failed to fetch order details')
}
const data = await response.json()
if (data.success) {
setSelectedOrder(data.order)
}
} catch (err) {
setError(err.message)
}
}
// Cancel order
const cancelOrder = async (orderId) => {
try {
setCancelling(orderId)
const response = await fetch(`${baseUrl}/customer/orders/${orderId}/cancel`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
// You can keep body for PUT requests, but since you're sending token in header,
// the body with token is redundant. Remove it unless your API specifically requires it.
// body: JSON.stringify({ token })
})
if (!response.ok) {
throw new Error('Failed to cancel order')
}
// Refresh orders after cancellation
await fetchOrders()
if (selectedOrder && selectedOrder._id === orderId) {
setSelectedOrder(null)
}
} catch (err) {
setError(err.message)
} finally {
setCancelling(null)
}
}
useEffect(() => {
if (token) {
fetchOrders()
}
}, [token])
// Filter and search orders
const filteredOrders = orders.filter(order => {
const matchesSearch = order.items.some(item =>
item.product.name.toLowerCase().includes(searchTerm.toLowerCase())
) || order._id.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === 'all' || order.status === statusFilter
return matchesSearch && matchesStatus
})
// Pagination
const indexOfLastOrder = currentPage * ordersPerPage
const indexOfFirstOrder = indexOfLastOrder - ordersPerPage
const currentOrders = filteredOrders.slice(indexOfFirstOrder, indexOfLastOrder)
const totalPages = Math.ceil(filteredOrders.length / ordersPerPage)
// Helper functions
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getStatusColor = (status) => {
const colors = {
'placed': 'bg-blue-100 text-blue-800',
'confirmed': 'bg-green-100 text-green-800',
'shipped': 'bg-purple-100 text-purple-800',
'delivered': 'bg-green-100 text-green-800',
'cancelled': 'bg-red-100 text-red-800'
}
return colors[status] || 'bg-gray-100 text-gray-800'
}
const getPaymentStatusColor = (status) => {
const colors = {
'pending': 'bg-yellow-100 text-yellow-800',
'paid': 'bg-green-100 text-green-800',
'failed': 'bg-red-100 text-red-800'
}
return colors[status] || 'bg-gray-100 text-gray-800'
}
if (loading) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"></div>
</div>
)
}
if (error) {
return (
<div className="max-w-6xl mx-auto p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h3 className="text-red-800 font-medium">Error</h3>
<p className="text-red-600">{error}</p>
<button
onClick={fetchOrders}
className="mt-2 bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
>
Retry
</button>
</div>
</div>
)
}
return (
<div className="max-w-6xl mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-800 mb-6">My Orders</h1>
{/* Search and Filter */}
<div className="mb-6 flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<input
type="text"
placeholder="Search orders by product name or order ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="all">All Status</option>
<option value="placed">Placed</option>
<option value="confirmed">Confirmed</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
{/* Orders Count */}
<div className="mb-4 text-gray-600">
Showing {currentOrders.length} of {filteredOrders.length} orders
</div>
{/* Orders List */}
{currentOrders.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-400 text-6xl mb-4">📦</div>
<h3 className="text-xl font-medium text-gray-600 mb-2">No orders found</h3>
<p className="text-gray-500">
{orders.length === 0 ? "You haven't placed any orders yet." : "No orders match your search criteria."}
</p>
</div>
) : (
<div className="space-y-4">
{currentOrders.map((order) => (
<div key={order._id} className="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-1">
Order #{order._id.slice(-8)}
</h3>
<p className="text-gray-600 text-sm">
Placed on {formatDate(order.createdAt)}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 mt-3 lg:mt-0">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(order.status)}`}>
{order.status.charAt(0).toUpperCase() + order.status.slice(1)}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getPaymentStatusColor(order.paymentStatus)}`}>
Payment: {order.paymentStatus.charAt(0).toUpperCase() + order.paymentStatus.slice(1)}
</span>
</div>
</div>
{/* Order Items */}
<div className="mb-4">
<h4 className="font-medium text-gray-700 mb-2">Items ({order.items.length})</h4>
<div className="space-y-2">
{order.items.map((item) => (
<div key={item._id} className="flex items-center gap-4 bg-gray-50 p-3 rounded-lg">
<img
src={item.product.images[0]?.url}
alt={item.product.name}
className="w-12 h-12 object-cover rounded-lg"
/>
<div className="flex-1">
<p className="font-medium text-gray-800">{item.product.name}</p>
<p className="text-sm text-gray-600">
Farmer: {item.farmer.name} • Qty: {item.quantity} • ₹{item.price}
</p>
</div>
</div>
))}
</div>
</div>
{/* Shipping Address */}
<div className="mb-4">
<h4 className="font-medium text-gray-700 mb-1">Shipping Address</h4>
<p className="text-sm text-gray-600">
{order.shippingAddress.street}, {order.shippingAddress.city}, {order.shippingAddress.state} - {order.shippingAddress.zipCode}
</p>
</div>
{/* Order Total and Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pt-4 border-t border-gray-200">
<div className="mb-3 sm:mb-0">
<span className="text-lg font-bold text-gray-800">Total: ₹{order.totalAmount}</span>
</div>
<div className="flex gap-2">
<button
onClick={() => fetchOrderDetails(order._id)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
View Details
</button>
{order.status === 'placed' && (
<button
onClick={() => cancelOrder(order._id)}
disabled={cancelling === order._id}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{cancelling === order._id ? 'Cancelling...' : 'Cancel Order'}
</button>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center items-center mt-8 gap-2">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<div className="flex gap-1">
{[...Array(totalPages)].map((_, index) => {
const pageNum = index + 1
if (pageNum === 1 || pageNum === totalPages || (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)) {
return (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`px-3 py-2 rounded-lg ${
currentPage === pageNum
? 'bg-green-600 text-white'
: 'border border-gray-300 hover:bg-gray-50'
}`}
>
{pageNum}
</button>
)
} else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) {
return <span key={pageNum} className="px-2 py-2">...</span>
}
return null
})}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
)}
{/* Order Details Modal */}
{selectedOrder && (
<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-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800">
Order Details #{selectedOrder._id.slice(-8)}
</h2>
<button
onClick={() => setSelectedOrder(null)}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
×
</button>
</div>
{/* Order Status Timeline */}
<div className="mb-6">
<h3 className="font-semibold text-gray-700 mb-3">Order Status History</h3>
<div className="space-y-2">
{selectedOrder.statusHistory.map((status, index) => (
<div key={status._id} className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${index === 0 ? 'bg-green-500' : 'bg-gray-300'}`}></div>
<div>
<p className="font-medium text-gray-800">
{status.status.charAt(0).toUpperCase() + status.status.slice(1)}
</p>
<p className="text-sm text-gray-600">{formatDate(status.timestamp)}</p>
{status.note && <p className="text-sm text-gray-500">{status.note}</p>}
</div>
</div>
))}
</div>
</div>
{/* Detailed Items */}
<div className="mb-6">
<h3 className="font-semibold text-gray-700 mb-3">Order Items</h3>
<div className="space-y-3">
{selectedOrder.items.map((item) => (
<div key={item._id} className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg">
<img
src={item.product.images[0]?.url}
alt={item.product.name}
className="w-16 h-16 object-cover rounded-lg"
/>
<div className="flex-1">
<h4 className="font-medium text-gray-800">{item.product.name}</h4>
<p className="text-sm text-gray-600 mb-1">
Farmer: {item.farmer.name}
</p>
{item.farmer.phone && (
<p className="text-sm text-gray-600">
Contact: {item.farmer.phone}
</p>
)}
</div>
<div className="text-right">
<p className="font-medium">Qty: {item.quantity}</p>
<p className="text-lg font-bold text-green-600">₹{item.price}</p>
</div>
</div>
))}
</div>
</div>
{/* Shipping & Payment Info */}
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div>
<h3 className="font-semibold text-gray-700 mb-2">Shipping Address</h3>
<div className="bg-gray-50 p-3 rounded-lg">
<p className="text-sm text-gray-800">
{selectedOrder.shippingAddress.street}<br />
{selectedOrder.shippingAddress.city}, {selectedOrder.shippingAddress.state}<br />
{selectedOrder.shippingAddress.zipCode}
</p>
</div>
</div>
<div>
<h3 className="font-semibold text-gray-700 mb-2">Payment Information</h3>
<div className="bg-gray-50 p-3 rounded-lg">
<p className="text-sm text-gray-800 mb-1">
Status: <span className={`px-2 py-1 rounded text-xs ${getPaymentStatusColor(selectedOrder.paymentStatus)}`}>
{selectedOrder.paymentStatus.charAt(0).toUpperCase() + selectedOrder.paymentStatus.slice(1)}
</span>
</p>
<p className="text-lg font-bold text-green-600">Total: ₹{selectedOrder.totalAmount}</p>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 pt-4 border-t">
{selectedOrder.status === 'placed' && (
<button
onClick={() => {
cancelOrder(selectedOrder._id)
setSelectedOrder(null)
}}
disabled={cancelling === selectedOrder._id}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
>
{cancelling === selectedOrder._id ? 'Cancelling...' : 'Cancel Order'}
</button>
)}
<button
onClick={() => setSelectedOrder(null)}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}
export default UserOrder