Skip to content

Commit ffac548

Browse files
feat: cart functionality using React (context and hooks)
1 parent 396cad6 commit ffac548

File tree

9 files changed

+301
-21
lines changed

9 files changed

+301
-21
lines changed

gatsby-browser.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@
55
*/
66

77
// You can delete this file if you're not using it
8+
9+
import React from "react";
10+
import CartProvider from "./src/context/cart";
11+
12+
export const wrapRootElement = ({ element }) => (
13+
<CartProvider>{element}</CartProvider>
14+
);

gatsby-ssr.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
*/
66

77
// You can delete this file if you're not using it
8+
export { wrapRootElement } from "./gatsby-browser";

src/components/cart-product.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from "react";
2+
import {
3+
Flex,
4+
Box,
5+
Heading,
6+
Text,
7+
Button,
8+
NumberInput,
9+
NumberInputField,
10+
NumberInputStepper,
11+
NumberIncrementStepper,
12+
NumberDecrementStepper,
13+
} from "@chakra-ui/core";
14+
15+
import { useCartStore } from "../context/cart";
16+
17+
const CartProduct = ({ product }) => {
18+
const { removeFromCart, updateCart } = useCartStore();
19+
const [quantity, setQuantity] = React.useState(product.quantity);
20+
const handleChange = value => {
21+
setQuantity(value);
22+
23+
updateCart({
24+
product,
25+
quantity: value,
26+
});
27+
};
28+
29+
return (
30+
<Flex
31+
width="100%"
32+
border="1px solid"
33+
borderColor="gray.700"
34+
borderRadius="4px"
35+
mb="3"
36+
>
37+
<Box p="4">
38+
<Heading as="h3" fontSize="xl">
39+
{product.name}
40+
</Heading>
41+
<Text>{product.brand}</Text>
42+
<Box>
43+
<Text as="strong">${product.price}</Text>
44+
</Box>
45+
</Box>
46+
<Box ml="auto" p="4">
47+
<NumberInput
48+
min={1}
49+
max={4}
50+
value={quantity}
51+
onChange={handleChange}
52+
precision={0}
53+
step={1}
54+
mb={3}
55+
size="sm"
56+
>
57+
<NumberInputField />
58+
<NumberInputStepper>
59+
<NumberIncrementStepper />
60+
<NumberDecrementStepper />
61+
</NumberInputStepper>
62+
</NumberInput>
63+
<Button
64+
variant="outline"
65+
size="sm"
66+
onClick={() =>
67+
removeFromCart({
68+
id: product.id,
69+
})
70+
}
71+
>
72+
Remove from cart
73+
</Button>
74+
</Box>
75+
</Flex>
76+
);
77+
};
78+
79+
export default CartProduct;

src/components/empty-cart.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from "react";
2+
import { Text } from "@chakra-ui/core";
3+
4+
const EmptyCart = () => {
5+
return <Text>Cart is Empty!</Text>;
6+
};
7+
8+
export default EmptyCart;

src/components/header.js

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import React from "react";
44
import { Flex, Box, Heading, Text } from "@chakra-ui/core";
55
import styled from "@emotion/styled";
66

