├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------