441 lines
No EOL
12 KiB
JavaScript
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; |