├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── _redirects └── vite.svg ├── src ├── App.jsx ├── assets │ ├── not-found.svg │ ├── react.svg │ └── wrappers │ │ ├── AboutPage.js │ │ ├── CocktailCard.js │ │ ├── CocktailList.js │ │ ├── CocktailPage.js │ │ ├── ErrorPage.js │ │ ├── Navbar.js │ │ └── SearchForm.js ├── components │ ├── CocktailCard.jsx │ ├── CocktailList.jsx │ ├── Navbar.jsx │ └── SearchForm.jsx ├── index.css ├── main.jsx └── pages │ ├── About.jsx │ ├── Cocktail.jsx │ ├── Error.jsx │ ├── HomeLayout.jsx │ ├── Landing.jsx │ ├── Newsletter.jsx │ ├── SinglePageError.jsx │ └── index.js └── vite.config.js /.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 | .env 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 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Steps 2 | 3 | #### Install and Setup 4 | 5 | - npm install 6 | - npm run dev 7 | 8 | #### SPA 9 | 10 | SPA stands for Single-Page Application, which is a web application that dynamically updates its content without requiring a full page reload. It achieves this by loading the initial HTML, CSS, and JavaScript resources and then dynamically fetching data and updating the DOM as users interact with the application. 11 | 12 | React Router is a JavaScript library used in React applications to handle routing and navigation. It provides a declarative way to define the routes of an application and render different components based on the current URL. React Router allows developers to create a seamless, client-side navigation experience within a SPA by mapping URLs to specific components and managing the history and URL changes. 13 | 14 | [React Router](https://reactrouter.com/en/main) 15 | 16 | ```sh 17 | npm i react-router-dom@6.11.2 18 | ``` 19 | 20 | App.jsx 21 | 22 | ```js 23 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 24 | 25 | const router = createBrowserRouter([ 26 | { 27 | path: '/', 28 | element:

home page

