├── 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 |

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 |
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 |
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 |
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 |
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 |
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 |
5 |
--------------------------------------------------------------------------------