├── src ├── components │ ├── Loading.jsx │ ├── Cocktail.jsx │ ├── CocktailList.jsx │ └── SearchForm.jsx ├── App.jsx ├── main.jsx ├── context.jsx ├── assets │ └── react.svg ├── index.css └── logo.svg ├── vite.config.js ├── .gitignore ├── index.html ├── package.json ├── public └── vite.svg └── README.md /src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |
7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /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/App.jsx: -------------------------------------------------------------------------------- 1 | import CocktailList from './components/CocktailList'; 2 | import SearchForm from './components/SearchForm'; 3 | export default function App() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import { AppProvider } from './context'; 5 | import './index.css'; 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /.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 | Debounce in React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Cocktail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default function Cocktail({ image, name, id, info, glass }) { 3 | return ( 4 |
5 |
6 | {name} 7 |
8 |
9 |

{name}

10 |

{glass}

11 |

{info}

12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cocktailsp", 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 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "5.2" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.27", 18 | "@types/react-dom": "^18.0.10", 19 | "@vitejs/plugin-react": "^3.1.0", 20 | "vite": "^4.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/CocktailList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cocktail from './Cocktail' 3 | import Loading from './Loading' 4 | import { useGlobalContext } from '../context' 5 | 6 | export default function CocktailList() { 7 | const { cocktails, loading } = useGlobalContext() 8 | if (loading) { 9 | return 10 | } 11 | if (cocktails.length < 1) { 12 | return ( 13 |

14 | no cocktails matched your search criteria 15 |

16 | ) 17 | } 18 | return ( 19 |
20 |

cocktails

21 |
22 | {cocktails.map((item) => { 23 | return 24 | })} 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SearchForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { useGlobalContext } from '../context'; 3 | export default function SearchForm() { 4 | const [searchTerm, setSearchTerm] = useState(''); 5 | const [timeoutId, setTimeoutId] = useState(null); 6 | 7 | const { fetchDrinks } = useGlobalContext(); 8 | 9 | const handleSubmit = (e) => { 10 | e.preventDefault(); 11 | }; 12 | 13 | const searchCocktail = (e) => { 14 | const searchTerm = e.target.value; 15 | setSearchTerm(searchTerm); 16 | clearTimeout(timeoutId); 17 | setTimeoutId( 18 | setTimeout(() => { 19 | // Call the API after the debounce timeout 20 | fetchDrinks(searchTerm); 21 | }, 1000) 22 | ); 23 | }; 24 | 25 | useEffect(() => { 26 | // Cleanup function to clear the timeout on unmount and re-render 27 | return () => { 28 | clearTimeout(timeoutId); 29 | }; 30 | }, [timeoutId]); 31 | 32 | return ( 33 |
34 |
35 |
36 | 37 | 44 |
45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/context.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | import { useCallback } from 'react'; 3 | 4 | const url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s='; 5 | const AppContext = React.createContext(); 6 | 7 | const AppProvider = ({ children }) => { 8 | const [loading, setLoading] = useState(true); 9 | const [cocktails, setCocktails] = useState([]); 10 | 11 | const fetchDrinks = async (searchTerm) => { 12 | setLoading(true); 13 | try { 14 | const response = await fetch(`${url}${searchTerm || 'a'}`); 15 | const data = await response.json(); 16 | const { drinks } = data; 17 | if (drinks) { 18 | const newCocktails = drinks.map((item) => { 19 | const { idDrink, strDrink, strDrinkThumb, strAlcoholic, strGlass } = 20 | item; 21 | 22 | return { 23 | id: idDrink, 24 | name: strDrink, 25 | image: strDrinkThumb, 26 | info: strAlcoholic, 27 | glass: strGlass, 28 | }; 29 | }); 30 | setCocktails(newCocktails); 31 | } else { 32 | setCocktails([]); 33 | } 34 | setLoading(false); 35 | } catch (error) { 36 | console.log(error); 37 | setLoading(false); 38 | } 39 | }; 40 | useEffect(() => { 41 | fetchDrinks(); 42 | }, []); 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | }; 49 | // make sure use 50 | export const useGlobalContext = () => { 51 | return useContext(AppContext); 52 | }; 53 | 54 | export { AppContext, AppProvider }; 55 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Course 2 | 3 | If you enjoy the content and my teaching style, you can always enroll in the full React course (link below) 4 | 5 | [My React Course](https://www.udemy.com/course/react-tutorial-and-projects-course/?referralCode=FEE6A921AF07E2563CEF) 6 | 7 | ## All My Courses 8 | 9 | [Project Based Web Dev Courses](https://www.johnsmilga.com/) 10 | 11 | 12 | 13 | ## Debounce in React 14 | 15 | ##### Debounce in Vanilla Javascript 16 | 17 | [Debounce in Vanilla Javascript](https://youtu.be/tYx6pXdvt1s) 18 | 19 | ##### useMemo 20 | 21 | [useMemo Hook](https://youtu.be/R49sY--qOqA) 22 | 23 | #### Initial Setup 24 | 25 | ```js 26 | import React, { useState } from 'react'; 27 | import { useGlobalContext } from '../context'; 28 | export default function SearchForm() { 29 | const [searchTerm, setSearchTerm] = useState(''); 30 | const { fetchDrinks } = useGlobalContext(); 31 | 32 | const handleSubmit = (e) => { 33 | e.preventDefault(); 34 | }; 35 | 36 | const searchCocktail = (e) => { 37 | const searchTerm = e.target.value; 38 | setSearchTerm(searchTerm); 39 | fetchDrinks(searchTerm); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 | 47 | 54 |
55 |
56 |
57 | ); 58 | } 59 | ``` 60 | 61 | #### Without Debounce 62 | 63 | ```js 64 | import React, { useState } from 'react'; 65 | import { useGlobalContext } from '../context'; 66 | export default function SearchForm() { 67 | const [searchTerm, setSearchTerm] = useState(''); 68 | const { fetchDrinks } = useGlobalContext(); 69 | 70 | const handleSubmit = (e) => { 71 | e.preventDefault(); 72 | }; 73 | 74 | const searchCocktail = () => { 75 | let timeoutId; 76 | return (e) => { 77 | const searchTerm = e.target.value; 78 | setSearchTerm(searchTerm); 79 | clearTimeout(timeoutId); 80 | timeoutId = setTimeout(() => { 81 | fetchDrinks(searchTerm); 82 | }, 1000); 83 | }; 84 | }; 85 | 86 | return ( 87 |
88 |
89 |
90 | 91 | 98 |
99 |
100 |
101 | ); 102 | } 103 | ``` 104 | 105 | #### Debounce With useMemo 106 | 107 | ```js 108 | import React, { useMemo, useState } from 'react'; 109 | import { useGlobalContext } from '../context'; 110 | export default function SearchForm() { 111 | const [searchTerm, setSearchTerm] = useState(''); 112 | const { fetchDrinks } = useGlobalContext(); 113 | 114 | const handleSubmit = (e) => { 115 | e.preventDefault(); 116 | }; 117 | 118 | const searchCocktail = () => { 119 | let timeoutId; 120 | return (e) => { 121 | const searchTerm = e.target.value; 122 | setSearchTerm(searchTerm); 123 | clearTimeout(timeoutId); 124 | timeoutId = setTimeout(() => { 125 | fetchDrinks(searchTerm); 126 | }, 1000); 127 | }; 128 | }; 129 | const debounceSearchCocktail = useMemo(() => searchCocktail(), []); 130 | return ( 131 |
132 |
133 |
134 | 135 | 142 |
143 |
144 |
145 | ); 146 | } 147 | ``` 148 | 149 | #### Debounce with useEffect 150 | 151 | ```js 152 | import React, { useEffect, useMemo, useState } from 'react'; 153 | import { useGlobalContext } from '../context'; 154 | export default function SearchForm() { 155 | const [searchTerm, setSearchTerm] = useState(''); 156 | const [timeoutId, setTimeoutId] = useState(null); 157 | 158 | const { fetchDrinks } = useGlobalContext(); 159 | 160 | const handleSubmit = (e) => { 161 | e.preventDefault(); 162 | }; 163 | 164 | const searchCocktail = (e) => { 165 | const searchTerm = e.target.value; 166 | setSearchTerm(searchTerm); 167 | clearTimeout(timeoutId); 168 | setTimeoutId( 169 | setTimeout(() => { 170 | // Call the API after the debounce timeout 171 | fetchDrinks(searchTerm); 172 | }, 1000) 173 | ); 174 | }; 175 | 176 | useEffect(() => { 177 | // Cleanup function to clear the timeout on unmount and re-render 178 | return () => { 179 | clearTimeout(timeoutId); 180 | }; 181 | }, [timeoutId]); 182 | 183 | return ( 184 |
185 |
186 |
187 | 188 | 195 |
196 |
197 |
198 | ); 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | ====== 3 | Variables 4 | ====== 5 | */ 6 | :root { 7 | --primaryLightColor: #d4e6a5; 8 | --primaryColor: #476a2e; 9 | --primaryDarkColor: #c02c03; 10 | --mainWhite: #fff; 11 | --offWhite: #f7f7f7; 12 | --mainBackground: #f1f5f8; 13 | --mainOverlay: rgba(35, 10, 36, 0.4); 14 | --mainBlack: #222; 15 | --mainGrey: #ececec; 16 | --darkGrey: #afafaf; 17 | --mainRed: #bd0303; 18 | --mainTransition: all 0.3s linear; 19 | --mainSpacing: 0.3rem; 20 | --lightShadow: 2px 5px 3px 0px rgba(0, 0, 0, 0.5); 21 | --darkShadow: 4px 10px 5px 0px rgba(0, 0, 0, 0.5); 22 | --mainBorderRadius: 0.25rem; 23 | --smallWidth: 85vw; 24 | --maxWidth: 40rem; 25 | --fullWidth: 1170px; 26 | } 27 | /* 28 | ====== 29 | Global Styles 30 | ====== 31 | */ 32 | * { 33 | margin: 0; 34 | padding: 0; 35 | box-sizing: border-box; 36 | } 37 | 38 | body { 39 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 40 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 41 | color: var(--mainBlack); 42 | background: var(--mainBackground); 43 | line-height: 1.4; 44 | font-size: 1rem; 45 | font-weight: 300; 46 | } 47 | h1, 48 | h2, 49 | h3, 50 | h4, 51 | h5, 52 | h6 { 53 | font-family: var(--slantedFont); 54 | margin-bottom: 1.25rem; 55 | letter-spacing: var(--mainSpacing); 56 | } 57 | p { 58 | margin-bottom: 1.25rem; 59 | } 60 | ul { 61 | list-style-type: none; 62 | } 63 | a { 64 | text-decoration: none; 65 | color: var(--mainBlack); 66 | } 67 | img { 68 | width: 100%; 69 | display: block; 70 | } 71 | 72 | /* 73 | ====== 74 | Buttons 75 | ====== 76 | */ 77 | .btn, 78 | .btn-white, 79 | .btn-primary { 80 | text-transform: uppercase; 81 | letter-spacing: var(--mainSpacing); 82 | color: var(--primaryColor); 83 | border: 2px solid var(--primaryColor); 84 | padding: 0.45rem 0.8rem; 85 | display: inline-block; 86 | transition: var(--mainTransition); 87 | cursor: pointer; 88 | font-size: 0.8rem; 89 | background: transparent; 90 | border-radius: var(--mainBorderRadius); 91 | display: inline-block; 92 | } 93 | .btn:hover { 94 | background: var(--primaryColor); 95 | color: var(--mainWhite); 96 | } 97 | .btn-white { 98 | background: transparent; 99 | color: var(--mainWhite); 100 | border-color: var(--mainWhite); 101 | } 102 | .btn-white:hover { 103 | background: var(--mainWhite); 104 | color: var(--primaryColor); 105 | } 106 | .btn-primary { 107 | background: var(--primaryColor); 108 | color: var(--mainWhite); 109 | border-color: transparent; 110 | } 111 | .btn-primary:hover { 112 | background: var(--primaryLightColor); 113 | color: var(--primaryColor); 114 | } 115 | .btn-block { 116 | width: 100%; 117 | display: block; 118 | margin: 0 auto; 119 | box-shadow: var(--lightShadow); 120 | text-align: center; 121 | } 122 | .btn-details { 123 | padding: 0.25rem 0.4rem; 124 | } 125 | .btn-details:hover { 126 | background: var(--primaryLightColor); 127 | border-color: var(--primaryLightColor); 128 | } 129 | /* 130 | ====== 131 | Navbar 132 | ====== 133 | */ 134 | .navbar { 135 | background: var(--mainWhite); 136 | height: 5rem; 137 | display: flex; 138 | align-items: center; 139 | border-bottom: 2px solid var(--primaryColor); 140 | box-shadow: var(--lightShadow); 141 | } 142 | .nav-center { 143 | display: flex; 144 | justify-content: space-between; 145 | align-items: center; 146 | width: var(--smallWidth); 147 | margin: 0 auto; 148 | max-width: var(--fullWidth); 149 | } 150 | .nav-links { 151 | display: flex; 152 | align-items: center; 153 | } 154 | .nav-links a { 155 | text-transform: capitalize; 156 | display: inline-block; 157 | font-weight: bold; 158 | margin-right: 0.5rem; 159 | font-weight: 400; 160 | letter-spacing: 2px; 161 | font-size: 1.2rem; 162 | padding: 0.25rem 0.5rem; 163 | transition: var(--mainTransition); 164 | } 165 | .nav-links a:hover { 166 | color: var(--primaryColor); 167 | } 168 | .logo { 169 | width: 12rem; 170 | } 171 | /* 172 | ====== 173 | About 174 | ====== 175 | */ 176 | .about-section { 177 | width: var(--smallWidth); 178 | max-width: var(--maxWidth); 179 | margin: 0 auto; 180 | } 181 | .about-section p { 182 | line-height: 2rem; 183 | font-weight: 400; 184 | letter-spacing: 2px; 185 | } 186 | /* 187 | ====== 188 | Error 189 | ====== 190 | */ 191 | .error-page { 192 | display: flex; 193 | justify-content: center; 194 | } 195 | .error-container { 196 | text-align: center; 197 | text-transform: capitalize; 198 | } 199 | /* 200 | ====== 201 | Cocktails List 202 | ====== 203 | */ 204 | 205 | .section { 206 | padding: 5rem 0; 207 | } 208 | .section-title { 209 | font-size: 2rem; 210 | text-transform: capitalize; 211 | letter-spacing: var(--mainSpacing); 212 | text-align: center; 213 | margin-bottom: 3.5rem; 214 | margin-top: 1rem; 215 | } 216 | .cocktails-center { 217 | width: var(--smallWidth); 218 | margin: 0 auto; 219 | max-width: var(--fullWidth); 220 | display: grid; 221 | row-gap: 2rem; 222 | column-gap: 2rem; 223 | /* align-items: start; */ 224 | } 225 | @media screen and (min-width: 576px) { 226 | .cocktails-center { 227 | grid-template-columns: repeat(auto-fill, minmax(338.8px, 1fr)); 228 | } 229 | } 230 | /* 231 | ====== 232 | Cocktail 233 | ====== 234 | */ 235 | 236 | .cocktail { 237 | background: var(--mainWhite); 238 | margin-bottom: 2rem; 239 | box-shadow: var(--lightShadow); 240 | transition: var(--mainTransition); 241 | display: grid; 242 | grid-template-rows: auto 1fr; 243 | border-radius: var(--mainBorderRadius); 244 | } 245 | .cocktail:hover { 246 | box-shadow: var(--darkShadow); 247 | } 248 | .cocktail img { 249 | height: 20rem; 250 | object-fit: cover; 251 | border-top-left-radius: var(--mainBorderRadius); 252 | border-top-right-radius: var(--mainBorderRadius); 253 | } 254 | .cocktail-footer { 255 | padding: 1.5rem; 256 | } 257 | .cocktail-footer h3, 258 | .cocktail-footer h4, 259 | .cocktail-footer p { 260 | margin-bottom: 0.3rem; 261 | } 262 | .cocktail-footer h3 { 263 | font-size: 2rem; 264 | } 265 | .cocktail-footer p { 266 | color: var(--darkGrey); 267 | margin-bottom: 0.5rem; 268 | } 269 | /* 270 | ====== 271 | Form 272 | ====== 273 | */ 274 | .search { 275 | margin-top: 1rem; 276 | padding-bottom: 0; 277 | } 278 | 279 | .search-form { 280 | width: var(--smallWidth); 281 | margin: 0 auto; 282 | max-width: var(--maxWidth); 283 | background: var(--mainWhite); 284 | padding: 2rem 2.5rem; 285 | text-transform: capitalize; 286 | border-radius: var(--mainBorderRadius); 287 | box-shadow: var(--lightShadow); 288 | } 289 | 290 | .form-control label { 291 | display: block; 292 | margin-bottom: 1.25rem; 293 | font-weight: bold; 294 | letter-spacing: 0.25rem; 295 | color: var(--primaryColor); 296 | } 297 | .form-control input { 298 | width: 100%; 299 | border: none; 300 | border-color: transparent; 301 | background: var(--mainBackground); 302 | border-radius: var(--mainBorderRadius); 303 | padding: 0.5rem; 304 | font-size: 1.2rem; 305 | } 306 | /* 307 | ====== 308 | Cocktail 309 | ====== 310 | */ 311 | 312 | .cocktail-section { 313 | text-align: center; 314 | } 315 | .drink { 316 | width: var(--smallWidth); 317 | max-width: var(--fullWidth); 318 | margin: 0 auto; 319 | text-align: left; 320 | } 321 | .drink img { 322 | border-radius: var(--mainBorderRadius); 323 | } 324 | .drink p { 325 | font-weight: bold; 326 | text-transform: capitalize; 327 | line-height: 1.8; 328 | } 329 | .drink span { 330 | margin-right: 0.5rem; 331 | } 332 | .drink-data { 333 | background: var(--primaryLightColor); 334 | padding: 0.25rem 0.5rem; 335 | border-radius: var(--mainBorderRadius); 336 | color: var(--primaryColor); 337 | } 338 | .drink-info { 339 | padding-top: 2rem; 340 | } 341 | @media screen and (min-width: 992px) { 342 | .drink { 343 | display: grid; 344 | grid-template-columns: 2fr 3fr; 345 | gap: 3rem; 346 | align-items: center; 347 | } 348 | .drink-info { 349 | padding-top: 0; 350 | } 351 | } 352 | .loader, 353 | .loader:before, 354 | .loader:after { 355 | background: transparent; 356 | -webkit-animation: load1 1s infinite ease-in-out; 357 | animation: load1 1s infinite ease-in-out; 358 | width: 1em; 359 | height: 4em; 360 | } 361 | .loader { 362 | color: var(--primaryColor); 363 | text-indent: -9999em; 364 | margin: 88px auto; 365 | margin-top: 20rem; 366 | position: relative; 367 | font-size: 3rem; 368 | -webkit-transform: translateZ(0); 369 | -ms-transform: translateZ(0); 370 | transform: translateZ(0); 371 | -webkit-animation-delay: -0.16s; 372 | animation-delay: -0.16s; 373 | } 374 | .loader:before, 375 | .loader:after { 376 | position: absolute; 377 | top: 0; 378 | content: ''; 379 | } 380 | .loader:before { 381 | left: -1.5em; 382 | -webkit-animation-delay: -0.32s; 383 | animation-delay: -0.32s; 384 | } 385 | .loader:after { 386 | left: 1.5em; 387 | } 388 | @-webkit-keyframes load1 { 389 | 0%, 390 | 80%, 391 | 100% { 392 | box-shadow: 0 0; 393 | height: 4em; 394 | } 395 | 40% { 396 | box-shadow: 0 -2em; 397 | height: 5em; 398 | } 399 | } 400 | @keyframes load1 { 401 | 0%, 402 | 80%, 403 | 100% { 404 | box-shadow: 0 0; 405 | height: 4em; 406 | } 407 | 40% { 408 | box-shadow: 0 -2em; 409 | height: 5em; 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------