├── public
├── robots.txt
├── favicon.ico
├── manifest.json
└── index.html
├── README.md
├── .gitignore
├── src
├── features
│ ├── apiSlice.js
│ └── useCartSlice.js
├── App.js
├── index.js
├── App.css
├── app
│ └── store.js
├── components
│ ├── products
│ │ └── Products.js
│ ├── header
│ │ └── Header.js
│ └── cart
│ │ └── Cart.js
└── assets
│ └── empty-cart.svg
└── package.json
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SinghDigamber/react-redux-shopping-cart/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Redux Persist Example
2 |
3 | [React Redux Save Data in Local Storage with Persist Tutorial](https://www.positronx.io/react-redux-save-data-in-local-storage-with-persist-tutorial)
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/features/apiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
2 |
3 | export const apiSlice = createApi({
4 | reducerPath: 'apiProductSlice',
5 | baseQuery: fetchBaseQuery({
6 | baseUrl: 'https://fakestoreapi.com',
7 | }),
8 | tagTypes: ['Product'],
9 | endpoints: (builder) => ({
10 | getProducts: builder.query({
11 | query: () => '/products',
12 | providesTags: ['Product'],
13 | }),
14 | }),
15 | })
16 |
17 | export const { useGetProductsQuery } = apiSlice
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
2 | import './App.css'
3 |
4 | import { Routes, Route } from 'react-router-dom'
5 |
6 | import Header from './components/header/Header'
7 | import Products from './components/products/Products'
8 | import Cart from './components/cart/Cart'
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 | } />
16 | } />
17 |
18 |
19 | )
20 | }
21 |
22 | export default App
23 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 |
5 | import { BrowserRouter } from 'react-router-dom'
6 | import { persistStore } from 'redux-persist'
7 | import { PersistGate } from 'redux-persist/integration/react'
8 | import { Provider } from 'react-redux'
9 |
10 | import store from './app/store'
11 | let persistor = persistStore(store)
12 |
13 | const root = ReactDOM.createRoot(document.getElementById('root'))
14 |
15 | root.render(
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | )
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .container {
2 | max-width: 960px;
3 | }
4 |
5 | .navbar {
6 | background-color: #e3f2fd;
7 | }
8 |
9 | .cart svg {
10 | width: 33px;
11 | height: 30px;
12 | }
13 |
14 | .cart .top-0 {
15 | top: 8px !important;
16 | }
17 |
18 | .cart {
19 | cursor: pointer;
20 | }
21 |
22 | .cart .thumbnail {
23 | width: 72px;
24 | height: 72px;
25 | }
26 |
27 | .product-card .img-grid {
28 | width: 200px;
29 | margin: 0 auto;
30 | height: 200px;
31 | padding: 15px;
32 | overflow: hidden;
33 | }
34 |
35 | .product-card .img-grid img {
36 | width: 100%;
37 | }
38 |
39 | .cart-table .media-object {
40 | width: 80px;
41 | }
42 |
43 | .cart-quantity span {
44 | width: 35px;
45 | display: inline-block;
46 | font-weight: bold;
47 | }
48 |
49 | .empty-cart .img-thumbnail {
50 | padding: 0.25rem;
51 | border: none;
52 | width: 380px;
53 | height: auto;
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-shopping-cart",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.3",
7 | "@testing-library/jest-dom": "^5.16.4",
8 | "@testing-library/react": "^13.3.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "autoprefixer": "10.4.5",
11 | "bootstrap": "^5.2.0-beta1",
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0",
14 | "react-redux": "^8.0.2",
15 | "react-router-dom": "^6.3.0",
16 | "react-scripts": "5.0.1",
17 | "redux-persist": "^6.0.0",
18 | "web-vitals": "^2.1.4"
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 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import { setupListeners } from '@reduxjs/toolkit/query'
3 |
4 | import { combineReducers } from '@reduxjs/toolkit'
5 | import { apiSlice } from '../features/apiSlice'
6 | import useCartReducer from '../features/useCartSlice'
7 |
8 | import storage from 'redux-persist/lib/storage'
9 |
10 | import {
11 | persistReducer,
12 | FLUSH,
13 | REHYDRATE,
14 | PAUSE,
15 | PERSIST,
16 | PURGE,
17 | REGISTER,
18 | } from 'redux-persist'
19 |
20 | const persistConfig = {
21 | key: 'root',
22 | storage: storage,
23 | blacklist: ['apiProductSlice'],
24 | }
25 |
26 | export const rootReducers = combineReducers({
27 | cart: useCartReducer,
28 | [apiSlice.reducerPath]: apiSlice.reducer,
29 | })
30 |
31 | const persistedReducer = persistReducer(persistConfig, rootReducers)
32 |
33 | const store = configureStore({
34 | reducer: persistedReducer,
35 | middleware: (getDefaultMiddleware) =>
36 | getDefaultMiddleware({
37 | serializableCheck: {
38 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
39 | },
40 | }).concat(apiSlice.middleware),
41 | })
42 |
43 | setupListeners(store.dispatch)
44 |
45 | export default store
46 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/features/useCartSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | const useCartSlice = createSlice({
4 | name: 'cart',
5 | initialState: {
6 | cartItems: [],
7 | totalCount: 0,
8 | tax: 0,
9 | subAmount: 0,
10 | totalAmount: 0,
11 | },
12 | reducers: {
13 | addCartProduct: {
14 | reducer: (state, action) => {
15 | let cartIndex = state.cartItems.findIndex(
16 | (item) => item.id === action.payload.id,
17 | )
18 | if (cartIndex >= 0) {
19 | state.cartItems[cartIndex].quantity += 1
20 | } else {
21 | let tempProduct = { ...action.payload, quantity: 1 }
22 | state.cartItems.push(tempProduct)
23 | }
24 | },
25 | },
26 | getCartProducts: (state, action) => {
27 | return {
28 | ...state,
29 | }
30 | },
31 | getCartCount: (state, action) => {
32 | let cartCount = state.cartItems.reduce((total, item) => {
33 | return item.quantity + total
34 | }, 0)
35 | state.totalCount = cartCount
36 | },
37 | getSubTotal: (state, action) => {
38 | state.subAmount = state.cartItems.reduce((acc, item) => {
39 | return acc + item.price * item.quantity
40 | }, 0)
41 | },
42 | removeCartItem: (state, action) => {
43 | let index = state.cartItems.findIndex(
44 | (item) => item.id === action.payload,
45 | )
46 | if (index !== -1) {
47 | state.cartItems.splice(index, 1)
48 | }
49 | },
50 | increment: (state, action) => {
51 | let index = state.cartItems.findIndex(
52 | (item) => item.id === action.payload,
53 | )
54 | state.cartItems[index].quantity += 1
55 | },
56 | decrement: (state, action) => {
57 | let index = state.cartItems.findIndex(
58 | (item) => item.id === action.payload,
59 | )
60 | if (state.cartItems[index].quantity <= 0) {
61 | state.cartItems[index].quantity = 0
62 | } else {
63 | state.cartItems[index].quantity -= 1
64 | }
65 | },
66 | calculateTax: (state, action) => {
67 | // GST value: 18% => action.payload
68 | let totalTax = (18 / 100) * state.subAmount
69 | state.tax = totalTax
70 | },
71 | getTotalAmount: (state, action) => {
72 | state.totalAmount = state.tax + state.subAmount
73 | },
74 | },
75 | })
76 |
77 | export const {
78 | addCartProduct,
79 | getCartProducts,
80 | removeCartItem,
81 | getCartCount,
82 | getSubTotal,
83 | increment,
84 | decrement,
85 | calculateTax,
86 | getTotalAmount,
87 | } = useCartSlice.actions
88 |
89 | export default useCartSlice.reducer
90 |
--------------------------------------------------------------------------------
/src/components/products/Products.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useGetProductsQuery } from '../../features/apiSlice'
3 | import {
4 | addCartProduct,
5 | getCartCount,
6 | getSubTotal,
7 | calculateTax,
8 | getTotalAmount,
9 | } from '../../features/useCartSlice'
10 | import { useDispatch } from 'react-redux'
11 |
12 | function Products() {
13 | const dispatch = useDispatch()
14 |
15 | let productObj = {
16 | id: '',
17 | title: '',
18 | price: '',
19 | image: '',
20 | }
21 |
22 | const addToCart = (item) => {
23 | productObj = {
24 | id: item.id,
25 | title: item.title,
26 | price: item.price,
27 | image: item.image,
28 | }
29 | dispatch(addCartProduct(productObj))
30 | dispatch(getCartCount())
31 | dispatch(getSubTotal())
32 | dispatch(calculateTax())
33 | dispatch(getTotalAmount())
34 | }
35 |
36 | const {
37 | data: products,
38 | isLoading: isProductLoading,
39 | isSuccess: isProductSuccess,
40 | isError: isProductError,
41 | error: prouctError,
42 | } = useGetProductsQuery({ refetchOnMountOrArgChange: true })
43 |
44 | useEffect(() => {}, [dispatch])
45 |
46 | let getData
47 |
48 | if (isProductLoading) {
49 | getData = (
50 |
51 |
52 | Loading...
53 |
54 |
55 | )
56 | } else if (isProductSuccess) {
57 | getData = products.map((item) => {
58 | return (
59 |
60 |
61 |
62 |

63 |
64 |
65 |
${item.price}
66 |
67 | {item.description.substring(0, 50)}...
68 |
69 |
70 |
78 |
79 |
80 |
81 | )
82 | })
83 | } else if (isProductError) {
84 | getData = (
85 |
86 | {prouctError.status} {JSON.stringify(prouctError)}
87 |
88 | )
89 | }
90 |
91 | return (
92 |
93 |
94 | {getData}
95 |
96 |
97 | )
98 | }
99 |
100 | export default Products
101 |
--------------------------------------------------------------------------------
/src/components/header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import { useSelector } from 'react-redux'
5 |
6 | function Header() {
7 | const { totalCount } = useSelector((state) => state.cart)
8 |
9 | return (
10 |
11 |
12 |
16 |
32 |
React cart example
33 |
34 |
35 |
55 |
56 |
57 | )
58 | }
59 |
60 | export default Header
61 |
--------------------------------------------------------------------------------
/src/assets/empty-cart.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/src/components/cart/Cart.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import { Link } from 'react-router-dom'
4 | import noCart from '../../assets/empty-cart.svg'
5 |
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import {
8 | getCartProducts,
9 | getSubTotal,
10 | removeCartItem,
11 | getCartCount,
12 | increment,
13 | decrement,
14 | calculateTax,
15 | getTotalAmount,
16 | } from '../../features/useCartSlice'
17 |
18 | function Cart() {
19 | const dispatch = useDispatch()
20 | const { cartItems, subAmount, tax, totalAmount } = useSelector(
21 | (state) => state.cart,
22 | )
23 |
24 | useEffect(() => {
25 | dispatch(getCartProducts())
26 | dispatch(getSubTotal())
27 | dispatch(getCartCount())
28 | dispatch(calculateTax())
29 | dispatch(getTotalAmount())
30 | }, [dispatch])
31 |
32 | let showCart
33 |
34 | if (cartItems !== undefined && cartItems.length > 0) {
35 | showCart = (
36 | <>
37 |
38 |
39 |
40 | | Product |
41 | Quantity |
42 | Price |
43 | Total |
44 |
45 |
46 |
47 | {cartItems !== undefined &&
48 | cartItems.length > 0 &&
49 | cartItems.map((product, index) => {
50 | return (
51 |
52 | |
53 |
58 | |
59 |
60 |
61 |
62 |
86 |
87 | {product.quantity}
88 |
89 |
110 |
111 | |
112 |
113 | {product.price}
114 | |
115 |
116 |
117 | {parseFloat(product.price * product.quantity).toFixed(
118 | 2,
119 | )}
120 |
121 | |
122 |
123 |
136 | |
137 |
138 | )
139 | })}
140 |
141 |
142 |
143 |
144 |
145 |
146 |
GST 18%
$
147 | {parseFloat(tax).toFixed(2)}
148 |
149 |
150 |
151 |
Subtotal
$
152 | {parseFloat(subAmount).toFixed(2)}
153 |
154 |
155 |
156 |
157 | Total Amount
158 |
159 |
160 | ${parseFloat(totalAmount).toFixed(2)}
161 |
162 |
163 |
164 |
167 |
168 |
169 |
170 | >
171 | )
172 | } else {
173 | showCart = (
174 |
175 |
176 |

181 |
182 |
Your cart is empty
183 |
184 | Looks like you have not added anything to your cart. Let's buy some
185 | products.
186 |
187 |
188 |
189 | Shop now
190 |
191 |
192 |
193 | )
194 | }
195 |
196 | return <>{showCart}>
197 | }
198 |
199 | export default Cart
200 |
--------------------------------------------------------------------------------