331 lines
No EOL
10 KiB
JavaScript
331 lines
No EOL
10 KiB
JavaScript
import React from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Paper,
|
|
Grid,
|
|
Divider,
|
|
Card,
|
|
CardMedia,
|
|
IconButton,
|
|
TextField,
|
|
CircularProgress,
|
|
Alert,
|
|
Breadcrumbs,
|
|
Link
|
|
} from '@mui/material';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import RemoveIcon from '@mui/icons-material/Remove';
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
|
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
|
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '@hooks/reduxHooks';
|
|
import { useGetCart, useUpdateCartItem, useClearCart } from '.@hooks/apiHooks';
|
|
import imageUtils from '@utils/imageUtils';
|
|
|
|
const CartPage = () => {
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
|
|
// Get cart data
|
|
const { data: cart, isLoading, error } = useGetCart(user?.id);
|
|
|
|
// Cart mutations
|
|
const updateCartItem = useUpdateCartItem();
|
|
const clearCart = useClearCart();
|
|
|
|
// Handle quantity change
|
|
const handleUpdateQuantity = (productId, newQuantity) => {
|
|
updateCartItem.mutate({
|
|
userId: user.id,
|
|
productId,
|
|
quantity: newQuantity
|
|
});
|
|
};
|
|
|
|
// Handle remove item
|
|
const handleRemoveItem = (productId) => {
|
|
updateCartItem.mutate({
|
|
userId: user.id,
|
|
productId,
|
|
quantity: 0
|
|
});
|
|
};
|
|
|
|
// Handle clear cart
|
|
const handleClearCart = () => {
|
|
clearCart.mutate(user.id);
|
|
};
|
|
|
|
// Handle proceed to checkout
|
|
const handleCheckout = () => {
|
|
navigate('/checkout');
|
|
};
|
|
|
|
// Loading state
|
|
if (isLoading) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', my: 8 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
return (
|
|
<Box sx={{ my: 4 }}>
|
|
<Alert severity="error" sx={{ mb: 3 }}>
|
|
Error loading your cart. Please try again.
|
|
</Alert>
|
|
<Button
|
|
variant="contained"
|
|
component={RouterLink}
|
|
to="/products"
|
|
>
|
|
Continue Shopping
|
|
</Button>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Empty cart state
|
|
if (!cart || !cart.items || cart.items.length === 0) {
|
|
return (
|
|
<Box sx={{ textAlign: 'center', py: 6 }}>
|
|
<ShoppingCartIcon sx={{ fontSize: 60, color: 'text.secondary', mb: 2 }} />
|
|
<Typography variant="h5" gutterBottom>
|
|
Your Cart is Empty
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" paragraph>
|
|
Looks like you haven't added any items to your cart yet.
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
component={RouterLink}
|
|
to="/products"
|
|
size="large"
|
|
sx={{ mt: 2 }}
|
|
>
|
|
Start Shopping
|
|
</Button>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
{/* Breadcrumbs navigation */}
|
|
<Breadcrumbs
|
|
separator={<NavigateNextIcon fontSize="small" />}
|
|
aria-label="breadcrumb"
|
|
sx={{ mb: 3 }}
|
|
>
|
|
<Link component={RouterLink} to="/" color="inherit">
|
|
Home
|
|
</Link>
|
|
<Typography color="text.primary">Your Cart</Typography>
|
|
</Breadcrumbs>
|
|
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
Your Shopping Cart
|
|
</Typography>
|
|
|
|
<Grid container spacing={4}>
|
|
{/* Cart items */}
|
|
<Grid item xs={12} lg={8}>
|
|
<Paper variant="outlined" sx={{ mb: { xs: 3, lg: 0 } }}>
|
|
<Box sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Typography variant="h6">
|
|
Cart Items ({cart.itemCount})
|
|
</Typography>
|
|
<Button
|
|
variant="text"
|
|
color="error"
|
|
onClick={handleClearCart}
|
|
disabled={clearCart.isLoading}
|
|
startIcon={<DeleteIcon />}
|
|
>
|
|
Clear Cart
|
|
</Button>
|
|
</Box>
|
|
|
|
<Divider />
|
|
|
|
{cart.items.map((item) => (
|
|
<React.Fragment key={item.product_id}>
|
|
<Box sx={{ p: 2 }}>
|
|
<Grid container spacing={2} alignItems="center">
|
|
{/* Product image */}
|
|
<Grid item xs={3} sm={2}>
|
|
<Card sx={{ height: '100%' }}>
|
|
<CardMedia
|
|
component="img"
|
|
image={imageUtils.getImageUrl(item.image_url || '/images/placeholder.jpg')}
|
|
alt={item.name}
|
|
sx={{ height: 80, objectFit: 'cover' }}
|
|
/>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Product details */}
|
|
<Grid item xs={9} sm={6}>
|
|
<Typography
|
|
variant="subtitle1"
|
|
component={RouterLink}
|
|
to={`/products/${item.product_id}`}
|
|
sx={{
|
|
textDecoration: 'none',
|
|
color: 'inherit',
|
|
'&:hover': { color: 'primary.main' },
|
|
display: 'block',
|
|
mb: 0.5
|
|
}}
|
|
>
|
|
{item.name}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
Category: {item.category_name}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
Price: ${parseFloat(item.price).toFixed(2)}
|
|
</Typography>
|
|
</Grid>
|
|
|
|
{/* Quantity controls */}
|
|
<Grid item xs={7} sm={2}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleUpdateQuantity(item.product_id, item.quantity - 1)}
|
|
disabled={item.quantity <= 1 || updateCartItem.isLoading}
|
|
>
|
|
<RemoveIcon fontSize="small" />
|
|
</IconButton>
|
|
|
|
<TextField
|
|
value={item.quantity}
|
|
onChange={(e) => {
|
|
const value = parseInt(e.target.value);
|
|
if (!isNaN(value) && value > 0) {
|
|
handleUpdateQuantity(item.product_id, value);
|
|
}
|
|
}}
|
|
inputProps={{ min: 1, style: { textAlign: 'center' } }}
|
|
size="small"
|
|
sx={{ width: 40, mx: 1 }}
|
|
/>
|
|
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleUpdateQuantity(item.product_id, item.quantity + 1)}
|
|
disabled={updateCartItem.isLoading}
|
|
>
|
|
<AddIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
</Grid>
|
|
|
|
{/* Subtotal and remove */}
|
|
<Grid item xs={5} sm={2} sx={{ textAlign: 'right' }}>
|
|
<Typography variant="subtitle1">
|
|
${(parseFloat(item.price) * item.quantity).toFixed(2)}
|
|
</Typography>
|
|
|
|
<IconButton
|
|
color="error"
|
|
size="small"
|
|
onClick={() => handleRemoveItem(item.product_id)}
|
|
disabled={updateCartItem.isLoading}
|
|
sx={{ ml: 1 }}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
<Divider />
|
|
</React.Fragment>
|
|
))}
|
|
|
|
<Box sx={{ p: 2, textAlign: 'right' }}>
|
|
<Button
|
|
component={RouterLink}
|
|
to="/products"
|
|
sx={{ mr: 2 }}
|
|
>
|
|
Continue Shopping
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
{/* Order summary */}
|
|
<Grid item xs={12} lg={4}>
|
|
<Paper variant="outlined" sx={{ p: 3 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Order Summary
|
|
</Typography>
|
|
|
|
<Box sx={{ my: 2 }}>
|
|
<Grid container spacing={1}>
|
|
<Grid item xs={8}>
|
|
<Typography variant="body1">
|
|
Subtotal ({cart.itemCount} items)
|
|
</Typography>
|
|
</Grid>
|
|
<Grid item xs={4} sx={{ textAlign: 'right' }}>
|
|
<Typography variant="body1">
|
|
${cart.total.toFixed(2)}
|
|
</Typography>
|
|
</Grid>
|
|
|
|
<Grid item xs={8}>
|
|
<Typography variant="body1">
|
|
Shipping
|
|
</Typography>
|
|
</Grid>
|
|
<Grid item xs={4} sx={{ textAlign: 'right' }}>
|
|
<Typography variant="body1">
|
|
Free
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
|
|
<Divider sx={{ my: 2 }} />
|
|
|
|
<Grid container spacing={1} sx={{ mb: 2 }}>
|
|
<Grid item xs={8}>
|
|
<Typography variant="h6">
|
|
Total
|
|
</Typography>
|
|
</Grid>
|
|
<Grid item xs={4} sx={{ textAlign: 'right' }}>
|
|
<Typography variant="h6">
|
|
${cart.total.toFixed(2)}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Button
|
|
variant="contained"
|
|
fullWidth
|
|
size="large"
|
|
onClick={handleCheckout}
|
|
disabled={cart.items.length === 0}
|
|
>
|
|
Proceed to Checkout
|
|
</Button>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default CartPage; |