├── vite.config.js ├── src ├── store.js ├── main.jsx ├── features │ ├── modal │ │ └── modal.js │ └── cart │ │ └── cartSlice.js ├── components │ ├── Navbar.jsx │ ├── Modal.jsx │ ├── CartItem.jsx │ └── CartContainer.jsx ├── cartItems.js ├── App.jsx ├── icons.jsx ├── assets │ └── react.svg └── index.css ├── .gitignore ├── README.md ├── index.html ├── package.json └── public └── vite.svg /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 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import cartReducer from './features/cart/cartSlice'; 3 | import modalReducer from './features/modal/modal'; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | cart: cartReducer, 8 | modal: modalReducer 9 | }, 10 | }); -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # como rodar o projeto? 2 | 3 | - npm install 4 | - npm run dev 5 | - acesse http://localhost:5173/ 6 | 7 | # imagens do projeto 8 | 9 | ![image](https://github.com/erik-monteiro/react-store/assets/91336496/27b05d1e-ba67-4350-845e-493c307352b7) 10 | 11 | # objetivo 12 | aprender a usar o ReactJS junto com o Redux, projeto desenvolvido neste curso: https://www.youtube.com/watch?v=Flbw5BX_AX0&t=33128s 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Project 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/features/modal/modal.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 | 23 | export default modalSlice.reducer; -------------------------------------------------------------------------------- /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.5", 13 | "axios": "^1.3.4", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.1.1" 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 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { CartIcon } from '../icons'; 2 | import { useSelector } from 'react-redux'; 3 | import React from 'react' 4 | 5 | const Navbar = () => { 6 | const amount = useSelector(state => state.cart.amount); 7 | 8 | return ( 9 | 20 | ) 21 | } 22 | 23 | export default Navbar; -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { clearCart } from '../features/cart/cartSlice'; 4 | import { closeModal } from '../features/modal/modal'; 5 | 6 | const Modal = () => { 7 | const dispatch = useDispatch(); 8 | 9 | return ( 10 | 17 | ) 18 | } 19 | 20 | export default Modal; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Navbar from "./components/Navbar"; 2 | import CartContainer from "./components/CartContainer"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { totalPrice, getCartItems } from "./features/cart/cartSlice"; 5 | import { useEffect } from "react"; 6 | import Modal from "./components/Modal"; 7 | 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(totalPrice()); 15 | }, [cartItems]); 16 | 17 | useEffect(() => { 18 | dispatch(getCartItems()); 19 | }, []); 20 | 21 | if (isLoading) { 22 | return

Loading...

23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | { isOpen && } 30 |
31 | ); 32 | } 33 | export default App; 34 | -------------------------------------------------------------------------------- /src/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChevronDown, ChevronUp } from '../icons'; 3 | import { useDispatch } from 'react-redux'; 4 | import { decrease, increase, removeItem } from '../features/cart/cartSlice'; 5 | 6 | const CartItem = ({ id, title, price, img, amount }) => { 7 | const dispatch = useDispatch(); 8 | 9 | return ( 10 |
11 | {title} 12 |
13 |

{title}

14 |

R$ {price}

15 | 16 |
17 |
18 | 21 |

{amount}

22 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default CartItem; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CartContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CartItem from './CartItem'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { clearCart, resetCart } from '../features/cart/cartSlice'; 5 | import { openModal } from '../features/modal/modal'; 6 | 7 | const CartContainer = () => { 8 | const dispatch = useDispatch(); 9 | const { cartItems, amount, total } = useSelector((state) => state.cart); 10 | 11 | if (amount < 1) { 12 | return ( 13 |
14 |

No products were found, add a product into your cart

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

There are {amount} products in your cart

24 |
25 |
26 | {cartItems.map(cartItem => { 27 | return 28 | })} 29 |
30 | 38 |
39 | ); 40 | } 41 | 42 | export default CartContainer; -------------------------------------------------------------------------------- /src/features/cart/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import cartItems from '../../cartItems'; 3 | import { createAsyncThunk } from "@reduxjs/toolkit"; 4 | 5 | const url = 'https://course-api.com/react-useReducer-cart-project'; 6 | 7 | const initialState = { 8 | cartItems: [], 9 | amount: 0, 10 | total: 0, 11 | isLoading: true, 12 | } 13 | 14 | export const getCartItems = createAsyncThunk('cart/getCartItems', () => { 15 | return fetch(url). 16 | then(res => res.json()). 17 | catch(error => console.log(error)); 18 | }); 19 | 20 | const cartSlice = createSlice({ 21 | name: 'cart', 22 | initialState, 23 | reducers: { 24 | clearCart: (state) => { 25 | state.cartItems = []; 26 | }, 27 | resetCart: (state) => { 28 | state.cartItems = cartItems; 29 | }, 30 | removeItem: (state, action) => { 31 | const itemId = action.payload; 32 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId); 33 | }, 34 | increase: (state, { payload }) => { 35 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 36 | cartItem.amount = cartItem.amount + 1; 37 | }, 38 | decrease: (state, { payload }) => { 39 | const cartItem = state.cartItems.find((item) => item.id === payload.id); 40 | if (cartItem.amount == 1) { 41 | cartItem.amount = cartItem.amount; 42 | } else { 43 | cartItem.amount = cartItem.amount - 1; 44 | } 45 | }, 46 | totalPrice: (state) => { 47 | let total = 0; 48 | let amount = 0; 49 | 50 | state.cartItems.forEach((item) => { 51 | amount += item.amount; 52 | total += item.amount * item.price; 53 | }); 54 | 55 | state.amount = amount; 56 | state.total = total; 57 | }, 58 | }, 59 | extraReducers: { 60 | [getCartItems.pending]: (state, action) => { 61 | state.isLoading = true; 62 | }, 63 | [getCartItems.fulfilled]: (state, action) => { 64 | state.isLoading = false; 65 | state.cartItems = action.payload; 66 | }, 67 | [getCartItems.rejected]: (state) => { 68 | state.isLoading = false; 69 | } 70 | } 71 | }); 72 | 73 | export const { clearCart, resetCart, removeItem, increase, decrease, totalPrice } = cartSlice.actions; 74 | ; 75 | 76 | export default cartSlice.reducer; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------