, 29 | }, 30 | { 31 | path: '/about', 32 | element: ( 33 |
34 |

about page

35 |
36 | ), 37 | }, 38 | ]); 39 | const App = () => { 40 | return ; 41 | }; 42 | export default App; 43 | ``` 44 | 45 | #### Setup Pages 46 | 47 | - pages are components 48 | - create src/pages 49 | - About, Cocktail, Error, HomeLayout, Landing, Newsletter, index.js 50 | - export from index.js 51 | 52 | pages/index.js 53 | 54 | ```js 55 | export { default as Landing } from './Landing'; 56 | export { default as About } from './About'; 57 | export { default as Cocktail } from './Cocktail'; 58 | export { default as Newsletter } from './Newsletter'; 59 | export { default as HomeLayout } from './HomeLayout'; 60 | export { default as Error } from './Error'; 61 | ``` 62 | 63 | App.jsx 64 | 65 | ```js 66 | import { 67 | HomeLayout, 68 | About, 69 | Landing, 70 | Error, 71 | Newsletter, 72 | Cocktail, 73 | } from './pages'; 74 | ``` 75 | 76 | #### Link Component 77 | 78 | HomeLayout.jsx 79 | 80 | ```js 81 | import { Link } from 'react-router-dom'; 82 | const HomeLayout = () => { 83 | return ( 84 |
85 |

HomeLayout

86 | About 87 |
88 | ); 89 | }; 90 | export default HomeLayout; 91 | ``` 92 | 93 | About.jsx 94 | 95 | ```js 96 | import { Link } from 'react-router-dom'; 97 | 98 | const About = () => { 99 | return ( 100 |
101 |

About

102 | Back Home 103 |
104 | ); 105 | }; 106 | export default About; 107 | ``` 108 | 109 | #### Nested Pages 110 | 111 | App.jsx 112 | 113 | ```js 114 | const router = createBrowserRouter([ 115 | { 116 | path: '/', 117 | element: , 118 | children: [ 119 | { 120 | path: 'landing', 121 | element: , 122 | }, 123 | { 124 | path: 'cocktail', 125 | element: , 126 | }, 127 | { 128 | path: 'newsletter', 129 | element: , 130 | }, 131 | { 132 | path: 'about', 133 | element: , 134 | }, 135 | ], 136 | }, 137 | ]); 138 | ``` 139 | 140 | HomeLayout.jsx 141 | 142 | ```js 143 | import { Link, Outlet } from 'react-router-dom'; 144 | const HomeLayout = () => { 145 | return ( 146 |
147 | 148 | 149 |
150 | ); 151 | }; 152 | export default HomeLayout; 153 | ``` 154 | 155 | App.jsx 156 | 157 | ```js 158 | { 159 | index:true 160 | element: , 161 | } 162 | ``` 163 | 164 | #### Navbar 165 | 166 | - create components/Navbar.jsx 167 | 168 | Navbar.jsx 169 | 170 | ```js 171 | import { NavLink } from 'react-router-dom'; 172 | 173 | const Navbar = () => { 174 | return ( 175 | 191 | ); 192 | }; 193 | 194 | export default Navbar; 195 | ``` 196 | 197 | - setup in HomeLayout 198 | 199 | #### Styled Components 200 | 201 | - CSS in JS 202 | - Styled Components 203 | - have logic and styles in component 204 | - no name collisions 205 | - apply javascript logic 206 | - [Styled Components Docs](https://styled-components.com/) 207 | - [Styled Components Course](https://www.udemy.com/course/styled-components-tutorial-and-project-course/?referralCode=9DABB172FCB2625B663F) 208 | 209 | ```sh 210 | npm install styled-components 211 | ``` 212 | 213 | ```js 214 | import styled from 'styled-components'; 215 | 216 | const El = styled.el` 217 | // styles go here 218 | `; 219 | ``` 220 | 221 | - no name collisions, since unique class 222 | - vscode-styled-components extension 223 | - colors and bugs 224 | 225 | ```js 226 | import styled from 'styled-components'; 227 | const StyledBtn = styled.button` 228 | background: red; 229 | color: white; 230 | font-size: 2rem; 231 | padding: 1rem; 232 | `; 233 | ``` 234 | 235 | #### Alternative Setup 236 | 237 | - style entire react component 238 | 239 | ```js 240 | const Wrapper = styled.el``; 241 | 242 | const Component = () => { 243 | return ( 244 | 245 |

Component

246 |
247 | ); 248 | }; 249 | ``` 250 | 251 | - only responsible for styling 252 | 253 | #### Assets 254 | 255 | - wrappers folder in assets 256 | 257 | Navbar.jsx 258 | 259 | ```js 260 | import { NavLink } from 'react-router-dom'; 261 | import styled from 'styled-components'; 262 | 263 | const Navbar = () => { 264 | return ( 265 | 266 |
267 | MixMaster 268 |
269 | 270 | Home 271 | 272 | 273 | About 274 | 275 | 276 | Newsletter 277 | 278 |
279 |
280 |
281 | ); 282 | }; 283 | 284 | const Wrapper = styled.nav` 285 | background: var(--white); 286 | .nav-center { 287 | width: var(--view-width); 288 | max-width: var(--max-width); 289 | margin: 0 auto; 290 | display: flex; 291 | flex-direction: column; 292 | padding: 1.5rem 2rem; 293 | } 294 | 295 | .logo { 296 | font-size: clamp(1.5rem, 3vw, 3rem); 297 | color: var(--primary-500); 298 | font-weight: 700; 299 | letter-spacing: 2px; 300 | } 301 | .nav-links { 302 | display: flex; 303 | flex-direction: column; 304 | gap: 0.5rem; 305 | margin-top: 1rem; 306 | } 307 | .nav-link { 308 | color: var(--grey-900); 309 | padding: 0.5rem 0.5rem 0.5rem 0; 310 | transition: var(--transition); 311 | letter-spacing: 1px; 312 | } 313 | .nav-link:hover { 314 | color: var(--primary-500); 315 | } 316 | .active { 317 | color: var(--primary-500); 318 | } 319 | 320 | @media (min-width: 768px) { 321 | .nav-center { 322 | flex-direction: row; 323 | justify-content: space-between; 324 | align-items: center; 325 | } 326 | .nav-links { 327 | flex-direction: row; 328 | margin-top: 0; 329 | } 330 | } 331 | `; 332 | 333 | export default Navbar; 334 | ``` 335 | 336 | #### About Page 337 | 338 | About.jsx 339 | 340 | ```jsx 341 | import Wrapper from '../assets/wrappers/AboutPage'; 342 | 343 | const About = () => { 344 | return ( 345 | 346 |

About Us

347 |

348 | Introducing "MixMaster," the ultimate party sidekick app that fetches 349 | cocktails from the hilarious Cocktails DB API. With a flick of your 350 | finger, you'll unlock a treasure trove of enchanting drink recipes 351 | that'll make your taste buds dance and your friends jump with joy. Get 352 | ready to shake up your mixology game, one fantastical mocktail at a 353 | time, and let the laughter and giggles flow! 354 |

355 |
356 | ); 357 | }; 358 | 359 | export default About; 360 | ``` 361 | 362 | #### Page CSS 363 | 364 | HomeLayout.jsx 365 | 366 | ```js 367 | import { Link, Outlet } from 'react-router-dom'; 368 | import Navbar from '../components/Navbar'; 369 | const HomeLayout = () => { 370 | return ( 371 | <> 372 | 373 |
374 | 375 |
376 | 377 | ); 378 | }; 379 | export default HomeLayout; 380 | ``` 381 | 382 | index.css 383 | 384 | ```css 385 | .page { 386 | width: var(--view-width); 387 | max-width: var(--max-width); 388 | margin: 0 auto; 389 | padding: 5rem 2rem; 390 | } 391 | ``` 392 | 393 | #### Error Page 394 | 395 | - wrong url 396 | 397 | Error.jsx 398 | 399 | ```js 400 | import Wrapper from '../assets/wrappers/ErrorPage'; 401 | import { Link, useRouteError } from 'react-router-dom'; 402 | import img from '../assets/not-found.svg'; 403 | 404 | const Error = () => { 405 | const error = useRouteError(); 406 | console.log(error); 407 | if (error.status === 404) { 408 | return ( 409 | 410 |
411 | not found 412 |

Ohh!

413 |

We can't seem to find the page you're looking for

414 | back home 415 |
416 |
417 | ); 418 | } 419 | return ( 420 | 421 |
422 |

something went wrong

423 |
424 |
425 | ); 426 | }; 427 | 428 | export default Error; 429 | ``` 430 | 431 | #### Error Page - CSS (optional) 432 | 433 | assets/wrappers/ErrorPage.js 434 | 435 | ```js 436 | import styled from 'styled-components'; 437 | 438 | const Wrapper = styled.div` 439 | min-height: 100vh; 440 | text-align: center; 441 | display: flex; 442 | align-items: center; 443 | justify-content: center; 444 | img { 445 | width: 90vw; 446 | max-width: 600px; 447 | display: block; 448 | margin-bottom: 2rem; 449 | margin-top: -3rem; 450 | } 451 | h3 { 452 | margin-bottom: 0.5rem; 453 | } 454 | 455 | p { 456 | line-height: 1.5; 457 | margin-top: 0.5rem; 458 | margin-bottom: 1rem; 459 | color: var(--grey-500); 460 | } 461 | a { 462 | color: var(--primary-500); 463 | text-transform: capitalize; 464 | } 465 | `; 466 | 467 | export default Wrapper; 468 | ``` 469 | 470 | #### Fetch 471 | 472 | - useEffect approach 473 | 474 | Landing.jsx 475 | 476 | ```js 477 | const fetchSomething = async () => { 478 | try { 479 | const response = await axios.get('/someUrl'); 480 | console.log(response.data); 481 | } catch (error) { 482 | console.error(error); 483 | } 484 | }; 485 | 486 | useEffect(() => { 487 | fetchSomething(); 488 | }, []); 489 | ``` 490 | 491 | #### Loader 492 | 493 | Each route can define a "loader" function to provide data to the route element before it renders. 494 | 495 | - must return something even "null" otherwise error 496 | 497 | Landing.jsx 498 | 499 | ```js 500 | import { useLoaderData } from 'react-router-dom'; 501 | 502 | export const loader = async () => { 503 | return 'something'; 504 | }; 505 | 506 | const Landing = () => { 507 | const data = useLoaderData(); 508 | console.log(data); 509 | return

Landing

; 510 | }; 511 | export default Landing; 512 | ``` 513 | 514 | ```js 515 | import { loader as landingLoader } from './pages/Landing.jsx'; 516 | 517 | const router = createBrowserRouter([ 518 | { 519 | path: '/', 520 | element: , 521 | errorElement: 522 | children: [ 523 | { 524 | index: true, 525 | loader: landingLoader, 526 | element: , 527 | }, 528 | // alternative approach 529 | { 530 | index: true, 531 | loader: () => { 532 | // do stuff here 533 | }, 534 | element: , 535 | 536 | }, 537 | // rest of the routes 538 | ], 539 | }, 540 | ]); 541 | ``` 542 | 543 | #### TheCocktailDB 544 | 545 | [API](https://www.thecocktaildb.com/) 546 | 547 | - Search cocktail by name 548 | www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita 549 | - Lookup full cocktail details by id 550 | www.thecocktaildb.com/api/json/v1/1/lookup.php?i=11007 551 | 552 | #### Landing - Fetch Drinks 553 | 554 | Landing.jsx 555 | 556 | ```js 557 | import { useLoaderData } from 'react-router-dom'; 558 | import axios from 'axios'; 559 | 560 | const cocktailSearchUrl = 561 | 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s='; 562 | 563 | export const loader = async () => { 564 | const searchTerm = 'margarita'; 565 | const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 566 | return { drinks: response.data.drinks, searchTerm }; 567 | }; 568 | 569 | const Landing = () => { 570 | const { searchTerm, drinks } = useLoaderData(); 571 | console.log(drinks); 572 | return

Landing page

; 573 | }; 574 | 575 | export default Landing; 576 | ``` 577 | 578 | - empty search term returns some default drinks 579 | - if search term yields not drinks drinks:null 580 | 581 | #### More Errors 582 | 583 | - bubbles up 584 | - no return from loader 585 | - wrong url 586 | 587 | App.jsx 588 | 589 | ```js 590 | const router = createBrowserRouter([ 591 | { 592 | path: '/', 593 | element: , 594 | errorElement: , 595 | children: [ 596 | { 597 | index: true, 598 | loader: landingLoader, 599 | errorElement:

There was an error...

, 600 | element: , 601 | }, 602 | ], 603 | }, 604 | ]); 605 | ``` 606 | 607 | #### SinglePageError Component 608 | 609 | - create pages/SinglePageError.jsx 610 | - export import (index.js) 611 | - use it in App.jsx 612 | 613 | ```js 614 | import { useRouteError } from 'react-router-dom'; 615 | const SinglePageError = () => { 616 | const error = useRouteError(); 617 | console.log(error); 618 | return

{error.message}

; 619 | }; 620 | export default SinglePageError; 621 | ``` 622 | 623 | #### More Components 624 | 625 | - in src/components create SearchForm, CocktailList, CocktailCard 626 | - render SearchForm and CocktailList in Landing 627 | - pass drinks, iterate over and render in CocktailCard 628 | 629 | Landing.jsx 630 | 631 | ```js 632 | const Landing = () => { 633 | const { searchTerm, drinks } = useLoaderData(); 634 | 635 | return ( 636 | <> 637 | 638 | 639 | 640 | ); 641 | }; 642 | ``` 643 | 644 | CocktailList.jsx 645 | 646 | ```jsx 647 | import CocktailCard from './CocktailCard'; 648 | import Wrapper from '../assets/wrappers/CocktailList'; 649 | const CocktailList = ({ drinks }) => { 650 | if (!drinks) { 651 | return ( 652 |

No matching cocktails found...

653 | ); 654 | } 655 | 656 | const formattedDrinks = drinks.map((item) => { 657 | const { idDrink, strDrink, strDrinkThumb, strAlcoholic, strGlass } = item; 658 | return { 659 | id: idDrink, 660 | name: strDrink, 661 | image: strDrinkThumb, 662 | info: strAlcoholic, 663 | glass: strGlass, 664 | }; 665 | }); 666 | return ( 667 | 668 | {formattedDrinks.map((item) => { 669 | return ; 670 | })} 671 | 672 | ); 673 | }; 674 | 675 | export default CocktailList; 676 | ``` 677 | 678 | ```jsx 679 | import { Link, useOutletContext } from 'react-router-dom'; 680 | import Wrapper from '../assets/wrappers/CocktailCard'; 681 | const CocktailCard = ({ image, name, id, info, glass }) => { 682 | // const data = useOutletContext(); 683 | // console.log(data); 684 | return ( 685 | 686 |
687 | {name} 688 |
689 |
690 |

{name}

691 |
{glass}
692 |

{info}

693 | 694 | 695 | details 696 | 697 |
698 |
699 | ); 700 | }; 701 | 702 | export default CocktailCard; 703 | ``` 704 | 705 | #### CocktailList and CocktailCard CSS (optional) 706 | 707 | #### Global Loading and Context 708 | 709 | HomeLayout.jsx 710 | 711 | ```js 712 | import { Outlet } from 'react-router-dom'; 713 | import Navbar from '../components/Navbar'; 714 | import { useNavigation } from 'react-router-dom'; 715 | const HomeLayout = () => { 716 | const navigation = useNavigation(); 717 | const isPageLoading = navigation.state === 'loading'; 718 | const value = 'some value'; 719 | return ( 720 | <> 721 | 722 |
723 | {isPageLoading ? ( 724 |
725 | ) : ( 726 | 727 | )} 728 |
729 | 730 | ); 731 | }; 732 | export default HomeLayout; 733 | ``` 734 | 735 | #### Single Cocktail 736 | 737 | App.jsx 738 | 739 | ```js 740 | import { loader as singleCocktailLoader } from './pages/Cocktail'; 741 | 742 | const router = createBrowserRouter([ 743 | { 744 | path: '/', 745 | element: , 746 | errorElement: , 747 | children: [ 748 | { 749 | path: 'cocktail/:id', 750 | loader: singleCocktailLoader, 751 | element: , 752 | errorElement: , 753 | }, 754 | // rest of the routes 755 | ], 756 | }, 757 | ]); 758 | ``` 759 | 760 | Cocktail.jsx 761 | 762 | ```js 763 | const singleCocktailUrl = 764 | 'https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i='; 765 | import { useLoaderData, Link } from 'react-router-dom'; 766 | import axios from 'axios'; 767 | 768 | import Wrapper from '../assets/wrappers/CocktailPage'; 769 | 770 | export const loader = async ({ params }) => { 771 | const { id } = params; 772 | const { data } = await axios.get(`${singleCocktailUrl}${id}`); 773 | return { id, data }; 774 | }; 775 | 776 | const Cocktail = () => { 777 | const { id, data } = useLoaderData(); 778 | 779 | const singleDrink = data.drinks[0]; 780 | const { 781 | strDrink: name, 782 | strDrinkThumb: image, 783 | strAlcoholic: info, 784 | strCategory: category, 785 | strGlass: glass, 786 | strInstructions: instructions, 787 | } = singleDrink; 788 | const validIngredients = Object.keys(singleDrink) 789 | .filter( 790 | (key) => key.startsWith('strIngredient') && singleDrink[key] !== null 791 | ) 792 | .map((key) => singleDrink[key]); 793 | 794 | return ( 795 | 796 |
797 | 798 | back home 799 | 800 |

{name}

801 |
802 |
803 | {name} 804 |
805 |

806 | name : {name} 807 |

808 |

809 | category : {category} 810 |

811 |

812 | info : {info} 813 |

814 |

815 | glass : {glass} 816 |

817 |

818 | ingredients : 819 | {validIngredients.map((item, index) => { 820 | return ( 821 | 822 | {item} {index < validIngredients.length - 1 ? ',' : ''} 823 | 824 | ); 825 | })} 826 |

827 |

828 | instructons : {instructions} 829 |

830 |
831 |
832 |
833 | ); 834 | }; 835 | 836 | export default Cocktail; 837 | ``` 838 | 839 | #### Additional Check 840 | 841 | ```js 842 | const Cocktail = () => { 843 | import { Navigate } from 'react-router-dom'; 844 | const { id, data } = useLoaderData(); 845 | // if (!data) return

something went wrong...

; 846 | if (!data) return ; 847 | return ....; 848 | }; 849 | ``` 850 | 851 | #### Single Cocktail CSS (optional) 852 | 853 | assets/wrappers/CocktailPage.js 854 | 855 | ```js 856 | import styled from 'styled-components'; 857 | 858 | const Wrapper = styled.div` 859 | header { 860 | text-align: center; 861 | margin-bottom: 3rem; 862 | .btn { 863 | margin-bottom: 1rem; 864 | } 865 | } 866 | 867 | .img { 868 | border-radius: var(--borderRadius); 869 | } 870 | .drink-info { 871 | padding-top: 2rem; 872 | } 873 | 874 | .drink p { 875 | font-weight: 700; 876 | text-transform: capitalize; 877 | line-height: 2; 878 | margin-bottom: 1rem; 879 | } 880 | .drink-data { 881 | margin-right: 0.5rem; 882 | background: var(--primary-300); 883 | padding: 0.25rem 0.5rem; 884 | border-radius: var(--borderRadius); 885 | color: var(--primary-700); 886 | letter-spacing: var(--letterSpacing); 887 | } 888 | 889 | .ing { 890 | display: inline-block; 891 | margin-right: 0.5rem; 892 | } 893 | @media screen and (min-width: 992px) { 894 | .drink { 895 | display: grid; 896 | grid-template-columns: 2fr 3fr; 897 | gap: 3rem; 898 | align-items: center; 899 | } 900 | .drink-info { 901 | padding-top: 0; 902 | } 903 | } 904 | `; 905 | 906 | export default Wrapper; 907 | ``` 908 | 909 | #### Setup React Toastify 910 | 911 | main.jsx 912 | 913 | ```js 914 | import 'react-toastify/dist/ReactToastify.css'; 915 | import { ToastContainer } from 'react-toastify'; 916 | 917 | ReactDOM.createRoot(document.getElementById('root')).render( 918 | 919 | 920 | 921 | 922 | ); 923 | ``` 924 | 925 | #### Newsletter 926 | 927 | Newsletter.jsx 928 | 929 | ```js 930 | const Newsletter = () => { 931 | return ( 932 |
933 |

934 | our newsletter 935 |

936 | {/* name */} 937 |
938 | 941 | 948 |
949 | {/* last name */} 950 |
951 | 954 | 961 |
962 | {/* name */} 963 |
964 | 967 | 974 |
975 | 982 |
983 | ); 984 | }; 985 | 986 | export default Newsletter; 987 | ``` 988 | 989 | #### Default Behavior 990 | 991 | The "method" attribute in an HTML form specifies the HTTP method to be used when submitting the form data to the server. The two commonly used values for the "method" attribute are: 992 | 993 | GET: This is the default method if the "method" attribute is not specified. When the form is submitted with the GET method, the form data is appended to the URL as a query string. The data becomes visible in the URL, which can be bookmarked and shared. GET requests are generally used for retrieving data from the server and should not have any side effects on the server. 994 | 995 | POST: When the form is submitted with the POST method, the form data is included in the request payload rather than being appended to the URL. POST requests are typically used when submitting sensitive or large amounts of data to the server, as the data is not directly visible in the URL. POST requests can have side effects on the server, such as updating or inserting data. 996 | 997 | - action attribute 998 | 999 | The "action" attribute in an HTML form specifies the URL or destination where the form data should be sent when the form is submitted. It defines the server-side script or endpoint that will receive and process the form data. 1000 | 1001 | If the action attribute is not provided in the HTML form, the browser will send the form data to the current URL, which means it will submit the form to the same page that the form is on. This behavior is referred to as a "self-submitting" form. 1002 | 1003 | #### FormData API 1004 | 1005 | - covered in React fundamentals 1006 | [JS Nuggets - FormData API](https://youtu.be/5-x4OUM-SP8) 1007 | 1008 | - a great solution when you have bunch of inputs 1009 | - inputs must have name attribute 1010 | 1011 | The FormData interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the fetch() or XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". 1012 | 1013 | #### React Router - Action 1014 | 1015 | Route actions are the "writes" to route loader "reads". They provide a way for apps to perform data mutations with simple HTML and HTTP semantics while React Router abstracts away the complexity of asynchronous UI and revalidation. This gives you the simple mental model of HTML + HTTP (where the browser handles the asynchrony and revalidation) with the behavior and UX capabilities of modern SPAs. 1016 | 1017 | Newsletter.jsx 1018 | 1019 | ```js 1020 | import { Form } from 'react-router-dom'; 1021 | 1022 | export const action = async ({ request }) => { 1023 | const formData = await request.formData(); 1024 | const data = Object.fromEntries(formData); 1025 | console.log(data); 1026 | return 'something'; 1027 | }; 1028 | 1029 | const Newsletter = () => { 1030 | return ( 1031 |
1032 | .....) 1033 | } 1034 | ``` 1035 | 1036 | App.jsx 1037 | 1038 | ```js 1039 | import { action as newsletterAction } from './pages/Newsletter'; 1040 | const router = createBrowserRouter([ 1041 | { 1042 | path: '/', 1043 | element: , 1044 | errorElement: , 1045 | children: [ 1046 | { 1047 | path: 'newsletter', 1048 | action: newsletterAction, 1049 | element: , 1050 | }, 1051 | ], 1052 | }, 1053 | ]); 1054 | ``` 1055 | 1056 | #### Newsletter Request 1057 | 1058 | const newsletterUrl = 'https://www.course-api.com/cocktails-newsletter'; 1059 | 1060 | Newsletter.jsx 1061 | 1062 | ```js 1063 | import { Form, redirect } from 'react-router-dom'; 1064 | import axios from 'axios'; 1065 | import { toast } from 'react-toastify'; 1066 | 1067 | const newsletterUrl = 'https://www.course-api.com/cocktails-newsletter'; 1068 | 1069 | export const action = async ({ request }) => { 1070 | const formData = await request.formData(); 1071 | const data = Object.fromEntries(formData); 1072 | 1073 | const response = await axios.post(newsletterUrl, data); 1074 | console.log(response); 1075 | return response; 1076 | }; 1077 | ``` 1078 | 1079 | #### Try/Catch 1080 | 1081 | Newsletter.jsx 1082 | 1083 | ```js 1084 | import { redirect } from 'react-router-dom'; 1085 | import { toast } from 'react-toastify'; 1086 | 1087 | export const action = async ({ request }) => { 1088 | const formData = await request.formData(); 1089 | const data = Object.fromEntries(formData); 1090 | try { 1091 | const response = await axios.post(newsletterUrl, data); 1092 | console.log(response); 1093 | toast.success(response.data.msg); 1094 | return redirect('/'); 1095 | } catch (error) { 1096 | console.log(error); 1097 | toast.error(error?.response?.data?.msg); 1098 | return error; 1099 | } 1100 | }; 1101 | ``` 1102 | 1103 | #### Submit State 1104 | 1105 | Newsletter.jsx 1106 | 1107 | ```js 1108 | import { Form, useNavigation } from 'react-router-dom'; 1109 | 1110 | const Newsletter = () => { 1111 | const navigation = useNavigation(); 1112 | const isSubmitting = navigation.state === 'submitting'; 1113 | return ( 1114 | 1115 | .... 1116 | 1124 | 1125 | ); 1126 | }; 1127 | ``` 1128 | 1129 | #### Attributes 1130 | 1131 | - remove defaultValue and add required 1132 | - cover required and defaultValue 1133 | 1134 | #### Search Form - Setup 1135 | 1136 | components/SearchForm.jsx 1137 | 1138 | ```js 1139 | import { Form, useNavigation } from 'react-router-dom'; 1140 | import Wrapper from '../assets/wrappers/SearchForm'; 1141 | const SearchForm = () => { 1142 | const navigation = useNavigation(); 1143 | const isSubmitting = navigation.state === 'submitting'; 1144 | return ( 1145 | 1146 |
1147 | 1153 | 1156 |
1157 |
1158 | ); 1159 | }; 1160 | 1161 | export default SearchForm; 1162 | ``` 1163 | 1164 | #### Query Params 1165 | 1166 | Landing.jsx 1167 | 1168 | ```js 1169 | export const loader = async ({ request }) => { 1170 | const url = new URL(request.url); 1171 | const searchTerm = url.searchParams.get('search') || ''; 1172 | const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 1173 | return { drinks: response.data.drinks, searchTerm }; 1174 | }; 1175 | ``` 1176 | 1177 | const url = new URL(request.url); 1178 | This line of code creates a new URL object using the URL constructor. The URL object represents a URL and provides methods and properties for working with URLs. In this case, the request.url is passed as an argument to the URL constructor to create a new URL object called url. 1179 | 1180 | The request.url is an input parameter representing the URL of an incoming HTTP request. By creating a URL object from the provided URL, you can easily extract specific components and perform operations on it. 1181 | 1182 | const searchTerm = url.searchParams.get('search') || ''; 1183 | This line of code retrieves the value of the search parameter from the query string of the URL. The searchParams property of the URL object provides a URLSearchParams object, which allows you to access and manipulate the query parameters of the URL. 1184 | 1185 | The get() method of the URLSearchParams object retrieves the value of a specific parameter by passing its name as an argument. In this case, 'search' is passed as the parameter name. If the search parameter exists in the URL's query string, its value will be assigned to the searchTerm variable. If the search parameter is not present or its value is empty, the expression '' (an empty string) is assigned to searchTerm using the logical OR operator (||). 1186 | 1187 | #### Controlled Input (kinda/sorta) 1188 | 1189 | Landing.js 1190 | 1191 | ```js 1192 | const Landing = () => { 1193 | const { searchTerm, drinks } = useLoaderData(); 1194 | 1195 | return ( 1196 | <> 1197 | 1198 | 1199 | 1200 | ); 1201 | }; 1202 | ``` 1203 | 1204 | SearchForm.jsx 1205 | 1206 | ```js 1207 | const SearchForm = ({ searchTerm }) => { 1208 | return ( 1209 | 1210 |
1211 | 1217 | ..... 1218 |
1219 |
1220 | ); 1221 | }; 1222 | 1223 | export default SearchForm; 1224 | ``` 1225 | 1226 | #### Search Form CSS (optional) 1227 | 1228 | assets/wrappers/SearchForm.js 1229 | 1230 | ```js 1231 | import styled from 'styled-components'; 1232 | 1233 | const Wrapper = styled.div` 1234 | margin-bottom: 6rem; 1235 | .form { 1236 | display: grid; 1237 | grid-template-columns: 1fr auto; 1238 | } 1239 | .form-input { 1240 | border-top-right-radius: 0; 1241 | border-bottom-right-radius: 0; 1242 | } 1243 | .btn { 1244 | border-top-left-radius: 0; 1245 | border-bottom-left-radius: 0; 1246 | } 1247 | `; 1248 | 1249 | export default Wrapper; 1250 | ``` 1251 | 1252 | #### React Query - Setup 1253 | 1254 | App.jsx 1255 | 1256 | ```js 1257 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 1258 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 1259 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 1260 | 1261 | const queryClient = new QueryClient({ 1262 | defaultOptions: { 1263 | queries: { 1264 | staleTime: 1000 * 60 * 5, 1265 | }, 1266 | }, 1267 | }); 1268 | ... 1269 | const App = () => { 1270 | return ( 1271 | 1272 | 1273 | 1274 | 1275 | ); 1276 | }; 1277 | export default App; 1278 | 1279 | ``` 1280 | 1281 | #### React Query - Landing Page 1282 | 1283 | Landing.jsx 1284 | 1285 | ```js 1286 | import { useQuery } from '@tanstack/react-query'; 1287 | 1288 | const searchCocktailsQuery = (searchTerm) => { 1289 | return { 1290 | queryKey: ['search', searchTerm || 'all'], 1291 | queryFn: async () => { 1292 | const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 1293 | return response.data.drinks; 1294 | }, 1295 | }; 1296 | }; 1297 | 1298 | export const loader = async ({ request }) => { 1299 | const url = new URL(request.url); 1300 | const searchTerm = url.searchParams.get('search') || ''; 1301 | // const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 1302 | return { searchTerm }; 1303 | }; 1304 | 1305 | const Landing = () => { 1306 | const { searchTerm } = useLoaderData(); 1307 | const { data: drinks } = useQuery(searchCocktailsQuery(searchTerm)); 1308 | return ( 1309 | <> 1310 | 1311 | 1312 | 1313 | ); 1314 | }; 1315 | 1316 | export default Landing; 1317 | ``` 1318 | 1319 | #### React Query - Landing Page Loader 1320 | 1321 | App.jsx 1322 | 1323 | ```js 1324 | const router = createBrowserRouter([ 1325 | { 1326 | path: '/', 1327 | element: , 1328 | errorElement: , 1329 | children: [ 1330 | { 1331 | index: true, 1332 | loader: landingLoader(queryClient), 1333 | element: , 1334 | }, 1335 | ], 1336 | }, 1337 | ]); 1338 | ``` 1339 | 1340 | Landing.jsx 1341 | 1342 | ```js 1343 | export const loader = 1344 | (queryClient) => 1345 | async ({ request }) => { 1346 | const url = new URL(request.url); 1347 | const searchTerm = url.searchParams.get('search') || ''; 1348 | await queryClient.ensureQueryData(searchCocktailsQuery(searchTerm)); 1349 | // const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 1350 | return { searchTerm }; 1351 | }; 1352 | ``` 1353 | 1354 | #### React Query - Cocktail 1355 | 1356 | App.jsx 1357 | 1358 | ```js 1359 | const router = createBrowserRouter([ 1360 | { 1361 | path: '/', 1362 | element: , 1363 | errorElement: , 1364 | children: [ 1365 | .... 1366 | { 1367 | path: 'cocktail/:id', 1368 | loader: singleCocktailLoader(queryClient), 1369 | errorElement:

There was an error...

, 1370 | element: , 1371 | }, 1372 | .... 1373 | ], 1374 | }, 1375 | ]); 1376 | ``` 1377 | 1378 | Cocktail.jsx 1379 | 1380 | ```js 1381 | import { useQuery } from '@tanstack/react-query'; 1382 | import Wrapper from '../assets/wrappers/CocktailPage'; 1383 | import { useLoaderData, Link } from 'react-router-dom'; 1384 | import axios from 'axios'; 1385 | 1386 | const singleCocktailUrl = 1387 | 'https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i='; 1388 | 1389 | const singleCocktailQuery = (id) => { 1390 | return { 1391 | queryKey: ['cocktail', id], 1392 | queryFn: async () => { 1393 | const { data } = await axios.get(`${singleCocktailUrl}${id}`); 1394 | return data; 1395 | }, 1396 | }; 1397 | }; 1398 | 1399 | export const loader = 1400 | (queryClient) => 1401 | async ({ params }) => { 1402 | const { id } = params; 1403 | await queryClient.ensureQueryData(singleCocktailQuery(id)); 1404 | return { id }; 1405 | }; 1406 | 1407 | const Cocktail = () => { 1408 | const { id } = useLoaderData(); 1409 | const { data } = useQuery(singleCocktailQuery(id)); 1410 | // rest of the code 1411 | }; 1412 | ``` 1413 | 1414 | #### Redirects 1415 | 1416 | - in public folder create "\_redirects" 1417 | 1418 | ``` 1419 | /* /index.html 200 1420 | ``` 1421 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MixMaster Starter 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "contentful", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "@tanstack/react-query": "^4.29.7", 12 | "@tanstack/react-query-devtools": "^4.29.7", 13 | "axios": "^1.4.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.11.2", 17 | "react-toastify": "^9.1.3", 18 | "styled-components": "^5.3.10" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.28", 22 | "@types/react-dom": "^18.0.11", 23 | "@vitejs/plugin-react": "^3.1.0", 24 | "vite": "^4.2.0" 25 | } 26 | }, 27 | "node_modules/@ampproject/remapping": { 28 | "version": "2.2.1", 29 | "dev": true, 30 | "license": "Apache-2.0", 31 | "dependencies": { 32 | "@jridgewell/gen-mapping": "^0.3.0", 33 | "@jridgewell/trace-mapping": "^0.3.9" 34 | }, 35 | "engines": { 36 | "node": ">=6.0.0" 37 | } 38 | }, 39 | "node_modules/@babel/code-frame": { 40 | "version": "7.21.4", 41 | "license": "MIT", 42 | "dependencies": { 43 | "@babel/highlight": "^7.18.6" 44 | }, 45 | "engines": { 46 | "node": ">=6.9.0" 47 | } 48 | }, 49 | "node_modules/@babel/compat-data": { 50 | "version": "7.21.9", 51 | "dev": true, 52 | "license": "MIT", 53 | "engines": { 54 | "node": ">=6.9.0" 55 | } 56 | }, 57 | "node_modules/@babel/core": { 58 | "version": "7.21.8", 59 | "dev": true, 60 | "license": "MIT", 61 | "dependencies": { 62 | "@ampproject/remapping": "^2.2.0", 63 | "@babel/code-frame": "^7.21.4", 64 | "@babel/generator": "^7.21.5", 65 | "@babel/helper-compilation-targets": "^7.21.5", 66 | "@babel/helper-module-transforms": "^7.21.5", 67 | "@babel/helpers": "^7.21.5", 68 | "@babel/parser": "^7.21.8", 69 | "@babel/template": "^7.20.7", 70 | "@babel/traverse": "^7.21.5", 71 | "@babel/types": "^7.21.5", 72 | "convert-source-map": "^1.7.0", 73 | "debug": "^4.1.0", 74 | "gensync": "^1.0.0-beta.2", 75 | "json5": "^2.2.2", 76 | "semver": "^6.3.0" 77 | }, 78 | "engines": { 79 | "node": ">=6.9.0" 80 | }, 81 | "funding": { 82 | "type": "opencollective", 83 | "url": "https://opencollective.com/babel" 84 | } 85 | }, 86 | "node_modules/@babel/generator": { 87 | "version": "7.21.9", 88 | "license": "MIT", 89 | "dependencies": { 90 | "@babel/types": "^7.21.5", 91 | "@jridgewell/gen-mapping": "^0.3.2", 92 | "@jridgewell/trace-mapping": "^0.3.17", 93 | "jsesc": "^2.5.1" 94 | }, 95 | "engines": { 96 | "node": ">=6.9.0" 97 | } 98 | }, 99 | "node_modules/@babel/helper-annotate-as-pure": { 100 | "version": "7.18.6", 101 | "license": "MIT", 102 | "dependencies": { 103 | "@babel/types": "^7.18.6" 104 | }, 105 | "engines": { 106 | "node": ">=6.9.0" 107 | } 108 | }, 109 | "node_modules/@babel/helper-compilation-targets": { 110 | "version": "7.21.5", 111 | "dev": true, 112 | "license": "MIT", 113 | "dependencies": { 114 | "@babel/compat-data": "^7.21.5", 115 | "@babel/helper-validator-option": "^7.21.0", 116 | "browserslist": "^4.21.3", 117 | "lru-cache": "^5.1.1", 118 | "semver": "^6.3.0" 119 | }, 120 | "engines": { 121 | "node": ">=6.9.0" 122 | }, 123 | "peerDependencies": { 124 | "@babel/core": "^7.0.0" 125 | } 126 | }, 127 | "node_modules/@babel/helper-environment-visitor": { 128 | "version": "7.21.5", 129 | "license": "MIT", 130 | "engines": { 131 | "node": ">=6.9.0" 132 | } 133 | }, 134 | "node_modules/@babel/helper-function-name": { 135 | "version": "7.21.0", 136 | "license": "MIT", 137 | "dependencies": { 138 | "@babel/template": "^7.20.7", 139 | "@babel/types": "^7.21.0" 140 | }, 141 | "engines": { 142 | "node": ">=6.9.0" 143 | } 144 | }, 145 | "node_modules/@babel/helper-hoist-variables": { 146 | "version": "7.18.6", 147 | "license": "MIT", 148 | "dependencies": { 149 | "@babel/types": "^7.18.6" 150 | }, 151 | "engines": { 152 | "node": ">=6.9.0" 153 | } 154 | }, 155 | "node_modules/@babel/helper-module-imports": { 156 | "version": "7.21.4", 157 | "license": "MIT", 158 | "dependencies": { 159 | "@babel/types": "^7.21.4" 160 | }, 161 | "engines": { 162 | "node": ">=6.9.0" 163 | } 164 | }, 165 | "node_modules/@babel/helper-module-transforms": { 166 | "version": "7.21.5", 167 | "dev": true, 168 | "license": "MIT", 169 | "dependencies": { 170 | "@babel/helper-environment-visitor": "^7.21.5", 171 | "@babel/helper-module-imports": "^7.21.4", 172 | "@babel/helper-simple-access": "^7.21.5", 173 | "@babel/helper-split-export-declaration": "^7.18.6", 174 | "@babel/helper-validator-identifier": "^7.19.1", 175 | "@babel/template": "^7.20.7", 176 | "@babel/traverse": "^7.21.5", 177 | "@babel/types": "^7.21.5" 178 | }, 179 | "engines": { 180 | "node": ">=6.9.0" 181 | } 182 | }, 183 | "node_modules/@babel/helper-plugin-utils": { 184 | "version": "7.21.5", 185 | "dev": true, 186 | "license": "MIT", 187 | "engines": { 188 | "node": ">=6.9.0" 189 | } 190 | }, 191 | "node_modules/@babel/helper-simple-access": { 192 | "version": "7.21.5", 193 | "dev": true, 194 | "license": "MIT", 195 | "dependencies": { 196 | "@babel/types": "^7.21.5" 197 | }, 198 | "engines": { 199 | "node": ">=6.9.0" 200 | } 201 | }, 202 | "node_modules/@babel/helper-split-export-declaration": { 203 | "version": "7.18.6", 204 | "license": "MIT", 205 | "dependencies": { 206 | "@babel/types": "^7.18.6" 207 | }, 208 | "engines": { 209 | "node": ">=6.9.0" 210 | } 211 | }, 212 | "node_modules/@babel/helper-string-parser": { 213 | "version": "7.21.5", 214 | "license": "MIT", 215 | "engines": { 216 | "node": ">=6.9.0" 217 | } 218 | }, 219 | "node_modules/@babel/helper-validator-identifier": { 220 | "version": "7.19.1", 221 | "license": "MIT", 222 | "engines": { 223 | "node": ">=6.9.0" 224 | } 225 | }, 226 | "node_modules/@babel/helper-validator-option": { 227 | "version": "7.21.0", 228 | "dev": true, 229 | "license": "MIT", 230 | "engines": { 231 | "node": ">=6.9.0" 232 | } 233 | }, 234 | "node_modules/@babel/helpers": { 235 | "version": "7.21.5", 236 | "dev": true, 237 | "license": "MIT", 238 | "dependencies": { 239 | "@babel/template": "^7.20.7", 240 | "@babel/traverse": "^7.21.5", 241 | "@babel/types": "^7.21.5" 242 | }, 243 | "engines": { 244 | "node": ">=6.9.0" 245 | } 246 | }, 247 | "node_modules/@babel/highlight": { 248 | "version": "7.18.6", 249 | "license": "MIT", 250 | "dependencies": { 251 | "@babel/helper-validator-identifier": "^7.18.6", 252 | "chalk": "^2.0.0", 253 | "js-tokens": "^4.0.0" 254 | }, 255 | "engines": { 256 | "node": ">=6.9.0" 257 | } 258 | }, 259 | "node_modules/@babel/parser": { 260 | "version": "7.21.9", 261 | "license": "MIT", 262 | "bin": { 263 | "parser": "bin/babel-parser.js" 264 | }, 265 | "engines": { 266 | "node": ">=6.0.0" 267 | } 268 | }, 269 | "node_modules/@babel/plugin-transform-react-jsx-self": { 270 | "version": "7.21.0", 271 | "dev": true, 272 | "license": "MIT", 273 | "dependencies": { 274 | "@babel/helper-plugin-utils": "^7.20.2" 275 | }, 276 | "engines": { 277 | "node": ">=6.9.0" 278 | }, 279 | "peerDependencies": { 280 | "@babel/core": "^7.0.0-0" 281 | } 282 | }, 283 | "node_modules/@babel/plugin-transform-react-jsx-source": { 284 | "version": "7.19.6", 285 | "dev": true, 286 | "license": "MIT", 287 | "dependencies": { 288 | "@babel/helper-plugin-utils": "^7.19.0" 289 | }, 290 | "engines": { 291 | "node": ">=6.9.0" 292 | }, 293 | "peerDependencies": { 294 | "@babel/core": "^7.0.0-0" 295 | } 296 | }, 297 | "node_modules/@babel/template": { 298 | "version": "7.21.9", 299 | "license": "MIT", 300 | "dependencies": { 301 | "@babel/code-frame": "^7.21.4", 302 | "@babel/parser": "^7.21.9", 303 | "@babel/types": "^7.21.5" 304 | }, 305 | "engines": { 306 | "node": ">=6.9.0" 307 | } 308 | }, 309 | "node_modules/@babel/traverse": { 310 | "version": "7.21.5", 311 | "license": "MIT", 312 | "dependencies": { 313 | "@babel/code-frame": "^7.21.4", 314 | "@babel/generator": "^7.21.5", 315 | "@babel/helper-environment-visitor": "^7.21.5", 316 | "@babel/helper-function-name": "^7.21.0", 317 | "@babel/helper-hoist-variables": "^7.18.6", 318 | "@babel/helper-split-export-declaration": "^7.18.6", 319 | "@babel/parser": "^7.21.5", 320 | "@babel/types": "^7.21.5", 321 | "debug": "^4.1.0", 322 | "globals": "^11.1.0" 323 | }, 324 | "engines": { 325 | "node": ">=6.9.0" 326 | } 327 | }, 328 | "node_modules/@babel/types": { 329 | "version": "7.21.5", 330 | "license": "MIT", 331 | "dependencies": { 332 | "@babel/helper-string-parser": "^7.21.5", 333 | "@babel/helper-validator-identifier": "^7.19.1", 334 | "to-fast-properties": "^2.0.0" 335 | }, 336 | "engines": { 337 | "node": ">=6.9.0" 338 | } 339 | }, 340 | "node_modules/@emotion/is-prop-valid": { 341 | "version": "1.2.1", 342 | "license": "MIT", 343 | "dependencies": { 344 | "@emotion/memoize": "^0.8.1" 345 | } 346 | }, 347 | "node_modules/@emotion/memoize": { 348 | "version": "0.8.1", 349 | "license": "MIT" 350 | }, 351 | "node_modules/@emotion/stylis": { 352 | "version": "0.8.5", 353 | "license": "MIT" 354 | }, 355 | "node_modules/@emotion/unitless": { 356 | "version": "0.7.5", 357 | "license": "MIT" 358 | }, 359 | "node_modules/@esbuild/darwin-x64": { 360 | "version": "0.17.19", 361 | "cpu": [ 362 | "x64" 363 | ], 364 | "dev": true, 365 | "license": "MIT", 366 | "optional": true, 367 | "os": [ 368 | "darwin" 369 | ], 370 | "engines": { 371 | "node": ">=12" 372 | } 373 | }, 374 | "node_modules/@jridgewell/gen-mapping": { 375 | "version": "0.3.3", 376 | "license": "MIT", 377 | "dependencies": { 378 | "@jridgewell/set-array": "^1.0.1", 379 | "@jridgewell/sourcemap-codec": "^1.4.10", 380 | "@jridgewell/trace-mapping": "^0.3.9" 381 | }, 382 | "engines": { 383 | "node": ">=6.0.0" 384 | } 385 | }, 386 | "node_modules/@jridgewell/resolve-uri": { 387 | "version": "3.1.0", 388 | "license": "MIT", 389 | "engines": { 390 | "node": ">=6.0.0" 391 | } 392 | }, 393 | "node_modules/@jridgewell/set-array": { 394 | "version": "1.1.2", 395 | "license": "MIT", 396 | "engines": { 397 | "node": ">=6.0.0" 398 | } 399 | }, 400 | "node_modules/@jridgewell/sourcemap-codec": { 401 | "version": "1.4.15", 402 | "license": "MIT" 403 | }, 404 | "node_modules/@jridgewell/trace-mapping": { 405 | "version": "0.3.18", 406 | "license": "MIT", 407 | "dependencies": { 408 | "@jridgewell/resolve-uri": "3.1.0", 409 | "@jridgewell/sourcemap-codec": "1.4.14" 410 | } 411 | }, 412 | "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { 413 | "version": "1.4.14", 414 | "license": "MIT" 415 | }, 416 | "node_modules/@remix-run/router": { 417 | "version": "1.6.2", 418 | "license": "MIT", 419 | "engines": { 420 | "node": ">=14" 421 | } 422 | }, 423 | "node_modules/@tanstack/match-sorter-utils": { 424 | "version": "8.8.4", 425 | "license": "MIT", 426 | "dependencies": { 427 | "remove-accents": "0.4.2" 428 | }, 429 | "engines": { 430 | "node": ">=12" 431 | }, 432 | "funding": { 433 | "type": "github", 434 | "url": "https://github.com/sponsors/kentcdodds" 435 | } 436 | }, 437 | "node_modules/@tanstack/query-core": { 438 | "version": "4.29.7", 439 | "license": "MIT", 440 | "funding": { 441 | "type": "github", 442 | "url": "https://github.com/sponsors/tannerlinsley" 443 | } 444 | }, 445 | "node_modules/@tanstack/react-query": { 446 | "version": "4.29.7", 447 | "license": "MIT", 448 | "dependencies": { 449 | "@tanstack/query-core": "4.29.7", 450 | "use-sync-external-store": "^1.2.0" 451 | }, 452 | "funding": { 453 | "type": "github", 454 | "url": "https://github.com/sponsors/tannerlinsley" 455 | }, 456 | "peerDependencies": { 457 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 458 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", 459 | "react-native": "*" 460 | }, 461 | "peerDependenciesMeta": { 462 | "react-dom": { 463 | "optional": true 464 | }, 465 | "react-native": { 466 | "optional": true 467 | } 468 | } 469 | }, 470 | "node_modules/@tanstack/react-query-devtools": { 471 | "version": "4.29.7", 472 | "license": "MIT", 473 | "dependencies": { 474 | "@tanstack/match-sorter-utils": "^8.7.0", 475 | "superjson": "^1.10.0", 476 | "use-sync-external-store": "^1.2.0" 477 | }, 478 | "funding": { 479 | "type": "github", 480 | "url": "https://github.com/sponsors/tannerlinsley" 481 | }, 482 | "peerDependencies": { 483 | "@tanstack/react-query": "4.29.7", 484 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 485 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" 486 | } 487 | }, 488 | "node_modules/@types/prop-types": { 489 | "version": "15.7.5", 490 | "dev": true, 491 | "license": "MIT" 492 | }, 493 | "node_modules/@types/react": { 494 | "version": "18.2.6", 495 | "dev": true, 496 | "license": "MIT", 497 | "dependencies": { 498 | "@types/prop-types": "*", 499 | "@types/scheduler": "*", 500 | "csstype": "^3.0.2" 501 | } 502 | }, 503 | "node_modules/@types/react-dom": { 504 | "version": "18.2.4", 505 | "dev": true, 506 | "license": "MIT", 507 | "dependencies": { 508 | "@types/react": "*" 509 | } 510 | }, 511 | "node_modules/@types/scheduler": { 512 | "version": "0.16.3", 513 | "dev": true, 514 | "license": "MIT" 515 | }, 516 | "node_modules/@vitejs/plugin-react": { 517 | "version": "3.1.0", 518 | "dev": true, 519 | "license": "MIT", 520 | "dependencies": { 521 | "@babel/core": "^7.20.12", 522 | "@babel/plugin-transform-react-jsx-self": "^7.18.6", 523 | "@babel/plugin-transform-react-jsx-source": "^7.19.6", 524 | "magic-string": "^0.27.0", 525 | "react-refresh": "^0.14.0" 526 | }, 527 | "engines": { 528 | "node": "^14.18.0 || >=16.0.0" 529 | }, 530 | "peerDependencies": { 531 | "vite": "^4.1.0-beta.0" 532 | } 533 | }, 534 | "node_modules/ansi-styles": { 535 | "version": "3.2.1", 536 | "license": "MIT", 537 | "dependencies": { 538 | "color-convert": "^1.9.0" 539 | }, 540 | "engines": { 541 | "node": ">=4" 542 | } 543 | }, 544 | "node_modules/asynckit": { 545 | "version": "0.4.0", 546 | "license": "MIT" 547 | }, 548 | "node_modules/axios": { 549 | "version": "1.4.0", 550 | "license": "MIT", 551 | "dependencies": { 552 | "follow-redirects": "^1.15.0", 553 | "form-data": "^4.0.0", 554 | "proxy-from-env": "^1.1.0" 555 | } 556 | }, 557 | "node_modules/babel-plugin-styled-components": { 558 | "version": "2.1.3", 559 | "license": "MIT", 560 | "dependencies": { 561 | "@babel/helper-annotate-as-pure": "^7.18.6", 562 | "@babel/helper-module-imports": "^7.21.4", 563 | "babel-plugin-syntax-jsx": "^6.18.0", 564 | "lodash": "^4.17.21", 565 | "picomatch": "^2.3.1" 566 | }, 567 | "peerDependencies": { 568 | "styled-components": ">= 2" 569 | } 570 | }, 571 | "node_modules/babel-plugin-syntax-jsx": { 572 | "version": "6.18.0", 573 | "license": "MIT" 574 | }, 575 | "node_modules/browserslist": { 576 | "version": "4.21.5", 577 | "dev": true, 578 | "funding": [ 579 | { 580 | "type": "opencollective", 581 | "url": "https://opencollective.com/browserslist" 582 | }, 583 | { 584 | "type": "tidelift", 585 | "url": "https://tidelift.com/funding/github/npm/browserslist" 586 | } 587 | ], 588 | "license": "MIT", 589 | "dependencies": { 590 | "caniuse-lite": "^1.0.30001449", 591 | "electron-to-chromium": "^1.4.284", 592 | "node-releases": "^2.0.8", 593 | "update-browserslist-db": "^1.0.10" 594 | }, 595 | "bin": { 596 | "browserslist": "cli.js" 597 | }, 598 | "engines": { 599 | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 600 | } 601 | }, 602 | "node_modules/camelize": { 603 | "version": "1.0.1", 604 | "license": "MIT", 605 | "funding": { 606 | "url": "https://github.com/sponsors/ljharb" 607 | } 608 | }, 609 | "node_modules/caniuse-lite": { 610 | "version": "1.0.30001489", 611 | "dev": true, 612 | "funding": [ 613 | { 614 | "type": "opencollective", 615 | "url": "https://opencollective.com/browserslist" 616 | }, 617 | { 618 | "type": "tidelift", 619 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 620 | }, 621 | { 622 | "type": "github", 623 | "url": "https://github.com/sponsors/ai" 624 | } 625 | ], 626 | "license": "CC-BY-4.0" 627 | }, 628 | "node_modules/chalk": { 629 | "version": "2.4.2", 630 | "license": "MIT", 631 | "dependencies": { 632 | "ansi-styles": "^3.2.1", 633 | "escape-string-regexp": "^1.0.5", 634 | "supports-color": "^5.3.0" 635 | }, 636 | "engines": { 637 | "node": ">=4" 638 | } 639 | }, 640 | "node_modules/clsx": { 641 | "version": "1.2.1", 642 | "license": "MIT", 643 | "engines": { 644 | "node": ">=6" 645 | } 646 | }, 647 | "node_modules/color-convert": { 648 | "version": "1.9.3", 649 | "license": "MIT", 650 | "dependencies": { 651 | "color-name": "1.1.3" 652 | } 653 | }, 654 | "node_modules/color-name": { 655 | "version": "1.1.3", 656 | "license": "MIT" 657 | }, 658 | "node_modules/combined-stream": { 659 | "version": "1.0.8", 660 | "license": "MIT", 661 | "dependencies": { 662 | "delayed-stream": "~1.0.0" 663 | }, 664 | "engines": { 665 | "node": ">= 0.8" 666 | } 667 | }, 668 | "node_modules/convert-source-map": { 669 | "version": "1.9.0", 670 | "dev": true, 671 | "license": "MIT" 672 | }, 673 | "node_modules/copy-anything": { 674 | "version": "3.0.5", 675 | "license": "MIT", 676 | "dependencies": { 677 | "is-what": "^4.1.8" 678 | }, 679 | "engines": { 680 | "node": ">=12.13" 681 | }, 682 | "funding": { 683 | "url": "https://github.com/sponsors/mesqueeb" 684 | } 685 | }, 686 | "node_modules/css-color-keywords": { 687 | "version": "1.0.0", 688 | "license": "ISC", 689 | "engines": { 690 | "node": ">=4" 691 | } 692 | }, 693 | "node_modules/css-to-react-native": { 694 | "version": "3.2.0", 695 | "license": "MIT", 696 | "dependencies": { 697 | "camelize": "^1.0.0", 698 | "css-color-keywords": "^1.0.0", 699 | "postcss-value-parser": "^4.0.2" 700 | } 701 | }, 702 | "node_modules/csstype": { 703 | "version": "3.1.2", 704 | "dev": true, 705 | "license": "MIT" 706 | }, 707 | "node_modules/debug": { 708 | "version": "4.3.4", 709 | "license": "MIT", 710 | "dependencies": { 711 | "ms": "2.1.2" 712 | }, 713 | "engines": { 714 | "node": ">=6.0" 715 | }, 716 | "peerDependenciesMeta": { 717 | "supports-color": { 718 | "optional": true 719 | } 720 | } 721 | }, 722 | "node_modules/delayed-stream": { 723 | "version": "1.0.0", 724 | "license": "MIT", 725 | "engines": { 726 | "node": ">=0.4.0" 727 | } 728 | }, 729 | "node_modules/electron-to-chromium": { 730 | "version": "1.4.405", 731 | "dev": true, 732 | "license": "ISC" 733 | }, 734 | "node_modules/esbuild": { 735 | "version": "0.17.19", 736 | "dev": true, 737 | "hasInstallScript": true, 738 | "license": "MIT", 739 | "bin": { 740 | "esbuild": "bin/esbuild" 741 | }, 742 | "engines": { 743 | "node": ">=12" 744 | }, 745 | "optionalDependencies": { 746 | "@esbuild/android-arm": "0.17.19", 747 | "@esbuild/android-arm64": "0.17.19", 748 | "@esbuild/android-x64": "0.17.19", 749 | "@esbuild/darwin-arm64": "0.17.19", 750 | "@esbuild/darwin-x64": "0.17.19", 751 | "@esbuild/freebsd-arm64": "0.17.19", 752 | "@esbuild/freebsd-x64": "0.17.19", 753 | "@esbuild/linux-arm": "0.17.19", 754 | "@esbuild/linux-arm64": "0.17.19", 755 | "@esbuild/linux-ia32": "0.17.19", 756 | "@esbuild/linux-loong64": "0.17.19", 757 | "@esbuild/linux-mips64el": "0.17.19", 758 | "@esbuild/linux-ppc64": "0.17.19", 759 | "@esbuild/linux-riscv64": "0.17.19", 760 | "@esbuild/linux-s390x": "0.17.19", 761 | "@esbuild/linux-x64": "0.17.19", 762 | "@esbuild/netbsd-x64": "0.17.19", 763 | "@esbuild/openbsd-x64": "0.17.19", 764 | "@esbuild/sunos-x64": "0.17.19", 765 | "@esbuild/win32-arm64": "0.17.19", 766 | "@esbuild/win32-ia32": "0.17.19", 767 | "@esbuild/win32-x64": "0.17.19" 768 | } 769 | }, 770 | "node_modules/escalade": { 771 | "version": "3.1.1", 772 | "dev": true, 773 | "license": "MIT", 774 | "engines": { 775 | "node": ">=6" 776 | } 777 | }, 778 | "node_modules/escape-string-regexp": { 779 | "version": "1.0.5", 780 | "license": "MIT", 781 | "engines": { 782 | "node": ">=0.8.0" 783 | } 784 | }, 785 | "node_modules/follow-redirects": { 786 | "version": "1.15.2", 787 | "funding": [ 788 | { 789 | "type": "individual", 790 | "url": "https://github.com/sponsors/RubenVerborgh" 791 | } 792 | ], 793 | "license": "MIT", 794 | "engines": { 795 | "node": ">=4.0" 796 | }, 797 | "peerDependenciesMeta": { 798 | "debug": { 799 | "optional": true 800 | } 801 | } 802 | }, 803 | "node_modules/form-data": { 804 | "version": "4.0.0", 805 | "license": "MIT", 806 | "dependencies": { 807 | "asynckit": "^0.4.0", 808 | "combined-stream": "^1.0.8", 809 | "mime-types": "^2.1.12" 810 | }, 811 | "engines": { 812 | "node": ">= 6" 813 | } 814 | }, 815 | "node_modules/fsevents": { 816 | "version": "2.3.2", 817 | "dev": true, 818 | "license": "MIT", 819 | "optional": true, 820 | "os": [ 821 | "darwin" 822 | ], 823 | "engines": { 824 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 825 | } 826 | }, 827 | "node_modules/gensync": { 828 | "version": "1.0.0-beta.2", 829 | "dev": true, 830 | "license": "MIT", 831 | "engines": { 832 | "node": ">=6.9.0" 833 | } 834 | }, 835 | "node_modules/globals": { 836 | "version": "11.12.0", 837 | "license": "MIT", 838 | "engines": { 839 | "node": ">=4" 840 | } 841 | }, 842 | "node_modules/has-flag": { 843 | "version": "3.0.0", 844 | "license": "MIT", 845 | "engines": { 846 | "node": ">=4" 847 | } 848 | }, 849 | "node_modules/hoist-non-react-statics": { 850 | "version": "3.3.2", 851 | "license": "BSD-3-Clause", 852 | "dependencies": { 853 | "react-is": "^16.7.0" 854 | } 855 | }, 856 | "node_modules/hoist-non-react-statics/node_modules/react-is": { 857 | "version": "16.13.1", 858 | "license": "MIT" 859 | }, 860 | "node_modules/is-what": { 861 | "version": "4.1.11", 862 | "license": "MIT", 863 | "engines": { 864 | "node": ">=12.13" 865 | }, 866 | "funding": { 867 | "url": "https://github.com/sponsors/mesqueeb" 868 | } 869 | }, 870 | "node_modules/js-tokens": { 871 | "version": "4.0.0", 872 | "license": "MIT" 873 | }, 874 | "node_modules/jsesc": { 875 | "version": "2.5.2", 876 | "license": "MIT", 877 | "bin": { 878 | "jsesc": "bin/jsesc" 879 | }, 880 | "engines": { 881 | "node": ">=4" 882 | } 883 | }, 884 | "node_modules/json5": { 885 | "version": "2.2.3", 886 | "dev": true, 887 | "license": "MIT", 888 | "bin": { 889 | "json5": "lib/cli.js" 890 | }, 891 | "engines": { 892 | "node": ">=6" 893 | } 894 | }, 895 | "node_modules/lodash": { 896 | "version": "4.17.21", 897 | "license": "MIT" 898 | }, 899 | "node_modules/loose-envify": { 900 | "version": "1.4.0", 901 | "license": "MIT", 902 | "dependencies": { 903 | "js-tokens": "^3.0.0 || ^4.0.0" 904 | }, 905 | "bin": { 906 | "loose-envify": "cli.js" 907 | } 908 | }, 909 | "node_modules/lru-cache": { 910 | "version": "5.1.1", 911 | "dev": true, 912 | "license": "ISC", 913 | "dependencies": { 914 | "yallist": "^3.0.2" 915 | } 916 | }, 917 | "node_modules/magic-string": { 918 | "version": "0.27.0", 919 | "dev": true, 920 | "license": "MIT", 921 | "dependencies": { 922 | "@jridgewell/sourcemap-codec": "^1.4.13" 923 | }, 924 | "engines": { 925 | "node": ">=12" 926 | } 927 | }, 928 | "node_modules/mime-db": { 929 | "version": "1.52.0", 930 | "license": "MIT", 931 | "engines": { 932 | "node": ">= 0.6" 933 | } 934 | }, 935 | "node_modules/mime-types": { 936 | "version": "2.1.35", 937 | "license": "MIT", 938 | "dependencies": { 939 | "mime-db": "1.52.0" 940 | }, 941 | "engines": { 942 | "node": ">= 0.6" 943 | } 944 | }, 945 | "node_modules/ms": { 946 | "version": "2.1.2", 947 | "license": "MIT" 948 | }, 949 | "node_modules/nanoid": { 950 | "version": "3.3.6", 951 | "dev": true, 952 | "funding": [ 953 | { 954 | "type": "github", 955 | "url": "https://github.com/sponsors/ai" 956 | } 957 | ], 958 | "license": "MIT", 959 | "bin": { 960 | "nanoid": "bin/nanoid.cjs" 961 | }, 962 | "engines": { 963 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 964 | } 965 | }, 966 | "node_modules/node-releases": { 967 | "version": "2.0.11", 968 | "dev": true, 969 | "license": "MIT" 970 | }, 971 | "node_modules/picocolors": { 972 | "version": "1.0.0", 973 | "dev": true, 974 | "license": "ISC" 975 | }, 976 | "node_modules/picomatch": { 977 | "version": "2.3.1", 978 | "license": "MIT", 979 | "engines": { 980 | "node": ">=8.6" 981 | }, 982 | "funding": { 983 | "url": "https://github.com/sponsors/jonschlinkert" 984 | } 985 | }, 986 | "node_modules/postcss": { 987 | "version": "8.4.23", 988 | "dev": true, 989 | "funding": [ 990 | { 991 | "type": "opencollective", 992 | "url": "https://opencollective.com/postcss/" 993 | }, 994 | { 995 | "type": "tidelift", 996 | "url": "https://tidelift.com/funding/github/npm/postcss" 997 | }, 998 | { 999 | "type": "github", 1000 | "url": "https://github.com/sponsors/ai" 1001 | } 1002 | ], 1003 | "license": "MIT", 1004 | "dependencies": { 1005 | "nanoid": "^3.3.6", 1006 | "picocolors": "^1.0.0", 1007 | "source-map-js": "^1.0.2" 1008 | }, 1009 | "engines": { 1010 | "node": "^10 || ^12 || >=14" 1011 | } 1012 | }, 1013 | "node_modules/postcss-value-parser": { 1014 | "version": "4.2.0", 1015 | "license": "MIT" 1016 | }, 1017 | "node_modules/proxy-from-env": { 1018 | "version": "1.1.0", 1019 | "license": "MIT" 1020 | }, 1021 | "node_modules/react": { 1022 | "version": "18.2.0", 1023 | "license": "MIT", 1024 | "dependencies": { 1025 | "loose-envify": "^1.1.0" 1026 | }, 1027 | "engines": { 1028 | "node": ">=0.10.0" 1029 | } 1030 | }, 1031 | "node_modules/react-dom": { 1032 | "version": "18.2.0", 1033 | "license": "MIT", 1034 | "dependencies": { 1035 | "loose-envify": "^1.1.0", 1036 | "scheduler": "^0.23.0" 1037 | }, 1038 | "peerDependencies": { 1039 | "react": "^18.2.0" 1040 | } 1041 | }, 1042 | "node_modules/react-is": { 1043 | "version": "18.2.0", 1044 | "license": "MIT", 1045 | "peer": true 1046 | }, 1047 | "node_modules/react-refresh": { 1048 | "version": "0.14.0", 1049 | "dev": true, 1050 | "license": "MIT", 1051 | "engines": { 1052 | "node": ">=0.10.0" 1053 | } 1054 | }, 1055 | "node_modules/react-router": { 1056 | "version": "6.11.2", 1057 | "license": "MIT", 1058 | "dependencies": { 1059 | "@remix-run/router": "1.6.2" 1060 | }, 1061 | "engines": { 1062 | "node": ">=14" 1063 | }, 1064 | "peerDependencies": { 1065 | "react": ">=16.8" 1066 | } 1067 | }, 1068 | "node_modules/react-router-dom": { 1069 | "version": "6.11.2", 1070 | "license": "MIT", 1071 | "dependencies": { 1072 | "@remix-run/router": "1.6.2", 1073 | "react-router": "6.11.2" 1074 | }, 1075 | "engines": { 1076 | "node": ">=14" 1077 | }, 1078 | "peerDependencies": { 1079 | "react": ">=16.8", 1080 | "react-dom": ">=16.8" 1081 | } 1082 | }, 1083 | "node_modules/react-toastify": { 1084 | "version": "9.1.3", 1085 | "license": "MIT", 1086 | "dependencies": { 1087 | "clsx": "^1.1.1" 1088 | }, 1089 | "peerDependencies": { 1090 | "react": ">=16", 1091 | "react-dom": ">=16" 1092 | } 1093 | }, 1094 | "node_modules/remove-accents": { 1095 | "version": "0.4.2", 1096 | "license": "MIT" 1097 | }, 1098 | "node_modules/rollup": { 1099 | "version": "3.23.0", 1100 | "dev": true, 1101 | "license": "MIT", 1102 | "bin": { 1103 | "rollup": "dist/bin/rollup" 1104 | }, 1105 | "engines": { 1106 | "node": ">=14.18.0", 1107 | "npm": ">=8.0.0" 1108 | }, 1109 | "optionalDependencies": { 1110 | "fsevents": "~2.3.2" 1111 | } 1112 | }, 1113 | "node_modules/scheduler": { 1114 | "version": "0.23.0", 1115 | "license": "MIT", 1116 | "dependencies": { 1117 | "loose-envify": "^1.1.0" 1118 | } 1119 | }, 1120 | "node_modules/semver": { 1121 | "version": "6.3.0", 1122 | "dev": true, 1123 | "license": "ISC", 1124 | "bin": { 1125 | "semver": "bin/semver.js" 1126 | } 1127 | }, 1128 | "node_modules/shallowequal": { 1129 | "version": "1.1.0", 1130 | "license": "MIT" 1131 | }, 1132 | "node_modules/source-map-js": { 1133 | "version": "1.0.2", 1134 | "dev": true, 1135 | "license": "BSD-3-Clause", 1136 | "engines": { 1137 | "node": ">=0.10.0" 1138 | } 1139 | }, 1140 | "node_modules/styled-components": { 1141 | "version": "5.3.10", 1142 | "license": "MIT", 1143 | "dependencies": { 1144 | "@babel/helper-module-imports": "^7.0.0", 1145 | "@babel/traverse": "^7.4.5", 1146 | "@emotion/is-prop-valid": "^1.1.0", 1147 | "@emotion/stylis": "^0.8.4", 1148 | "@emotion/unitless": "^0.7.4", 1149 | "babel-plugin-styled-components": ">= 1.12.0", 1150 | "css-to-react-native": "^3.0.0", 1151 | "hoist-non-react-statics": "^3.0.0", 1152 | "shallowequal": "^1.1.0", 1153 | "supports-color": "^5.5.0" 1154 | }, 1155 | "engines": { 1156 | "node": ">=10" 1157 | }, 1158 | "funding": { 1159 | "type": "opencollective", 1160 | "url": "https://opencollective.com/styled-components" 1161 | }, 1162 | "peerDependencies": { 1163 | "react": ">= 16.8.0", 1164 | "react-dom": ">= 16.8.0", 1165 | "react-is": ">= 16.8.0" 1166 | } 1167 | }, 1168 | "node_modules/superjson": { 1169 | "version": "1.12.3", 1170 | "license": "MIT", 1171 | "dependencies": { 1172 | "copy-anything": "^3.0.2" 1173 | }, 1174 | "engines": { 1175 | "node": ">=10" 1176 | } 1177 | }, 1178 | "node_modules/supports-color": { 1179 | "version": "5.5.0", 1180 | "license": "MIT", 1181 | "dependencies": { 1182 | "has-flag": "^3.0.0" 1183 | }, 1184 | "engines": { 1185 | "node": ">=4" 1186 | } 1187 | }, 1188 | "node_modules/to-fast-properties": { 1189 | "version": "2.0.0", 1190 | "license": "MIT", 1191 | "engines": { 1192 | "node": ">=4" 1193 | } 1194 | }, 1195 | "node_modules/update-browserslist-db": { 1196 | "version": "1.0.11", 1197 | "dev": true, 1198 | "funding": [ 1199 | { 1200 | "type": "opencollective", 1201 | "url": "https://opencollective.com/browserslist" 1202 | }, 1203 | { 1204 | "type": "tidelift", 1205 | "url": "https://tidelift.com/funding/github/npm/browserslist" 1206 | }, 1207 | { 1208 | "type": "github", 1209 | "url": "https://github.com/sponsors/ai" 1210 | } 1211 | ], 1212 | "license": "MIT", 1213 | "dependencies": { 1214 | "escalade": "^3.1.1", 1215 | "picocolors": "^1.0.0" 1216 | }, 1217 | "bin": { 1218 | "update-browserslist-db": "cli.js" 1219 | }, 1220 | "peerDependencies": { 1221 | "browserslist": ">= 4.21.0" 1222 | } 1223 | }, 1224 | "node_modules/use-sync-external-store": { 1225 | "version": "1.2.0", 1226 | "license": "MIT", 1227 | "peerDependencies": { 1228 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 1229 | } 1230 | }, 1231 | "node_modules/vite": { 1232 | "version": "4.3.8", 1233 | "dev": true, 1234 | "license": "MIT", 1235 | "dependencies": { 1236 | "esbuild": "^0.17.5", 1237 | "postcss": "^8.4.23", 1238 | "rollup": "^3.21.0" 1239 | }, 1240 | "bin": { 1241 | "vite": "bin/vite.js" 1242 | }, 1243 | "engines": { 1244 | "node": "^14.18.0 || >=16.0.0" 1245 | }, 1246 | "optionalDependencies": { 1247 | "fsevents": "~2.3.2" 1248 | }, 1249 | "peerDependencies": { 1250 | "@types/node": ">= 14", 1251 | "less": "*", 1252 | "sass": "*", 1253 | "stylus": "*", 1254 | "sugarss": "*", 1255 | "terser": "^5.4.0" 1256 | }, 1257 | "peerDependenciesMeta": { 1258 | "@types/node": { 1259 | "optional": true 1260 | }, 1261 | "less": { 1262 | "optional": true 1263 | }, 1264 | "sass": { 1265 | "optional": true 1266 | }, 1267 | "stylus": { 1268 | "optional": true 1269 | }, 1270 | "sugarss": { 1271 | "optional": true 1272 | }, 1273 | "terser": { 1274 | "optional": true 1275 | } 1276 | } 1277 | }, 1278 | "node_modules/yallist": { 1279 | "version": "3.1.1", 1280 | "dev": true, 1281 | "license": "ISC" 1282 | } 1283 | }, 1284 | "dependencies": { 1285 | "@ampproject/remapping": { 1286 | "version": "2.2.1", 1287 | "dev": true, 1288 | "requires": { 1289 | "@jridgewell/gen-mapping": "^0.3.0", 1290 | "@jridgewell/trace-mapping": "^0.3.9" 1291 | } 1292 | }, 1293 | "@babel/code-frame": { 1294 | "version": "7.21.4", 1295 | "requires": { 1296 | "@babel/highlight": "^7.18.6" 1297 | } 1298 | }, 1299 | "@babel/compat-data": { 1300 | "version": "7.21.9", 1301 | "dev": true 1302 | }, 1303 | "@babel/core": { 1304 | "version": "7.21.8", 1305 | "dev": true, 1306 | "requires": { 1307 | "@ampproject/remapping": "^2.2.0", 1308 | "@babel/code-frame": "^7.21.4", 1309 | "@babel/generator": "^7.21.5", 1310 | "@babel/helper-compilation-targets": "^7.21.5", 1311 | "@babel/helper-module-transforms": "^7.21.5", 1312 | "@babel/helpers": "^7.21.5", 1313 | "@babel/parser": "^7.21.8", 1314 | "@babel/template": "^7.20.7", 1315 | "@babel/traverse": "^7.21.5", 1316 | "@babel/types": "^7.21.5", 1317 | "convert-source-map": "^1.7.0", 1318 | "debug": "^4.1.0", 1319 | "gensync": "^1.0.0-beta.2", 1320 | "json5": "^2.2.2", 1321 | "semver": "^6.3.0" 1322 | } 1323 | }, 1324 | "@babel/generator": { 1325 | "version": "7.21.9", 1326 | "requires": { 1327 | "@babel/types": "^7.21.5", 1328 | "@jridgewell/gen-mapping": "^0.3.2", 1329 | "@jridgewell/trace-mapping": "^0.3.17", 1330 | "jsesc": "^2.5.1" 1331 | } 1332 | }, 1333 | "@babel/helper-annotate-as-pure": { 1334 | "version": "7.18.6", 1335 | "requires": { 1336 | "@babel/types": "^7.18.6" 1337 | } 1338 | }, 1339 | "@babel/helper-compilation-targets": { 1340 | "version": "7.21.5", 1341 | "dev": true, 1342 | "requires": { 1343 | "@babel/compat-data": "^7.21.5", 1344 | "@babel/helper-validator-option": "^7.21.0", 1345 | "browserslist": "^4.21.3", 1346 | "lru-cache": "^5.1.1", 1347 | "semver": "^6.3.0" 1348 | } 1349 | }, 1350 | "@babel/helper-environment-visitor": { 1351 | "version": "7.21.5" 1352 | }, 1353 | "@babel/helper-function-name": { 1354 | "version": "7.21.0", 1355 | "requires": { 1356 | "@babel/template": "^7.20.7", 1357 | "@babel/types": "^7.21.0" 1358 | } 1359 | }, 1360 | "@babel/helper-hoist-variables": { 1361 | "version": "7.18.6", 1362 | "requires": { 1363 | "@babel/types": "^7.18.6" 1364 | } 1365 | }, 1366 | "@babel/helper-module-imports": { 1367 | "version": "7.21.4", 1368 | "requires": { 1369 | "@babel/types": "^7.21.4" 1370 | } 1371 | }, 1372 | "@babel/helper-module-transforms": { 1373 | "version": "7.21.5", 1374 | "dev": true, 1375 | "requires": { 1376 | "@babel/helper-environment-visitor": "^7.21.5", 1377 | "@babel/helper-module-imports": "^7.21.4", 1378 | "@babel/helper-simple-access": "^7.21.5", 1379 | "@babel/helper-split-export-declaration": "^7.18.6", 1380 | "@babel/helper-validator-identifier": "^7.19.1", 1381 | "@babel/template": "^7.20.7", 1382 | "@babel/traverse": "^7.21.5", 1383 | "@babel/types": "^7.21.5" 1384 | } 1385 | }, 1386 | "@babel/helper-plugin-utils": { 1387 | "version": "7.21.5", 1388 | "dev": true 1389 | }, 1390 | "@babel/helper-simple-access": { 1391 | "version": "7.21.5", 1392 | "dev": true, 1393 | "requires": { 1394 | "@babel/types": "^7.21.5" 1395 | } 1396 | }, 1397 | "@babel/helper-split-export-declaration": { 1398 | "version": "7.18.6", 1399 | "requires": { 1400 | "@babel/types": "^7.18.6" 1401 | } 1402 | }, 1403 | "@babel/helper-string-parser": { 1404 | "version": "7.21.5" 1405 | }, 1406 | "@babel/helper-validator-identifier": { 1407 | "version": "7.19.1" 1408 | }, 1409 | "@babel/helper-validator-option": { 1410 | "version": "7.21.0", 1411 | "dev": true 1412 | }, 1413 | "@babel/helpers": { 1414 | "version": "7.21.5", 1415 | "dev": true, 1416 | "requires": { 1417 | "@babel/template": "^7.20.7", 1418 | "@babel/traverse": "^7.21.5", 1419 | "@babel/types": "^7.21.5" 1420 | } 1421 | }, 1422 | "@babel/highlight": { 1423 | "version": "7.18.6", 1424 | "requires": { 1425 | "@babel/helper-validator-identifier": "^7.18.6", 1426 | "chalk": "^2.0.0", 1427 | "js-tokens": "^4.0.0" 1428 | } 1429 | }, 1430 | "@babel/parser": { 1431 | "version": "7.21.9" 1432 | }, 1433 | "@babel/plugin-transform-react-jsx-self": { 1434 | "version": "7.21.0", 1435 | "dev": true, 1436 | "requires": { 1437 | "@babel/helper-plugin-utils": "^7.20.2" 1438 | } 1439 | }, 1440 | "@babel/plugin-transform-react-jsx-source": { 1441 | "version": "7.19.6", 1442 | "dev": true, 1443 | "requires": { 1444 | "@babel/helper-plugin-utils": "^7.19.0" 1445 | } 1446 | }, 1447 | "@babel/template": { 1448 | "version": "7.21.9", 1449 | "requires": { 1450 | "@babel/code-frame": "^7.21.4", 1451 | "@babel/parser": "^7.21.9", 1452 | "@babel/types": "^7.21.5" 1453 | } 1454 | }, 1455 | "@babel/traverse": { 1456 | "version": "7.21.5", 1457 | "requires": { 1458 | "@babel/code-frame": "^7.21.4", 1459 | "@babel/generator": "^7.21.5", 1460 | "@babel/helper-environment-visitor": "^7.21.5", 1461 | "@babel/helper-function-name": "^7.21.0", 1462 | "@babel/helper-hoist-variables": "^7.18.6", 1463 | "@babel/helper-split-export-declaration": "^7.18.6", 1464 | "@babel/parser": "^7.21.5", 1465 | "@babel/types": "^7.21.5", 1466 | "debug": "^4.1.0", 1467 | "globals": "^11.1.0" 1468 | } 1469 | }, 1470 | "@babel/types": { 1471 | "version": "7.21.5", 1472 | "requires": { 1473 | "@babel/helper-string-parser": "^7.21.5", 1474 | "@babel/helper-validator-identifier": "^7.19.1", 1475 | "to-fast-properties": "^2.0.0" 1476 | } 1477 | }, 1478 | "@emotion/is-prop-valid": { 1479 | "version": "1.2.1", 1480 | "requires": { 1481 | "@emotion/memoize": "^0.8.1" 1482 | } 1483 | }, 1484 | "@emotion/memoize": { 1485 | "version": "0.8.1" 1486 | }, 1487 | "@emotion/stylis": { 1488 | "version": "0.8.5" 1489 | }, 1490 | "@emotion/unitless": { 1491 | "version": "0.7.5" 1492 | }, 1493 | "@esbuild/darwin-x64": { 1494 | "version": "0.17.19", 1495 | "dev": true, 1496 | "optional": true 1497 | }, 1498 | "@jridgewell/gen-mapping": { 1499 | "version": "0.3.3", 1500 | "requires": { 1501 | "@jridgewell/set-array": "^1.0.1", 1502 | "@jridgewell/sourcemap-codec": "^1.4.10", 1503 | "@jridgewell/trace-mapping": "^0.3.9" 1504 | } 1505 | }, 1506 | "@jridgewell/resolve-uri": { 1507 | "version": "3.1.0" 1508 | }, 1509 | "@jridgewell/set-array": { 1510 | "version": "1.1.2" 1511 | }, 1512 | "@jridgewell/sourcemap-codec": { 1513 | "version": "1.4.15" 1514 | }, 1515 | "@jridgewell/trace-mapping": { 1516 | "version": "0.3.18", 1517 | "requires": { 1518 | "@jridgewell/resolve-uri": "3.1.0", 1519 | "@jridgewell/sourcemap-codec": "1.4.14" 1520 | }, 1521 | "dependencies": { 1522 | "@jridgewell/sourcemap-codec": { 1523 | "version": "1.4.14" 1524 | } 1525 | } 1526 | }, 1527 | "@remix-run/router": { 1528 | "version": "1.6.2" 1529 | }, 1530 | "@tanstack/match-sorter-utils": { 1531 | "version": "8.8.4", 1532 | "requires": { 1533 | "remove-accents": "0.4.2" 1534 | } 1535 | }, 1536 | "@tanstack/query-core": { 1537 | "version": "4.29.7" 1538 | }, 1539 | "@tanstack/react-query": { 1540 | "version": "4.29.7", 1541 | "requires": { 1542 | "@tanstack/query-core": "4.29.7", 1543 | "use-sync-external-store": "^1.2.0" 1544 | } 1545 | }, 1546 | "@tanstack/react-query-devtools": { 1547 | "version": "4.29.7", 1548 | "requires": { 1549 | "@tanstack/match-sorter-utils": "^8.7.0", 1550 | "superjson": "^1.10.0", 1551 | "use-sync-external-store": "^1.2.0" 1552 | } 1553 | }, 1554 | "@types/prop-types": { 1555 | "version": "15.7.5", 1556 | "dev": true 1557 | }, 1558 | "@types/react": { 1559 | "version": "18.2.6", 1560 | "dev": true, 1561 | "requires": { 1562 | "@types/prop-types": "*", 1563 | "@types/scheduler": "*", 1564 | "csstype": "^3.0.2" 1565 | } 1566 | }, 1567 | "@types/react-dom": { 1568 | "version": "18.2.4", 1569 | "dev": true, 1570 | "requires": { 1571 | "@types/react": "*" 1572 | } 1573 | }, 1574 | "@types/scheduler": { 1575 | "version": "0.16.3", 1576 | "dev": true 1577 | }, 1578 | "@vitejs/plugin-react": { 1579 | "version": "3.1.0", 1580 | "dev": true, 1581 | "requires": { 1582 | "@babel/core": "^7.20.12", 1583 | "@babel/plugin-transform-react-jsx-self": "^7.18.6", 1584 | "@babel/plugin-transform-react-jsx-source": "^7.19.6", 1585 | "magic-string": "^0.27.0", 1586 | "react-refresh": "^0.14.0" 1587 | } 1588 | }, 1589 | "ansi-styles": { 1590 | "version": "3.2.1", 1591 | "requires": { 1592 | "color-convert": "^1.9.0" 1593 | } 1594 | }, 1595 | "asynckit": { 1596 | "version": "0.4.0" 1597 | }, 1598 | "axios": { 1599 | "version": "1.4.0", 1600 | "requires": { 1601 | "follow-redirects": "^1.15.0", 1602 | "form-data": "^4.0.0", 1603 | "proxy-from-env": "^1.1.0" 1604 | } 1605 | }, 1606 | "babel-plugin-styled-components": { 1607 | "version": "2.1.3", 1608 | "requires": { 1609 | "@babel/helper-annotate-as-pure": "^7.18.6", 1610 | "@babel/helper-module-imports": "^7.21.4", 1611 | "babel-plugin-syntax-jsx": "^6.18.0", 1612 | "lodash": "^4.17.21", 1613 | "picomatch": "^2.3.1" 1614 | } 1615 | }, 1616 | "babel-plugin-syntax-jsx": { 1617 | "version": "6.18.0" 1618 | }, 1619 | "browserslist": { 1620 | "version": "4.21.5", 1621 | "dev": true, 1622 | "requires": { 1623 | "caniuse-lite": "^1.0.30001449", 1624 | "electron-to-chromium": "^1.4.284", 1625 | "node-releases": "^2.0.8", 1626 | "update-browserslist-db": "^1.0.10" 1627 | } 1628 | }, 1629 | "camelize": { 1630 | "version": "1.0.1" 1631 | }, 1632 | "caniuse-lite": { 1633 | "version": "1.0.30001489", 1634 | "dev": true 1635 | }, 1636 | "chalk": { 1637 | "version": "2.4.2", 1638 | "requires": { 1639 | "ansi-styles": "^3.2.1", 1640 | "escape-string-regexp": "^1.0.5", 1641 | "supports-color": "^5.3.0" 1642 | } 1643 | }, 1644 | "clsx": { 1645 | "version": "1.2.1" 1646 | }, 1647 | "color-convert": { 1648 | "version": "1.9.3", 1649 | "requires": { 1650 | "color-name": "1.1.3" 1651 | } 1652 | }, 1653 | "color-name": { 1654 | "version": "1.1.3" 1655 | }, 1656 | "combined-stream": { 1657 | "version": "1.0.8", 1658 | "requires": { 1659 | "delayed-stream": "~1.0.0" 1660 | } 1661 | }, 1662 | "convert-source-map": { 1663 | "version": "1.9.0", 1664 | "dev": true 1665 | }, 1666 | "copy-anything": { 1667 | "version": "3.0.5", 1668 | "requires": { 1669 | "is-what": "^4.1.8" 1670 | } 1671 | }, 1672 | "css-color-keywords": { 1673 | "version": "1.0.0" 1674 | }, 1675 | "css-to-react-native": { 1676 | "version": "3.2.0", 1677 | "requires": { 1678 | "camelize": "^1.0.0", 1679 | "css-color-keywords": "^1.0.0", 1680 | "postcss-value-parser": "^4.0.2" 1681 | } 1682 | }, 1683 | "csstype": { 1684 | "version": "3.1.2", 1685 | "dev": true 1686 | }, 1687 | "debug": { 1688 | "version": "4.3.4", 1689 | "requires": { 1690 | "ms": "2.1.2" 1691 | } 1692 | }, 1693 | "delayed-stream": { 1694 | "version": "1.0.0" 1695 | }, 1696 | "electron-to-chromium": { 1697 | "version": "1.4.405", 1698 | "dev": true 1699 | }, 1700 | "esbuild": { 1701 | "version": "0.17.19", 1702 | "dev": true, 1703 | "requires": { 1704 | "@esbuild/android-arm": "0.17.19", 1705 | "@esbuild/android-arm64": "0.17.19", 1706 | "@esbuild/android-x64": "0.17.19", 1707 | "@esbuild/darwin-arm64": "0.17.19", 1708 | "@esbuild/darwin-x64": "0.17.19", 1709 | "@esbuild/freebsd-arm64": "0.17.19", 1710 | "@esbuild/freebsd-x64": "0.17.19", 1711 | "@esbuild/linux-arm": "0.17.19", 1712 | "@esbuild/linux-arm64": "0.17.19", 1713 | "@esbuild/linux-ia32": "0.17.19", 1714 | "@esbuild/linux-loong64": "0.17.19", 1715 | "@esbuild/linux-mips64el": "0.17.19", 1716 | "@esbuild/linux-ppc64": "0.17.19", 1717 | "@esbuild/linux-riscv64": "0.17.19", 1718 | "@esbuild/linux-s390x": "0.17.19", 1719 | "@esbuild/linux-x64": "0.17.19", 1720 | "@esbuild/netbsd-x64": "0.17.19", 1721 | "@esbuild/openbsd-x64": "0.17.19", 1722 | "@esbuild/sunos-x64": "0.17.19", 1723 | "@esbuild/win32-arm64": "0.17.19", 1724 | "@esbuild/win32-ia32": "0.17.19", 1725 | "@esbuild/win32-x64": "0.17.19" 1726 | } 1727 | }, 1728 | "escalade": { 1729 | "version": "3.1.1", 1730 | "dev": true 1731 | }, 1732 | "escape-string-regexp": { 1733 | "version": "1.0.5" 1734 | }, 1735 | "follow-redirects": { 1736 | "version": "1.15.2" 1737 | }, 1738 | "form-data": { 1739 | "version": "4.0.0", 1740 | "requires": { 1741 | "asynckit": "^0.4.0", 1742 | "combined-stream": "^1.0.8", 1743 | "mime-types": "^2.1.12" 1744 | } 1745 | }, 1746 | "fsevents": { 1747 | "version": "2.3.2", 1748 | "dev": true, 1749 | "optional": true 1750 | }, 1751 | "gensync": { 1752 | "version": "1.0.0-beta.2", 1753 | "dev": true 1754 | }, 1755 | "globals": { 1756 | "version": "11.12.0" 1757 | }, 1758 | "has-flag": { 1759 | "version": "3.0.0" 1760 | }, 1761 | "hoist-non-react-statics": { 1762 | "version": "3.3.2", 1763 | "requires": { 1764 | "react-is": "^16.7.0" 1765 | }, 1766 | "dependencies": { 1767 | "react-is": { 1768 | "version": "16.13.1" 1769 | } 1770 | } 1771 | }, 1772 | "is-what": { 1773 | "version": "4.1.11" 1774 | }, 1775 | "js-tokens": { 1776 | "version": "4.0.0" 1777 | }, 1778 | "jsesc": { 1779 | "version": "2.5.2" 1780 | }, 1781 | "json5": { 1782 | "version": "2.2.3", 1783 | "dev": true 1784 | }, 1785 | "lodash": { 1786 | "version": "4.17.21" 1787 | }, 1788 | "loose-envify": { 1789 | "version": "1.4.0", 1790 | "requires": { 1791 | "js-tokens": "^3.0.0 || ^4.0.0" 1792 | } 1793 | }, 1794 | "lru-cache": { 1795 | "version": "5.1.1", 1796 | "dev": true, 1797 | "requires": { 1798 | "yallist": "^3.0.2" 1799 | } 1800 | }, 1801 | "magic-string": { 1802 | "version": "0.27.0", 1803 | "dev": true, 1804 | "requires": { 1805 | "@jridgewell/sourcemap-codec": "^1.4.13" 1806 | } 1807 | }, 1808 | "mime-db": { 1809 | "version": "1.52.0" 1810 | }, 1811 | "mime-types": { 1812 | "version": "2.1.35", 1813 | "requires": { 1814 | "mime-db": "1.52.0" 1815 | } 1816 | }, 1817 | "ms": { 1818 | "version": "2.1.2" 1819 | }, 1820 | "nanoid": { 1821 | "version": "3.3.6", 1822 | "dev": true 1823 | }, 1824 | "node-releases": { 1825 | "version": "2.0.11", 1826 | "dev": true 1827 | }, 1828 | "picocolors": { 1829 | "version": "1.0.0", 1830 | "dev": true 1831 | }, 1832 | "picomatch": { 1833 | "version": "2.3.1" 1834 | }, 1835 | "postcss": { 1836 | "version": "8.4.23", 1837 | "dev": true, 1838 | "requires": { 1839 | "nanoid": "^3.3.6", 1840 | "picocolors": "^1.0.0", 1841 | "source-map-js": "^1.0.2" 1842 | } 1843 | }, 1844 | "postcss-value-parser": { 1845 | "version": "4.2.0" 1846 | }, 1847 | "proxy-from-env": { 1848 | "version": "1.1.0" 1849 | }, 1850 | "react": { 1851 | "version": "18.2.0", 1852 | "requires": { 1853 | "loose-envify": "^1.1.0" 1854 | } 1855 | }, 1856 | "react-dom": { 1857 | "version": "18.2.0", 1858 | "requires": { 1859 | "loose-envify": "^1.1.0", 1860 | "scheduler": "^0.23.0" 1861 | } 1862 | }, 1863 | "react-is": { 1864 | "version": "18.2.0", 1865 | "peer": true 1866 | }, 1867 | "react-refresh": { 1868 | "version": "0.14.0", 1869 | "dev": true 1870 | }, 1871 | "react-router": { 1872 | "version": "6.11.2", 1873 | "requires": { 1874 | "@remix-run/router": "1.6.2" 1875 | } 1876 | }, 1877 | "react-router-dom": { 1878 | "version": "6.11.2", 1879 | "requires": { 1880 | "@remix-run/router": "1.6.2", 1881 | "react-router": "6.11.2" 1882 | } 1883 | }, 1884 | "react-toastify": { 1885 | "version": "9.1.3", 1886 | "requires": { 1887 | "clsx": "^1.1.1" 1888 | } 1889 | }, 1890 | "remove-accents": { 1891 | "version": "0.4.2" 1892 | }, 1893 | "rollup": { 1894 | "version": "3.23.0", 1895 | "dev": true, 1896 | "requires": { 1897 | "fsevents": "~2.3.2" 1898 | } 1899 | }, 1900 | "scheduler": { 1901 | "version": "0.23.0", 1902 | "requires": { 1903 | "loose-envify": "^1.1.0" 1904 | } 1905 | }, 1906 | "semver": { 1907 | "version": "6.3.0", 1908 | "dev": true 1909 | }, 1910 | "shallowequal": { 1911 | "version": "1.1.0" 1912 | }, 1913 | "source-map-js": { 1914 | "version": "1.0.2", 1915 | "dev": true 1916 | }, 1917 | "styled-components": { 1918 | "version": "5.3.10", 1919 | "requires": { 1920 | "@babel/helper-module-imports": "^7.0.0", 1921 | "@babel/traverse": "^7.4.5", 1922 | "@emotion/is-prop-valid": "^1.1.0", 1923 | "@emotion/stylis": "^0.8.4", 1924 | "@emotion/unitless": "^0.7.4", 1925 | "babel-plugin-styled-components": ">= 1.12.0", 1926 | "css-to-react-native": "^3.0.0", 1927 | "hoist-non-react-statics": "^3.0.0", 1928 | "shallowequal": "^1.1.0", 1929 | "supports-color": "^5.5.0" 1930 | } 1931 | }, 1932 | "superjson": { 1933 | "version": "1.12.3", 1934 | "requires": { 1935 | "copy-anything": "^3.0.2" 1936 | } 1937 | }, 1938 | "supports-color": { 1939 | "version": "5.5.0", 1940 | "requires": { 1941 | "has-flag": "^3.0.0" 1942 | } 1943 | }, 1944 | "to-fast-properties": { 1945 | "version": "2.0.0" 1946 | }, 1947 | "update-browserslist-db": { 1948 | "version": "1.0.11", 1949 | "dev": true, 1950 | "requires": { 1951 | "escalade": "^3.1.1", 1952 | "picocolors": "^1.0.0" 1953 | } 1954 | }, 1955 | "use-sync-external-store": { 1956 | "version": "1.2.0", 1957 | "requires": {} 1958 | }, 1959 | "vite": { 1960 | "version": "4.3.8", 1961 | "dev": true, 1962 | "requires": { 1963 | "esbuild": "^0.17.5", 1964 | "fsevents": "~2.3.2", 1965 | "postcss": "^8.4.23", 1966 | "rollup": "^3.21.0" 1967 | } 1968 | }, 1969 | "yallist": { 1970 | "version": "3.1.1", 1971 | "dev": true 1972 | } 1973 | } 1974 | } 1975 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful", 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 | "@tanstack/react-query": "^4.29.7", 13 | "@tanstack/react-query-devtools": "^4.29.7", 14 | "axios": "^1.4.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.11.2", 18 | "react-toastify": "^9.1.3", 19 | "styled-components": "^5.3.10" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@vitejs/plugin-react": "^3.1.0", 25 | "vite": "^4.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createBrowserRouter } from 'react-router-dom'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 4 | import { 5 | About, 6 | HomeLayout, 7 | Landing, 8 | Error, 9 | Newsletter, 10 | Cocktail, 11 | SinglePageError, 12 | } from './pages'; 13 | 14 | import { loader as landingLoader } from './pages/Landing'; 15 | import { loader as singleCocktailLoader } from './pages/Cocktail'; 16 | import { action as newsletterAction } from './pages/Newsletter'; 17 | 18 | const queryClient = new QueryClient({ 19 | defaultOptions: { 20 | queries: { 21 | staleTime: 1000 * 60 * 5, 22 | }, 23 | }, 24 | }); 25 | 26 | const router = createBrowserRouter([ 27 | { 28 | path: '/', 29 | element: , 30 | errorElement: , 31 | children: [ 32 | { 33 | index: true, 34 | element: , 35 | errorElement: , 36 | loader: landingLoader(queryClient), 37 | }, 38 | { 39 | path: 'cocktail/:id', 40 | errorElement: , 41 | loader: singleCocktailLoader(queryClient), 42 | element: , 43 | }, 44 | { 45 | path: 'newsletter', 46 | element: , 47 | action: newsletterAction, 48 | errorElement: , 49 | }, 50 | { 51 | path: 'about', 52 | element: , 53 | }, 54 | ], 55 | }, 56 | ]); 57 | 58 | const App = () => { 59 | return ( 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | export default App; 67 | -------------------------------------------------------------------------------- /src/assets/not-found.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/wrappers/AboutPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | p { 5 | line-height: 2; 6 | color: var(--grey-500); 7 | margin-top: 2rem; 8 | } 9 | `; 10 | 11 | export default Wrapper; 12 | -------------------------------------------------------------------------------- /src/assets/wrappers/CocktailCard.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | const Wrapper = styled.article` 3 | background: var(--white); 4 | box-shadow: var(--shadow-2); 5 | transition: var(--transition); 6 | display: grid; 7 | grid-template-rows: auto 1fr; 8 | border-radius: var(--borderRadius); 9 | :hover { 10 | box-shadow: var(--shadow-4); 11 | } 12 | img { 13 | height: 15rem; 14 | border-top-left-radius: var(--borderRadius); 15 | border-top-right-radius: var(--borderRadius); 16 | } 17 | .footer { 18 | padding: 1.5rem; 19 | h4, 20 | h5 { 21 | margin-bottom: 0.5rem; 22 | } 23 | h4 { 24 | font-weight: 700; 25 | } 26 | p { 27 | margin-bottom: 1rem; 28 | color: var(--grey-500); 29 | } 30 | } 31 | `; 32 | 33 | export default Wrapper; 34 | -------------------------------------------------------------------------------- /src/assets/wrappers/CocktailList.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | display: grid; 5 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 6 | gap: 2rem; 7 | `; 8 | 9 | export default Wrapper; 10 | -------------------------------------------------------------------------------- /src/assets/wrappers/CocktailPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | header { 5 | text-align: center; 6 | margin-bottom: 3rem; 7 | .btn { 8 | margin-bottom: 1rem; 9 | } 10 | } 11 | .img { 12 | border-radius: var(--borderRadius); 13 | } 14 | .drink-info { 15 | padding-top: 2rem; 16 | } 17 | .drink p { 18 | font-weight: 700; 19 | text-transform: capitalize; 20 | line-height: 2; 21 | margin-bottom: 1rem; 22 | } 23 | .drink-data { 24 | margin-right: 0.5rem; 25 | background: var(--primary-300); 26 | padding: 0.25rem 0.5rem; 27 | border-radius: var(--borderRadius); 28 | color: var(--primary-700); 29 | letter-spacing: var(--letterSpacing); 30 | } 31 | .ing { 32 | display: inline-block; 33 | margin-right: 0.5rem; 34 | } 35 | @media (min-width: 992px) { 36 | .drink { 37 | display: grid; 38 | grid-template-columns: 2fr 3fr; 39 | gap: 3rem; 40 | align-items: center; 41 | } 42 | .drink-info { 43 | padding-top: 0; 44 | } 45 | } 46 | `; 47 | 48 | export default Wrapper; 49 | -------------------------------------------------------------------------------- /src/assets/wrappers/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | min-height: 100vh; 5 | text-align: center; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | img { 10 | width: 90vw; 11 | max-width: 600px; 12 | display: block; 13 | margin-bottom: 2rem; 14 | margin-top: -3rem; 15 | } 16 | h3 { 17 | margin-bottom: 0.5rem; 18 | } 19 | 20 | p { 21 | line-height: 1.5; 22 | margin-top: 0.5rem; 23 | margin-bottom: 1rem; 24 | color: var(--grey-500); 25 | } 26 | a { 27 | color: var(--primary-500); 28 | text-transform: capitalize; 29 | } 30 | `; 31 | 32 | export default Wrapper; 33 | -------------------------------------------------------------------------------- /src/assets/wrappers/Navbar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.nav` 4 | background: var(--white); 5 | .nav-center { 6 | width: var(--view-width); 7 | max-width: var(--max-width); 8 | margin: 0 auto; 9 | display: flex; 10 | flex-direction: column; 11 | padding: 1.5rem 2rem; 12 | } 13 | .logo { 14 | font-size: clamp(1.5rem, 3vw, 3rem); 15 | color: var(--primary-500); 16 | font-weight: 700; 17 | letter-spacing: 2px; 18 | } 19 | .nav-links { 20 | display: flex; 21 | flex-direction: column; 22 | gap: 0.5rem; 23 | margin-top: 1rem; 24 | } 25 | .nav-link { 26 | color: var(--grey-900); 27 | padding: 0.5rem 0.5rem 0.5rem 0; 28 | transition: var(--transition); 29 | letter-spacing: 2px; 30 | } 31 | .nav-link:hover { 32 | color: var(--primary-500); 33 | } 34 | .active { 35 | color: var(--primary-500); 36 | } 37 | @media (min-width: 768px) { 38 | .nav-center { 39 | flex-direction: row; 40 | justify-content: space-between; 41 | align-items: center; 42 | } 43 | .nav-links { 44 | flex-direction: row; 45 | margin-top: 0; 46 | } 47 | } 48 | `; 49 | 50 | export default Wrapper; 51 | -------------------------------------------------------------------------------- /src/assets/wrappers/SearchForm.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | margin-bottom: 6rem; 5 | .form { 6 | display: grid; 7 | grid-template-columns: 1fr auto; 8 | } 9 | .form-input { 10 | border-top-right-radius: 0; 11 | border-bottom-right-radius: 0; 12 | } 13 | .btn { 14 | border-top-left-radius: 0; 15 | border-bottom-left-radius: 0; 16 | } 17 | `; 18 | 19 | export default Wrapper; 20 | -------------------------------------------------------------------------------- /src/components/CocktailCard.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useOutletContext } from 'react-router-dom'; 2 | import Wrapper from '../assets/wrappers/CocktailCard'; 3 | const CocktailCard = ({ image, name, id, info, glass }) => { 4 | // const data = useOutletContext(); 5 | // console.log(data); 6 | return ( 7 | 8 |
9 | {name} 10 |
11 |
12 |

{name}

13 |
{glass}
14 |

{info}

15 | 16 | details 17 | 18 |
19 |
20 | ); 21 | }; 22 | export default CocktailCard; 23 | -------------------------------------------------------------------------------- /src/components/CocktailList.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/CocktailList'; 2 | import CocktailCard from './CocktailCard'; 3 | const CocktailList = ({ drinks }) => { 4 | if (!drinks) { 5 | return ( 6 |

