E-Commerce-Module/frontend/src/pages/CheckoutPage.jsx

441 lines
No EOL
12 KiB
JavaScript

import React, { useState } from 'react';
import {
Box,
Typography,
Paper,
Grid,
Stepper,
Step,
StepLabel,
Button,
Divider,
TextField,
FormControlLabel,
Checkbox,
CircularProgress,
List,
ListItem,
ListItemText,
Alert
} from '@mui/material';
import { useNavigate, Link as RouterLink } from 'react-router-dom';
import { useAuth, useCart } from '../hooks/reduxHooks';
import { useCheckout } from '../hooks/apiHooks';
// Checkout steps
const steps = ['Shipping Address', 'Review Order', 'Payment', 'Confirmation'];
const CheckoutPage = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { items, total, itemCount } = useCart();
const checkout = useCheckout();
// State for checkout steps
const [activeStep, setActiveStep] = useState(0);
// State for form data
const [formData, setFormData] = useState({
firstName: user?.first_name || '',
lastName: user?.last_name || '',
email: user?.email || '',
address: '',
city: '',
state: '',
zipCode: '',
country: '',
saveAddress: false,
});
// Handle form changes
const handleChange = (e) => {
const { name, value, checked } = e.target;
setFormData({
...formData,
[name]: name === 'saveAddress' ? checked : value,
});
};
// Handle next step
const handleNext = () => {
if (activeStep === steps.length - 1) {
// Complete checkout - placeholder
return;
}
// If on shipping address step, validate form
if (activeStep === 0) {
if (!validateShippingForm()) {
return;
}
}
// If on review step, process checkout
if (activeStep === 1) {
handlePlaceOrder();
return;
}
setActiveStep((prevStep) => prevStep + 1);
};
// Handle back step
const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1);
};
// Validate shipping form
const validateShippingForm = () => {
const requiredFields = ['firstName', 'lastName', 'email', 'address', 'city', 'state', 'zipCode', 'country'];
for (const field of requiredFields) {
if (!formData[field]) {
// In a real app, you'd set specific errors for each field
alert(`Please fill in all required fields`);
return false;
}
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
alert('Please enter a valid email address');
return false;
}
return true;
};
// Handle place order
const handlePlaceOrder = () => {
if (!user || !items || items.length === 0) {
return;
}
// Format shipping address for API
const shippingAddress = `${formData.firstName} ${formData.lastName}
${formData.address}
${formData.city}, ${formData.state} ${formData.zipCode}
${formData.country}
${formData.email}`;
checkout.mutate({
userId: user,
shippingAddress
}, {
onSuccess: () => {
// Move to confirmation step
setActiveStep(3);
}
});
};
// If no items in cart, redirect to cart page
if (!items || items.length === 0) {
return (
<Box sx={{ textAlign: 'center', py: 6 }}>
<Typography variant="h5" gutterBottom>
Your cart is empty
</Typography>
<Typography variant="body1" paragraph>
You need to add items to your cart before checkout.
</Typography>
<Button
variant="contained"
component={RouterLink}
to="/products"
sx={{ mr: 2 }}
>
Browse Products
</Button>
<Button
variant="outlined"
component={RouterLink}
to="/cart"
>
View Cart
</Button>
</Box>
);
}
// Render different step content based on active step
const getStepContent = (step) => {
switch (step) {
case 0:
return (
<Box component="form" sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="firstName"
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="lastName"
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
id="address"
label="Address"
name="address"
value={formData.address}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="city"
label="City"
name="city"
value={formData.city}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="state"
label="State/Province"
name="state"
value={formData.state}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="zipCode"
label="Zip / Postal code"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
fullWidth
id="country"
label="Country"
name="country"
value={formData.country}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Checkbox
name="saveAddress"
color="primary"
checked={formData.saveAddress}
onChange={handleChange}
/>
}
label="Save this address for future orders"
/>
</Grid>
</Grid>
</Box>
);
case 1:
return (
<Box sx={{ mt: 3 }}>
{/* Order summary */}
<Typography variant="h6" gutterBottom>
Order Summary
</Typography>
<List disablePadding>
{items.map((item) => (
<ListItem key={item.product_id} sx={{ py: 1, px: 0 }}>
<ListItemText
primary={item.name}
secondary={`Quantity: ${item.quantity}`}
/>
<Typography variant="body2">
${(parseFloat(item.price) * item.quantity).toFixed(2)}
</Typography>
</ListItem>
))}
<ListItem sx={{ py: 1, px: 0 }}>
<ListItemText primary="Shipping" />
<Typography variant="body2">Free</Typography>
</ListItem>
<ListItem sx={{ py: 1, px: 0 }}>
<ListItemText primary="Total" />
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
${total.toFixed(2)}
</Typography>
</ListItem>
</List>
<Divider sx={{ my: 2 }} />
{/* Shipping address */}
<Typography variant="h6" gutterBottom>
Shipping Address
</Typography>
<Typography gutterBottom>
{formData.firstName} {formData.lastName}
</Typography>
<Typography gutterBottom>
{formData.address}
</Typography>
<Typography gutterBottom>
{formData.city}, {formData.state} {formData.zipCode}
</Typography>
<Typography gutterBottom>
{formData.country}
</Typography>
<Typography gutterBottom>
{formData.email}
</Typography>
</Box>
);
case 2:
// Placeholder for payment (in a real app, this would have a payment form)
return (
<Box sx={{ mt: 3 }}>
<Alert severity="info" sx={{ mb: 3 }}>
This is a demo application. No actual payment will be processed.
</Alert>
<Typography variant="h6" gutterBottom>
Payment Method
</Typography>
<Typography paragraph>
For this demo, we'll simulate a successful payment.
</Typography>
<Typography paragraph>
Total to pay: ${total.toFixed(2)}
</Typography>
</Box>
);
case 3:
return (
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Alert severity="success" sx={{ mb: 3 }}>
Your order has been placed successfully!
</Alert>
<Typography variant="h5" gutterBottom>
Thank you for your order
</Typography>
<Typography paragraph>
Your order number is: #{Math.floor(100000 + Math.random() * 900000)}
</Typography>
<Typography paragraph>
We will send you a confirmation email with your order details.
</Typography>
<Button
variant="contained"
component={RouterLink}
to="/products"
sx={{ mt: 3 }}
>
Continue Shopping
</Button>
</Box>
);
default:
return <Typography>Unknown step</Typography>;
}
};
return (
<Box sx={{ mb: 8 }}>
<Typography variant="h4" component="h1" gutterBottom>
Checkout
</Typography>
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Paper variant="outlined" sx={{ p: 3 }}>
{getStepContent(activeStep)}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
{activeStep !== 0 && activeStep !== 3 && (
<Button
onClick={handleBack}
sx={{ mr: 1 }}
disabled={checkout.isLoading}
>
Back
</Button>
)}
{activeStep !== 3 ? (
<Button
variant="contained"
onClick={handleNext}
disabled={checkout.isLoading}
>
{activeStep === steps.length - 2 ? 'Place Order' : 'Next'}
{checkout.isLoading && (
<CircularProgress size={24} sx={{ ml: 1 }} />
)}
</Button>
) : (
<Button
variant="contained"
component={RouterLink}
to="/"
>
Return to Home
</Button>
)}
</Box>
</Paper>
</Box>
);
};
export default CheckoutPage;