├── README.md ├── final ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── cartItems.js │ ├── components │ ├── CartContainer.js │ ├── CartItem.js │ ├── Modal.js │ └── Navbar.js │ ├── features │ ├── cart │ │ └── cartSlice.js │ └── modal │ │ └── modalSlice.js │ ├── icons.js │ ├── index.css │ ├── index.js │ └── store.js ├── starter ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── cartItems.js │ ├── icons.js │ ├── index.css │ └── index.js ├── vite-final ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ └── react.svg │ ├── cartItems.js │ ├── components │ │ ├── CartContainer.jsx │ │ ├── CartItem.jsx │ │ ├── Modal.jsx │ │ └── Navbar.jsx │ ├── features │ │ ├── cart │ │ │ └── cartSlice.js │ │ └── modal │ │ │ └── modalSlice.js │ ├── icons.jsx │ ├── index.css │ ├── main.jsx │ └── store.js └── vite.config.js └── vite-starter ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── App.jsx ├── assets │ └── react.svg ├── cartItems.js ├── icons.jsx ├── index.css └── main.jsx └── vite.config.js /README.md: -------------------------------------------------------------------------------- 1 | ## Vite Option 2 | 3 | I have added two folders called vite-starter and vite-final. If you prefer to work with Vite instead of CRA, simply use the Vite folders. The Redux Toolkit functionality is the same in both setups. 4 | -------------------------------------------------------------------------------- /final/.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 | -------------------------------------------------------------------------------- /final/README.md: -------------------------------------------------------------------------------- 1 | # Redux Toolkit 2 | 3 | #### React Course 4 | 5 | [My React Course](https://www.udemy.com/course/react-tutorial-and-projects-course/?referralCode=FEE6A921AF07E2563CEF) 6 | 7 | #### Support 8 | 9 | Find the App Useful? [You can always buy me a coffee](https://www.buymeacoffee.com/johnsmilga) 10 | 11 | #### Docs 12 | 13 | [Redux Toolkit Docs](https://redux-toolkit.js.org/introduction/getting-started) 14 | 15 | #### Install Template 16 | 17 | ```sh 18 | npx create-react-app my-app --template redux 19 | ``` 20 | 21 | - @latest 22 | 23 | ```sh 24 | npx create-react-app@latest my-app --template redux 25 | ``` 26 | 27 | #### Existing App 28 | 29 | ```sh 30 | npm install @reduxjs/toolkit react-redux 31 | ``` 32 | 33 | #### @reduxjs/toolkit 34 | 35 | consists of few libraries 36 | 37 | - redux (core library, state management) 38 | - immer (allows to mutate state) 39 | - redux-thunk (handles async actions) 40 | - reselect (simplifies reducer functions) 41 | 42 | #### Extras 43 | 44 | - redux devtools 45 | - combine reducers 46 | 47 | #### react-redux 48 | 49 | connects our app to redux 50 | 51 | #### Setup Store 52 | 53 | - create store.js 54 | 55 | ```js 56 | import { configureStore } from '@reduxjs/toolkit'; 57 | 58 | export const store = configureStore({ 59 | reducer: {}, 60 | }); 61 | ``` 62 | 63 | #### Setup Provider 64 | 65 | - index.js 66 | 67 | ```js 68 | import React from 'react'; 69 | import ReactDOM from 'react-dom'; 70 | import './index.css'; 71 | import App from './App'; 72 | // import store and provider 73 | import { store } from './store'; 74 | import { Provider } from 'react-redux'; 75 | 76 | ReactDOM.render( 77 | 78 | 79 | 80 | 81 | , 82 | document.getElementById('root') 83 | ); 84 | ``` 85 | 86 | #### Setup Cart Slice 87 | 88 | - application feature 89 | - create features folder/cart 90 | - create cartSlice.js 91 | 92 | ```js 93 | import { createSlice } from '@reduxjs/toolkit'; 94 | 95 | const initialState = { 96 | cartItems: [], 97 | amount: 0, 98 | total: 0, 99 | isLoading: true, 100 | }; 101 | 102 | const cartSlice = createSlice({ 103 | name: 'cart', 104 | initialState, 105 | }); 106 | 107 | console.log(cartSlice); 108 | 109 | export default cartSlice.reducer; 110 | ``` 111 | 112 | - store.js 113 | 114 | ```js 115 | import { configureStore } from '@reduxjs/toolkit'; 116 | import cartReducer from './features/cart/cartSlice'; 117 | 118 | export const store = configureStore({ 119 | reducer: { 120 | cart: cartReducer, 121 | }, 122 | }); 123 | ``` 124 | 125 | #### Redux DevTools 126 | 127 | - extension 128 | 129 | #### Access store value 130 | 131 | - create components/Navbar.js 132 | 133 | ```js 134 | import { CartIcon } from '../icons'; 135 | import { useSelector } from 'react-redux'; 136 | 137 | const Navbar = () => { 138 | const { amount } = useSelector((state) => state.cart); 139 | 140 | return ( 141 | 152 | ); 153 | }; 154 | export default Navbar; 155 | ``` 156 | 157 | #### Hero Icons 158 | 159 | - [Hero Icons](https://heroicons.com/) 160 | 161 | ```css 162 | nav svg { 163 | width: 40px; 164 | color: var(--clr-white); 165 | } 166 | ``` 167 | 168 | #### Setup Cart 169 | 170 | - cartSlice.js 171 | 172 | ```js 173 | import cartItems from '../../cartItems'; 174 | 175 | const initialState = { 176 | cartItems: cartItems, 177 | amount: 0, 178 | total: 0, 179 | isLoading: true, 180 | }; 181 | ``` 182 | 183 | - create CartContainer.js and CartItem.js 184 | - CartContainer.js 185 | 186 | ```js 187 | import React from 'react'; 188 | import CartItem from './CartItem'; 189 | import { useSelector } from 'react-redux'; 190 | 191 | const CartContainer = () => { 192 | const { cartItems, total, amount } = useSelector((state) => state.cart); 193 | 194 | if (amount < 1) { 195 | return ( 196 |
197 | {/* cart header */} 198 |
199 |

your bag

200 |

is currently empty