No matching cocktails found...

7 | ); 8 | } 9 | const formattedDrinks = drinks.map((item) => { 10 | const { idDrink, strDrink, strDrinkThumb, strAlcoholic, strGlass } = item; 11 | return { 12 | id: idDrink, 13 | name: strDrink, 14 | image: strDrinkThumb, 15 | info: strAlcoholic, 16 | glass: strGlass, 17 | }; 18 | }); 19 | return ( 20 | 21 | {formattedDrinks.map((item) => { 22 | return ; 23 | })} 24 | 25 | ); 26 | }; 27 | export default CocktailList; 28 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | import Wrapper from '../assets/wrappers/Navbar'; 3 | const Navbar = () => { 4 | return ( 5 | 6 |
7 | MixMaster 8 |
9 | 10 | Home 11 | 12 | 13 | About 14 | 15 | 16 | Newsletter 17 | 18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Navbar; 25 | -------------------------------------------------------------------------------- /src/components/SearchForm.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/SearchForm'; 2 | import { Form, useNavigation } from 'react-router-dom'; 3 | 4 | const SearchForm = ({ searchTerm }) => { 5 | const navigation = useNavigation(); 6 | const isSubmitting = navigation.state === 'submitting'; 7 | return ( 8 | 9 |
10 | 16 | 19 |
20 |
21 | ); 22 | }; 23 | export default SearchForm; 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* ============= GLOBAL CSS =============== */ 2 | 3 | *, 4 | ::after, 5 | ::before { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | font-size: 100%; 13 | } /*16px*/ 14 | 15 | :root { 16 | /* colors */ 17 | --primary-100: #d1fae5; 18 | --primary-200: #a7f3d0; 19 | --primary-300: #6ee7b7; 20 | --primary-400: #34d399; 21 | --primary-500: #10b981; 22 | --primary-600: #059669; 23 | --primary-700: #047857; 24 | --primary-800: #065f46; 25 | --primary-900: #064e3b; 26 | 27 | /* grey */ 28 | --grey-50: #f8fafc; 29 | --grey-100: #f1f5f9; 30 | --grey-200: #e2e8f0; 31 | --grey-300: #cbd5e1; 32 | --grey-400: #94a3b8; 33 | --grey-500: #64748b; 34 | --grey-600: #475569; 35 | --grey-700: #334155; 36 | --grey-800: #1e293b; 37 | --grey-900: #0f172a; 38 | /* rest of the colors */ 39 | --black: #222; 40 | --white: #fff; 41 | --red-light: #f8d7da; 42 | --red-dark: #842029; 43 | --green-light: #d1e7dd; 44 | --green-dark: #0f5132; 45 | 46 | --small-text: 0.875rem; 47 | --extra-small-text: 0.7em; 48 | /* rest of the vars */ 49 | --backgroundColor: var(--grey-50); 50 | --textColor: var(--grey-900); 51 | --borderRadius: 0.25rem; 52 | --letterSpacing: 1px; 53 | --transition: 0.3s ease-in-out all; 54 | --max-width: 1120px; 55 | --fixed-width: 600px; 56 | --view-width: 90vw; 57 | /* box shadow*/ 58 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 59 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 60 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 61 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 62 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 63 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 64 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 65 | } 66 | 67 | body { 68 | background: var(--backgroundColor); 69 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 70 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 71 | font-weight: 400; 72 | line-height: 1; 73 | color: var(--textColor); 74 | } 75 | p { 76 | margin: 0; 77 | } 78 | h1, 79 | h2, 80 | h3, 81 | h4, 82 | h5 { 83 | margin: 0; 84 | font-family: var(--headingFont); 85 | font-weight: 400; 86 | line-height: 1; 87 | text-transform: capitalize; 88 | letter-spacing: var(--letterSpacing); 89 | } 90 | 91 | h1 { 92 | font-size: clamp(2rem, 5vw, 5rem); /* Large heading */ 93 | } 94 | 95 | h2 { 96 | font-size: clamp(1.5rem, 3vw, 3rem); /* Medium heading */ 97 | } 98 | 99 | h3 { 100 | font-size: clamp(1.25rem, 2.5vw, 2.5rem); /* Small heading */ 101 | } 102 | 103 | h4 { 104 | font-size: clamp(1rem, 2vw, 2rem); /* Extra small heading */ 105 | } 106 | 107 | h5 { 108 | font-size: clamp(0.875rem, 1.5vw, 1.5rem); /* Tiny heading */ 109 | } 110 | 111 | /* BIGGER FONTS */ 112 | /* h1 { 113 | font-size: clamp(3rem, 6vw, 6rem); 114 | } 115 | 116 | h2 { 117 | font-size: clamp(2.5rem, 5vw, 5rem); 118 | } 119 | 120 | h3 { 121 | font-size: clamp(2rem, 4vw, 4rem); 122 | } 123 | 124 | h4 { 125 | font-size: clamp(1.5rem, 3vw, 3rem); 126 | } 127 | 128 | h5 { 129 | font-size: clamp(1rem, 2vw, 2rem); 130 | } 131 | */ 132 | 133 | .text { 134 | margin-bottom: 1.5rem; 135 | max-width: 40em; 136 | } 137 | 138 | small, 139 | .text-small { 140 | font-size: var(--small-text); 141 | } 142 | 143 | a { 144 | text-decoration: none; 145 | } 146 | ul { 147 | list-style-type: none; 148 | padding: 0; 149 | } 150 | 151 | .img { 152 | width: 100%; 153 | display: block; 154 | object-fit: cover; 155 | } 156 | /* buttons */ 157 | 158 | .btn { 159 | cursor: pointer; 160 | color: var(--white); 161 | background: var(--primary-500); 162 | border: transparent; 163 | border-radius: var(--borderRadius); 164 | letter-spacing: var(--letterSpacing); 165 | padding: 0.375rem 0.75rem; 166 | box-shadow: var(--shadow-1); 167 | transition: var(--transition); 168 | text-transform: capitalize; 169 | display: inline-block; 170 | } 171 | .btn:hover { 172 | background: var(--primary-700); 173 | box-shadow: var(--shadow-3); 174 | } 175 | .btn-hipster { 176 | color: var(--primary-500); 177 | background: var(--primary-200); 178 | } 179 | .btn-hipster:hover { 180 | color: var(--primary-200); 181 | background: var(--primary-700); 182 | } 183 | .btn-block { 184 | width: 100%; 185 | } 186 | 187 | /* alerts */ 188 | .alert { 189 | padding: 0.375rem 0.75rem; 190 | margin-bottom: 1rem; 191 | border-color: transparent; 192 | border-radius: var(--borderRadius); 193 | } 194 | 195 | .alert-danger { 196 | color: var(--red-dark); 197 | background: var(--red-light); 198 | } 199 | .alert-success { 200 | color: var(--green-dark); 201 | background: var(--green-light); 202 | } 203 | /* form */ 204 | 205 | .form { 206 | width: 100%; 207 | max-width: var(--fixed-width); 208 | background: var(--white); 209 | border-radius: var(--borderRadius); 210 | box-shadow: var(--shadow-2); 211 | padding: 2rem 2.5rem; 212 | margin: 3rem auto; 213 | } 214 | .form-label { 215 | display: block; 216 | font-size: var(--small-text); 217 | margin-bottom: 0.5rem; 218 | text-transform: capitalize; 219 | letter-spacing: var(--letterSpacing); 220 | } 221 | .form-input, 222 | .form-textarea { 223 | width: 100%; 224 | padding: 0.375rem 0.75rem; 225 | border-radius: var(--borderRadius); 226 | background: var(--backgroundColor); 227 | border: 1px solid var(--grey-200); 228 | } 229 | 230 | .form-row { 231 | margin-bottom: 1rem; 232 | } 233 | 234 | .form-textarea { 235 | height: 7rem; 236 | } 237 | ::placeholder { 238 | font-family: inherit; 239 | color: var(--grey-400); 240 | } 241 | .form-alert { 242 | color: var(--red-dark); 243 | letter-spacing: var(--letterSpacing); 244 | text-transform: capitalize; 245 | } 246 | /* alert */ 247 | 248 | @keyframes spinner { 249 | to { 250 | transform: rotate(360deg); 251 | } 252 | } 253 | 254 | .loading { 255 | width: 6rem; 256 | height: 6rem; 257 | border: 5px solid var(--grey-400); 258 | border-radius: 50%; 259 | border-top-color: var(--primary-500); 260 | animation: spinner 0.6s linear infinite; 261 | } 262 | 263 | /* title */ 264 | 265 | .title { 266 | text-align: center; 267 | } 268 | 269 | .title-underline { 270 | background: var(--primary-500); 271 | width: 4rem; 272 | height: 0.25rem; 273 | margin: 0 auto; 274 | margin-top: 1rem; 275 | } 276 | 277 | /* 278 | ============= 279 | PROJECT CSS 280 | ============= 281 | */ 282 | .page { 283 | width: var(--view-width); 284 | max-width: var(--max-width); 285 | margin: 0 auto; 286 | padding: 5rem 2rem; 287 | } 288 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | 6 | import './index.css'; 7 | import { ToastContainer } from 'react-toastify'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')).render( 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/AboutPage'; 2 | 3 | const About = () => { 4 | return ( 5 | 6 |

About Us

7 |

8 | Introducing "MixMaster," the ultimate party sidekick app that fetches 9 | cocktails from the hilarious Cocktails DB API. With a flick of your 10 | finger, you'll unlock a treasure trove of enchanting drink recipes 11 | that'll make your taste buds dance and your friends jump with joy. Get 12 | ready to shake up your mixology game, one fantastical mocktail at a 13 | time, and let the laughter and giggles flow! 14 |

15 |
16 | ); 17 | }; 18 | export default About; 19 | -------------------------------------------------------------------------------- /src/pages/Cocktail.jsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData, Link, Navigate, useNavigate } from 'react-router-dom'; 2 | import axios from 'axios'; 3 | import Wrapper from '../assets/wrappers/CocktailPage'; 4 | const singleCocktailUrl = 5 | 'https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i='; 6 | import { useQuery } from '@tanstack/react-query'; 7 | 8 | const singleCocktailQuery = (id) => { 9 | return { 10 | queryKey: ['cocktail', id], 11 | queryFn: async () => { 12 | const { data } = await axios.get(`${singleCocktailUrl}${id}`); 13 | return data; 14 | }, 15 | }; 16 | }; 17 | 18 | export const loader = 19 | (queryClient) => 20 | async ({ params }) => { 21 | const { id } = params; 22 | await queryClient.ensureQueryData(singleCocktailQuery(id)); 23 | return { id }; 24 | }; 25 | 26 | const Cocktail = () => { 27 | const { id } = useLoaderData(); 28 | const navigate = useNavigate(); 29 | const { data } = useQuery(singleCocktailQuery(id)); 30 | if (!data) return ; 31 | 32 | const singleDrink = data.drinks[0]; 33 | 34 | const { 35 | strDrink: name, 36 | strDrinkThumb: image, 37 | strAlcoholic: info, 38 | strCategory: category, 39 | strGlass: glass, 40 | strInstructions: instructions, 41 | } = singleDrink; 42 | 43 | const validIngredients = Object.keys(singleDrink) 44 | .filter( 45 | (key) => key.startsWith('strIngredient') && singleDrink[key] !== null 46 | ) 47 | .map((key) => singleDrink[key]); 48 | 49 | return ( 50 | 51 |
52 | 55 |

{name}

56 |
57 |
58 | {name} 59 |
60 |

61 | name : 62 | {name} 63 |

64 |

65 | category : 66 | {category} 67 |

68 |

69 | info : 70 | {info} 71 |

72 |

73 | glass : 74 | {glass} 75 |

76 |

77 | ingredients : 78 | {validIngredients.map((item, index) => { 79 | return ( 80 | 81 | {item} 82 | {index < validIngredients.length - 1 ? ',' : ''} 83 | 84 | ); 85 | })} 86 |

87 |

88 | instructions : 89 | {instructions} 90 |

91 |
92 |
93 |
94 | ); 95 | }; 96 | export default Cocktail; 97 | -------------------------------------------------------------------------------- /src/pages/Error.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/ErrorPage'; 2 | import { Link, useRouteError } from 'react-router-dom'; 3 | import img from '../assets/not-found.svg'; 4 | const Error = () => { 5 | const error = useRouteError(); 6 | console.log(error); 7 | if (error.status === 404) { 8 | return ( 9 | 10 |
11 | not found 12 |

