├── vite.config.js ├── src ├── store.js ├── main.jsx ├── features │ ├── modal │ │ └── modalSlice.js │ └── cart │ │ └── cartSlice.js ├── components │ ├── NavBar.jsx │ ├── Modal.jsx │ ├── CartContainer.jsx │ └── CartItem.jsx ├── cartItems.js ├── App.jsx ├── icons.jsx ├── assets │ └── react.svg └── index.css ├── .gitignore ├── 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 cartSlice from './features/cart/cartSlice'; 3 | import modalSlice from './features/modal/modalSlice'; 4 | export const store = configureStore({ 5 | reducer:{ 6 | cart:cartSlice, 7 | modal:modalSlice 8 | }, 9 | }) -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | import { store } from './store'; 6 | import {Provider} from 'react-redux'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')).render( 9 | 10 | 11 | 12 | 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /src/features/modal/modalSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | isOpen: false, 5 | }; 6 | const modalSlice = createSlice({ 7 | name: "modal", 8 | initialState, 9 | reducers: { 10 | openModal: (state) => { 11 | state.isOpen = true; 12 | }, 13 | closeModal: (state) => { 14 | state.isOpen = false; 15 | }, 16 | }, 17 | }); 18 | export const { openModal, closeModal } = modalSlice.actions; 19 | export default modalSlice.reducer; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-redux-warm-up", 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 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-redux": "^8.0.5" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.28", 19 | "@types/react-dom": "^18.0.11", 20 | "@vitejs/plugin-react": "^3.1.0", 21 | "vite": "^4.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import {CartIcon} from '../icons.jsx'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const NavBar = () => { 5 | const {amount,cartItem} = useSelector(store => store.cart); 6 | return( 7 | 18 | ) 19 | } 20 | export default NavBar -------------------------------------------------------------------------------- /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 { useEffect } from 'react'; 2 | import { useDispatch,useSelector } from 'react-redux'; 3 | import NavBar from './components/NavBar.jsx' 4 | import CartContainer from './components/CartContainer.jsx' 5 | import Modal from './components/Modal.jsx'; 6 | import { calculateTotals,getCartItem } from './features/cart/cartSlice.js'; 7 | import { store } from './store.js'; 8 | 9 | function App() { 10 | const {cartItems,isLoading} = useSelector(store=>store.cart) 11 | const {isOpen} = useSelector(store=>store.modal) 12 | const dispatch = useDispatch() 13 | useEffect(()=>{ 14 | dispatch(calculateTotals()) 15 | },[cartItems]) 16 | 17 | useEffect(()=>{ 18 | dispatch(getCartItem()) 19 | },[]) 20 | 21 | if (isLoading) { 22 | return ( 23 |
24 |

Loading...

25 |
26 | ) 27 | } 28 | return ( 29 |
30 | {isOpen &&} 31 | 32 | 33 |
34 | ) 35 | } 36 | 37 | export default App 38 | -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { closeModal } from "../features/modal/modalSlice"; 3 | import { clearCart } from "../features/cart/cartSlice"; 4 | const Modal = () => { 5 | const dispatch = useDispatch(); 6 | return ( 7 | 31 | ); 32 | }; 33 | export default Modal; 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/CartContainer.jsx: -------------------------------------------------------------------------------- 1 | import { openModal } from "../features/modal/modalSlice"; 2 | import CartItem from "./CartItem"; 3 | import { useSelector, useDispatch} from "react-redux"; 4 | const CartContainer = () => { 5 | const { cartItems, total, amount } = useSelector((store) => store.cart); 6 | const dispatch = useDispatch() 7 | if (amount < 1) { 8 | return ( 9 |
10 |
11 |

your bag

12 |

is currently empty

: 13 |
14 |
15 | ); 16 | } 17 | 18 | return ( 19 |
20 |
21 |

your bag

22 |
23 | {cartItems.map((item) => ( 24 | 25 | ))} 26 |
27 |
28 | 35 |
36 | ); 37 | }; 38 | export default CartContainer; 39 | -------------------------------------------------------------------------------- /src/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import { decreaseAmount, increaseAmount, removeItem } from "../features/cart/cartSlice"; 2 | import { ChevronDown, ChevronUp } from "../icons"; 3 | import { useDispatch } from "react-redux"; 4 | const CartItem = ({id,img,title,price,amount}) => { 5 | const dispatch = useDispatch() 6 | return ( 7 |
8 | {title} 9 |
10 |

{title}

11 |

${price}

12 | 13 |
14 |
15 | 18 |

{amount}

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