├── .env
├── .gitignore
├── README.md
├── package.json
├── public
└── index.html
└── src
├── assets
└── commerce.png
├── components
├── CartItem
│ ├── index.jsx
│ └── styles.js
├── Navbar
│ ├── index.jsx
│ └── styles.js
├── Product
│ ├── index.jsx
│ └── styles.js
├── Spinner
│ ├── index.jsx
│ └── styles.css
└── index.js
├── containers
├── App.js
├── Cart
│ ├── index.jsx
│ └── styles.js
└── Products
│ ├── index.jsx
│ └── styles.js
├── index.js
├── lib
└── commerce.js
├── providers
├── cart-context.js
└── products-context.js
└── reportWebVitals.js
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_CHEC_PUBLIC_KEY=pk_test_30228b2c577deac64666f19e218b1b06089e4a31df898
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pooridev/reactjs-shopping-cart/a432ce6b15e33bb13c63149d89ef68458b6b49c6/README.md
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shopping_cart",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@chec/commerce.js": "^2.7.2",
7 | "@material-ui/core": "^4.12.1",
8 | "@material-ui/icons": "^4.11.2",
9 | "@stripe/react-stripe-js": "^1.4.1",
10 | "@stripe/stripe-js": "^1.16.0",
11 | "@testing-library/jest-dom": "^5.11.4",
12 | "@testing-library/react": "^11.1.0",
13 | "@testing-library/user-event": "^12.1.10",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2",
16 | "react-router-dom": "^5.2.0",
17 | "react-scripts": "4.0.3",
18 | "web-vitals": "^1.0.1"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React.js - Shopping Cart
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/commerce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pooridev/reactjs-shopping-cart/a432ce6b15e33bb13c63149d89ef68458b6b49c6/src/assets/commerce.png
--------------------------------------------------------------------------------
/src/components/CartItem/index.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Typography,
3 | Card,
4 | CardActions,
5 | CardMedia,
6 | CardContent,
7 | Button
8 | } from '@material-ui/core';
9 |
10 | import useStyles from './styles';
11 |
12 | const CartItem = ({ item, onRemoveFromCart, onUpdateCartQty }) => {
13 | const classes = useStyles();
14 | const handleUpdateCartQty = (lineItemId, newQuantity) =>
15 | onUpdateCartQty(lineItemId, newQuantity);
16 |
17 | const handleRemoveFromCart = lineItemId => onRemoveFromCart(lineItemId);
18 | return (
19 |
20 |
25 |
26 | {item.name}
27 |
28 | {item.line_total.formatted_with_symbol}
29 |
30 |
31 |
32 |
33 |
39 | {item.quantity}
40 |
46 |
47 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default CartItem;
60 |
--------------------------------------------------------------------------------
/src/components/CartItem/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(theme => ({
4 | media: {
5 | height: 260
6 | },
7 | cardContent: {
8 | display: 'flex',
9 | justifyContent: 'space-between'
10 | },
11 | cardActions: {
12 | justifyContent: 'space-between'
13 | },
14 | buttons: {
15 | display: 'flex',
16 | alignItems: 'center'
17 | },
18 | itemName: {
19 | [theme.breakpoints.up('sm')]: {
20 | fontSize: '1.4rem'
21 | }
22 | }
23 | }));
24 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.jsx:
--------------------------------------------------------------------------------
1 | import Logo from '../../assets/commerce.png';
2 | import { AppBar, Badge, Typography, IconButton } from '@material-ui/core';
3 | import { ShoppingCart } from '@material-ui/icons';
4 | import useStyles from './styles';
5 | import { Link, useLocation } from 'react-router-dom';
6 | import { useCart } from './../../providers/cart-context';
7 | import { useEffect } from 'react';
8 |
9 | const Navbar = () => {
10 | const { fetchCart, cart } = useCart();
11 |
12 | const classes = useStyles();
13 |
14 | useEffect(() => fetchCart(), [fetchCart]);
15 |
16 | const location = useLocation();
17 | let cartLink = null;
18 |
19 | if (location.pathname === '/') {
20 | cartLink = (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | return (
32 |
37 |
38 |
44 | Shopping Cart
45 |
46 | {cartLink}
47 |
48 | );
49 | };
50 |
51 | export default Navbar;
52 |
--------------------------------------------------------------------------------
/src/components/Navbar/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | const drawerWidth = 0;
4 |
5 | export default makeStyles(theme => ({
6 | appBar: {
7 | boxShadow: 'none',
8 | borderBottom: '1px solid rgba(0, 0, 0, 0.12)',
9 | [theme.breakpoints.up('sm')]: {
10 | width: `calc(100% - ${drawerWidth}px)`,
11 | marginLeft: drawerWidth
12 | },
13 | display: 'flex',
14 | justifyContent: 'space-between',
15 | flexDirection: 'row',
16 | padding: '0.5rem 1.7rem'
17 | },
18 |
19 | title: {
20 | alignItems: 'center',
21 | display: 'flex',
22 | textDecoration: 'none'
23 | },
24 | image: {
25 | marginRight: '10px'
26 | },
27 | menuButton: {
28 | marginRight: theme.spacing(2),
29 | [theme.breakpoints.up('sm')]: {
30 | display: 'none'
31 | }
32 | }
33 | }));
34 |
--------------------------------------------------------------------------------
/src/components/Product/index.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardActions,
4 | Typography,
5 | CardContent,
6 | IconButton,
7 | CardMedia
8 | } from '@material-ui/core';
9 |
10 | import { AddShoppingCart } from '@material-ui/icons';
11 | import { useCart } from '../../providers/cart-context';
12 | import useStyles from './styles';
13 |
14 | function Product({ product }) {
15 | const { handleAddToCart } = useCart();
16 | const classes = useStyles();
17 |
18 | return (
19 |
20 |
25 |
26 |
27 |
28 | {product.name}
29 |
30 |
31 | {product.price.formatted_with_symbol}
32 |
33 |
34 |
39 |
40 |
41 | handleAddToCart(product.id, 1)}
43 | aria-label='Add To Cart'>
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default Product;
52 |
--------------------------------------------------------------------------------
/src/components/Product/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(() => ({
4 | root: {
5 | // maxWidth: 345, original width style
6 | maxWidth: '100%'
7 | },
8 | media: {
9 | height: 0,
10 | paddingTop: '56.25%' // 16:9
11 | },
12 | cardActions: {
13 | display: 'flex',
14 | justifyContent: 'flex-end'
15 | },
16 | cardContent: {
17 | display: 'flex',
18 | justifyContent: 'space-between'
19 | }
20 | }));
21 |
--------------------------------------------------------------------------------
/src/components/Spinner/index.jsx:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 |
3 | const Spinner = () => (
4 |
8 | );
9 |
10 | export default Spinner;
11 |
--------------------------------------------------------------------------------
/src/components/Spinner/styles.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | position: relative;
3 | width: 80px;
4 | height: 80px;
5 | margin: 100px auto;
6 | }
7 | .spinner div {
8 | position: absolute;
9 | border: 4px solid #2c2c2c;
10 | opacity: 1;
11 | border-radius: 50%;
12 | animation: spinner 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
13 | }
14 | .spinner div:nth-child(2) {
15 | animation-delay: -0.5s;
16 | }
17 | @keyframes spinner {
18 | 0% {
19 | top: 36px;
20 | left: 36px;
21 | width: 0;
22 | height: 0;
23 | opacity: 1;
24 | }
25 | 100% {
26 | top: 0px;
27 | left: 0px;
28 | width: 72px;
29 | height: 72px;
30 | opacity: 0;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar';
2 | export { default as Products } from '../containers/Products';
3 | export { default as Cart } from '../containers/Cart';
4 |
--------------------------------------------------------------------------------
/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import { Products, Navbar, Cart } from '../components/index';
2 |
3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
4 |
5 | function App() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/src/containers/Cart/index.jsx:
--------------------------------------------------------------------------------
1 | import { Container, Typography, Grid, Button } from '@material-ui/core';
2 | import CartItem from '../../components/CartItem';
3 | import { Link } from 'react-router-dom';
4 | import { useCart } from './../../providers/cart-context';
5 | import { useEffect } from 'react';
6 | import Spinner from './../../components/Spinner/index';
7 |
8 | import useStyles from './styles';
9 |
10 | const Cart = () => {
11 | const {
12 | cart,
13 | fetchCart,
14 | handleUpdateCartQty,
15 | handleRemoveFromCart,
16 | handleEmptyCart
17 | } = useCart();
18 |
19 | useEffect(() => fetchCart(), [fetchCart]);
20 |
21 | const classes = useStyles();
22 |
23 | const EmptyCart = () => (
24 |
25 | You have no items in your shopping cart,{' '}
26 | start adding some!
27 |
28 | );
29 |
30 | if (!cart.line_items) return ;
31 |
32 | const FilledCart = () => (
33 | <>
34 |
35 | {cart.line_items.map(item => (
36 |
37 |
42 |
43 | ))}
44 |
45 |
46 |
47 | Total price: {cart.subtotal.formatted_with_symbol}
48 |
49 |
50 |
59 |
60 |
61 | >
62 | );
63 |
64 | return (
65 |
66 |
67 |
68 | Your shopping cart
69 |
70 | {!cart.line_items.length ? : }
71 |
72 | );
73 | };
74 |
75 | export default Cart;
76 |
--------------------------------------------------------------------------------
/src/containers/Cart/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(theme => ({
4 | toolbar: theme.mixins.toolbar,
5 | title: {
6 | marginTop: '5%',
7 | fontSize: '2rem',
8 | [theme.breakpoints.up('sm')]: {
9 | fontSize: '2.4rem'
10 | }
11 | },
12 | emptyButton: {
13 | minWidth: '150px',
14 | [theme.breakpoints.down('xs')]: {
15 | marginBottom: '5px'
16 | },
17 | [theme.breakpoints.up('xs')]: {
18 | marginRight: '20px'
19 | }
20 | },
21 | checkoutButton: {
22 | minWidth: '150px'
23 | },
24 | link: {
25 | textDecoration: 'none'
26 | },
27 | cardDetails: {
28 | display: 'flex',
29 | marginTop: '10%',
30 | flexWrap: 'wrap',
31 | width: '100%',
32 | justifyContent: 'space-between',
33 | alignItems: 'center'
34 | },
35 | subtotal: {
36 | [theme.breakpoints.up('sm')]: {
37 | fontSize: '2rem'
38 | }
39 | }
40 | }));
41 |
--------------------------------------------------------------------------------
/src/containers/Products/index.jsx:
--------------------------------------------------------------------------------
1 | import { Grid } from '@material-ui/core';
2 | import Product from '../../components/Product';
3 | import useStyles from './styles';
4 | import { useProducts } from './../../providers/products-context';
5 | import Spinner from '../../components/Spinner';
6 | import { useEffect } from 'react';
7 |
8 | function Products() {
9 | const { products, fetchProducts } = useProducts();
10 |
11 | const classes = useStyles();
12 |
13 | useEffect(() => {
14 | fetchProducts();
15 | }, [fetchProducts]);
16 |
17 | if (!products) return ;
18 |
19 | return (
20 | <>
21 |
22 |
29 | {products.map(product => (
30 |
31 |
32 |
33 | ))}
34 |
35 | >
36 | );
37 | }
38 |
39 | export default Products;
40 |
--------------------------------------------------------------------------------
/src/containers/Products/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(theme => ({
4 | toolbar: theme.mixins.toolbar,
5 | content: {
6 | flexGrow: 1,
7 | backgroundColor: theme.palette.background.default,
8 | padding: theme.spacing(3)
9 | },
10 | root: {
11 | flexGrow: 1
12 | }
13 | }));
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './containers/App';
4 | import reportWebVitals from './reportWebVitals';
5 | import { CartProvider } from './providers/cart-context';
6 | import { ProductsProvider } from './providers/products-context';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/src/lib/commerce.js:
--------------------------------------------------------------------------------
1 | import Commerce from '@chec/commerce.js';
2 |
3 | export const commerce = new Commerce(process.env.REACT_APP_CHEC_PUBLIC_KEY, true);
4 |
--------------------------------------------------------------------------------
/src/providers/cart-context.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useMemo } from 'react';
2 | import { createContext, useState } from 'react';
3 | import { commerce } from './../lib/commerce';
4 |
5 | export const CartContext = createContext({
6 | cart: {},
7 |
8 | fetchCart: () => {},
9 |
10 | handleUpdateCartQty: () => {},
11 | handleRemoveFromCart: () => {},
12 | handleEmptyCart: () => {},
13 | handleAddToCart: () => {}
14 | });
15 |
16 | export const CartProvider = ({ children }) => {
17 | const [cart, setCart] = useState({});
18 |
19 | // for fetching cart after mounting
20 | const fetchCart = useCallback(async () => {
21 | const cart = await commerce.cart.retrieve();
22 |
23 | setCart(cart);
24 | }, []);
25 |
26 | // for adding and removing cart items
27 | const handleUpdateCartQty = async (lineItemId, quantity) => {
28 | const { cart } = await commerce.cart.update(lineItemId, { quantity });
29 |
30 | setCart(cart);
31 | };
32 |
33 | // for deleting specific item from the cart
34 | const handleRemoveFromCart = async lineItemId => {
35 | const { cart } = await commerce.cart.remove(lineItemId);
36 |
37 | setCart(cart);
38 | };
39 |
40 | // for emptying cart
41 | const handleEmptyCart = async () => {
42 | const { cart } = await commerce.cart.empty();
43 |
44 | setCart(cart);
45 | };
46 |
47 | // for adding one item at the time to the cart
48 | const handleAddToCart = async (productId, quantity) => {
49 | const { cart } = await commerce.cart.add(productId, quantity);
50 |
51 | setCart(cart);
52 | };
53 |
54 | const contextValue = useMemo(
55 | () => ({
56 | cart,
57 | fetchCart,
58 | handleUpdateCartQty,
59 | handleRemoveFromCart,
60 | handleEmptyCart,
61 | handleAddToCart
62 | }),
63 | [cart, fetchCart]
64 | );
65 |
66 | return ;
67 | };
68 |
69 | export const useCart = () => useContext(CartContext);
70 |
--------------------------------------------------------------------------------
/src/providers/products-context.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useMemo, useState } from 'react';
2 | import { createContext } from 'react';
3 | import { commerce } from './../lib/commerce';
4 |
5 | export const ProductsContext = createContext({
6 | products: null,
7 |
8 | fetchProducts: () => {}
9 | });
10 |
11 | export const ProductsProvider = ({ children }) => {
12 | const [products, setProducts] = useState(null);
13 |
14 | const fetchProducts = useCallback(async () => {
15 | const { data } = await commerce.products.list();
16 | setProducts(data);
17 | }, []);
18 |
19 | const contextValue = useMemo(
20 | () => ({
21 | products,
22 | fetchProducts
23 | }),
24 | [products, fetchProducts]
25 | );
26 |
27 | return ;
28 | };
29 |
30 | export const useProducts = () => useContext(ProductsContext);
31 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------