├── .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 | Mine Commerce 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 |
5 |
6 |
7 |
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 | --------------------------------------------------------------------------------