Today we are going to create a simple shopping cart with React.js we are going to consume the API of Fakestoreapi.com we will also use material-ui that will provide us with Style Components to shape our Web Application, we will also learn to use react-query to get the API data |
We will create a folder on the desktop named shopping-cart
/
Once the folder is created, we navigate to it and execute
npx create-react-app .
We will install the following modules:
npm i react-query @material/core @material/icons
index.js
import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import App from "./App";
const client = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>,
document.getElementById("root")
);
App.js
import { useState } from "react";
import { useQuery } from "react-query";
//Components
import Item from "./components/Item";
import CartList from "./components/CartList";
import Navbar from "./components/Navbar";
import Drawer from "@material-ui/core/Drawer";
import LinearProgress from "@material-ui/core/LinearProgress";
import Grid from "@material-ui/core/Grid";
import "./custom.css";
const getProducts = async () =>
await (await fetch("https://fakestoreapi.com/products/")).json();
const App = () => {
const { isLoading, error, data } = useQuery("products", getProducts);
const [cartOpen, setCartOpen] = useState(false);
const [cartItems, setCartItems] = useState([]);
const getTotalItems = (cartItems) =>
cartItems.reduce((acum, i) => acum + i.amount, 0);
const handleAddItemToCart = (item) => {
setCartItems((prev) => {
// Search the item in the array
const isItemInTheCart = prev.find((i) => i.id === item.id);
if (isItemInTheCart) {
return prev.map((i) =>
i.id === item.id ? { ...i, amount: i.amount + 1 } : i
);
}
return [...prev, { ...item, amount: 1 }];
});
};
const handleRemoveItemFromCart = (id) => {
setCartItems((prev) => {
const foundItem = prev.find((i) => i.id === id);
if (foundItem) {
if (foundItem.amount === 1) {
const newArray = prev.filter((i) => i.id !== id);
return newArray;
} else {
return prev.map((i) =>
i.id === id ? { ...i, amount: i.amount - 1 } : i
);
}
} else {
return prev;
}
});
};
if (isLoading) return <LinearProgress>;
if (error) return error.message;
return (
<>
<Navbar
getTotalItems={getTotalItems(cartItems)}
setCartOpen={setCartOpen}
></Navbar>
<div className="main">
<Drawer
anchor="right"
open={cartOpen}
onClose={() => setCartOpen(false)}
>
<CartList
cartItems={cartItems}
handleAddItemToCart={handleAddItemToCart}
handleRemoveItemFromCart={handleRemoveItemFromCart}
/>
</Drawer>
<Grid container spacing={3}>
{data?.map((item) => (
<Grid key={item.id} item xs={12} sm={4}>
<Item item={item} handleAddItemToCart={handleAddItemToCart} />
</Grid>
))}
</Grid>
</div>
</>
);
};
export default App;
We create a custom css file
custom.css
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300&display=swap');
body {
margin :0;
padding:0;
font-family: 'Quicksand', sans-serif;
}
.main {
padding:40px
}
.cartIcon {
position: fixed;
top: 10px;
right: 140px;
cursor:pointer;
z-index: 100;
background: yellow;
}
aside {
width:460px;
margin : 20px;
}
.itemCart {
display:flex;
justify-content: space-between;
}
.itemCart img{
max-width: 80px;
object-fit: cover;
margin-left: 40px;
}
.itemCart div{
flex : 1
}
.itemInfo {
display: flex;
justify-content : space-between
}
.buttons {
display:flex;
justify-content : space-between
}
Components
components/Navbar.js
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import Badge from "@material-ui/core/Badge";
import Menu from "@material-ui/core/Menu";
import AddShoppingCart from "@material-ui/icons/AddShoppingCart";
const useStyles = makeStyles(() => ({
grow: {
flexGrow: 1,
}
}));
export default function Navbar({ getTotalItems, setCartOpen }) {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null);
const isMenuOpen = Boolean(anchorEl);
const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
const handleMobileMenuClose = () => {
setMobileMoreAnchorEl(null);
};
const handleMenuClose = () => {
setAnchorEl(null);
handleMobileMenuClose();
};
const menuId = "primary-search-account-menu";
const renderMenu = (
<Menu
anchorEl={anchorEl}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
id={menuId}
keepMounted
transformOrigin={{ vertical: "top", horizontal: "right" }}
open={isMenuOpen}
onClose={handleMenuClose}
></Menu>
);
const mobileMenuId = "primary-search-account-menu-mobile";
const renderMobileMenu = (
<Menu
anchorEl={mobileMoreAnchorEl}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
id={mobileMenuId}
keepMounted
transformOrigin={{ vertical: "top", horizontal: "right" }}
open={isMobileMenuOpen}
onClose={handleMobileMenuClose}
></Menu>
);
return (
<div className={classes.grow}>
<AppBar position="static">
<Toolbar>
<b>Shopping Cart</b> |
<div className={classes.grow} />
<div onClick={() => setCartOpen(true)}
>
<IconButton aria-label="show 4 new mails" color="inherit">
<Badge badgeContent={getTotalItems} color="secondary">
<AddShoppingCart />
</Badge>
</IconButton>
</div>
</Toolbar>
</AppBar>
{renderMobileMenu}
{renderMenu}
</div>
);
}
components/Item.js
import Card from "@material-ui/core/Card";
import { makeStyles } from '@material-ui/core/styles';
import CardActionArea from "@material-ui/core/CardActionArea";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import Button from "@material-ui/core/Button";
const useStyles = makeStyles({
root: {
maxWidth: 345,
},
});
export default function Item({item, handleAddItemToCart }) {
const classes = useStyles();
return (
<Card className={classes.root}>
<CardActionArea>
<CardMedia
component="img"
alt={item.title}
height="200"
image={item.image}
title={item.title}
/>
<CardContent>
{item.description}
</CardContent>
</CardActionArea>
<CardActions>
<Button size="small" color="secondary">
$ {item.price}
</Button>
<Button size="small" color="primary" onClick = { () => handleAddItemToCart(item)}>
Add to Cart
</Button>
</CardActions>
</Card>
);
}
components/CartList.js
import CartItem from "./CartItem";
const CartList = ({ cartItems, handleAddItemToCart, handleRemoveItemFromCart }) => {
const calculeTotal = cartItems.reduce(
(sum, i) => sum + i.amount * i.price,
0
);
return (
<aside>
<h1>Cart</h1>
{cartItems.length === 0 ? <h3>No products yet...</h3> : null}
<div>
{cartItems.map((i) => (
<CartItem item={i} handleAddItemToCart={handleAddItemToCart} handleRemoveItemFromCart={handleRemoveItemFromCart} />
))}
</div>
<h2>Total: {calculeTotal.toFixed(2)}</h2>
</aside>
);
};
export default CartList;
components/CartItem.js
import Button from "@material-ui/core/Button";
const CartItem = ({ item, handleAddItemToCart, handleRemoveItemFromCart }) => {
return (
<aside>
<div className="itemCart">
<div>
<h3>{item.title}</h3>
<div className="itemInfo">
<p>Precio: ${item.price}</p>
<p>Total: ${(item.amount * item.price).toFixed(2)}</p>
</div>
<div className="buttons">
<Button onClick={ () => handleRemoveItemFromCart(item.id)} size="small" disableElevation variant="contained">
-
</Button>
<p> {item.amount}
<Button
size="small"
disableElevation
variant="contained"
onClick={() => handleAddItemToCart(item)}
>
+
</Button>
</div>
</div>
<img src={item.image} />
</div>
</aside>
);
};
export default CartItem;
And with those friends we reached the end of the tutorial, I hope you enjoyed it and until next time! |