├── 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 |
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 |
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 |
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 |
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 |
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 |
17 | );
18 | };
19 |
20 | export const ChevronDown = () => {
21 | return (
22 |
32 | );
33 | };
34 |
35 | export const ChevronUp = () => {
36 | return (
37 |
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 |
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 |
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 |
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 |
17 | );
18 | };
19 |
20 | export const ChevronDown = () => {
21 | return (
22 |
32 | );
33 | };
34 |
35 | export const ChevronUp = () => {
36 | return (
37 |
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 |
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 |
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 |
17 | );
18 | };
19 |
20 | export const ChevronDown = () => {
21 | return (
22 |
32 | );
33 | };
34 |
35 | export const ChevronUp = () => {
36 | return (
37 |
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 |
17 | );
18 | };
19 |
20 | export const ChevronDown = () => {
21 | return (
22 |
32 | );
33 | };
34 |
35 | export const ChevronUp = () => {
36 | return (
37 |
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 |
--------------------------------------------------------------------------------