Ohh!

13 |

We can't seem to find page you are looking for

14 | back home 15 |
16 |
17 | ); 18 | } 19 | return ( 20 | 21 |
22 |

something went wrong

23 |
24 |
25 | ); 26 | }; 27 | export default Error; 28 | -------------------------------------------------------------------------------- /src/pages/HomeLayout.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, useNavigation } from 'react-router-dom'; 2 | import Navbar from '../components/Navbar'; 3 | 4 | const HomeLayout = () => { 5 | const navigation = useNavigation(); 6 | 7 | const isPageLoading = navigation.state === 'loading'; 8 | const value = 'some value'; 9 | return ( 10 | <> 11 | 12 |
13 | {isPageLoading ? ( 14 |
15 | ) : ( 16 | 17 | )} 18 |
19 | 20 | ); 21 | }; 22 | export default HomeLayout; 23 | -------------------------------------------------------------------------------- /src/pages/Landing.jsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from 'react-router-dom'; 2 | import axios from 'axios'; 3 | import CocktailList from '../components/CocktailList'; 4 | import SearchForm from '../components/SearchForm'; 5 | const cocktailSearchUrl = 6 | 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s='; 7 | 8 | import { useQuery } from '@tanstack/react-query'; 9 | 10 | const searchCocktailsQuery = (searchTerm) => { 11 | return { 12 | queryKey: ['search', searchTerm || 'all'], 13 | queryFn: async () => { 14 | searchTerm = searchTerm || 'a'; 15 | const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`); 16 | return response.data.drinks; 17 | }, 18 | }; 19 | }; 20 | 21 | export const loader = 22 | (queryClient) => 23 | async ({ request }) => { 24 | const url = new URL(request.url); 25 | 26 | const searchTerm = url.searchParams.get('search') || ''; 27 | await queryClient.ensureQueryData(searchCocktailsQuery(searchTerm)); 28 | return { searchTerm }; 29 | }; 30 | 31 | const Landing = () => { 32 | const { searchTerm } = useLoaderData(); 33 | const { data: drinks } = useQuery(searchCocktailsQuery(searchTerm)); 34 | 35 | return ( 36 | <> 37 | 38 | 39 | 40 | ); 41 | }; 42 | export default Landing; 43 | -------------------------------------------------------------------------------- /src/pages/Newsletter.jsx: -------------------------------------------------------------------------------- 1 | import { Form, redirect, useNavigation } from 'react-router-dom'; 2 | import axios from 'axios'; 3 | import { toast } from 'react-toastify'; 4 | 5 | const newsletterUrl = 'https://www.course-api.com/cocktails-newsletter'; 6 | 7 | export const action = async ({ request }) => { 8 | const formData = await request.formData(); 9 | const data = Object.fromEntries(formData); 10 | 11 | try { 12 | const response = await axios.post(newsletterUrl, data); 13 | 14 | toast.success(response.data.msg); 15 | return redirect('/'); 16 | } catch (error) { 17 | console.log(error); 18 | toast.error(error?.response?.data?.msg); 19 | return error; 20 | } 21 | }; 22 | 23 | const Newsletter = () => { 24 | const navigation = useNavigation(); 25 | const isSubmitting = navigation.state === 'submitting'; 26 | return ( 27 |
28 |

29 | our newsletter 30 |

31 | {/* name */} 32 |
33 | 36 | 43 |
44 | {/* lastName */} 45 |
46 | 49 | 56 |
57 | {/* email */} 58 |
59 | 62 | 70 |
71 | 79 |
80 | ); 81 | }; 82 | export default Newsletter; 83 | -------------------------------------------------------------------------------- /src/pages/SinglePageError.jsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | 3 | const SinglePageError = () => { 4 | const error = useRouteError(); 5 | console.log(error); 6 | // return

{error.message}

; 7 | return

there was an error...

; 8 | }; 9 | export default SinglePageError; 10 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as Landing } from './Landing'; 2 | export { default as About } from './About'; 3 | export { default as Cocktail } from './Cocktail'; 4 | export { default as Newsletter } from './Newsletter'; 5 | export { default as HomeLayout } from './HomeLayout'; 6 | export { default as Error } from './Error'; 7 | export { default as SinglePageError } from './SinglePageError'; 8 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------