{"id":26218087,"url":"https://github.com/arnobt78/mixmaster-cocktail-recipes--react-query","last_synced_at":"2025-03-12T13:15:28.583Z","repository":{"id":279438750,"uuid":"938811846","full_name":"arnobt78/MixMaster-Cocktail-Recipes--React-Query","owner":"arnobt78","description":"MixMaster is a ReactVite Single-Page Application (SPA) that fetches cocktail recipes from the Cocktails DB API. It provides a seamless, client-side navigation experience using react-router-dom, tanstack/react-query, axios, responsive-web-design and styled-components for styling.","archived":false,"fork":false,"pushed_at":"2025-02-25T14:54:29.000Z","size":7257,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-25T15:43:37.859Z","etag":null,"topics":["axios","cocktail-recipes","cocktails-api","mixmaster","netlify-deployment","react","react-query","react-query-devtools","react-router-dom","react-toastify","react-vite","reactjs","responsive-web-design","serach-engine","styled-components","tanstack-react-query"],"latest_commit_sha":null,"homepage":"https://mixmaster-arnob.netlify.app/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arnobt78.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-25T14:41:55.000Z","updated_at":"2025-02-25T14:59:09.000Z","dependencies_parsed_at":"2025-02-25T15:53:45.605Z","dependency_job_id":null,"html_url":"https://github.com/arnobt78/MixMaster-Cocktail-Recipes--React-Query","commit_stats":null,"previous_names":["arnobt78/mixmaster-cocktail-recipes--react-query"],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arnobt78%2FMixMaster-Cocktail-Recipes--React-Query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arnobt78%2FMixMaster-Cocktail-Recipes--React-Query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arnobt78%2FMixMaster-Cocktail-Recipes--React-Query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arnobt78%2FMixMaster-Cocktail-Recipes--React-Query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arnobt78","download_url":"https://codeload.github.com/arnobt78/MixMaster-Cocktail-Recipes--React-Query/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243222183,"owners_count":20256229,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axios","cocktail-recipes","cocktails-api","mixmaster","netlify-deployment","react","react-query","react-query-devtools","react-router-dom","react-toastify","react-vite","reactjs","responsive-web-design","serach-engine","styled-components","tanstack-react-query"],"created_at":"2025-03-12T13:15:27.723Z","updated_at":"2025-03-12T13:15:28.568Z","avatar_url":"https://github.com/arnobt78.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## MixMaster Cocktail Recipes - React-Query App\n\n\u003cimg width=\"1135\" alt=\"Screenshot 2025-02-25 at 15 50 33\" src=\"https://github.com/user-attachments/assets/c878eb2d-4886-46ba-97df-5ef71838916a\" /\u003e\u003cimg width=\"1092\" alt=\"Screenshot 2025-02-25 at 15 50 52\" src=\"https://github.com/user-attachments/assets/d5d5c30d-9d44-4a90-a1f1-78eeaf1aacfe\" /\u003e\u003cimg width=\"992\" alt=\"Screenshot 2025-02-25 at 15 51 37\" src=\"https://github.com/user-attachments/assets/1159851f-44f9-4e07-bedd-3b5d2dcd1fb9\" /\u003e\u003cimg width=\"1057\" alt=\"Screenshot 2025-02-25 at 15 51 55\" src=\"https://github.com/user-attachments/assets/82025192-dfd7-4c3a-8fd6-941b88f42588\" /\u003e\n\nMixMaster is a ReactVite Single-Page Application (SPA) that fetches cocktail recipes from the Cocktails DB API. It provides a seamless, client-side navigation experience using react-router-dom, tanstack/react-query, axios, responsive-web-design and styled-components for styling.\n\n**Online Live:** https://mixmaster-arnob.netlify.app/\n\n## Steps\n\n### Install and Setup\n\n```sh\nnpm install\n```\n\n```sh\nnpm run dev\n```\n\n### SPA\n\nSPA 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.\n\nReact 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.\n\n[React Router](https://reactrouter.com/en/main)\n\n```sh\nnpm i react-router-dom@6.11.2\n```\n\nApp.jsx\n\n```js\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003ch2\u003ehome page\u003c/h2\u003e,\n  },\n  {\n    path: \"/about\",\n    element: (\n      \u003cdiv\u003e\n        \u003ch2\u003eabout page\u003c/h2\u003e\n      \u003c/div\u003e\n    ),\n  },\n]);\nconst App = () =\u003e {\n  return \u003cRouterProvider router={router} /\u003e;\n};\nexport default App;\n```\n\n### Setup Pages\n\n- pages are components\n- create src/pages\n- About, Cocktail, Error, HomeLayout, Landing, Newsletter, index.js\n- export from index.js\n\npages/index.js\n\n```js\nexport { default as Landing } from \"./Landing\";\nexport { default as About } from \"./About\";\nexport { default as Cocktail } from \"./Cocktail\";\nexport { default as Newsletter } from \"./Newsletter\";\nexport { default as HomeLayout } from \"./HomeLayout\";\nexport { default as Error } from \"./Error\";\n```\n\nApp.jsx\n\n```js\nimport {\n  HomeLayout,\n  About,\n  Landing,\n  Error,\n  Newsletter,\n  Cocktail,\n} from \"./pages\";\n```\n\n#### Link Component\n\nHomeLayout.jsx\n\n```js\nimport { Link } from \"react-router-dom\";\nconst HomeLayout = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eHomeLayout\u003c/h1\u003e\n      \u003cLink to=\"/about\"\u003eAbout\u003c/Link\u003e\n    \u003c/div\u003e\n  );\n};\nexport default HomeLayout;\n```\n\nAbout.jsx\n\n```js\nimport { Link } from \"react-router-dom\";\n\nconst About = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eAbout\u003c/h1\u003e\n      \u003cLink to=\"/\"\u003eBack Home\u003c/Link\u003e\n    \u003c/div\u003e\n  );\n};\nexport default About;\n```\n\n#### Nested Pages\n\nApp.jsx\n\n```js\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003cHomeLayout /\u003e,\n    children: [\n      {\n        path: \"landing\",\n        element: \u003cLanding /\u003e,\n      },\n      {\n        path: \"cocktail\",\n        element: \u003cCocktail /\u003e,\n      },\n      {\n        path: \"newsletter\",\n        element: \u003cNewsletter /\u003e,\n      },\n      {\n        path: \"about\",\n        element: \u003cAbout /\u003e,\n      },\n    ],\n  },\n]);\n```\n\nHomeLayout.jsx\n\n```js\nimport { Link, Outlet } from \"react-router-dom\";\nconst HomeLayout = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003cnav\u003enavbar\u003c/nav\u003e\n      \u003cOutlet /\u003e\n    \u003c/div\u003e\n  );\n};\nexport default HomeLayout;\n```\n\nApp.jsx\n\n```js\n{\n  index:true\n  element: \u003cLanding /\u003e,\n}\n```\n\n#### Navbar\n\n- create components/Navbar.jsx\n\nNavbar.jsx\n\n```js\nimport { NavLink } from \"react-router-dom\";\n\nconst Navbar = () =\u003e {\n  return (\n    \u003cnav\u003e\n      \u003cdiv className=\"nav-center\"\u003e\n        \u003cspan className=\"logo\"\u003eMixMaster\u003c/span\u003e\n        \u003cdiv className=\"nav-links\"\u003e\n          \u003cNavLink to=\"/\" className=\"nav-link\"\u003e\n            Home\n          \u003c/NavLink\u003e\n          \u003cNavLink to=\"/about\" className=\"nav-link\"\u003e\n            About\n          \u003c/NavLink\u003e\n          \u003cNavLink to=\"/newsletter\" className=\"nav-link\"\u003e\n            Newsletter\n          \u003c/NavLink\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/nav\u003e\n  );\n};\n\nexport default Navbar;\n```\n\n- setup in HomeLayout\n\n#### Styled Components\n\n- CSS in JS\n- Styled Components\n- have logic and styles in component\n- no name collisions\n- apply javascript logic\n- [Styled Components Docs](https://styled-components.com/)\n- [Styled Components Course](https://www.udemy.com/course/styled-components-tutorial-and-project-course/?referralCode=9DABB172FCB2625B663F)\n\n```sh\nnpm install styled-components\n```\n\n```js\nimport styled from \"styled-components\";\n\nconst El = styled.el`\n  // styles go here\n`;\n```\n\n- no name collisions, since unique class\n- vscode-styled-components extension\n- colors and bugs\n\n```js\nimport styled from \"styled-components\";\nconst StyledBtn = styled.button`\n  background: red;\n  color: white;\n  font-size: 2rem;\n  padding: 1rem;\n`;\n```\n\n#### Alternative Setup\n\n- style entire react component\n\n```js\nconst Wrapper = styled.el``;\n\nconst Component = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003ch1\u003e Component\u003c/h1\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n```\n\n- only responsible for styling\n\n#### Assets\n\n- wrappers folder in assets\n\nNavbar.jsx\n\n```js\nimport { NavLink } from \"react-router-dom\";\nimport styled from \"styled-components\";\n\nconst Navbar = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv className=\"nav-center\"\u003e\n        \u003cspan className=\"logo\"\u003eMixMaster\u003c/span\u003e\n        \u003cdiv className=\"nav-links\"\u003e\n          \u003cNavLink to=\"/\" className=\"nav-link\"\u003e\n            Home\n          \u003c/NavLink\u003e\n          \u003cNavLink to=\"/about\" className=\"nav-link\"\u003e\n            About\n          \u003c/NavLink\u003e\n          \u003cNavLink to=\"/newsletter\" className=\"nav-link\"\u003e\n            Newsletter\n          \u003c/NavLink\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nconst Wrapper = styled.nav`\n  background: var(--white);\n  .nav-center {\n    width: var(--view-width);\n    max-width: var(--max-width);\n    margin: 0 auto;\n    display: flex;\n    flex-direction: column;\n    padding: 1.5rem 2rem;\n  }\n\n  .logo {\n    font-size: clamp(1.5rem, 3vw, 3rem);\n    color: var(--primary-500);\n    font-weight: 700;\n    letter-spacing: 2px;\n  }\n  .nav-links {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n    margin-top: 1rem;\n  }\n  .nav-link {\n    color: var(--grey-900);\n    padding: 0.5rem 0.5rem 0.5rem 0;\n    transition: var(--transition);\n    letter-spacing: 1px;\n  }\n  .nav-link:hover {\n    color: var(--primary-500);\n  }\n  .active {\n    color: var(--primary-500);\n  }\n\n  @media (min-width: 768px) {\n    .nav-center {\n      flex-direction: row;\n      justify-content: space-between;\n      align-items: center;\n    }\n    .nav-links {\n      flex-direction: row;\n      margin-top: 0;\n    }\n  }\n`;\n\nexport default Navbar;\n```\n\n#### About Page\n\nAbout.jsx\n\n```jsx\nimport Wrapper from \"../assets/wrappers/AboutPage\";\n\nconst About = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003ch3\u003eAbout Us\u003c/h3\u003e\n      \u003cp\u003e\n        Introducing \"MixMaster,\" the ultimate party sidekick app that fetches\n        cocktails from the hilarious Cocktails DB API. With a flick of your\n        finger, you'll unlock a treasure trove of enchanting drink recipes\n        that'll make your taste buds dance and your friends jump with joy. Get\n        ready to shake up your mixology game, one fantastical mocktail at a\n        time, and let the laughter and giggles flow!\n      \u003c/p\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default About;\n```\n\n#### Page CSS\n\nHomeLayout.jsx\n\n```js\nimport { Link, Outlet } from \"react-router-dom\";\nimport Navbar from \"../components/Navbar\";\nconst HomeLayout = () =\u003e {\n  return (\n    \u003c\u003e\n      \u003cNavbar /\u003e\n      \u003csection className=\"page\"\u003e\n        \u003cOutlet /\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\nexport default HomeLayout;\n```\n\nindex.css\n\n```css\n.page {\n  width: var(--view-width);\n  max-width: var(--max-width);\n  margin: 0 auto;\n  padding: 5rem 2rem;\n}\n```\n\n### Error Page\n\n- wrong url\n\nError.jsx\n\n```js\nimport Wrapper from \"../assets/wrappers/ErrorPage\";\nimport { Link, useRouteError } from \"react-router-dom\";\nimport img from \"../assets/not-found.svg\";\n\nconst Error = () =\u003e {\n  const error = useRouteError();\n  console.log(error);\n  if (error.status === 404) {\n    return (\n      \u003cWrapper\u003e\n        \u003cdiv\u003e\n          \u003cimg src={img} alt=\"not found\" /\u003e\n          \u003ch3\u003eOhh! \u003c/h3\u003e\n          \u003cp\u003eWe can't seem to find the page you're looking for\u003c/p\u003e\n          \u003cLink to=\"/\"\u003eback home\u003c/Link\u003e\n        \u003c/div\u003e\n      \u003c/Wrapper\u003e\n    );\n  }\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv\u003e\n        \u003ch3\u003esomething went wrong\u003c/h3\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Error;\n```\n\n#### Error Page - CSS (optional)\n\nassets/wrappers/ErrorPage.js\n\n```js\nimport styled from \"styled-components\";\n\nconst Wrapper = styled.div`\n  min-height: 100vh;\n  text-align: center;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  img {\n    width: 90vw;\n    max-width: 600px;\n    display: block;\n    margin-bottom: 2rem;\n    margin-top: -3rem;\n  }\n  h3 {\n    margin-bottom: 0.5rem;\n  }\n\n  p {\n    line-height: 1.5;\n    margin-top: 0.5rem;\n    margin-bottom: 1rem;\n    color: var(--grey-500);\n  }\n  a {\n    color: var(--primary-500);\n    text-transform: capitalize;\n  }\n`;\n\nexport default Wrapper;\n```\n\n### Fetch\n\n- useEffect approach\n\nLanding.jsx\n\n```js\nconst fetchSomething = async () =\u003e {\n  try {\n    const response = await axios.get(\"/someUrl\");\n    console.log(response.data);\n  } catch (error) {\n    console.error(error);\n  }\n};\n\nuseEffect(() =\u003e {\n  fetchSomething();\n}, []);\n```\n\n### Loader\n\nEach route can define a \"loader\" function to provide data to the route element before it renders.\n\n- must return something even \"null\" otherwise error\n\nLanding.jsx\n\n```js\nimport { useLoaderData } from \"react-router-dom\";\n\nexport const loader = async () =\u003e {\n  return \"something\";\n};\n\nconst Landing = () =\u003e {\n  const data = useLoaderData();\n  console.log(data);\n  return \u003ch1\u003eLanding\u003c/h1\u003e;\n};\nexport default Landing;\n```\n\n```js\nimport { loader as landingLoader } from './pages/Landing.jsx';\n\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n    errorElement:\u003cError/\u003e\n    children: [\n      {\n        index: true,\n        loader: landingLoader,\n        element: \u003cLanding /\u003e,\n      },\n      // alternative approach\n      {\n        index: true,\n        loader: () =\u003e {\n          // do stuff here\n        },\n        element: \u003cLanding /\u003e,\n\n      },\n      // rest of the routes\n    ],\n  },\n]);\n```\n\n### TheCocktailDB\n\n[API](https://www.thecocktaildb.com/)\n\n- Search cocktail by name\n  www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita\n- Lookup full cocktail details by id\n  www.thecocktaildb.com/api/json/v1/1/lookup.php?i=11007\n\n### Landing - Fetch Drinks\n\nLanding.jsx\n\n```js\nimport { useLoaderData } from \"react-router-dom\";\nimport axios from \"axios\";\n\nconst cocktailSearchUrl =\n  \"https://www.thecocktaildb.com/api/json/v1/1/search.php?s=\";\n\nexport const loader = async () =\u003e {\n  const searchTerm = \"margarita\";\n  const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`);\n  return { drinks: response.data.drinks, searchTerm };\n};\n\nconst Landing = () =\u003e {\n  const { searchTerm, drinks } = useLoaderData();\n  console.log(drinks);\n  return \u003ch1\u003eLanding page\u003c/h1\u003e;\n};\n\nexport default Landing;\n```\n\n- empty search term returns some default drinks\n- if search term yields not drinks drinks:null\n\n### More Errors\n\n- bubbles up\n- no return from loader\n- wrong url\n\nApp.jsx\n\n```js\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    children: [\n      {\n        index: true,\n        loader: landingLoader,\n        errorElement: \u003ch2\u003eThere was an error...\u003c/h2\u003e,\n        element: \u003cLanding /\u003e,\n      },\n    ],\n  },\n]);\n```\n\n### SinglePageError Component\n\n- create pages/SinglePageError.jsx\n- export import (index.js)\n- use it in App.jsx\n\n```js\nimport { useRouteError } from \"react-router-dom\";\nconst SinglePageError = () =\u003e {\n  const error = useRouteError();\n  console.log(error);\n  return \u003ch2\u003e{error.message}\u003c/h2\u003e;\n};\nexport default SinglePageError;\n```\n\n### More Components\n\n- in src/components create SearchForm, CocktailList, CocktailCard\n- render SearchForm and CocktailList in Landing\n- pass drinks, iterate over and render in CocktailCard\n\nLanding.jsx\n\n```js\nconst Landing = () =\u003e {\n  const { searchTerm, drinks } = useLoaderData();\n\n  return (\n    \u003c\u003e\n      \u003cSearchForm /\u003e\n      \u003cCocktailList drinks={drinks} /\u003e\n    \u003c/\u003e\n  );\n};\n```\n\nCocktailList.jsx\n\n```jsx\nimport CocktailCard from \"./CocktailCard\";\nimport Wrapper from \"../assets/wrappers/CocktailList\";\nconst CocktailList = ({ drinks }) =\u003e {\n  if (!drinks) {\n    return (\n      \u003ch4 style={{ textAlign: \"center\" }}\u003eNo matching cocktails found...\u003c/h4\u003e\n    );\n  }\n\n  const formattedDrinks = drinks.map((item) =\u003e {\n    const { idDrink, strDrink, strDrinkThumb, strAlcoholic, strGlass } = item;\n    return {\n      id: idDrink,\n      name: strDrink,\n      image: strDrinkThumb,\n      info: strAlcoholic,\n      glass: strGlass,\n    };\n  });\n  return (\n    \u003cWrapper\u003e\n      {formattedDrinks.map((item) =\u003e {\n        return \u003cCocktailCard key={item.id} {...item} /\u003e;\n      })}\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default CocktailList;\n```\n\n```jsx\nimport { Link, useOutletContext } from \"react-router-dom\";\nimport Wrapper from \"../assets/wrappers/CocktailCard\";\nconst CocktailCard = ({ image, name, id, info, glass }) =\u003e {\n  // const data = useOutletContext();\n  // console.log(data);\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv className=\"img-container\"\u003e\n        \u003cimg src={image} alt={name} className=\"img\" /\u003e\n      \u003c/div\u003e\n      \u003cdiv className=\"footer\"\u003e\n        \u003ch4\u003e{name}\u003c/h4\u003e\n        \u003ch5\u003e{glass}\u003c/h5\u003e\n        \u003cp\u003e{info}\u003c/p\u003e\n\n        \u003cLink to={`/cocktail/${id}`} className=\"btn\"\u003e\n          details\n        \u003c/Link\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default CocktailCard;\n```\n\n### CocktailList and CocktailCard CSS (optional)\n\n### Global Loading and Context\n\nHomeLayout.jsx\n\n```js\nimport { Outlet } from \"react-router-dom\";\nimport Navbar from \"../components/Navbar\";\nimport { useNavigation } from \"react-router-dom\";\nconst HomeLayout = () =\u003e {\n  const navigation = useNavigation();\n  const isPageLoading = navigation.state === \"loading\";\n  const value = \"some value\";\n  return (\n    \u003c\u003e\n      \u003cNavbar /\u003e\n      \u003csection className=\"page\"\u003e\n        {isPageLoading ? (\n          \u003cdiv className=\"loading\" /\u003e\n        ) : (\n          \u003cOutlet context={{ value }} /\u003e\n        )}\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\nexport default HomeLayout;\n```\n\n### Single Cocktail\n\nApp.jsx\n\n```js\nimport { loader as singleCocktailLoader } from \"./pages/Cocktail\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    children: [\n      {\n        path: \"cocktail/:id\",\n        loader: singleCocktailLoader,\n        element: \u003cCocktail /\u003e,\n        errorElement: \u003cSinglePageError /\u003e,\n      },\n      // rest of the routes\n    ],\n  },\n]);\n```\n\nCocktail.jsx\n\n```js\nconst singleCocktailUrl =\n  \"https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=\";\nimport { useLoaderData, Link } from \"react-router-dom\";\nimport axios from \"axios\";\n\nimport Wrapper from \"../assets/wrappers/CocktailPage\";\n\nexport const loader = async ({ params }) =\u003e {\n  const { id } = params;\n  const { data } = await axios.get(`${singleCocktailUrl}${id}`);\n  return { id, data };\n};\n\nconst Cocktail = () =\u003e {\n  const { id, data } = useLoaderData();\n\n  const singleDrink = data.drinks[0];\n  const {\n    strDrink: name,\n    strDrinkThumb: image,\n    strAlcoholic: info,\n    strCategory: category,\n    strGlass: glass,\n    strInstructions: instructions,\n  } = singleDrink;\n  const validIngredients = Object.keys(singleDrink)\n    .filter(\n      (key) =\u003e key.startsWith(\"strIngredient\") \u0026\u0026 singleDrink[key] !== null\n    )\n    .map((key) =\u003e singleDrink[key]);\n\n  return (\n    \u003cWrapper\u003e\n      \u003cheader\u003e\n        \u003cLink to=\"/\" className=\"btn\"\u003e\n          back home\n        \u003c/Link\u003e\n        \u003ch3\u003e{name}\u003c/h3\u003e\n      \u003c/header\u003e\n      \u003cdiv className=\"drink\"\u003e\n        \u003cimg src={image} alt={name} className=\"img\"\u003e\u003c/img\u003e\n        \u003cdiv className=\"drink-info\"\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003ename :\u003c/span\u003e {name}\n          \u003c/p\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003ecategory :\u003c/span\u003e {category}\n          \u003c/p\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003einfo :\u003c/span\u003e {info}\n          \u003c/p\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003eglass :\u003c/span\u003e {glass}\n          \u003c/p\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003eingredients :\u003c/span\u003e\n            {validIngredients.map((item, index) =\u003e {\n              return (\n                \u003cspan className=\"ing\" key={item}\u003e\n                  {item} {index \u003c validIngredients.length - 1 ? \",\" : \"\"}\n                \u003c/span\u003e\n              );\n            })}\n          \u003c/p\u003e\n          \u003cp\u003e\n            \u003cspan className=\"drink-data\"\u003einstructions :\u003c/span\u003e {instructions}\n          \u003c/p\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Cocktail;\n```\n\n### Additional Check\n\n```js\nconst Cocktail = () =\u003e {\n  import { Navigate } from \"react-router-dom\";\n  const { id, data } = useLoaderData();\n  // if (!data) return \u003ch2\u003esomething went wrong...\u003c/h2\u003e;\n  if (!data) return \u003cNavigate to=\"/\" /\u003e;\n  return \u003cWrapper\u003e....\u003c/Wrapper\u003e;\n};\n```\n\n### Single Cocktail CSS (optional)\n\nassets/wrappers/CocktailPage.js\n\n```js\nimport styled from \"styled-components\";\n\nconst Wrapper = styled.div`\n  header {\n    text-align: center;\n    margin-bottom: 3rem;\n    .btn {\n      margin-bottom: 1rem;\n    }\n  }\n\n  .img {\n    border-radius: var(--borderRadius);\n  }\n  .drink-info {\n    padding-top: 2rem;\n  }\n\n  .drink p {\n    font-weight: 700;\n    text-transform: capitalize;\n    line-height: 2;\n    margin-bottom: 1rem;\n  }\n  .drink-data {\n    margin-right: 0.5rem;\n    background: var(--primary-300);\n    padding: 0.25rem 0.5rem;\n    border-radius: var(--borderRadius);\n    color: var(--primary-700);\n    letter-spacing: var(--letterSpacing);\n  }\n\n  .ing {\n    display: inline-block;\n    margin-right: 0.5rem;\n  }\n  @media screen and (min-width: 992px) {\n    .drink {\n      display: grid;\n      grid-template-columns: 2fr 3fr;\n      gap: 3rem;\n      align-items: center;\n    }\n    .drink-info {\n      padding-top: 0;\n    }\n  }\n`;\n\nexport default Wrapper;\n```\n\n### Setup React Toastify\n\nmain.jsx\n\n```js\nimport \"react-toastify/dist/ReactToastify.css\";\nimport { ToastContainer } from \"react-toastify\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  \u003cReact.StrictMode\u003e\n    \u003cToastContainer position=\"top-center\" autoClose={2000} /\u003e\n    \u003cApp /\u003e\n  \u003c/React.StrictMode\u003e\n);\n```\n\n### Newsletter\n\nNewsletter.jsx\n\n```js\nconst Newsletter = () =\u003e {\n  return (\n    \u003cform className=\"form\"\u003e\n      \u003ch4 style={{ textAlign: \"center\", marginBottom: \"2rem\" }}\u003e\n        our newsletter\n      \u003c/h4\u003e\n      {/* name */}\n      \u003cdiv className=\"form-row\"\u003e\n        \u003clabel htmlFor=\"name\" className=\"form-label\"\u003e\n          name\n        \u003c/label\u003e\n        \u003cinput\n          type=\"text\"\n          className=\"form-input\"\n          name=\"name\"\n          id=\"name\"\n          defaultValue=\"john\"\n        /\u003e\n      \u003c/div\u003e\n      {/* last name */}\n      \u003cdiv className=\"form-row\"\u003e\n        \u003clabel htmlFor=\"lastName\" className=\"form-label\"\u003e\n          last name\n        \u003c/label\u003e\n        \u003cinput\n          type=\"text\"\n          className=\"form-input\"\n          name=\"lastName\"\n          id=\"lastName\"\n          defaultValue=\"smith\"\n        /\u003e\n      \u003c/div\u003e\n      {/* name */}\n      \u003cdiv className=\"form-row\"\u003e\n        \u003clabel htmlFor=\"email\" className=\"form-label\"\u003e\n          email\n        \u003c/label\u003e\n        \u003cinput\n          type=\"email\"\n          className=\"form-input\"\n          name=\"email\"\n          id=\"email\"\n          defaultValue=\"test@test.com\"\n        /\u003e\n      \u003c/div\u003e\n      \u003cbutton\n        type=\"submit\"\n        className=\"btn btn-block\"\n        style={{ marginTop: \"0.5rem\" }}\n      \u003e\n        submit\n      \u003c/button\u003e\n    \u003c/form\u003e\n  );\n};\n\nexport default Newsletter;\n```\n\n### Default Behavior\n\nThe \"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:\n\nGET: 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.\n\nPOST: 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.\n\n- action attribute\n\n  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.\n\nIf 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.\n\n### FormData API\n\n- covered in React fundamentals\n  [JS Nuggets - FormData API](https://youtu.be/5-x4OUM-SP8)\n\n- a great solution when you have bunch of inputs\n- inputs must have name attribute\n\nThe 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\".\n\n### React Router - Action\n\nRoute 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.\n\nNewsletter.jsx\n\n```js\nimport { Form } from 'react-router-dom';\n\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n  console.log(data);\n  return 'something';\n};\n\nconst Newsletter = () =\u003e {\n  return (\n    \u003cForm className='form' method='POST'\u003e\n    .....)\n}\n```\n\nApp.jsx\n\n```js\nimport { action as newsletterAction } from \"./pages/Newsletter\";\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    children: [\n      {\n        path: \"newsletter\",\n        action: newsletterAction,\n        element: \u003cNewsletter /\u003e,\n      },\n    ],\n  },\n]);\n```\n\n### Newsletter Request\n\nconst newsletterUrl = 'https://www.course-api.com/cocktails-newsletter';\n\nNewsletter.jsx\n\n```js\nimport { Form, redirect } from \"react-router-dom\";\nimport axios from \"axios\";\nimport { toast } from \"react-toastify\";\n\nconst newsletterUrl = \"https://www.course-api.com/cocktails-newsletter\";\n\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n\n  const response = await axios.post(newsletterUrl, data);\n  console.log(response);\n  return response;\n};\n```\n\n### Try/Catch\n\nNewsletter.jsx\n\n```js\nimport { redirect } from \"react-router-dom\";\nimport { toast } from \"react-toastify\";\n\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n  try {\n    const response = await axios.post(newsletterUrl, data);\n    console.log(response);\n    toast.success(response.data.msg);\n    return redirect(\"/\");\n  } catch (error) {\n    console.log(error);\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n```\n\n### Submit State\n\nNewsletter.jsx\n\n```js\nimport { Form, useNavigation } from \"react-router-dom\";\n\nconst Newsletter = () =\u003e {\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === \"submitting\";\n  return (\n    \u003cForm className=\"form\" method=\"POST\"\u003e\n      ....\n      \u003cbutton\n        type=\"submit\"\n        className=\"btn btn-block\"\n        style={{ marginTop: \"0.5rem\" }}\n        disabled={isSubmitting}\n      \u003e\n        {isSubmitting ? \"submitting...\" : \"submit\"}\n      \u003c/button\u003e\n    \u003c/Form\u003e\n  );\n};\n```\n\n### Attributes\n\n- remove defaultValue and add required\n- cover required and defaultValue\n\n### Search Form - Setup\n\ncomponents/SearchForm.jsx\n\n```js\nimport { Form, useNavigation } from \"react-router-dom\";\nimport Wrapper from \"../assets/wrappers/SearchForm\";\nconst SearchForm = () =\u003e {\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === \"submitting\";\n  return (\n    \u003cWrapper\u003e\n      \u003cForm className=\"form\"\u003e\n        \u003cinput\n          type=\"search\"\n          name=\"search\"\n          className=\"form-input\"\n          defaultValue=\"vodka\"\n        /\u003e\n        \u003cbutton type=\"submit\" className=\"btn\" disabled={isSubmitting}\u003e\n          {isSubmitting ? \"searching...\" : \"search\"}\n        \u003c/button\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default SearchForm;\n```\n\n### Query Params\n\nLanding.jsx\n\n```js\nexport const loader = async ({ request }) =\u003e {\n  const url = new URL(request.url);\n  const searchTerm = url.searchParams.get(\"search\") || \"\";\n  const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`);\n  return { drinks: response.data.drinks, searchTerm };\n};\n```\n\nconst url = new URL(request.url);\nThis 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.\n\nThe 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.\n\nconst searchTerm = url.searchParams.get('search') || '';\nThis 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.\n\nThe 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 (||).\n\n### Controlled Input (kinda/sorta)\n\nLanding.js\n\n```js\nconst Landing = () =\u003e {\n  const { searchTerm, drinks } = useLoaderData();\n\n  return (\n    \u003c\u003e\n      \u003cSearchForm searchTerm={searchTerm} /\u003e\n      \u003cCocktailList drinks={drinks} /\u003e\n    \u003c/\u003e\n  );\n};\n```\n\nSearchForm.jsx\n\n```js\nconst SearchForm = ({ searchTerm }) =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cForm className=\"form\"\u003e\n        \u003cinput\n          type=\"search\"\n          name=\"search\"\n          className=\"form-input\"\n          defaultValue={searchTerm}\n        /\u003e\n        .....\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default SearchForm;\n```\n\n### React Query - Setup\n\nApp.jsx\n\n```js\nimport { createBrowserRouter, RouterProvider } from 'react-router-dom';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60 * 5,\n    },\n  },\n});\n...\nconst App = () =\u003e {\n  return (\n    \u003cQueryClientProvider client={queryClient}\u003e\n      \u003cRouterProvider router={router} /\u003e\n      \u003cReactQueryDevtools initialIsOpen={false} /\u003e\n    \u003c/QueryClientProvider\u003e\n  );\n};\nexport default App;\n\n```\n\n### Important Update !!!\n\nSince the API does not return drinks with empty searchTerm, code below contains additional logic !!!\n\n### React Query - Landing Page\n\nLanding.jsx\n\n```js\nimport { useQuery } from \"@tanstack/react-query\";\n\nconst searchCocktailsQuery = (searchTerm) =\u003e {\n  return {\n    queryKey: [\"search\", searchTerm || \"all\"],\n    queryFn: async () =\u003e {\n      // Default to 'a' if no search term is provided since API has changed\n      searchTerm = searchTerm || \"a\";\n\n      const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`);\n      return response.data.drinks;\n    },\n  };\n};\n\nexport const loader = async ({ request }) =\u003e {\n  const url = new URL(request.url);\n  const searchTerm = url.searchParams.get(\"search\") || \"\";\n  // const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`);\n  return { searchTerm };\n};\n\nconst Landing = () =\u003e {\n  const { searchTerm } = useLoaderData();\n  const { data: drinks } = useQuery(searchCocktailsQuery(searchTerm));\n  return (\n    \u003c\u003e\n      \u003cSearchForm searchTerm={searchTerm} /\u003e\n      \u003cCocktailList drinks={drinks} /\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default Landing;\n```\n\n### React Query - Landing Page Loader\n\nApp.jsx\n\n```js\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    children: [\n      {\n        index: true,\n        loader: landingLoader(queryClient),\n        element: \u003cLanding /\u003e,\n      },\n    ],\n  },\n]);\n```\n\nLanding.jsx\n\n```js\nexport const loader =\n  (queryClient) =\u003e\n  async ({ request }) =\u003e {\n    const url = new URL(request.url);\n    const searchTerm = url.searchParams.get(\"search\") || \"\";\n    await queryClient.ensureQueryData(searchCocktailsQuery(searchTerm));\n    // const response = await axios.get(`${cocktailSearchUrl}${searchTerm}`);\n    return { searchTerm };\n  };\n```\n\n### React Query - Cocktail\n\nApp.jsx\n\n```js\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    children: [\n    ....\n      {\n        path: 'cocktail/:id',\n        loader: singleCocktailLoader(queryClient),\n        errorElement: \u003ch2\u003eThere was an error...\u003c/h2\u003e,\n        element: \u003cCocktail /\u003e,\n      },\n      ....\n    ],\n  },\n]);\n```\n\nCocktail.jsx\n\n```js\nimport { useQuery } from \"@tanstack/react-query\";\nimport Wrapper from \"../assets/wrappers/CocktailPage\";\nimport { useLoaderData, Link } from \"react-router-dom\";\nimport axios from \"axios\";\n\nconst singleCocktailUrl =\n  \"https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=\";\n\nconst singleCocktailQuery = (id) =\u003e {\n  return {\n    queryKey: [\"cocktail\", id],\n    queryFn: async () =\u003e {\n      const { data } = await axios.get(`${singleCocktailUrl}${id}`);\n      return data;\n    },\n  };\n};\n\nexport const loader =\n  (queryClient) =\u003e\n  async ({ params }) =\u003e {\n    const { id } = params;\n    await queryClient.ensureQueryData(singleCocktailQuery(id));\n    return { id };\n  };\n\nconst Cocktail = () =\u003e {\n  const { id } = useLoaderData();\n  const { data } = useQuery(singleCocktailQuery(id));\n  // rest of the code\n};\n```\n\n### Redirects\n\n- in public folder create \"\\_redirects\"\n\n```\n/* /index.html 200\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farnobt78%2Fmixmaster-cocktail-recipes--react-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farnobt78%2Fmixmaster-cocktail-recipes--react-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farnobt78%2Fmixmaster-cocktail-recipes--react-query/lists"}