7+
import { useCartStore } from "../context/cart";
8+
79
const LogoLink = styled(Link)`
810
text-decoration: none;
911
letter-spacing: 0.2rem;
@@ -13,32 +15,38 @@ const NavLink = styled(Link)`
1315
text-decoration: none;
1416
padding: 20px 0;
1517
font-size: 20px;
16-
letter-spacing: 0.1rem;
18+
letter-spacing: 0.05rem;
1719
border-bottom: 5px solid transparent;
20+
margin-right: 15px;
1821
&:hover {
1922
color: ${props => `${props.theme.colors.gray[400]}`};
2023
border-color: ${props => `${props.theme.colors.yellow[400]}`};
2124
}
2225
`;
2326

24-
const Header = ({ siteTitle }) => (
25-
<Box as="header" borderBottom="1px solid" borderColor="gray.700" py="3">
26-
<Flex width="100%" maxW="720px" m="0 auto" alignItems="center">
27-
<Box>
28-
<Heading as="h1" m="0" fontSize="32px">
29-
<LogoLink to="/">
30-
<Text as="span" color="yellow.400">
31-
{siteTitle}
32-
</Text>
33-
</LogoLink>
34-
</Heading>
35-
</Box>
36-
<Box as="nav" ml="auto">
37-
<NavLink to="/about">about</NavLink>
38-
</Box>
39-
</Flex>
40-
</Box>
41-
);
27+
const Header = ({ siteTitle }) => {
28+
const { cart } = useCartStore();
29+
30+
return (
31+
<Box as="header" borderBottom="1px solid" borderColor="gray.700" py="3">
32+
<Flex width="100%" maxW="720px" m="0 auto" alignItems="center">
33+
<Box>
34+
<Heading as="h1" m="0" fontSize="32px">
35+
<LogoLink to="/">
36+
<Text as="span" color="yellow.400">
37+
{siteTitle}
38+
</Text>
39+
</LogoLink>
40+
</Heading>
41+
</Box>
42+
<Box as="nav" ml="auto">
43+
<NavLink to="/about">about</NavLink>
44+
<NavLink to="/cart">cart ({cart.length})</NavLink>
45+
</Box>
46+
</Flex>
47+
</Box>
48+
);
49+
};
4250

4351
Header.propTypes = {
4452
siteTitle: PropTypes.string,

src/components/product.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
} from "@chakra-ui/core";
1212
import { useStripe } from "@stripe/react-stripe-js";
1313

14+
import { useCartStore } from "../context/cart";
15+
1416
const Categories = ({ tags }) => {
1517
return (
1618
<Box ml="auto">
@@ -34,6 +36,7 @@ const Categories = ({ tags }) => {
3436

3537
const Product = ({ product }) => {
3638
const stripe = useStripe();
39+
const { addToCart } = useCartStore();
3740

3841
const placeOrder = sku => {
3942
stripe.redirectToCheckout({
@@ -48,6 +51,13 @@ const Product = ({ product }) => {
4851
});
4952
};
5053

54+
const addProductToCart = product => {
55+
addToCart({
56+
...product,
57+
quantity: 1,
58+
});
59+
};
60+
5161
return (
5262
<Flex
5363
border="1px solid"
@@ -71,7 +81,12 @@ const Product = ({ product }) => {
7181
</Flex>
7282
<Divider m="0" />
7383
<Box px="4" py="3">
74-
<Button variantColor="yellow" size="sm" mr="2">
84+
<Button
85+
variantColor="yellow"
86+
size="sm"
87+
mr="2"
88+
onClick={() => addProductToCart(product)}
89+
>
7590
Add to cart
7691
</Button>
7792
<Button

src/context/cart.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { createContext, useContext, useReducer } from "react";
2+
3+
const CartContext = createContext({});
4+
5+
export const useCartStore = () => {
6+
const context = useContext(CartContext);
7+
8+
if (!context) {
9+
throw new Error("Consumer should be wrapped inside the Provider");
10+
}
11+
12+
return context;
13+
};
14+
15+
const cartReducer = (state, action) => {
16+
switch (action.type) {
17+
case "ADD": {
18+
const { id } = action.payload;
19+
const newState = [...state];
20+
const index = state.findIndex(cartItem => {
21+
return cartItem.id === id;
22+
});
23+
if (index > -1) {
24+
let quantity = newState[index].quantity + 1;
25+
if (quantity > 4) quantity = 4;
26+
newState[index] = {
27+
...newState[index],
28+
quantity,
29+
};
30+
} else {
31+
newState.push({
32+
...action.payload,
33+
});
34+
}
35+
return [...newState];
36+
}
37+
case "UPDATE": {
38+
const { id } = action.payload;
39+
const updatedItems = state.map(cartItem => {
40+
if (cartItem.id === id) {
41+
return {
42+
...action.payload,
43+
};
44+
}
45+
return cartItem;
46+
});
47+
return [...updatedItems];
48+
}
49+
case "REMOVE": {
50+
const { id } = action.payload;
51+
const remainingItems = state.filter(cartItem => cartItem.id !== id);
52+
return [...remainingItems];
53+
}
54+
default:
55+
return state;
56+
}
57+
};
58+
59+
const CartProvider = ({ children }) => {
60+
const [cart, dispatch] = useReducer(cartReducer, []);
61+
62+
const addToCart = payload => {
63+
dispatch({ type: "ADD", payload });
64+
};
65+
66+
const removeFromCart = payload => {
67+
dispatch({ type: "REMOVE", payload });
68+
};
69+
70+
const updateCart = payload => {
71+
dispatch({ type: "UPDATE", payload });
72+
};
73+
74+
const context = {
75+
cart,
76+
addToCart,
77+
updateCart,
78+
removeFromCart,
79+
};
80+
81+
return (
82+
<CartContext.Provider value={context}>{children}</CartContext.Provider>
83+
);
84+
};
85+
86+
export default CartProvider;

src/data/products.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"description": "You know your dad’s incredible vintage bomber jacket? The Nirvana dakota tan leather sofa is that jacket, but in couch form. With super-plush down-filled cushions, a corner-blocked wooden frame, and a leather patina that only gets better with age, the Nirvana will have you looking cool and feeling peaceful every time you take a seat. Looks pretty great with a sheepskin throw, if we may say so. With use, this leather will become softer and more wrinkled and the cushions will take on a lived-in look, like your favorite leather jacket.",
3838
"brand": "Jason Bourne",
3939
"currentInventory": 10,
40-
"image": "https://picsum.photos/id/40/200/200"
40+
"image": "https://picsum.photos/id/41/200/200"
4141
},
4242
{
4343
"id": "sku_HGA25LOxtn6J6Q",

src/pages/cart.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from "react";
2+
import { Flex, Box, Button, Heading } from "@chakra-ui/core";
3+
import { useStripe } from "@stripe/react-stripe-js";
4+
5+
import Layout from "../components/layout";
6+
import SEO from "../components/seo";
7+
import EmptyCart from "../components/empty-cart";
8+
import CartProduct from "../components/cart-product";
9+
10+
import { useCartStore } from "../context/cart";
11+
12+
const BuyAll = ({ cart }) => {
13+
const stripe = useStripe();
14+
15+
const placeOrder = () => {
16+
const items = cart.map(product => {
17+
return {
18+
sku: product.id,
19+
quantity: product.quantity,
20+
};
21+
});
22+
stripe.redirectToCheckout({
23+
items,
24+
successUrl: "http://localhost:8000/success",
25+
cancelUrl: "http://localhost:8000/cancel",
26+
});
27+
};
28+
29+
return (
30+
<Box py="4">
31+
<Button variantColor="yellow" size="sm" mr="2" onClick={placeOrder}>
32+
Buy all
33+
</Button>
34+
</Box>
35+
);
36+
};
37+
38+
const CartPage = () => {
39+
const { cart } = useCartStore();
40+
41+
return (
42+
<Layout>
43+
<SEO title="Cart" />
44+
<Flex
45+
as="section"
46+
maxW="720px"
47+
minH="calc(100vh - 190px)"
48+
mx="auto"
49+
my="3"
50+
alignItems={cart.length === 0 ? "center" : "flex-start"}
51+
>
52+
{cart.length === 0 ? (
53+
<EmptyCart />
54+
) : (
55+
<Flex flexWrap="wrap">
56+
<Heading
57+
as="h2"
58+
fontSize="28px"
59+
my="30px"
60+
textAlign="center"
61+
width="100%"
62+
>
63+
Cart products
64+
</Heading>
65+
<BuyAll cart={cart} />
66+
{cart.map(product => {
67+
return <CartProduct key={product.name} product={product} />;
68+
})}
69+
</Flex>
70+
)}
71+
</Flex>
72+
</Layout>
73+
);
74+
};
75+
76+
export default CartPage;

0 commit comments

Comments
 (0)