├── .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 | navbar
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 |
176 |
177 |
MixMaster
178 |
179 |
180 | Home
181 |
182 |
183 | About
184 |
185 |
186 | Newsletter
187 |
188 |
189 |
190 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | navigate(-1)}>
53 | back home
54 |
55 | {name}
56 |
57 |
58 |
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 |
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 |
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 |
--------------------------------------------------------------------------------