Hands-on: Shopping Cart Answer
- Install
decimal.jswithnpm install decimal.js. - Create a
Cart.tsxfile insrc/pages, and userafceto generate a function component (page). - Import the
Cartcomponent insrc/main.tsxand register its route.
import Cart from "./pages/Cart";
createRoot(document.getElementById('root')!).render( <BrowserRouter> <Routes> <Route path="/cart" element={<Cart />} /> { /* ... other routes */ } </Routes> </BrowserRouter>);- Add a button in
src/pages/HelloWorld.tsxthat goes to/cart.
<button onClick={() => navigate('/cart')}>Cart</button>Define the variables
Section titled “Define the variables”From here on, everything happens inside the Cart function component in Cart.tsx.
- Set up the
hookswe need. This step is needed because someantdcomponents have to be set up with a bit of extra code, such asmessageandTypography.
const { Title, Text } = Typography;const [messageApi, contextHolder] = message.useMessage();- Define the data structure.
type Product = { id: number, name: string, price: number, discount: number, stock: number, image: string, realPrice: number}- Define the product list and the cart
const [products, setProducts] = useState<Product[]>([]);
const [cart, setCart] = useState< { id: number, quantity: number }[]>([]);- Fetch the data
useEffect(() => { myAPI.get('products.json') .then(response => { const newProducts = response.data.products as Product[]; newProducts.forEach(product => { product.realPrice = new Decimal(product.price).minus(product.discount).toDecimalPlaces(2).toNumber();
}); setProducts(newProducts); });}, []);Helper functions
Section titled “Helper functions”- We need a
total()function to calculate the total price of the cart.
const total = () => { let sum = new Decimal(0); cart.forEach(item => { const product = products.find(product => product.id === item.id); if (product) { sum = sum.plus(new Decimal(product.realPrice).times(item.quantity)); } }); return sum.toDecimalPlaces(2).toNumber();}- We also need an
updateCart(id, quantity)function to update the quantity of an item in the cart, wherequantityis the amount to add. Don’t forget to update the stock as well!
const updateCart = (id: number, quantity: number) => { const product = products.find(product => product.id === id); if (!product || product.stock < quantity) { return; } let newCart = [...cart]; let found = false; newCart.forEach(item => { if (item.id === id) { item.quantity += quantity; found = true; if (item.quantity <= 0) { newCart = newCart.filter(item => item.id !== id); } } }); if (!found && quantity > 0) { newCart.push({ id, quantity }); } setCart(newCart); const updateProducts = [...products]; updateProducts.forEach(product => { if (product.id === id) { product.stock -= quantity; } }); setProducts(updateProducts);}The page
Section titled “The page”- First, let’s build the frame.
<Card> {contextHolder} { /* code from the antd docs that displays messages */ } <Row> <Col span={16}> <Title level={3} style={{ marginTop: 0 }}>Fruit Store 🍎</Title> <Flex gap={5} wrap> { /* product list */ } </Flex> </Col> <Col span={8}> <Card title="Cart"> <Flex gap={5} vertical> { /* cart items */ } <Title level={5}>Total: ${total()}</Title> { /* checkout button; note how onClick is written */ } <Button type="primary" onClick={() => { messageApi.success('Checked out'); setCart([]) }}>Check out</Button> </Flex> </Card> </Col> </Row></Card>- The product list
{products.map(product => ( <Card key={product.id}> <Flex gap={1} vertical> <Title level={5} style={{ marginTop: -10 }}> <Tag color="cyan">#{product.id}</Tag> {product.name} </Title> <Text delete={product.discount > 0}>Price: ${product.price}</Text> {product.discount > 0 && <Text mark>Sale price: ${product.realPrice}</Text>} <Text type="secondary">Stock: {product.stock}</Text> <Image width={100} src={product.image} /> <Button style={{ marginTop: 10 }} onClick={() => updateCart(product.id, 1)} disabled={product.stock <= 0}>Add to cart</Button> </Flex> </Card>))}- The cart items
{cart.map(item => ( <Card key={item.id} size="small" style={{ marginBottom: 10 }}> <DeleteOutlined style={{ marginRight: 10, color: 'red' }} onClick={() => updateCart(item.id, -item.quantity)} /> {products.find(product => product.id === item.id)?.name} - ${products.find(product => product.id === item.id)?.realPrice} | Qty: <MinusCircleOutlined onClick={() => updateCart(item.id, -1)} /> {item.quantity} <PlusCircleOutlined onClick={() => updateCart(item.id, +1)} /> </Card>))}Full code
Section titled “Full code”import { Card, Col, Flex, Row, Typography, Image, Tag, Button, message } from "antd"import { useEffect, useState } from "react";import myAPI from "../axios";import { DeleteOutlined, MinusCircleOutlined, PlusCircleOutlined } from "@ant-design/icons";import Decimal from 'decimal.js';
const Cart = () => { const { Title, Text } = Typography; const [messageApi, contextHolder] = message.useMessage();
type Product = { id: number, name: string, price: number, discount: number, stock: number, image: string, realPrice: number }
const [products, setProducts] = useState<Product[]>([]);
const [cart, setCart] = useState< { id: number, quantity: number }[] >([]);
useEffect(() => { myAPI.get('products.json') .then(response => { const newProducts = response.data.products as Product[]; newProducts.forEach(product => { product.realPrice = new Decimal(product.price).minus(product.discount).toDecimalPlaces(2).toNumber();
}); setProducts(newProducts); }); }, []);
const total = () => { let sum = new Decimal(0); cart.forEach(item => { const product = products.find(product => product.id === item.id); if (product) { sum = sum.plus(new Decimal(product.realPrice).times(item.quantity)); } }); return sum.toDecimalPlaces(2).toNumber(); }
const updateCart = (id: number, quantity: number) => { const product = products.find(product => product.id === id); if (!product || product.stock < quantity) { return; } let newCart = [...cart]; let found = false; newCart.forEach(item => { if (item.id === id) { item.quantity += quantity; found = true; if (item.quantity <= 0) { newCart = newCart.filter(item => item.id !== id); } } }); if (!found && quantity > 0) { newCart.push({ id, quantity }); } setCart(newCart); const updateProducts = [...products]; updateProducts.forEach(product => { if (product.id === id) { product.stock -= quantity; } }); setProducts(updateProducts); }
return ( <Card> {contextHolder} <Row> <Col span={16}> <Title level={3} style={{ marginTop: 0 }}>Fruit Store 🍎</Title> <Flex gap={5} wrap> {products.map(product => ( <Card key={product.id}> <Flex gap={1} vertical> <Title level={5} style={{ marginTop: -10 }}> <Tag color="cyan">#{product.id}</Tag> {product.name} </Title> <Text delete={product.discount > 0}>Price: ${product.price}</Text> {product.discount > 0 && <Text mark>Sale price: ${product.realPrice}</Text>} <Text type="secondary">Stock: {product.stock}</Text> <Image width={100} src={product.image} /> <Button style={{ marginTop: 10 }} onClick={() => updateCart(product.id, 1)} disabled={product.stock <= 0}>Add to cart</Button> </Flex> </Card> ))} </Flex> </Col> <Col span={8}> <Card title="Cart"> <Flex gap={5} vertical> {cart.map(item => ( <Card key={item.id} size="small" style={{ marginBottom: 10 }}> <DeleteOutlined style={{ marginRight: 10, color: 'red' }} onClick={() => updateCart(item.id, -item.quantity)} /> {products.find(product => product.id === item.id)?.name} - ${products.find(product => product.id === item.id)?.realPrice} | Qty: <MinusCircleOutlined onClick={() => updateCart(item.id, -1)} /> {item.quantity} <PlusCircleOutlined onClick={() => updateCart(item.id, +1)} /> </Card> ))} <Title level={5}>Total: ${total()}</Title> <Button type="primary" onClick={() => { messageApi.success('Checked out'); setCart([]) }}>Check out</Button> </Flex> </Card> </Col> </Row> </Card> )}
export default CartClick to reveal answer