201 |
202 |
203 | ); 204 | } 205 | return ( 206 |
207 | {/* cart header */} 208 |
209 |

your bag

210 |
211 | {/* cart items */} 212 |
213 | {cartItems.map((item) => { 214 | return ; 215 | })} 216 |
217 | {/* cart footer */} 218 | 227 |
228 | ); 229 | }; 230 | 231 | export default CartContainer; 232 | ``` 233 | 234 | - CartItem.js 235 | 236 | ```js 237 | import React from 'react'; 238 | import { ChevronDown, ChevronUp } from '../icons'; 239 | 240 | const CartItem = ({ id, img, title, price, amount }) => { 241 | return ( 242 |
243 | {title} 244 |
245 |

{title}

246 |

${price}

247 | {/* remove button */} 248 | 249 |
250 |
251 | {/* increase amount */} 252 | 255 | {/* amount */} 256 |

{amount}

257 | {/* decrease amount */} 258 | 261 |
262 |
263 | ); 264 | }; 265 | 266 | export default CartItem; 267 | ``` 268 | 269 | #### First Reducer 270 | 271 | - cartSlice.js 272 | - Immer library 273 | 274 | ```js 275 | const cartSlice = createSlice({ 276 | name: 'cart', 277 | initialState, 278 | reducers: { 279 | clearCart: (state) => { 280 | state.cartItems = []; 281 | }, 282 | }, 283 | }); 284 | 285 | export const { clearCart } = cartSlice.actions; 286 | ``` 287 | 288 | - create action 289 | 290 | ```js 291 | const ACTION_TYPE = 'ACTION_TYPE'; 292 | 293 | const actionCreator = (payload) => { 294 | return { type: ACTION_TYPE, payload: payload }; 295 | }; 296 | ``` 297 | 298 | - CartContainer.js 299 | 300 | ```js 301 | import React from 'react'; 302 | import CartItem from './CartItem'; 303 | import { useDispatch, useSelector } from 'react-redux'; 304 | 305 | const CartContainer = () => { 306 | const dispatch = useDispatch(); 307 | 308 | return ( 309 | 317 | ); 318 | }; 319 | 320 | export default CartContainer; 321 | ``` 322 | 323 | #### Remove, Increase, Decrease 324 | 325 | - cartSlice.js 326 | 327 | ```js 328 | import { createSlice } from '@reduxjs/toolkit'; 329 | import cartItems from '../../cartItems'; 330 | 331 | const initialState = { 332 | cartItems: [], 333 | amount: 0, 334 | total: 0, 335 | isLoading: true, 336 | }; 337 | 338 | const cartSlice = createSlice({ 339 | name: 'cart', 340 | initialState, 341 | reducers: { 342 | clearCart: (state) => { 343 | state.cartItems = []; 344 | }, 345 | removeItem: (state, action) => { 346 | const itemId = action.payload; 347 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId); 348 | }, 349 | increase: (state, { payload }) => { 350 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 351 | cartItem.amount = cartItem.amount + 1; 352 | }, 353 | decrease: (state, { payload }) => { 354 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 355 | cartItem.amount = cartItem.amount - 1; 356 | }, 357 | calculateTotals: (state) => { 358 | let amount = 0; 359 | let total = 0; 360 | state.cartItems.forEach((item) => { 361 | amount += item.amount; 362 | total += item.amount * item.price; 363 | }); 364 | state.amount = amount; 365 | state.total = total; 366 | }, 367 | }, 368 | }); 369 | 370 | export const { clearCart, removeItem, increase, decrease, calculateTotals } = 371 | cartSlice.actions; 372 | 373 | export default cartSlice.reducer; 374 | ``` 375 | 376 | - CartItem.js 377 | 378 | ```js 379 | import React from 'react'; 380 | import { ChevronDown, ChevronUp } from '../icons'; 381 | 382 | import { useDispatch } from 'react-redux'; 383 | import { removeItem, increase, decrease } from '../features/cart/cartSlice'; 384 | 385 | const CartItem = ({ id, img, title, price, amount }) => { 386 | const dispatch = useDispatch(); 387 | 388 | return ( 389 |
390 | {title} 391 |
392 |

{title}

393 |

${price}

394 | {/* remove button */} 395 | 403 |
404 |
405 | {/* increase amount */} 406 | 414 | {/* amount */} 415 |

{amount}

416 | {/* decrease amount */} 417 | 429 |
430 |
431 | ); 432 | }; 433 | 434 | export default CartItem; 435 | ``` 436 | 437 | - App.js 438 | 439 | ```js 440 | import { useEffect } from 'react'; 441 | import Navbar from './components/Navbar'; 442 | import CartContainer from './components/CartContainer'; 443 | import { useSelector, useDispatch } from 'react-redux'; 444 | import { calculateTotals } from './features/cart/cartSlice'; 445 | 446 | function App() { 447 | const { cartItems } = useSelector((state) => state.cart); 448 | const dispatch = useDispatch(); 449 | useEffect(() => { 450 | dispatch(calculateTotals()); 451 | }, [cartItems]); 452 | 453 | return ( 454 |
455 | 456 | 457 |
458 | ); 459 | } 460 | 461 | export default App; 462 | ``` 463 | 464 | #### Modal 465 | 466 | - create components/Modal.js 467 | 468 | ```js 469 | const Modal = () => { 470 | return ( 471 | 484 | ); 485 | }; 486 | export default Modal; 487 | ``` 488 | 489 | - App.js 490 | 491 | ```js 492 | return ( 493 |
494 | 495 | 496 | 497 |
498 | ); 499 | ``` 500 | 501 | #### modal slice 502 | 503 | - create features/modal/modalSlice.js 504 | 505 | ```js 506 | import { createSlice } from '@reduxjs/toolkit'; 507 | const initialState = { 508 | isOpen: false, 509 | }; 510 | 511 | const modalSlice = createSlice({ 512 | name: 'modal', 513 | initialState, 514 | reducers: { 515 | openModal: (state, action) => { 516 | state.isOpen = true; 517 | }, 518 | closeModal: (state, action) => { 519 | state.isOpen = false; 520 | }, 521 | }, 522 | }); 523 | 524 | export const { openModal, closeModal } = modalSlice.actions; 525 | export default modalSlice.reducer; 526 | ``` 527 | 528 | - App.js 529 | 530 | ```js 531 | const { isOpen } = useSelector((state) => state.modal); 532 | 533 | return ( 534 |
535 | {isOpen && } 536 | 537 | 538 |
539 | ); 540 | ``` 541 | 542 | #### toggle modal 543 | 544 | - CartContainer.js 545 | 546 | ```js 547 | import { openModal } from '../features/modal/modalSlice'; 548 | 549 | return ( 550 | 558 | ); 559 | ``` 560 | 561 | - Modal.js 562 | 563 | ```js 564 | import { closeModal } from '../features/modal/modalSlice'; 565 | import { useDispatch } from 'react-redux'; 566 | import { clearCart } from '../features/cart/cartSlice'; 567 | 568 | const Modal = () => { 569 | const dispatch = useDispatch(); 570 | 571 | return ( 572 | 598 | ); 599 | }; 600 | export default Modal; 601 | ``` 602 | 603 | #### async functionality with createAsyncThunk 604 | 605 | - [Course API](https://course-api.com/) 606 | - https://course-api.com/react-useReducer-cart-project 607 | - cartSlice.js 608 | 609 | - action type 610 | - callback function 611 | - lifecycle actions 612 | 613 | ```js 614 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 615 | 616 | const url = 'https://course-api.com/react-useReducer-cart-project'; 617 | 618 | export const getCartItems = createAsyncThunk('cart/getCartItems', () => { 619 | return fetch(url) 620 | .then((resp) => resp.json()) 621 | .catch((err) => console.log(error)); 622 | }); 623 | 624 | const cartSlice = createSlice({ 625 | name: 'cart', 626 | initialState, 627 | extraReducers: { 628 | [getCartItems.pending]: (state) => { 629 | state.isLoading = true; 630 | }, 631 | [getCartItems.fulfilled]: (state, action) => { 632 | console.log(action); 633 | state.isLoading = false; 634 | state.cartItems = action.payload; 635 | }, 636 | [getCartItems.rejected]: (state) => { 637 | state.isLoading = false; 638 | }, 639 | }, 640 | }); 641 | ``` 642 | 643 | - App.js 644 | 645 | ```js 646 | import { calculateTotals, getCartItems } from './features/cart/cartSlice'; 647 | 648 | function App() { 649 | const { cartItems, isLoading } = useSelector((state) => state.cart); 650 | 651 | useEffect(() => { 652 | dispatch(getCartItems()); 653 | }, []); 654 | 655 | if (isLoading) { 656 | return ( 657 |
658 |

Loading...

659 |
660 | ); 661 | } 662 | 663 | return ( 664 |
665 | {isOpen && } 666 | 667 | 668 |
669 | ); 670 | } 671 | 672 | export default App; 673 | ``` 674 | 675 | #### Options 676 | 677 | ```sh 678 | npm install axios 679 | ``` 680 | 681 | - cartSlice.js 682 | 683 | ```js 684 | export const getCartItems = createAsyncThunk( 685 | 'cart/getCartItems', 686 | async (name, thunkAPI) => { 687 | try { 688 | // console.log(name); 689 | // console.log(thunkAPI); 690 | // console.log(thunkAPI.getState()); 691 | // thunkAPI.dispatch(openModal()); 692 | const resp = await axios(url); 693 | 694 | return resp.data; 695 | } catch (error) { 696 | return thunkAPI.rejectWithValue('something went wrong'); 697 | } 698 | } 699 | ); 700 | ``` 701 | 702 | #### The extraReducers "builder callback" notation 703 | 704 | cart/cartSlice 705 | 706 | ```js 707 | const cartSlice = createSlice({ 708 | name: 'cart', 709 | initialState, 710 | reducers: { 711 | // reducers 712 | }, 713 | extraReducers: (builder) => { 714 | builder 715 | .addCase(getCartItems.pending, (state) => { 716 | state.isLoading = true; 717 | }) 718 | .addCase(getCartItems.fulfilled, (state, action) => { 719 | // console.log(action); 720 | state.isLoading = false; 721 | state.cartItems = action.payload; 722 | }) 723 | .addCase(getCartItems.rejected, (state, action) => { 724 | console.log(action); 725 | state.isLoading = false; 726 | }); 727 | }, 728 | }); 729 | ``` 730 | -------------------------------------------------------------------------------- /final/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-toolkit-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.9.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.1.3", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-redux": "^8.0.5", 14 | "react-scripts": "5.0.1", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /final/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/final/public/favicon.ico -------------------------------------------------------------------------------- /final/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Redux Toolkit 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /final/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/final/public/logo192.png -------------------------------------------------------------------------------- /final/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/final/public/logo512.png -------------------------------------------------------------------------------- /final/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 | -------------------------------------------------------------------------------- /final/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /final/src/App.js: -------------------------------------------------------------------------------- 1 | import Navbar from './components/Navbar'; 2 | import CartContainer from './components/CartContainer'; 3 | 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { calculateTotals, getCartItems } from './features/cart/cartSlice'; 6 | import { useEffect } from 'react'; 7 | import Modal from './components/Modal'; 8 | function App() { 9 | const { cartItems, isLoading } = useSelector((store) => store.cart); 10 | const { isOpen } = useSelector((store) => store.modal); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | dispatch(calculateTotals()); 15 | }, [cartItems]); 16 | 17 | useEffect(() => { 18 | dispatch(getCartItems('random')); 19 | }, []); 20 | 21 | if (isLoading) { 22 | return ( 23 |
24 |

Loading...

25 |
26 | ); 27 | } 28 | 29 | return ( 30 |
31 | {isOpen && } 32 | 33 | 34 |
35 | ); 36 | } 37 | export default App; 38 | -------------------------------------------------------------------------------- /final/src/cartItems.js: -------------------------------------------------------------------------------- 1 | const cartItems = [ 2 | { 3 | id: 'rec1JZlfCIBOPdcT2', 4 | title: 'Samsung Galaxy S8', 5 | price: '399.99', 6 | img: 'https://images2.imgbox.com/c2/14/zedmXgs6_o.png', 7 | amount: 1, 8 | }, 9 | { 10 | id: 'recB6qcHPxb62YJ75', 11 | title: 'google pixel', 12 | price: '499.99', 13 | img: 'https://images2.imgbox.com/fb/3d/O4TPmhlt_o.png', 14 | amount: 1, 15 | }, 16 | { 17 | id: 'recdRxBsE14Rr2VuJ', 18 | title: 'Xiaomi Redmi Note 2', 19 | price: '699.99', 20 | img: 'https://images2.imgbox.com/4f/3d/WN3GvciF_o.png', 21 | amount: 1, 22 | }, 23 | { 24 | id: 'recwTo160XST3PIoW', 25 | title: 'Samsung Galaxy S7', 26 | price: '599.99 ', 27 | img: 'https://images2.imgbox.com/2e/7c/yFsJ4Zkb_o.png', 28 | amount: 1, 29 | }, 30 | ]; 31 | export default cartItems; 32 | -------------------------------------------------------------------------------- /final/src/components/CartContainer.js: -------------------------------------------------------------------------------- 1 | import CartItem from './CartItem'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { openModal } from '../features/modal/modalSlice'; 4 | 5 | const CartContainer = () => { 6 | const dispatch = useDispatch(); 7 | const { cartItems, total, amount } = useSelector((store) => store.cart); 8 | 9 | if (amount < 1) { 10 | return ( 11 |
12 |
13 |

your bag

14 |

is currently empty

15 |
16 |
17 | ); 18 | } 19 | 20 | return ( 21 |
22 |
23 |

your bag

24 |
25 |
26 | {cartItems.map((item) => { 27 | return ; 28 | })} 29 |
30 | 41 |
42 | ); 43 | }; 44 | export default CartContainer; 45 | -------------------------------------------------------------------------------- /final/src/components/CartItem.js: -------------------------------------------------------------------------------- 1 | import { ChevronDown, ChevronUp } from '../icons'; 2 | import { removeItem, increase, decrease } from '../features/cart/cartSlice'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | const CartItem = ({ id, img, title, price, amount }) => { 6 | const dispatch = useDispatch(); 7 | return ( 8 |
9 | {title} 10 |
11 |

{title}

12 |

${price}

13 | 21 |
22 |
23 | 31 |

{amount}

32 | 44 |
45 |
46 | ); 47 | }; 48 | export default CartItem; 49 | -------------------------------------------------------------------------------- /final/src/components/Modal.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { clearCart } from '../features/cart/cartSlice'; 3 | import { closeModal } from '../features/modal/modalSlice'; 4 | 5 | const Modal = () => { 6 | const dispatch = useDispatch(); 7 | return ( 8 | 34 | ); 35 | }; 36 | export default Modal; 37 | -------------------------------------------------------------------------------- /final/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { CartIcon } from '../icons'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const Navbar = () => { 5 | const { amount } = useSelector((store) => store.cart); 6 | return ( 7 | <> 8 | 19 | 20 | ); 21 | }; 22 | export default Navbar; 23 | -------------------------------------------------------------------------------- /final/src/features/cart/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | import axios from 'axios'; 3 | import { openModal } from '../modal/modalSlice'; 4 | 5 | const url = 'https://course-api.com/react-useReducer-cart-project'; 6 | 7 | const initialState = { 8 | cartItems: [], 9 | amount: 4, 10 | total: 0, 11 | isLoading: true, 12 | }; 13 | 14 | export const getCartItems = createAsyncThunk( 15 | 'cart/getCartItems', 16 | async (name, thunkAPI) => { 17 | try { 18 | // console.log(name); 19 | // console.log(thunkAPI); 20 | // console.log(thunkAPI.getState()); 21 | // thunkAPI.dispatch(openModal()); 22 | const resp = await axios(url); 23 | 24 | return resp.data; 25 | } catch (error) { 26 | return thunkAPI.rejectWithValue('something went wrong'); 27 | } 28 | } 29 | ); 30 | 31 | const cartSlice = createSlice({ 32 | name: 'cart', 33 | initialState, 34 | reducers: { 35 | clearCart: (state) => { 36 | state.cartItems = []; 37 | }, 38 | removeItem: (state, action) => { 39 | const itemId = action.payload; 40 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId); 41 | }, 42 | increase: (state, { payload }) => { 43 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 44 | cartItem.amount = cartItem.amount + 1; 45 | }, 46 | decrease: (state, { payload }) => { 47 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 48 | cartItem.amount = cartItem.amount - 1; 49 | }, 50 | calculateTotals: (state) => { 51 | let amount = 0; 52 | let total = 0; 53 | state.cartItems.forEach((item) => { 54 | amount += item.amount; 55 | total += item.amount * item.price; 56 | }); 57 | state.amount = amount; 58 | state.total = total; 59 | }, 60 | }, 61 | extraReducers: (builder) => { 62 | builder 63 | .addCase(getCartItems.pending, (state) => { 64 | state.isLoading = true; 65 | }) 66 | .addCase(getCartItems.fulfilled, (state, action) => { 67 | // console.log(action); 68 | state.isLoading = false; 69 | state.cartItems = action.payload; 70 | }) 71 | .addCase(getCartItems.rejected, (state, action) => { 72 | console.log(action); 73 | state.isLoading = false; 74 | }); 75 | }, 76 | }); 77 | 78 | // console.log(cartSlice); 79 | export const { clearCart, removeItem, increase, decrease, calculateTotals } = 80 | cartSlice.actions; 81 | 82 | export default cartSlice.reducer; 83 | -------------------------------------------------------------------------------- /final/src/features/modal/modalSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | isOpen: false, 5 | }; 6 | 7 | const modalSlice = createSlice({ 8 | name: 'modal', 9 | initialState, 10 | reducers: { 11 | openModal: (state, action) => { 12 | state.isOpen = true; 13 | }, 14 | closeModal: (state, action) => { 15 | state.isOpen = false; 16 | }, 17 | }, 18 | }); 19 | 20 | export const { openModal, closeModal } = modalSlice.actions; 21 | 22 | export default modalSlice.reducer; 23 | -------------------------------------------------------------------------------- /final/src/icons.js: -------------------------------------------------------------------------------- 1 | export const CartIcon = () => { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export const ChevronDown = () => { 21 | return ( 22 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const ChevronUp = () => { 36 | return ( 37 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /final/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | --clr-primary: #645cff; 9 | --clr-primary-dark: #282566; 10 | --clr-primary-light: #a29dff; 11 | --clr-grey-1: #102a42; 12 | --clr-grey-5: #617d98; 13 | --clr-grey-10: #f1f5f8; 14 | --clr-white: #fff; 15 | --clr-red-dark: hsl(360, 67%, 44%); 16 | --clr-red-light: hsl(360, 71%, 66%); 17 | --transition: all 0.3s linear; 18 | --spacing: 0.25rem; 19 | --radius: 0.25rem; 20 | --large-screen-width: 1170px; 21 | --small-screen-width: 90vw; 22 | --fixed-width: 50rem; 23 | } 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | box-sizing: border-box; 28 | } 29 | body { 30 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 31 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 32 | background: var(--clr-grey-10); 33 | color: var(--clr-grey-1); 34 | line-height: 1.5; 35 | font-size: 0.875rem; 36 | } 37 | a { 38 | text-decoration: none; 39 | } 40 | img { 41 | width: 100%; 42 | display: block; 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4 { 48 | letter-spacing: var(--spacing); 49 | text-transform: capitalize; 50 | line-height: 1.25; 51 | margin-bottom: 0.75rem; 52 | } 53 | h1 { 54 | font-size: 3rem; 55 | } 56 | h2 { 57 | font-size: 2rem; 58 | } 59 | h3 { 60 | font-size: 1.5rem; 61 | } 62 | h4 { 63 | font-size: 0.875rem; 64 | } 65 | p { 66 | margin-bottom: 1.25rem; 67 | } 68 | @media screen and (min-width: 800px) { 69 | h1 { 70 | font-size: 4rem; 71 | } 72 | h2 { 73 | font-size: 2.5rem; 74 | } 75 | h3 { 76 | font-size: 2rem; 77 | } 78 | h4 { 79 | font-size: 1rem; 80 | } 81 | body { 82 | font-size: 1rem; 83 | } 84 | h1, 85 | h2, 86 | h3, 87 | h4 { 88 | line-height: 1; 89 | } 90 | } 91 | /* more global css */ 92 | 93 | .btn { 94 | text-transform: uppercase; 95 | background: var(--clr-primary); 96 | color: var(--clr-white); 97 | padding: 0.375rem 0.75rem; 98 | letter-spacing: var(--spacing); 99 | display: inline-block; 100 | font-weight: 700; 101 | transition: var(--transition); 102 | font-size: 0.875rem; 103 | border: none; 104 | cursor: pointer; 105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 106 | } 107 | .btn:hover { 108 | color: var(--clr-primary); 109 | background: var(--clr-primary-light); 110 | } 111 | 112 | /* 113 | =============== 114 | Navbar 115 | =============== 116 | */ 117 | .loading { 118 | text-align: center; 119 | margin-top: 5rem; 120 | } 121 | nav { 122 | background: var(--clr-primary); 123 | padding: 1.25rem 2rem; 124 | } 125 | .nav-center { 126 | max-width: var(--fixed-width); 127 | width: 100%; 128 | margin: 0 auto; 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | } 133 | nav h3 { 134 | margin-bottom: 0; 135 | letter-spacing: 1px; 136 | color: var(--clr-white); 137 | } 138 | .nav-container { 139 | display: block; 140 | position: relative; 141 | } 142 | nav svg { 143 | width: 40px; 144 | color: var(--clr-white); 145 | } 146 | .amount-container { 147 | position: absolute; 148 | top: -0.6rem; 149 | right: -0.6rem; 150 | width: 1.75rem; 151 | height: 1.75rem; 152 | border-radius: 50%; 153 | background: var(--clr-primary-light); 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | } 158 | .total-amount { 159 | color: var(--clr-white); 160 | margin-bottom: 0; 161 | font-size: 1.25rem; 162 | } 163 | /* 164 | =============== 165 | Cart 166 | =============== 167 | */ 168 | .cart { 169 | min-height: calc(100vh - 120px); 170 | width: 90vw; 171 | margin: 0 auto; 172 | margin-top: 40px; 173 | padding: 2.5rem 0; 174 | max-width: var(--fixed-width); 175 | } 176 | .cart h2 { 177 | text-transform: uppercase; 178 | text-align: center; 179 | margin-bottom: 3rem; 180 | } 181 | .empty-cart { 182 | text-transform: lowercase; 183 | color: var(--clr-grey-5); 184 | margin-top: 1rem; 185 | text-align: center; 186 | } 187 | .cart footer { 188 | margin-top: 4rem; 189 | text-align: center; 190 | } 191 | .cart-total h4 { 192 | text-transform: capitalize; 193 | display: flex; 194 | justify-content: space-between; 195 | margin-top: 1rem; 196 | } 197 | .clear-btn, 198 | .confirm-btn { 199 | background: transparent; 200 | padding: 0.5rem 1rem; 201 | color: var(--clr-red-dark); 202 | border: 1px solid var(--clr-red-dark); 203 | margin-top: 2.25rem; 204 | border-radius: var(--radius); 205 | } 206 | .clear-btn:hover { 207 | background: var(--clr-red-light); 208 | color: var(--clr-red-dark); 209 | border-color: var(--clr-red-light); 210 | } 211 | .confirm-btn { 212 | border-color: var(--clr-primary); 213 | color: var(--clr-primary); 214 | } 215 | /* 216 | =============== 217 | Cart Item 218 | =============== 219 | */ 220 | .cart-item { 221 | display: grid; 222 | align-items: center; 223 | grid-template-columns: auto 1fr auto; 224 | grid-column-gap: 1.5rem; 225 | margin: 1.5rem 0; 226 | } 227 | .cart-item img { 228 | width: 5rem; 229 | height: 5rem; 230 | object-fit: cover; 231 | } 232 | .cart-item h4 { 233 | margin-bottom: 0.5rem; 234 | font-weight: 500; 235 | letter-spacing: 2px; 236 | } 237 | .item-price { 238 | color: var(--clr-grey-5); 239 | } 240 | .remove-btn { 241 | color: var(--clr-primary); 242 | letter-spacing: var(--spacing); 243 | cursor: pointer; 244 | font-size: 0.85rem; 245 | background: transparent; 246 | border: none; 247 | margin-top: 0.375rem; 248 | transition: var(--transition); 249 | } 250 | .remove-btn:hover { 251 | color: var(--clr-primary-light); 252 | } 253 | .amount-btn { 254 | width: 24px; 255 | background: transparent; 256 | border: none; 257 | cursor: pointer; 258 | } 259 | .amount-btn svg { 260 | color: var(--clr-primary); 261 | } 262 | .amount-btn:hover svg { 263 | color: var(--clr-primary-light); 264 | } 265 | .amount { 266 | text-align: center; 267 | margin-bottom: 0; 268 | font-size: 1.25rem; 269 | line-height: 1; 270 | } 271 | hr { 272 | background: var(--clr-grey-5); 273 | border-color: transparent; 274 | border-width: 0.25px; 275 | } 276 | 277 | .modal-container { 278 | position: fixed; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | height: 100%; 283 | background: rgba(0, 0, 0, 0.7); 284 | z-index: 10; 285 | display: flex; 286 | align-items: center; 287 | justify-content: center; 288 | } 289 | 290 | .modal { 291 | background: var(--clr-white); 292 | width: 80vw; 293 | max-width: 400px; 294 | border-radius: var(--radius); 295 | padding: 2rem 1rem; 296 | text-align: center; 297 | } 298 | .modal h4 { 299 | margin-bottom: 0; 300 | line-height: 1.5; 301 | } 302 | .modal .clear-btn, 303 | .modal .confirm-btn { 304 | margin-top: 1rem; 305 | } 306 | .btn-container { 307 | display: flex; 308 | justify-content: space-around; 309 | } 310 | -------------------------------------------------------------------------------- /final/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | import { store } from './store'; 7 | import { Provider } from 'react-redux'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /final/src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import cartReducer from './features/cart/cartSlice'; 3 | import modalReducer from './features/modal/modalSlice'; 4 | export const store = configureStore({ 5 | reducer: { 6 | cart: cartReducer, 7 | modal: modalReducer, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /starter/.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 | -------------------------------------------------------------------------------- /starter/README.md: -------------------------------------------------------------------------------- 1 | # Redux Toolkit 2 | 3 | #### React Course 4 | 5 | [My React Course](https://www.udemy.com/course/react-tutorial-and-projects-course/?referralCode=FEE6A921AF07E2563CEF) 6 | 7 | #### Support 8 | 9 | Find the App Useful? [You can always buy me a coffee](https://www.buymeacoffee.com/johnsmilga) 10 | 11 | #### Docs 12 | 13 | [Redux Toolkit Docs](https://redux-toolkit.js.org/introduction/getting-started) 14 | 15 | #### Install Template 16 | 17 | ```sh 18 | npx create-react-app my-app --template redux 19 | ``` 20 | 21 | - @latest 22 | 23 | ```sh 24 | npx create-react-app@latest my-app --template redux 25 | ``` 26 | 27 | #### Existing App 28 | 29 | ```sh 30 | npm install @reduxjs/toolkit react-redux 31 | ``` 32 | 33 | #### @reduxjs/toolkit 34 | 35 | consists of few libraries 36 | 37 | - redux (core library, state management) 38 | - immer (allows to mutate state) 39 | - redux-thunk (handles async actions) 40 | - reselect (simplifies reducer functions) 41 | 42 | #### Extras 43 | 44 | - redux devtools 45 | - combine reducers 46 | 47 | #### react-redux 48 | 49 | connects our app to redux 50 | 51 | #### Setup Store 52 | 53 | - create store.js 54 | 55 | ```js 56 | import { configureStore } from '@reduxjs/toolkit'; 57 | 58 | export const store = configureStore({ 59 | reducer: {}, 60 | }); 61 | ``` 62 | 63 | #### Setup Provider 64 | 65 | - index.js 66 | 67 | ```js 68 | import React from 'react'; 69 | import ReactDOM from 'react-dom'; 70 | import './index.css'; 71 | import App from './App'; 72 | // import store and provider 73 | import { store } from './store'; 74 | import { Provider } from 'react-redux'; 75 | 76 | ReactDOM.render( 77 | 78 | 79 | 80 | 81 | , 82 | document.getElementById('root') 83 | ); 84 | ``` 85 | 86 | #### Setup Cart Slice 87 | 88 | - application feature 89 | - create features folder/cart 90 | - create cartSlice.js 91 | 92 | ```js 93 | import { createSlice } from '@reduxjs/toolkit'; 94 | 95 | const initialState = { 96 | cartItems: [], 97 | amount: 0, 98 | total: 0, 99 | isLoading: true, 100 | }; 101 | 102 | const cartSlice = createSlice({ 103 | name: 'cart', 104 | initialState, 105 | }); 106 | 107 | console.log(cartSlice); 108 | 109 | export default cartSlice.reducer; 110 | ``` 111 | 112 | - store.js 113 | 114 | ```js 115 | import { configureStore } from '@reduxjs/toolkit'; 116 | import cartReducer from './features/cart/cartSlice'; 117 | 118 | export const store = configureStore({ 119 | reducer: { 120 | cart: cartReducer, 121 | }, 122 | }); 123 | ``` 124 | 125 | #### Redux DevTools 126 | 127 | - extension 128 | 129 | #### Access store value 130 | 131 | - create components/Navbar.js 132 | 133 | ```js 134 | import { CartIcon } from '../icons'; 135 | import { useSelector } from 'react-redux'; 136 | 137 | const Navbar = () => { 138 | const { amount } = useSelector((state) => state.cart); 139 | 140 | return ( 141 | 152 | ); 153 | }; 154 | export default Navbar; 155 | ``` 156 | 157 | #### Hero Icons 158 | 159 | - [Hero Icons](https://heroicons.com/) 160 | 161 | ```css 162 | nav svg { 163 | width: 40px; 164 | color: var(--clr-white); 165 | } 166 | ``` 167 | 168 | #### Setup Cart 169 | 170 | - cartSlice.js 171 | 172 | ```js 173 | import cartItems from '../../cartItems'; 174 | 175 | const initialState = { 176 | cartItems: cartItems, 177 | amount: 0, 178 | total: 0, 179 | isLoading: true, 180 | }; 181 | ``` 182 | 183 | - create CartContainer.js and CartItem.js 184 | - CartContainer.js 185 | 186 | ```js 187 | import React from 'react'; 188 | import CartItem from './CartItem'; 189 | import { useSelector } from 'react-redux'; 190 | 191 | const CartContainer = () => { 192 | const { cartItems, total, amount } = useSelector((state) => state.cart); 193 | 194 | if (amount < 1) { 195 | return ( 196 |
197 | {/* cart header */} 198 |
199 |

your bag

200 |

is currently empty

201 |
202 |
203 | ); 204 | } 205 | return ( 206 |
207 | {/* cart header */} 208 |
209 |

your bag

210 |
211 | {/* cart items */} 212 |
213 | {cartItems.map((item) => { 214 | return ; 215 | })} 216 |
217 | {/* cart footer */} 218 | 227 |
228 | ); 229 | }; 230 | 231 | export default CartContainer; 232 | ``` 233 | 234 | - CartItem.js 235 | 236 | ```js 237 | import React from 'react'; 238 | import { ChevronDown, ChevronUp } from '../icons'; 239 | 240 | const CartItem = ({ id, img, title, price, amount }) => { 241 | return ( 242 |
243 | {title} 244 |
245 |

{title}

246 |

${price}

247 | {/* remove button */} 248 | 249 |
250 |
251 | {/* increase amount */} 252 | 255 | {/* amount */} 256 |

{amount}

257 | {/* decrease amount */} 258 | 261 |
262 |
263 | ); 264 | }; 265 | 266 | export default CartItem; 267 | ``` 268 | 269 | #### First Reducer 270 | 271 | - cartSlice.js 272 | - Immer library 273 | 274 | ```js 275 | const cartSlice = createSlice({ 276 | name: 'cart', 277 | initialState, 278 | reducers: { 279 | clearCart: (state) => { 280 | state.cartItems = []; 281 | }, 282 | }, 283 | }); 284 | 285 | export const { clearCart } = cartSlice.actions; 286 | ``` 287 | 288 | - create action 289 | 290 | ```js 291 | const ACTION_TYPE = 'ACTION_TYPE'; 292 | 293 | const actionCreator = (payload) => { 294 | return { type: ACTION_TYPE, payload: payload }; 295 | }; 296 | ``` 297 | 298 | - CartContainer.js 299 | 300 | ```js 301 | import React from 'react'; 302 | import CartItem from './CartItem'; 303 | import { useDispatch, useSelector } from 'react-redux'; 304 | 305 | const CartContainer = () => { 306 | const dispatch = useDispatch(); 307 | 308 | return ( 309 | 317 | ); 318 | }; 319 | 320 | export default CartContainer; 321 | ``` 322 | 323 | #### Remove, Increase, Decrease 324 | 325 | - cartSlice.js 326 | 327 | ```js 328 | import { createSlice } from '@reduxjs/toolkit'; 329 | import cartItems from '../../cartItems'; 330 | 331 | const initialState = { 332 | cartItems: [], 333 | amount: 0, 334 | total: 0, 335 | isLoading: true, 336 | }; 337 | 338 | const cartSlice = createSlice({ 339 | name: 'cart', 340 | initialState, 341 | reducers: { 342 | clearCart: (state) => { 343 | state.cartItems = []; 344 | }, 345 | removeItem: (state, action) => { 346 | const itemId = action.payload; 347 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId); 348 | }, 349 | increase: (state, { payload }) => { 350 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 351 | cartItem.amount = cartItem.amount + 1; 352 | }, 353 | decrease: (state, { payload }) => { 354 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 355 | cartItem.amount = cartItem.amount - 1; 356 | }, 357 | calculateTotals: (state) => { 358 | let amount = 0; 359 | let total = 0; 360 | state.cartItems.forEach((item) => { 361 | amount += item.amount; 362 | total += item.amount * item.price; 363 | }); 364 | state.amount = amount; 365 | state.total = total; 366 | }, 367 | }, 368 | }); 369 | 370 | export const { clearCart, removeItem, increase, decrease, calculateTotals } = 371 | cartSlice.actions; 372 | 373 | export default cartSlice.reducer; 374 | ``` 375 | 376 | - CartItem.js 377 | 378 | ```js 379 | import React from 'react'; 380 | import { ChevronDown, ChevronUp } from '../icons'; 381 | 382 | import { useDispatch } from 'react-redux'; 383 | import { removeItem, increase, decrease } from '../features/cart/cartSlice'; 384 | 385 | const CartItem = ({ id, img, title, price, amount }) => { 386 | const dispatch = useDispatch(); 387 | 388 | return ( 389 |
390 | {title} 391 |
392 |

{title}

393 |

${price}

394 | {/* remove button */} 395 | 403 |
404 |
405 | {/* increase amount */} 406 | 414 | {/* amount */} 415 |

{amount}

416 | {/* decrease amount */} 417 | 429 |
430 |
431 | ); 432 | }; 433 | 434 | export default CartItem; 435 | ``` 436 | 437 | - App.js 438 | 439 | ```js 440 | import { useEffect } from 'react'; 441 | import Navbar from './components/Navbar'; 442 | import CartContainer from './components/CartContainer'; 443 | import { useSelector, useDispatch } from 'react-redux'; 444 | import { calculateTotals } from './features/cart/cartSlice'; 445 | 446 | function App() { 447 | const { cartItems } = useSelector((state) => state.cart); 448 | const dispatch = useDispatch(); 449 | useEffect(() => { 450 | dispatch(calculateTotals()); 451 | }, [cartItems]); 452 | 453 | return ( 454 |
455 | 456 | 457 |
458 | ); 459 | } 460 | 461 | export default App; 462 | ``` 463 | 464 | #### Modal 465 | 466 | - create components/Modal.js 467 | 468 | ```js 469 | const Modal = () => { 470 | return ( 471 | 484 | ); 485 | }; 486 | export default Modal; 487 | ``` 488 | 489 | - App.js 490 | 491 | ```js 492 | return ( 493 |
494 | 495 | 496 | 497 |
498 | ); 499 | ``` 500 | 501 | #### modal slice 502 | 503 | - create features/modal/modalSlice.js 504 | 505 | ```js 506 | import { createSlice } from '@reduxjs/toolkit'; 507 | const initialState = { 508 | isOpen: false, 509 | }; 510 | 511 | const modalSlice = createSlice({ 512 | name: 'modal', 513 | initialState, 514 | reducers: { 515 | openModal: (state, action) => { 516 | state.isOpen = true; 517 | }, 518 | closeModal: (state, action) => { 519 | state.isOpen = false; 520 | }, 521 | }, 522 | }); 523 | 524 | export const { openModal, closeModal } = modalSlice.actions; 525 | export default modalSlice.reducer; 526 | ``` 527 | 528 | - App.js 529 | 530 | ```js 531 | const { isOpen } = useSelector((state) => state.modal); 532 | 533 | return ( 534 |
535 | {isOpen && } 536 | 537 | 538 |
539 | ); 540 | ``` 541 | 542 | #### toggle modal 543 | 544 | - CartContainer.js 545 | 546 | ```js 547 | import { openModal } from '../features/modal/modalSlice'; 548 | 549 | return ( 550 | 558 | ); 559 | ``` 560 | 561 | - Modal.js 562 | 563 | ```js 564 | import { closeModal } from '../features/modal/modalSlice'; 565 | import { useDispatch } from 'react-redux'; 566 | import { clearCart } from '../features/cart/cartSlice'; 567 | 568 | const Modal = () => { 569 | const dispatch = useDispatch(); 570 | 571 | return ( 572 | 598 | ); 599 | }; 600 | export default Modal; 601 | ``` 602 | 603 | #### async functionality with createAsyncThunk 604 | 605 | - [Course API](https://course-api.com/) 606 | - https://course-api.com/react-useReducer-cart-project 607 | - cartSlice.js 608 | 609 | - action type 610 | - callback function 611 | - lifecycle actions 612 | 613 | ```js 614 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 615 | 616 | const url = 'https://course-api.com/react-useReducer-cart-project'; 617 | 618 | export const getCartItems = createAsyncThunk('cart/getCartItems', () => { 619 | return fetch(url) 620 | .then((resp) => resp.json()) 621 | .catch((err) => console.log(error)); 622 | }); 623 | 624 | const cartSlice = createSlice({ 625 | name: 'cart', 626 | initialState, 627 | extraReducers: { 628 | [getCartItems.pending]: (state) => { 629 | state.isLoading = true; 630 | }, 631 | [getCartItems.fulfilled]: (state, action) => { 632 | console.log(action); 633 | state.isLoading = false; 634 | state.cartItems = action.payload; 635 | }, 636 | [getCartItems.rejected]: (state) => { 637 | state.isLoading = false; 638 | }, 639 | }, 640 | }); 641 | ``` 642 | 643 | - App.js 644 | 645 | ```js 646 | import { calculateTotals, getCartItems } from './features/cart/cartSlice'; 647 | 648 | function App() { 649 | const { cartItems, isLoading } = useSelector((state) => state.cart); 650 | 651 | useEffect(() => { 652 | dispatch(getCartItems()); 653 | }, []); 654 | 655 | if (isLoading) { 656 | return ( 657 |
658 |

Loading...

659 |
660 | ); 661 | } 662 | 663 | return ( 664 |
665 | {isOpen && } 666 | 667 | 668 |
669 | ); 670 | } 671 | 672 | export default App; 673 | ``` 674 | 675 | #### Options 676 | 677 | ```sh 678 | npm install axios 679 | ``` 680 | 681 | - cartSlice.js 682 | 683 | ```js 684 | export const getCartItems = createAsyncThunk( 685 | 'cart/getCartItems', 686 | async (name, thunkAPI) => { 687 | try { 688 | // console.log(name); 689 | // console.log(thunkAPI); 690 | // console.log(thunkAPI.getState()); 691 | // thunkAPI.dispatch(openModal()); 692 | const resp = await axios(url); 693 | 694 | return resp.data; 695 | } catch (error) { 696 | return thunkAPI.rejectWithValue('something went wrong'); 697 | } 698 | } 699 | ); 700 | ``` 701 | 702 | #### The extraReducers "builder callback" notation 703 | 704 | cart/cartSlice 705 | 706 | ```js 707 | const cartSlice = createSlice({ 708 | name: 'cart', 709 | initialState, 710 | reducers: { 711 | // reducers 712 | }, 713 | extraReducers: (builder) => { 714 | builder 715 | .addCase(getCartItems.pending, (state) => { 716 | state.isLoading = true; 717 | }) 718 | .addCase(getCartItems.fulfilled, (state, action) => { 719 | // console.log(action); 720 | state.isLoading = false; 721 | state.cartItems = action.payload; 722 | }) 723 | .addCase(getCartItems.rejected, (state, action) => { 724 | console.log(action); 725 | state.isLoading = false; 726 | }); 727 | }, 728 | }); 729 | ``` 730 | -------------------------------------------------------------------------------- /starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-toolkit-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.9.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.1.3", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-redux": "^8.0.5", 14 | "react-scripts": "5.0.1", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /starter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/starter/public/favicon.ico -------------------------------------------------------------------------------- /starter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Redux Toolkit 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /starter/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/starter/public/logo192.png -------------------------------------------------------------------------------- /starter/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/redux-toolkit-tutorial/f4bae75974fb8d78c40397d9d0c95299cc4609e2/starter/public/logo512.png -------------------------------------------------------------------------------- /starter/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 | -------------------------------------------------------------------------------- /starter/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /starter/src/App.js: -------------------------------------------------------------------------------- 1 | function App() { 2 | return

Redux Toolkit

; 3 | } 4 | export default App; 5 | -------------------------------------------------------------------------------- /starter/src/cartItems.js: -------------------------------------------------------------------------------- 1 | const cartItems = [ 2 | { 3 | id: 'rec1JZlfCIBOPdcT2', 4 | title: 'Samsung Galaxy S8', 5 | price: '399.99', 6 | img: 'https://images2.imgbox.com/c2/14/zedmXgs6_o.png', 7 | amount: 1, 8 | }, 9 | { 10 | id: 'recB6qcHPxb62YJ75', 11 | title: 'google pixel', 12 | price: '499.99', 13 | img: 'https://images2.imgbox.com/fb/3d/O4TPmhlt_o.png', 14 | amount: 1, 15 | }, 16 | { 17 | id: 'recdRxBsE14Rr2VuJ', 18 | title: 'Xiaomi Redmi Note 2', 19 | price: '699.99', 20 | img: 'https://images2.imgbox.com/4f/3d/WN3GvciF_o.png', 21 | amount: 1, 22 | }, 23 | { 24 | id: 'recwTo160XST3PIoW', 25 | title: 'Samsung Galaxy S7', 26 | price: '599.99 ', 27 | img: 'https://images2.imgbox.com/2e/7c/yFsJ4Zkb_o.png', 28 | amount: 1, 29 | }, 30 | ]; 31 | export default cartItems; 32 | -------------------------------------------------------------------------------- /starter/src/icons.js: -------------------------------------------------------------------------------- 1 | export const CartIcon = () => { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export const ChevronDown = () => { 21 | return ( 22 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const ChevronUp = () => { 36 | return ( 37 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /starter/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | --clr-primary: #645cff; 9 | --clr-primary-dark: #282566; 10 | --clr-primary-light: #a29dff; 11 | --clr-grey-1: #102a42; 12 | --clr-grey-5: #617d98; 13 | --clr-grey-10: #f1f5f8; 14 | --clr-white: #fff; 15 | --clr-red-dark: hsl(360, 67%, 44%); 16 | --clr-red-light: hsl(360, 71%, 66%); 17 | --transition: all 0.3s linear; 18 | --spacing: 0.25rem; 19 | --radius: 0.25rem; 20 | --large-screen-width: 1170px; 21 | --small-screen-width: 90vw; 22 | --fixed-width: 50rem; 23 | } 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | box-sizing: border-box; 28 | } 29 | body { 30 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 31 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 32 | background: var(--clr-grey-10); 33 | color: var(--clr-grey-1); 34 | line-height: 1.5; 35 | font-size: 0.875rem; 36 | } 37 | a { 38 | text-decoration: none; 39 | } 40 | img { 41 | width: 100%; 42 | display: block; 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4 { 48 | letter-spacing: var(--spacing); 49 | text-transform: capitalize; 50 | line-height: 1.25; 51 | margin-bottom: 0.75rem; 52 | } 53 | h1 { 54 | font-size: 3rem; 55 | } 56 | h2 { 57 | font-size: 2rem; 58 | } 59 | h3 { 60 | font-size: 1.5rem; 61 | } 62 | h4 { 63 | font-size: 0.875rem; 64 | } 65 | p { 66 | margin-bottom: 1.25rem; 67 | } 68 | @media screen and (min-width: 800px) { 69 | h1 { 70 | font-size: 4rem; 71 | } 72 | h2 { 73 | font-size: 2.5rem; 74 | } 75 | h3 { 76 | font-size: 2rem; 77 | } 78 | h4 { 79 | font-size: 1rem; 80 | } 81 | body { 82 | font-size: 1rem; 83 | } 84 | h1, 85 | h2, 86 | h3, 87 | h4 { 88 | line-height: 1; 89 | } 90 | } 91 | /* more global css */ 92 | 93 | .btn { 94 | text-transform: uppercase; 95 | background: var(--clr-primary); 96 | color: var(--clr-white); 97 | padding: 0.375rem 0.75rem; 98 | letter-spacing: var(--spacing); 99 | display: inline-block; 100 | font-weight: 700; 101 | transition: var(--transition); 102 | font-size: 0.875rem; 103 | border: none; 104 | cursor: pointer; 105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 106 | } 107 | .btn:hover { 108 | color: var(--clr-primary); 109 | background: var(--clr-primary-light); 110 | } 111 | 112 | /* 113 | =============== 114 | Navbar 115 | =============== 116 | */ 117 | .loading { 118 | text-align: center; 119 | margin-top: 5rem; 120 | } 121 | nav { 122 | background: var(--clr-primary); 123 | padding: 1.25rem 2rem; 124 | } 125 | .nav-center { 126 | max-width: var(--fixed-width); 127 | width: 100%; 128 | margin: 0 auto; 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | } 133 | nav h3 { 134 | margin-bottom: 0; 135 | letter-spacing: 1px; 136 | color: var(--clr-white); 137 | } 138 | .nav-container { 139 | display: block; 140 | position: relative; 141 | } 142 | nav svg { 143 | width: 40px; 144 | color: var(--clr-white); 145 | } 146 | .amount-container { 147 | position: absolute; 148 | top: -0.6rem; 149 | right: -0.6rem; 150 | width: 1.75rem; 151 | height: 1.75rem; 152 | border-radius: 50%; 153 | background: var(--clr-primary-light); 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | } 158 | .total-amount { 159 | color: var(--clr-white); 160 | margin-bottom: 0; 161 | font-size: 1.25rem; 162 | } 163 | /* 164 | =============== 165 | Cart 166 | =============== 167 | */ 168 | .cart { 169 | min-height: calc(100vh - 120px); 170 | width: 90vw; 171 | margin: 0 auto; 172 | margin-top: 40px; 173 | padding: 2.5rem 0; 174 | max-width: var(--fixed-width); 175 | } 176 | .cart h2 { 177 | text-transform: uppercase; 178 | text-align: center; 179 | margin-bottom: 3rem; 180 | } 181 | .empty-cart { 182 | text-transform: lowercase; 183 | color: var(--clr-grey-5); 184 | margin-top: 1rem; 185 | text-align: center; 186 | } 187 | .cart footer { 188 | margin-top: 4rem; 189 | text-align: center; 190 | } 191 | .cart-total h4 { 192 | text-transform: capitalize; 193 | display: flex; 194 | justify-content: space-between; 195 | margin-top: 1rem; 196 | } 197 | .clear-btn, 198 | .confirm-btn { 199 | background: transparent; 200 | padding: 0.5rem 1rem; 201 | color: var(--clr-red-dark); 202 | border: 1px solid var(--clr-red-dark); 203 | margin-top: 2.25rem; 204 | border-radius: var(--radius); 205 | } 206 | .clear-btn:hover { 207 | background: var(--clr-red-light); 208 | color: var(--clr-red-dark); 209 | border-color: var(--clr-red-light); 210 | } 211 | .confirm-btn { 212 | border-color: var(--clr-primary); 213 | color: var(--clr-primary); 214 | } 215 | /* 216 | =============== 217 | Cart Item 218 | =============== 219 | */ 220 | .cart-item { 221 | display: grid; 222 | align-items: center; 223 | grid-template-columns: auto 1fr auto; 224 | grid-column-gap: 1.5rem; 225 | margin: 1.5rem 0; 226 | } 227 | .cart-item img { 228 | width: 5rem; 229 | height: 5rem; 230 | object-fit: cover; 231 | } 232 | .cart-item h4 { 233 | margin-bottom: 0.5rem; 234 | font-weight: 500; 235 | letter-spacing: 2px; 236 | } 237 | .item-price { 238 | color: var(--clr-grey-5); 239 | } 240 | .remove-btn { 241 | color: var(--clr-primary); 242 | letter-spacing: var(--spacing); 243 | cursor: pointer; 244 | font-size: 0.85rem; 245 | background: transparent; 246 | border: none; 247 | margin-top: 0.375rem; 248 | transition: var(--transition); 249 | } 250 | .remove-btn:hover { 251 | color: var(--clr-primary-light); 252 | } 253 | .amount-btn { 254 | width: 24px; 255 | background: transparent; 256 | border: none; 257 | cursor: pointer; 258 | } 259 | .amount-btn svg { 260 | color: var(--clr-primary); 261 | } 262 | .amount-btn:hover svg { 263 | color: var(--clr-primary-light); 264 | } 265 | .amount { 266 | text-align: center; 267 | margin-bottom: 0; 268 | font-size: 1.25rem; 269 | line-height: 1; 270 | } 271 | hr { 272 | background: var(--clr-grey-5); 273 | border-color: transparent; 274 | border-width: 0.25px; 275 | } 276 | 277 | .modal-container { 278 | position: fixed; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | height: 100%; 283 | background: rgba(0, 0, 0, 0.7); 284 | z-index: 10; 285 | display: flex; 286 | align-items: center; 287 | justify-content: center; 288 | } 289 | 290 | .modal { 291 | background: var(--clr-white); 292 | width: 80vw; 293 | max-width: 400px; 294 | border-radius: var(--radius); 295 | padding: 2rem 1rem; 296 | text-align: center; 297 | } 298 | .modal h4 { 299 | margin-bottom: 0; 300 | line-height: 1.5; 301 | } 302 | .modal .clear-btn, 303 | .modal .confirm-btn { 304 | margin-top: 1rem; 305 | } 306 | .btn-container { 307 | display: flex; 308 | justify-content: space-around; 309 | } 310 | -------------------------------------------------------------------------------- /starter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /vite-final/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite-final/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vite-final/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-finalp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@reduxjs/toolkit": "^1.9.3", 13 | "axios": "^1.3.4", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.0.5" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.27", 20 | "@types/react-dom": "^18.0.10", 21 | "@vitejs/plugin-react": "^3.1.0", 22 | "vite": "^4.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vite-final/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite-final/src/App.jsx: -------------------------------------------------------------------------------- 1 | import Navbar from './components/Navbar'; 2 | import CartContainer from './components/CartContainer'; 3 | 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { calculateTotals, getCartItems } from './features/cart/cartSlice'; 6 | import { useEffect } from 'react'; 7 | import Modal from './components/Modal'; 8 | function App() { 9 | const { cartItems, isLoading } = useSelector((store) => store.cart); 10 | const { isOpen } = useSelector((store) => store.modal); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | dispatch(calculateTotals()); 15 | }, [cartItems]); 16 | 17 | useEffect(() => { 18 | dispatch(getCartItems('random')); 19 | }, []); 20 | 21 | if (isLoading) { 22 | return ( 23 |
24 |

Loading...

25 |
26 | ); 27 | } 28 | 29 | return ( 30 |
31 | {isOpen && } 32 | 33 | 34 |
35 | ); 36 | } 37 | export default App; 38 | -------------------------------------------------------------------------------- /vite-final/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite-final/src/cartItems.js: -------------------------------------------------------------------------------- 1 | const cartItems = [ 2 | { 3 | id: 'rec1JZlfCIBOPdcT2', 4 | title: 'Samsung Galaxy S8', 5 | price: '399.99', 6 | img: 'https://images2.imgbox.com/c2/14/zedmXgs6_o.png', 7 | amount: 1, 8 | }, 9 | { 10 | id: 'recB6qcHPxb62YJ75', 11 | title: 'google pixel', 12 | price: '499.99', 13 | img: 'https://images2.imgbox.com/fb/3d/O4TPmhlt_o.png', 14 | amount: 1, 15 | }, 16 | { 17 | id: 'recdRxBsE14Rr2VuJ', 18 | title: 'Xiaomi Redmi Note 2', 19 | price: '699.99', 20 | img: 'https://images2.imgbox.com/4f/3d/WN3GvciF_o.png', 21 | amount: 1, 22 | }, 23 | { 24 | id: 'recwTo160XST3PIoW', 25 | title: 'Samsung Galaxy S7', 26 | price: '599.99 ', 27 | img: 'https://images2.imgbox.com/2e/7c/yFsJ4Zkb_o.png', 28 | amount: 1, 29 | }, 30 | ]; 31 | export default cartItems; 32 | -------------------------------------------------------------------------------- /vite-final/src/components/CartContainer.jsx: -------------------------------------------------------------------------------- 1 | import CartItem from './CartItem'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { openModal } from '../features/modal/modalSlice'; 4 | 5 | const CartContainer = () => { 6 | const dispatch = useDispatch(); 7 | const { cartItems, total, amount } = useSelector((store) => store.cart); 8 | 9 | if (amount < 1) { 10 | return ( 11 |
12 |
13 |

your bag

14 |

is currently empty

15 |
16 |
17 | ); 18 | } 19 | 20 | return ( 21 |
22 |
23 |

your bag

24 |
25 |
26 | {cartItems.map((item) => { 27 | return ; 28 | })} 29 |
30 | 41 |
42 | ); 43 | }; 44 | export default CartContainer; 45 | -------------------------------------------------------------------------------- /vite-final/src/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import { ChevronDown, ChevronUp } from '../icons'; 2 | import { removeItem, increase, decrease } from '../features/cart/cartSlice'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | const CartItem = ({ id, img, title, price, amount }) => { 6 | const dispatch = useDispatch(); 7 | return ( 8 |
9 | {title} 10 |
11 |

{title}

12 |

${price}

13 | 21 |
22 |
23 | 31 |

{amount}

32 | 44 |
45 |
46 | ); 47 | }; 48 | export default CartItem; 49 | -------------------------------------------------------------------------------- /vite-final/src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { clearCart } from '../features/cart/cartSlice'; 3 | import { closeModal } from '../features/modal/modalSlice'; 4 | 5 | const Modal = () => { 6 | const dispatch = useDispatch(); 7 | return ( 8 | 34 | ); 35 | }; 36 | export default Modal; 37 | -------------------------------------------------------------------------------- /vite-final/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { CartIcon } from '../icons'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const Navbar = () => { 5 | const { amount } = useSelector((store) => store.cart); 6 | return ( 7 | <> 8 | 19 | 20 | ); 21 | }; 22 | export default Navbar; 23 | -------------------------------------------------------------------------------- /vite-final/src/features/cart/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | import axios from 'axios'; 3 | import { openModal } from '../modal/modalSlice'; 4 | 5 | const url = 'https://course-api.com/react-useReducer-cart-project'; 6 | 7 | const initialState = { 8 | cartItems: [], 9 | amount: 4, 10 | total: 0, 11 | isLoading: true, 12 | }; 13 | 14 | export const getCartItems = createAsyncThunk( 15 | 'cart/getCartItems', 16 | async (name, thunkAPI) => { 17 | try { 18 | // console.log(name); 19 | // console.log(thunkAPI); 20 | // console.log(thunkAPI.getState()); 21 | // thunkAPI.dispatch(openModal()); 22 | const resp = await axios(url); 23 | 24 | return resp.data; 25 | } catch (error) { 26 | return thunkAPI.rejectWithValue('something went wrong'); 27 | } 28 | } 29 | ); 30 | 31 | const cartSlice = createSlice({ 32 | name: 'cart', 33 | initialState, 34 | reducers: { 35 | clearCart: (state) => { 36 | state.cartItems = []; 37 | }, 38 | removeItem: (state, action) => { 39 | const itemId = action.payload; 40 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId); 41 | }, 42 | increase: (state, { payload }) => { 43 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 44 | cartItem.amount = cartItem.amount + 1; 45 | }, 46 | decrease: (state, { payload }) => { 47 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 48 | cartItem.amount = cartItem.amount - 1; 49 | }, 50 | calculateTotals: (state) => { 51 | let amount = 0; 52 | let total = 0; 53 | state.cartItems.forEach((item) => { 54 | amount += item.amount; 55 | total += item.amount * item.price; 56 | }); 57 | state.amount = amount; 58 | state.total = total; 59 | }, 60 | }, 61 | extraReducers: (builder) => { 62 | builder 63 | .addCase(getCartItems.pending, (state) => { 64 | state.isLoading = true; 65 | }) 66 | .addCase(getCartItems.fulfilled, (state, action) => { 67 | // console.log(action); 68 | state.isLoading = false; 69 | state.cartItems = action.payload; 70 | }) 71 | .addCase(getCartItems.rejected, (state, action) => { 72 | console.log(action); 73 | state.isLoading = false; 74 | }); 75 | }, 76 | }); 77 | 78 | // console.log(cartSlice); 79 | export const { clearCart, removeItem, increase, decrease, calculateTotals } = 80 | cartSlice.actions; 81 | 82 | export default cartSlice.reducer; 83 | -------------------------------------------------------------------------------- /vite-final/src/features/modal/modalSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | isOpen: false, 5 | }; 6 | 7 | const modalSlice = createSlice({ 8 | name: 'modal', 9 | initialState, 10 | reducers: { 11 | openModal: (state, action) => { 12 | state.isOpen = true; 13 | }, 14 | closeModal: (state, action) => { 15 | state.isOpen = false; 16 | }, 17 | }, 18 | }); 19 | 20 | export const { openModal, closeModal } = modalSlice.actions; 21 | 22 | export default modalSlice.reducer; 23 | -------------------------------------------------------------------------------- /vite-final/src/icons.jsx: -------------------------------------------------------------------------------- 1 | export const CartIcon = () => { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export const ChevronDown = () => { 21 | return ( 22 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const ChevronUp = () => { 36 | return ( 37 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /vite-final/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | --clr-primary: #645cff; 9 | --clr-primary-dark: #282566; 10 | --clr-primary-light: #a29dff; 11 | --clr-grey-1: #102a42; 12 | --clr-grey-5: #617d98; 13 | --clr-grey-10: #f1f5f8; 14 | --clr-white: #fff; 15 | --clr-red-dark: hsl(360, 67%, 44%); 16 | --clr-red-light: hsl(360, 71%, 66%); 17 | --transition: all 0.3s linear; 18 | --spacing: 0.25rem; 19 | --radius: 0.25rem; 20 | --large-screen-width: 1170px; 21 | --small-screen-width: 90vw; 22 | --fixed-width: 50rem; 23 | } 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | box-sizing: border-box; 28 | } 29 | body { 30 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 31 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 32 | background: var(--clr-grey-10); 33 | color: var(--clr-grey-1); 34 | line-height: 1.5; 35 | font-size: 0.875rem; 36 | } 37 | a { 38 | text-decoration: none; 39 | } 40 | img { 41 | width: 100%; 42 | display: block; 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4 { 48 | letter-spacing: var(--spacing); 49 | text-transform: capitalize; 50 | line-height: 1.25; 51 | margin-bottom: 0.75rem; 52 | } 53 | h1 { 54 | font-size: 3rem; 55 | } 56 | h2 { 57 | font-size: 2rem; 58 | } 59 | h3 { 60 | font-size: 1.5rem; 61 | } 62 | h4 { 63 | font-size: 0.875rem; 64 | } 65 | p { 66 | margin-bottom: 1.25rem; 67 | } 68 | @media screen and (min-width: 800px) { 69 | h1 { 70 | font-size: 4rem; 71 | } 72 | h2 { 73 | font-size: 2.5rem; 74 | } 75 | h3 { 76 | font-size: 2rem; 77 | } 78 | h4 { 79 | font-size: 1rem; 80 | } 81 | body { 82 | font-size: 1rem; 83 | } 84 | h1, 85 | h2, 86 | h3, 87 | h4 { 88 | line-height: 1; 89 | } 90 | } 91 | /* more global css */ 92 | 93 | .btn { 94 | text-transform: uppercase; 95 | background: var(--clr-primary); 96 | color: var(--clr-white); 97 | padding: 0.375rem 0.75rem; 98 | letter-spacing: var(--spacing); 99 | display: inline-block; 100 | font-weight: 700; 101 | transition: var(--transition); 102 | font-size: 0.875rem; 103 | border: none; 104 | cursor: pointer; 105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 106 | } 107 | .btn:hover { 108 | color: var(--clr-primary); 109 | background: var(--clr-primary-light); 110 | } 111 | 112 | /* 113 | =============== 114 | Navbar 115 | =============== 116 | */ 117 | .loading { 118 | text-align: center; 119 | margin-top: 5rem; 120 | } 121 | nav { 122 | background: var(--clr-primary); 123 | padding: 1.25rem 2rem; 124 | } 125 | .nav-center { 126 | max-width: var(--fixed-width); 127 | width: 100%; 128 | margin: 0 auto; 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | } 133 | nav h3 { 134 | margin-bottom: 0; 135 | letter-spacing: 1px; 136 | color: var(--clr-white); 137 | } 138 | .nav-container { 139 | display: block; 140 | position: relative; 141 | } 142 | nav svg { 143 | width: 40px; 144 | color: var(--clr-white); 145 | } 146 | .amount-container { 147 | position: absolute; 148 | top: -0.6rem; 149 | right: -0.6rem; 150 | width: 1.75rem; 151 | height: 1.75rem; 152 | border-radius: 50%; 153 | background: var(--clr-primary-light); 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | } 158 | .total-amount { 159 | color: var(--clr-white); 160 | margin-bottom: 0; 161 | font-size: 1.25rem; 162 | } 163 | /* 164 | =============== 165 | Cart 166 | =============== 167 | */ 168 | .cart { 169 | min-height: calc(100vh - 120px); 170 | width: 90vw; 171 | margin: 0 auto; 172 | margin-top: 40px; 173 | padding: 2.5rem 0; 174 | max-width: var(--fixed-width); 175 | } 176 | .cart h2 { 177 | text-transform: uppercase; 178 | text-align: center; 179 | margin-bottom: 3rem; 180 | } 181 | .empty-cart { 182 | text-transform: lowercase; 183 | color: var(--clr-grey-5); 184 | margin-top: 1rem; 185 | text-align: center; 186 | } 187 | .cart footer { 188 | margin-top: 4rem; 189 | text-align: center; 190 | } 191 | .cart-total h4 { 192 | text-transform: capitalize; 193 | display: flex; 194 | justify-content: space-between; 195 | margin-top: 1rem; 196 | } 197 | .clear-btn, 198 | .confirm-btn { 199 | background: transparent; 200 | padding: 0.5rem 1rem; 201 | color: var(--clr-red-dark); 202 | border: 1px solid var(--clr-red-dark); 203 | margin-top: 2.25rem; 204 | border-radius: var(--radius); 205 | } 206 | .clear-btn:hover { 207 | background: var(--clr-red-light); 208 | color: var(--clr-red-dark); 209 | border-color: var(--clr-red-light); 210 | } 211 | .confirm-btn { 212 | border-color: var(--clr-primary); 213 | color: var(--clr-primary); 214 | } 215 | /* 216 | =============== 217 | Cart Item 218 | =============== 219 | */ 220 | .cart-item { 221 | display: grid; 222 | align-items: center; 223 | grid-template-columns: auto 1fr auto; 224 | grid-column-gap: 1.5rem; 225 | margin: 1.5rem 0; 226 | } 227 | .cart-item img { 228 | width: 5rem; 229 | height: 5rem; 230 | object-fit: cover; 231 | } 232 | .cart-item h4 { 233 | margin-bottom: 0.5rem; 234 | font-weight: 500; 235 | letter-spacing: 2px; 236 | } 237 | .item-price { 238 | color: var(--clr-grey-5); 239 | } 240 | .remove-btn { 241 | color: var(--clr-primary); 242 | letter-spacing: var(--spacing); 243 | cursor: pointer; 244 | font-size: 0.85rem; 245 | background: transparent; 246 | border: none; 247 | margin-top: 0.375rem; 248 | transition: var(--transition); 249 | } 250 | .remove-btn:hover { 251 | color: var(--clr-primary-light); 252 | } 253 | .amount-btn { 254 | width: 24px; 255 | background: transparent; 256 | border: none; 257 | cursor: pointer; 258 | } 259 | .amount-btn svg { 260 | color: var(--clr-primary); 261 | } 262 | .amount-btn:hover svg { 263 | color: var(--clr-primary-light); 264 | } 265 | .amount { 266 | text-align: center; 267 | margin-bottom: 0; 268 | font-size: 1.25rem; 269 | line-height: 1; 270 | } 271 | hr { 272 | background: var(--clr-grey-5); 273 | border-color: transparent; 274 | border-width: 0.25px; 275 | } 276 | 277 | .modal-container { 278 | position: fixed; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | height: 100%; 283 | background: rgba(0, 0, 0, 0.7); 284 | z-index: 10; 285 | display: flex; 286 | align-items: center; 287 | justify-content: center; 288 | } 289 | 290 | .modal { 291 | background: var(--clr-white); 292 | width: 80vw; 293 | max-width: 400px; 294 | border-radius: var(--radius); 295 | padding: 2rem 1rem; 296 | text-align: center; 297 | } 298 | .modal h4 { 299 | margin-bottom: 0; 300 | line-height: 1.5; 301 | } 302 | .modal .clear-btn, 303 | .modal .confirm-btn { 304 | margin-top: 1rem; 305 | } 306 | .btn-container { 307 | display: flex; 308 | justify-content: space-around; 309 | } 310 | -------------------------------------------------------------------------------- /vite-final/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | import { store } from './store'; 7 | import { Provider } from 'react-redux'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /vite-final/src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import cartReducer from './features/cart/cartSlice'; 3 | import modalReducer from './features/modal/modalSlice'; 4 | export const store = configureStore({ 5 | reducer: { 6 | cart: cartReducer, 7 | modal: modalReducer, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /vite-final/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /vite-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite-starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vite-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-finalp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@reduxjs/toolkit": "^1.9.3", 13 | "axios": "^1.3.4", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.0.5" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.27", 20 | "@types/react-dom": "^18.0.10", 21 | "@vitejs/plugin-react": "^3.1.0", 22 | "vite": "^4.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vite-starter/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite-starter/src/App.jsx: -------------------------------------------------------------------------------- 1 | function App() { 2 | return

Redux Toolkit

; 3 | } 4 | export default App; 5 | -------------------------------------------------------------------------------- /vite-starter/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite-starter/src/cartItems.js: -------------------------------------------------------------------------------- 1 | const cartItems = [ 2 | { 3 | id: 'rec1JZlfCIBOPdcT2', 4 | title: 'Samsung Galaxy S8', 5 | price: '399.99', 6 | img: 'https://images2.imgbox.com/c2/14/zedmXgs6_o.png', 7 | amount: 1, 8 | }, 9 | { 10 | id: 'recB6qcHPxb62YJ75', 11 | title: 'google pixel', 12 | price: '499.99', 13 | img: 'https://images2.imgbox.com/fb/3d/O4TPmhlt_o.png', 14 | amount: 1, 15 | }, 16 | { 17 | id: 'recdRxBsE14Rr2VuJ', 18 | title: 'Xiaomi Redmi Note 2', 19 | price: '699.99', 20 | img: 'https://images2.imgbox.com/4f/3d/WN3GvciF_o.png', 21 | amount: 1, 22 | }, 23 | { 24 | id: 'recwTo160XST3PIoW', 25 | title: 'Samsung Galaxy S7', 26 | price: '599.99 ', 27 | img: 'https://images2.imgbox.com/2e/7c/yFsJ4Zkb_o.png', 28 | amount: 1, 29 | }, 30 | ]; 31 | export default cartItems; 32 | -------------------------------------------------------------------------------- /vite-starter/src/icons.jsx: -------------------------------------------------------------------------------- 1 | export const CartIcon = () => { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export const ChevronDown = () => { 21 | return ( 22 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const ChevronUp = () => { 36 | return ( 37 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /vite-starter/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | --clr-primary: #645cff; 9 | --clr-primary-dark: #282566; 10 | --clr-primary-light: #a29dff; 11 | --clr-grey-1: #102a42; 12 | --clr-grey-5: #617d98; 13 | --clr-grey-10: #f1f5f8; 14 | --clr-white: #fff; 15 | --clr-red-dark: hsl(360, 67%, 44%); 16 | --clr-red-light: hsl(360, 71%, 66%); 17 | --transition: all 0.3s linear; 18 | --spacing: 0.25rem; 19 | --radius: 0.25rem; 20 | --large-screen-width: 1170px; 21 | --small-screen-width: 90vw; 22 | --fixed-width: 50rem; 23 | } 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | box-sizing: border-box; 28 | } 29 | body { 30 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 31 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 32 | background: var(--clr-grey-10); 33 | color: var(--clr-grey-1); 34 | line-height: 1.5; 35 | font-size: 0.875rem; 36 | } 37 | a { 38 | text-decoration: none; 39 | } 40 | img { 41 | width: 100%; 42 | display: block; 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4 { 48 | letter-spacing: var(--spacing); 49 | text-transform: capitalize; 50 | line-height: 1.25; 51 | margin-bottom: 0.75rem; 52 | } 53 | h1 { 54 | font-size: 3rem; 55 | } 56 | h2 { 57 | font-size: 2rem; 58 | } 59 | h3 { 60 | font-size: 1.5rem; 61 | } 62 | h4 { 63 | font-size: 0.875rem; 64 | } 65 | p { 66 | margin-bottom: 1.25rem; 67 | } 68 | @media screen and (min-width: 800px) { 69 | h1 { 70 | font-size: 4rem; 71 | } 72 | h2 { 73 | font-size: 2.5rem; 74 | } 75 | h3 { 76 | font-size: 2rem; 77 | } 78 | h4 { 79 | font-size: 1rem; 80 | } 81 | body { 82 | font-size: 1rem; 83 | } 84 | h1, 85 | h2, 86 | h3, 87 | h4 { 88 | line-height: 1; 89 | } 90 | } 91 | /* more global css */ 92 | 93 | .btn { 94 | text-transform: uppercase; 95 | background: var(--clr-primary); 96 | color: var(--clr-white); 97 | padding: 0.375rem 0.75rem; 98 | letter-spacing: var(--spacing); 99 | display: inline-block; 100 | font-weight: 700; 101 | transition: var(--transition); 102 | font-size: 0.875rem; 103 | border: none; 104 | cursor: pointer; 105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 106 | } 107 | .btn:hover { 108 | color: var(--clr-primary); 109 | background: var(--clr-primary-light); 110 | } 111 | 112 | /* 113 | =============== 114 | Navbar 115 | =============== 116 | */ 117 | .loading { 118 | text-align: center; 119 | margin-top: 5rem; 120 | } 121 | nav { 122 | background: var(--clr-primary); 123 | padding: 1.25rem 2rem; 124 | } 125 | .nav-center { 126 | max-width: var(--fixed-width); 127 | width: 100%; 128 | margin: 0 auto; 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | } 133 | nav h3 { 134 | margin-bottom: 0; 135 | letter-spacing: 1px; 136 | color: var(--clr-white); 137 | } 138 | .nav-container { 139 | display: block; 140 | position: relative; 141 | } 142 | nav svg { 143 | width: 40px; 144 | color: var(--clr-white); 145 | } 146 | .amount-container { 147 | position: absolute; 148 | top: -0.6rem; 149 | right: -0.6rem; 150 | width: 1.75rem; 151 | height: 1.75rem; 152 | border-radius: 50%; 153 | background: var(--clr-primary-light); 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | } 158 | .total-amount { 159 | color: var(--clr-white); 160 | margin-bottom: 0; 161 | font-size: 1.25rem; 162 | } 163 | /* 164 | =============== 165 | Cart 166 | =============== 167 | */ 168 | .cart { 169 | min-height: calc(100vh - 120px); 170 | width: 90vw; 171 | margin: 0 auto; 172 | margin-top: 40px; 173 | padding: 2.5rem 0; 174 | max-width: var(--fixed-width); 175 | } 176 | .cart h2 { 177 | text-transform: uppercase; 178 | text-align: center; 179 | margin-bottom: 3rem; 180 | } 181 | .empty-cart { 182 | text-transform: lowercase; 183 | color: var(--clr-grey-5); 184 | margin-top: 1rem; 185 | text-align: center; 186 | } 187 | .cart footer { 188 | margin-top: 4rem; 189 | text-align: center; 190 | } 191 | .cart-total h4 { 192 | text-transform: capitalize; 193 | display: flex; 194 | justify-content: space-between; 195 | margin-top: 1rem; 196 | } 197 | .clear-btn, 198 | .confirm-btn { 199 | background: transparent; 200 | padding: 0.5rem 1rem; 201 | color: var(--clr-red-dark); 202 | border: 1px solid var(--clr-red-dark); 203 | margin-top: 2.25rem; 204 | border-radius: var(--radius); 205 | } 206 | .clear-btn:hover { 207 | background: var(--clr-red-light); 208 | color: var(--clr-red-dark); 209 | border-color: var(--clr-red-light); 210 | } 211 | .confirm-btn { 212 | border-color: var(--clr-primary); 213 | color: var(--clr-primary); 214 | } 215 | /* 216 | =============== 217 | Cart Item 218 | =============== 219 | */ 220 | .cart-item { 221 | display: grid; 222 | align-items: center; 223 | grid-template-columns: auto 1fr auto; 224 | grid-column-gap: 1.5rem; 225 | margin: 1.5rem 0; 226 | } 227 | .cart-item img { 228 | width: 5rem; 229 | height: 5rem; 230 | object-fit: cover; 231 | } 232 | .cart-item h4 { 233 | margin-bottom: 0.5rem; 234 | font-weight: 500; 235 | letter-spacing: 2px; 236 | } 237 | .item-price { 238 | color: var(--clr-grey-5); 239 | } 240 | .remove-btn { 241 | color: var(--clr-primary); 242 | letter-spacing: var(--spacing); 243 | cursor: pointer; 244 | font-size: 0.85rem; 245 | background: transparent; 246 | border: none; 247 | margin-top: 0.375rem; 248 | transition: var(--transition); 249 | } 250 | .remove-btn:hover { 251 | color: var(--clr-primary-light); 252 | } 253 | .amount-btn { 254 | width: 24px; 255 | background: transparent; 256 | border: none; 257 | cursor: pointer; 258 | } 259 | .amount-btn svg { 260 | color: var(--clr-primary); 261 | } 262 | .amount-btn:hover svg { 263 | color: var(--clr-primary-light); 264 | } 265 | .amount { 266 | text-align: center; 267 | margin-bottom: 0; 268 | font-size: 1.25rem; 269 | line-height: 1; 270 | } 271 | hr { 272 | background: var(--clr-grey-5); 273 | border-color: transparent; 274 | border-width: 0.25px; 275 | } 276 | 277 | .modal-container { 278 | position: fixed; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | height: 100%; 283 | background: rgba(0, 0, 0, 0.7); 284 | z-index: 10; 285 | display: flex; 286 | align-items: center; 287 | justify-content: center; 288 | } 289 | 290 | .modal { 291 | background: var(--clr-white); 292 | width: 80vw; 293 | max-width: 400px; 294 | border-radius: var(--radius); 295 | padding: 2rem 1rem; 296 | text-align: center; 297 | } 298 | .modal h4 { 299 | margin-bottom: 0; 300 | line-height: 1.5; 301 | } 302 | .modal .clear-btn, 303 | .modal .confirm-btn { 304 | margin-top: 1rem; 305 | } 306 | .btn-container { 307 | display: flex; 308 | justify-content: space-around; 309 | } 310 | -------------------------------------------------------------------------------- /vite-starter/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /vite-starter/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------