├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── components ├── Cart.js ├── Filters.js ├── Header.js ├── Home.js ├── Rating.js ├── SingleProduct.js └── styles.css ├── context ├── Context.js └── Reducers.js ├── index.css └── index.js /.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: -------------------------------------------------------------------------------- 1 | # Shopping Cart Built in React JS with Context API and useReducer 2 | 3 | ### Live Demo - https://shopping-cart-with-reactjs.netlify.app/ 4 | ## [Watch Full Tutorial on Roadside Coder Channel](https://www.youtube.com/roadsidecoder) 5 | 6 | ![SHOPPING CART](https://user-images.githubusercontent.com/51760520/137257892-5bc9526b-4c59-4054-b255-337dc2c90123.png) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "context-with-reducer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "bootstrap": "4.6.0", 10 | "faker": "^5.5.3", 11 | "react": "^17.0.2", 12 | "react-bootstrap": "^1.6.1", 13 | "react-dom": "^17.0.2", 14 | "react-icons": "^4.2.0", 15 | "react-router-dom": "^5.2.0", 16 | "react-scripts": "^5.0.0", 17 | "web-vitals": "^1.0.1" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/React-shopping-cart-context-with-reducer/1953149fda5ec62f465c93a7276d57ab9036ffbc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Shopping Cart 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/React-shopping-cart-context-with-reducer/1953149fda5ec62f465c93a7276d57ab9036ffbc/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/React-shopping-cart-context-with-reducer/1953149fda5ec62f465c93a7276d57ab9036ffbc/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none !important; 3 | color: inherit !important; 4 | } 5 | 6 | /* Scroll bar */ 7 | body::-webkit-scrollbar { 8 | width: 5px; 9 | } 10 | body::-webkit-scrollbar-track { 11 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 12 | } 13 | 14 | body::-webkit-scrollbar-thumb { 15 | background-color: darkgrey; 16 | outline: 1px solid slategrey; 17 | } 18 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Header from "./components/Header"; 3 | import { BrowserRouter, Route } from "react-router-dom"; 4 | import Home from "./components/Home"; 5 | import Cart from "./components/Cart"; 6 | 7 | function App() { 8 | return ( 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/components/Cart.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Button, Col, Form, Image, ListGroup, Row } from "react-bootstrap"; 3 | import { AiFillDelete } from "react-icons/ai"; 4 | import { CartState } from "../context/Context"; 5 | import Rating from "./Rating"; 6 | 7 | const Cart = () => { 8 | const { 9 | state: { cart }, 10 | dispatch, 11 | } = CartState(); 12 | const [total, setTotal] = useState(); 13 | 14 | useEffect(() => { 15 | setTotal( 16 | cart.reduce((acc, curr) => acc + Number(curr.price) * curr.qty, 0) 17 | ); 18 | }, [cart]); 19 | 20 | return ( 21 |
22 |
23 | 24 | {cart.map((prod) => ( 25 | 26 | 27 | 28 | {prod.name} 29 | 30 | 31 | {prod.name} 32 | 33 | ₹ {prod.price} 34 | 35 | 36 | 37 | 38 | 42 | dispatch({ 43 | type: "CHANGE_CART_QTY", 44 | payload: { 45 | id: prod.id, 46 | qty: e.target.value, 47 | }, 48 | }) 49 | } 50 | > 51 | {[...Array(prod.inStock).keys()].map((x) => ( 52 | 53 | ))} 54 | 55 | 56 | 57 | 69 | 70 | 71 | 72 | ))} 73 | 74 |
75 |
76 | Subtotal ({cart.length}) items 77 | Total: ₹ {total} 78 | 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Cart; 87 | -------------------------------------------------------------------------------- /src/components/Filters.js: -------------------------------------------------------------------------------- 1 | import { Button, Form } from "react-bootstrap"; 2 | import { CartState } from "../context/Context"; 3 | import Rating from "./Rating"; 4 | 5 | const Filters = () => { 6 | const { 7 | productDispatch, 8 | productState: { byStock, byFastDelivery, sort, byRating }, 9 | } = CartState(); 10 | 11 | // make state for rating 12 | 13 | return ( 14 |
15 | Filter Products 16 | 17 | 24 | productDispatch({ 25 | type: "SORT_BY_PRICE", 26 | payload: "lowToHigh", 27 | }) 28 | } 29 | checked={sort === "lowToHigh" ? true : false} 30 | /> 31 | 32 | 33 | 40 | productDispatch({ 41 | type: "SORT_BY_PRICE", 42 | payload: "highToLow", 43 | }) 44 | } 45 | checked={sort === "highToLow" ? true : false} 46 | /> 47 | 48 | 49 | 56 | productDispatch({ 57 | type: "FILTER_BY_STOCK", 58 | }) 59 | } 60 | checked={byStock} 61 | /> 62 | 63 | 64 | 71 | productDispatch({ 72 | type: "FILTER_BY_DELIVERY", 73 | }) 74 | } 75 | checked={byFastDelivery} 76 | /> 77 | 78 | 79 | 80 | 83 | productDispatch({ 84 | type: "FILTER_BY_RATING", 85 | payload: i + 1, 86 | }) 87 | } 88 | style={{ cursor: "pointer" }} 89 | /> 90 | 91 | 101 |
102 | ); 103 | }; 104 | 105 | export default Filters; 106 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { FaShoppingCart } from "react-icons/fa"; 2 | import { AiFillDelete } from "react-icons/ai"; 3 | import { 4 | Badge, 5 | Button, 6 | Container, 7 | Dropdown, 8 | FormControl, 9 | Nav, 10 | Navbar, 11 | } from "react-bootstrap"; 12 | import { Link, useLocation } from "react-router-dom"; 13 | import { CartState } from "../context/Context"; 14 | import "./styles.css"; 15 | 16 | const Header = () => { 17 | const { 18 | state: { cart }, 19 | dispatch, 20 | productDispatch, 21 | } = CartState(); 22 | 23 | return ( 24 | 25 | 26 | 27 | Shopping Cart 28 | 29 | {useLocation().pathname.split("/")[1] !== "cart" && ( 30 | 31 | { 38 | productDispatch({ 39 | type: "FILTER_BY_SEARCH", 40 | payload: e.target.value, 41 | }); 42 | }} 43 | /> 44 | 45 | )} 46 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | export default Header; 97 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import { CartState } from "../context/Context"; 2 | import Filters from "./Filters"; 3 | import SingleProduct from "./SingleProduct"; 4 | 5 | const Home = () => { 6 | const { 7 | state: { products }, 8 | productState: { sort, byStock, byFastDelivery, byRating, searchQuery }, 9 | } = CartState(); 10 | 11 | const transformProducts = () => { 12 | let sortedProducts = products; 13 | 14 | if (sort) { 15 | sortedProducts = sortedProducts.sort((a, b) => 16 | sort === "lowToHigh" ? a.price - b.price : b.price - a.price 17 | ); 18 | } 19 | 20 | if (!byStock) { 21 | sortedProducts = sortedProducts.filter((prod) => prod.inStock); 22 | } 23 | 24 | if (byFastDelivery) { 25 | sortedProducts = sortedProducts.filter((prod) => prod.fastDelivery); 26 | } 27 | 28 | if (byRating) { 29 | sortedProducts = sortedProducts.filter( 30 | (prod) => prod.ratings >= byRating 31 | ); 32 | } 33 | 34 | if (searchQuery) { 35 | sortedProducts = sortedProducts.filter((prod) => 36 | prod.name.toLowerCase().includes(searchQuery) 37 | ); 38 | } 39 | 40 | return sortedProducts; 41 | }; 42 | 43 | return ( 44 |
45 | 46 |
47 | {transformProducts().map((prod) => ( 48 | 49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Home; 56 | -------------------------------------------------------------------------------- /src/components/Rating.js: -------------------------------------------------------------------------------- 1 | import { AiFillStar, AiOutlineStar } from "react-icons/ai"; 2 | 3 | const Rating = ({ rating, onClick, style }) => { 4 | return ( 5 | <> 6 | {[...Array(5)].map((_, i) => ( 7 | onClick(i)} style={style}> 8 | {rating > i ? ( 9 | 10 | ) : ( 11 | 12 | )} 13 | 14 | ))} 15 | 16 | ); 17 | }; 18 | 19 | export default Rating; 20 | -------------------------------------------------------------------------------- /src/components/SingleProduct.js: -------------------------------------------------------------------------------- 1 | import { Card, Button } from "react-bootstrap"; 2 | import { CartState } from "../context/Context"; 3 | import Rating from "./Rating"; 4 | 5 | const SingleProduct = ({ prod }) => { 6 | const { 7 | state: { cart }, 8 | dispatch, 9 | } = CartState(); 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 | {prod.name} 17 | 18 | ₹ {prod.price.split(".")[0]} 19 | {prod.fastDelivery ? ( 20 |
Fast Delivery
21 | ) : ( 22 |
4 days delivery
23 | )} 24 | 25 |
26 | {cart.some((p) => p.id === prod.id) ? ( 27 | 38 | ) : ( 39 | 50 | )} 51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | export default SingleProduct; 58 | -------------------------------------------------------------------------------- /src/components/styles.css: -------------------------------------------------------------------------------- 1 | /* header styles */ 2 | 3 | .header { 4 | display: block; 5 | text-align: center; 6 | font-size: 30px; 7 | margin: 10px 0; 8 | } 9 | 10 | .cartitem { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | margin: 0 20px; 15 | margin-bottom: 20px; 16 | } 17 | 18 | .cartItemImg { 19 | border-radius: 50%; 20 | width: 50px; 21 | height: 50px; 22 | object-fit: cover; 23 | } 24 | 25 | .cartItemDetail { 26 | display: flex; 27 | flex: 1; 28 | padding: 0 20px; 29 | flex-direction: column; 30 | } 31 | 32 | /* Home Page */ 33 | 34 | .home { 35 | display: flex; 36 | } 37 | 38 | .filters { 39 | background-color: #343a40; 40 | color: white; 41 | padding: 20px; 42 | display: flex; 43 | flex-direction: column; 44 | width: 20%; 45 | margin: 10px; 46 | height: 86vh; 47 | } 48 | 49 | .filters > span { 50 | padding-bottom: 20px; 51 | } 52 | 53 | .title { 54 | font-size: 30px; 55 | } 56 | 57 | .productContainer { 58 | display: flex; 59 | width: 78%; 60 | padding: 20px; 61 | flex-wrap: wrap; 62 | justify-content: space-around; 63 | } 64 | 65 | .products { 66 | width: 30%; 67 | margin: 10px; 68 | } 69 | 70 | .summary { 71 | width: 30%; 72 | } 73 | 74 | /* Media Queries */ 75 | @media (max-width: 771px) { 76 | .filters { 77 | width: 40%; 78 | padding: 10px; 79 | margin: 5px; 80 | } 81 | 82 | .filters > span { 83 | font-size: 10px; 84 | } 85 | 86 | .title { 87 | font-size: 18px !important; 88 | } 89 | 90 | .productContainer { 91 | width: 58%; 92 | padding: 0; 93 | } 94 | 95 | .search { 96 | display: none !important; 97 | } 98 | 99 | .products { 100 | width: 100%; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/context/Context.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useReducer } from "react"; 2 | import faker from "faker"; 3 | import { cartReducer, productReducer } from "./Reducers"; 4 | 5 | const Cart = createContext(); 6 | faker.seed(99); 7 | 8 | const Context = ({ children }) => { 9 | const products = [...Array(20)].map(() => ({ 10 | id: faker.datatype.uuid(), 11 | name: faker.commerce.productName(), 12 | price: faker.commerce.price(), 13 | image: faker.random.image(), 14 | inStock: faker.random.arrayElement([0, 3, 5, 6, 7]), 15 | fastDelivery: faker.datatype.boolean(), 16 | ratings: faker.random.arrayElement([1, 2, 3, 4, 5]), 17 | })); 18 | 19 | const [state, dispatch] = useReducer(cartReducer, { 20 | products: products, 21 | cart: [], 22 | }); 23 | 24 | const [productState, productDispatch] = useReducer(productReducer, { 25 | byStock: false, 26 | byFastDelivery: false, 27 | byRating: 0, 28 | searchQuery: "", 29 | }); 30 | 31 | console.log(productState); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | export const CartState = () => { 41 | return useContext(Cart); 42 | }; 43 | 44 | export default Context; 45 | -------------------------------------------------------------------------------- /src/context/Reducers.js: -------------------------------------------------------------------------------- 1 | export const cartReducer = (state, action) => { 2 | switch (action.type) { 3 | case "ADD_TO_CART": 4 | return { ...state, cart: [...state.cart, { ...action.payload, qty: 1 }] }; 5 | case "REMOVE_FROM_CART": 6 | return { 7 | ...state, 8 | cart: state.cart.filter((c) => c.id !== action.payload.id), 9 | }; 10 | case "CHANGE_CART_QTY": 11 | return { 12 | ...state, 13 | cart: state.cart.filter((c) => 14 | c.id === action.payload.id ? (c.qty = action.payload.qty) : c.qty 15 | ), 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export const productReducer = (state, action) => { 23 | switch (action.type) { 24 | case "SORT_BY_PRICE": 25 | return { ...state, sort: action.payload }; 26 | case "FILTER_BY_STOCK": 27 | return { ...state, byStock: !state.byStock }; 28 | case "FILTER_BY_DELIVERY": 29 | return { ...state, byFastDelivery: !state.byFastDelivery }; 30 | case "FILTER_BY_RATING": 31 | return { ...state, byRating: action.payload }; 32 | case "FILTER_BY_SEARCH": 33 | return { ...state, searchQuery: action.payload }; 34 | case "CLEAR_FILTERS": 35 | return { byStock: false, byFastDelivery: false, byRating: 0 }; 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import Context from "./context/Context"; 6 | import "bootstrap/dist/css/bootstrap.min.css"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | --------------------------------------------------------------------------------