├── 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 | {item.title} 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 | 24 | Bootstrap 25 | 31 | 32 | React cart example 33 | 34 | 35 | 55 |
56 |
57 | ) 58 | } 59 | 60 | export default Header 61 | -------------------------------------------------------------------------------- /src/assets/empty-cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 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 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {cartItems !== undefined && 48 | cartItems.length > 0 && 49 | cartItems.map((product, index) => { 50 | return ( 51 | 52 | 59 | 60 | 112 | 115 | 122 | 137 | 138 | ) 139 | })} 140 | 141 |
ProductQuantityPriceTotal
53 |
54 | 55 | {product.title} 56 | 57 |
58 |
61 |
62 | 86 | 87 | {product.quantity} 88 | 89 | 110 |
111 |
113 | {product.price} 114 | 116 | 117 | {parseFloat(product.price * product.quantity).toFixed( 118 | 2, 119 | )} 120 | 121 | 123 | 136 |
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 | Empty cart 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 | --------------------------------------------------------------------------------