{"id":25073456,"url":"https://github.com/bourgui07/jobify","last_synced_at":"2025-09-02T16:35:28.965Z","repository":{"id":274602402,"uuid":"923196650","full_name":"BOURGUI07/jobify","owner":"BOURGUI07","description":null,"archived":false,"fork":false,"pushed_at":"2025-01-28T19:08:16.000Z","size":268,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-06T23:22:34.670Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/BOURGUI07.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-01-27T19:57:30.000Z","updated_at":"2025-01-28T19:08:18.000Z","dependencies_parsed_at":"2025-01-28T10:24:34.840Z","dependency_job_id":"32b0a9fc-c359-44ee-9c59-ddcfc1e0c825","html_url":"https://github.com/BOURGUI07/jobify","commit_stats":null,"previous_names":["bourgui07/jobify"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BOURGUI07%2Fjobify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BOURGUI07%2Fjobify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BOURGUI07%2Fjobify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BOURGUI07%2Fjobify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BOURGUI07","download_url":"https://codeload.github.com/BOURGUI07/jobify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246523437,"owners_count":20791438,"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":[],"created_at":"2025-02-06T23:20:37.561Z","updated_at":"2025-03-31T18:46:50.853Z","avatar_url":"https://github.com/BOURGUI07.png","language":"JavaScript","readme":"#### Complete App\n\n[Jobify](https://jobify.live/)\n\n#### Create React APP\n\n[VITE](https://vitejs.dev/guide/)\n\n```sh\nnpm create vite@latest projectName -- --template react\n```\n\n#### Vite - Folder and File Structure\n\n```sh\nnpm i\n```\n\n```sh\nnpm run dev\n```\n\n- APP running on http://localhost:5173/\n- .jsx extension\n\n#### Remove Boilerplate\n\n- remove App.css\n- remove all code in index.css\n\n  App.jsx\n\n```jsx\nconst App = () =\u003e {\n  return \u003ch1\u003eJobify App\u003c/h1\u003e;\n};\nexport default App;\n```\n\n#### Project Assets\n\n- get assets folder from complete project\n- copy index.css\n- copy/move README.md (steps)\n  - work independently\n  - reference\n  - troubleshoot\n  - copy\n\n#### Global Styles\n\n- saves times on the setup\n- less lines of css\n- speeds up the development\n\n- if any questions about specific styles\n- Coding Addict - [Default Starter Video](https://youtu.be/UDdyGNlQK5w)\n- Repo - [Default Starter Repo](https://github.com/john-smilga/default-starter)\n\n#### Title and Favicon\n\n- add favicon.ico in public\n- change title and favicon in index.html\n\n```html\n\u003chead\u003e\n  \u003clink rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.ico\" /\u003e\n  \u003ctitle\u003eJobify\u003c/title\u003e\n\u003c/head\u003e\n```\n\n- resource [Generate Favicons](https://favicon.io/)\n\n#### Install Packages (Optional)\n\n- yes, specific package versions\n- specific commands will be provided later\n- won't need to stop/start server\n\n```sh\nnpm install @tanstack/react-query@4.29.5 @tanstack/react-query-devtools@4.29.6 axios@1.3.6 dayjs@1.11.7 react-icons@4.8.0 react-router-dom@6.10.0 react-toastify@9.1.2 recharts@2.5.0 styled-components@5.3.10\n\n```\n\n#### Router\n\n[React Router](https://reactrouter.com/en/main)\n\n- version 6.4 brought significant changes (loader and action)\n- pages as independent entities\n- less need for global state\n- more pages\n\n#### Setup Router\n\n- all my examples will include version !!!\n\n```sh\nnpm i react-router-dom@6.10.0\n```\n\nApp.jsx\n\n```jsx\nimport { createBrowserRouter, RouterProvider } from 'react-router-dom';\n\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: \u003ch1\u003ehome\u003c/h1\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]);\n\nconst App = () =\u003e {\n  return \u003cRouterProvider router={router} /\u003e;\n};\nexport default App;\n```\n\n#### Create Pages\n\n- create src/pages directory\n- setup index.js and following pages :\n\n  AddJob.jsx\n  Admin.jsx\n  AllJobs.jsx\n  DashboardLayout.jsx\n  DeleteJob.jsx\n  EditJob.jsx\n  Error.jsx\n  HomeLayout.jsx\n  Landing.jsx\n  Login.jsx\n  Profile.jsx\n  Register.jsx\n  Stats.jsx\n\n```jsx\nconst AddJob = () =\u003e {\n  return \u003ch1\u003eAddJob\u003c/h1\u003e;\n};\nexport default AddJob;\n```\n\n#### Index\n\nApp.jsx\n\n```jsx\nimport HomeLayout from '../ pages/HomeLayout';\n```\n\npages/index.js\n\n```js\nexport { default as DashboardLayout } from './DashboardLayout';\nexport { default as Landing } from './Landing';\nexport { default as HomeLayout } from './HomeLayout';\nexport { default as Register } from './Register';\nexport { default as Login } from './Login';\nexport { default as Error } from './Error';\nexport { default as Stats } from './Stats';\nexport { default as AllJobs } from './AllJobs';\nexport { default as AddJob } from './AddJob';\nexport { default as EditJob } from './EditJob';\nexport { default as Profile } from './Profile';\nexport { default as Admin } from './Admin';\n```\n\nApp.jsx\n\n```jsx\nimport {\n  HomeLayout,\n  Landing,\n  Register,\n  Login,\n  DashboardLayout,\n  Error,\n} from './pages';\n\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n  },\n  {\n    path: '/register',\n    element: \u003cRegister /\u003e,\n  },\n  {\n    path: '/login',\n    element: \u003cLogin /\u003e,\n  },\n  {\n    path: '/dashboard',\n    element: \u003cDashboardLayout /\u003e,\n  },\n]);\n```\n\n#### Link Component\n\n- navigate around project\n- client side routing\n\nRegister.jsx\n\n```jsx\nimport { Link } from 'react-router-dom';\n\nconst Register = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eRegister\u003c/h1\u003e\n      \u003cLink to='/login'\u003eLogin Page\u003c/Link\u003e\n    \u003c/div\u003e\n  );\n};\nexport default Register;\n```\n\nLogin.jsx\n\n```jsx\nimport { Link } from 'react-router-dom';\n\nconst Login = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eLogin\u003c/h1\u003e\n      \u003cLink to='/register'\u003eRegister Page\u003c/Link\u003e\n    \u003c/div\u003e\n  );\n};\nexport default Login;\n```\n\n#### Nested Routes\n\n- what about Navbar?\n- decide on root (parent route)\n- make path relative\n- for time being only home layout will be visible\n\nApp.jsx\n\n```jsx\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n    children: [\n      {\n        path: 'register',\n        element: \u003cRegister /\u003e,\n      },\n      {\n        path: 'login',\n        element: \u003cLogin /\u003e,\n      },\n      {\n        path: 'dashboard',\n        element: \u003cDashboardLayout /\u003e,\n      },\n    ],\n  },\n]);\n```\n\nHomeLayout.jsx\n\n```jsx\nimport { Outlet } from 'react-router-dom';\n\nconst HomeLayout = () =\u003e {\n  return (\n    \u003c\u003e\n      {/* add things like Navbar */}\n      {/* \u003ch1\u003ehome layout\u003c/h1\u003e */}\n      \u003cOutlet /\u003e\n    \u003c/\u003e\n  );\n};\nexport default HomeLayout;\n```\n\n#### Index (Home) Page\n\nApp.jsx\n\n```jsx\n{\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n    children: [\n      {\n        index: true,\n        element: \u003cLanding /\u003e,\n      },\n...\n      ]\n}\n```\n\n#### Error Page\n\n- bubbles up\n\nApp.jsx\n\n```jsx\n{\n    path: '/',\n    element: \u003cHomeLayout /\u003e,\n    errorElement: \u003cError /\u003e,\n    ...\n}\n```\n\nError.jsx\n\n```jsx\nimport { Link, useRouteError } from 'react-router-dom';\n\nconst Error = () =\u003e {\n  const error = useRouteError();\n  console.log(error);\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eError Page !!!\u003c/h1\u003e\n      \u003cLink to='/dashboard'\u003eback home\u003c/Link\u003e\n    \u003c/div\u003e\n  );\n};\nexport default Error;\n```\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@5.3.10\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\nLanding.jsx\n\n```jsx\nimport styled from 'styled-components';\n\nconst Landing = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eLanding\u003c/h1\u003e\n      \u003cStyledButton\u003eClick Me\u003c/StyledButton\u003e\n    \u003c/div\u003e\n  );\n};\n\nconst StyledButton = styled.button`\n  background-color: red;\n  color: white;\n`;\nexport default Landing;\n```\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- wrappers folder in assets\n\nLanding.jsx\n\n```jsx\nimport styled from 'styled-components';\n\nconst Landing = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003ch1\u003eLanding\u003c/h1\u003e\n      \u003cdiv className='content'\u003esome content\u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nconst Wrapper = styled.div`\n  background-color: red;\n  h1 {\n    color: white;\n  }\n  .content {\n    background-color: blue;\n    color: yellow;\n  }\n`;\nexport default Landing;\n```\n\n#### Landing Page\n\n```jsx\nimport main from '../assets/images/main.svg';\nimport { Link } from 'react-router-dom';\nimport logo from '../assets/images/logo.svg';\nimport styled from 'styled-components';\nconst Landing = () =\u003e {\n  return (\n    \u003cStyledWrapper\u003e\n      \u003cnav\u003e\n        \u003cimg src={logo} alt='jobify' className='logo' /\u003e\n      \u003c/nav\u003e\n      \u003cdiv className='container page'\u003e\n        {/* info */}\n        \u003cdiv className='info'\u003e\n          \u003ch1\u003e\n            job \u003cspan\u003etracking\u003c/span\u003e app\n          \u003c/h1\u003e\n          \u003cp\u003e\n            I'm baby wayfarers hoodie next level taiyaki brooklyn cliche blue\n            bottle single-origin coffee chia. Aesthetic post-ironic venmo,\n            quinoa lo-fi tote bag adaptogen everyday carry meggings +1 brunch\n            narwhal.\n          \u003c/p\u003e\n          \u003cLink to='/register' className='btn register-link'\u003e\n            Register\n          \u003c/Link\u003e\n          \u003cLink to='/login' className='btn'\u003e\n            Login / Demo User\n          \u003c/Link\u003e\n        \u003c/div\u003e\n        \u003cimg src={main} alt='job hunt' className='img main-img' /\u003e\n      \u003c/div\u003e\n    \u003c/StyledWrapper\u003e\n  );\n};\n\nconst StyledWrapper = styled.section`\n  nav {\n    width: var(--fluid-width);\n    max-width: var(--max-width);\n    margin: 0 auto;\n    height: var(--nav-height);\n    display: flex;\n    align-items: center;\n  }\n  .page {\n    min-height: calc(100vh - var(--nav-height));\n    display: grid;\n    align-items: center;\n    margin-top: -3rem;\n  }\n  h1 {\n    font-weight: 700;\n    span {\n      color: var(--primary-500);\n    }\n    margin-bottom: 1.5rem;\n  }\n  p {\n    line-height: 2;\n    color: var(--text-secondary-color);\n    margin-bottom: 1.5rem;\n    max-width: 35em;\n  }\n  .register-link {\n    margin-right: 1rem;\n  }\n  .main-img {\n    display: none;\n  }\n  .btn {\n    padding: 0.75rem 1rem;\n  }\n  @media (min-width: 992px) {\n    .page {\n      grid-template-columns: 1fr 400px;\n      column-gap: 3rem;\n    }\n    .main-img {\n      display: block;\n    }\n  }\n`;\n\nexport default Landing;\n```\n\n#### Assets/Wrappers\n\n- css optional\n\n  Landing.jsx\n\n```jsx\nimport Wrapper from '../assets/wrappers/LandingPage';\n```\n\n#### Logo Component\n\n- create src/components/Logo.jsx\n- import logo and setup component\n- in components setup index.js import/export (just like pages)\n- replace in Landing\n\n  Logo.jsx\n\n```jsx\nimport logo from '../assets/images/logo.svg';\n\nconst Logo = () =\u003e {\n  return \u003cimg src={logo} alt='jobify' className='logo' /\u003e;\n};\n\nexport default Logo;\n```\n\n#### Logo and Images\n\n- logo built in Figma\n- [Cool Images](https://undraw.co/)\n\n#### Error Page\n\nError.jsx\n\n```jsx\nimport { Link, useRouteError } from 'react-router-dom';\nimport img from '../assets/images/not-found.svg';\nimport Wrapper from '../assets/wrappers/ErrorPage';\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! page not found\u003c/h3\u003e\n          \u003cp\u003eWe can't seem to find the page you're looking for\u003c/p\u003e\n          \u003cLink to='/dashboard'\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/Error.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.main`\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  p {\n    line-height: 1.5;\n    margin-top: 0.5rem;\n    margin-bottom: 1rem;\n    color: var(--text-secondary-color);\n  }\n  a {\n    color: var(--primary-500);\n    text-transform: capitalize;\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### Register Page\n\nRegister.jsx\n\n```jsx\nimport { Logo } from '../components';\nimport Wrapper from '../assets/wrappers/RegisterAndLoginPage';\nimport { Link } from 'react-router-dom';\n\nconst Register = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cform className='form'\u003e\n        \u003cLogo /\u003e\n        \u003ch4\u003eRegister\u003c/h4\u003e\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            id='name'\n            name='name'\n            className='form-input'\n            defaultValue='john'\n            required\n          /\u003e\n        \u003c/div\u003e\n\n        \u003cbutton type='submit' className='btn btn-block'\u003e\n          submit\n        \u003c/button\u003e\n        \u003cp\u003e\n          Already a member?\n          \u003cLink to='/login' className='member-btn'\u003e\n            Login\n          \u003c/Link\u003e\n        \u003c/p\u003e\n      \u003c/form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Register;\n```\n\n- required attribute\n\n  In HTML, the \"required\" attribute is used to indicate that a form input field must be filled out before the form can be submitted. It is typically applied to input elements such as text fields, checkboxes, and radio buttons. When the \"required\" attribute is added to an input element, the browser will prevent form submission if the field is left empty, providing a validation message to prompt the user to enter the required information.\n\n- default value\n\nIn React, the defaultValue prop is used to set the initial or default value of an input component. It is similar to the value attribute in HTML, but with a slightly different behavior.\n\n#### FormRow Component\n\n- create components/FormRow.jsx (export/import)\n\nFormRow.jsx\n\n```jsx\nconst FormRow = ({ type, name, labelText, defaultValue = '' }) =\u003e {\n  return (\n    \u003cdiv className='form-row'\u003e\n      \u003clabel htmlFor={name} className='form-label'\u003e\n        {labelText || name}\n      \u003c/label\u003e\n      \u003cinput\n        type={type}\n        id={name}\n        name={name}\n        className='form-input'\n        defaultValue={defaultValue}\n        required\n      /\u003e\n    \u003c/div\u003e\n  );\n};\n\nexport default FormRow;\n```\n\nRegister.jsx\n\n```jsx\nimport { Logo, FormRow } from '../components';\nimport Wrapper from '../assets/wrappers/RegisterAndLoginPage';\nimport { Link } from 'react-router-dom';\n\nconst Register = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cform className='form'\u003e\n        \u003cLogo /\u003e\n        \u003ch4\u003eRegister\u003c/h4\u003e\n        \u003cFormRow type='text' name='name' /\u003e\n        \u003cFormRow type='text' name='lastName' labelText='last name' /\u003e\n        \u003cFormRow type='text' name='location' /\u003e\n        \u003cFormRow type='email' name='email' /\u003e\n\n        \u003cFormRow type='password' name='password' /\u003e\n\n        \u003cbutton type='submit' className='btn btn-block'\u003e\n          submit\n        \u003c/button\u003e\n        \u003cp\u003e\n          Already a member?\n          \u003cLink to='/login' className='member-btn'\u003e\n            Login\n          \u003c/Link\u003e\n        \u003c/p\u003e\n      \u003c/form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Register;\n```\n\n#### Login Page\n\nLogin Page\n\n```jsx\nimport { Logo, FormRow } from '../components';\nimport Wrapper from '../assets/wrappers/RegisterAndLoginPage';\n\nimport { Link } from 'react-router-dom';\n\nconst Login = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cform className='form'\u003e\n        \u003cLogo /\u003e\n        \u003ch4\u003eLogin\u003c/h4\u003e\n        \u003cFormRow type='email' name='email' defaultValue='john@gmail.com' /\u003e\n        \u003cFormRow type='password' name='password' defaultValue='secret123' /\u003e\n        \u003cbutton type='submit' className='btn btn-block'\u003e\n          submit\n        \u003c/button\u003e\n        \u003cbutton type='button' className='btn btn-block'\u003e\n          explore the app\n        \u003c/button\u003e\n        \u003cp\u003e\n          Not a member yet?\n          \u003cLink to='/register' className='member-btn'\u003e\n            Register\n          \u003c/Link\u003e\n        \u003c/p\u003e\n      \u003c/form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Login;\n```\n\n#### Register and Login CSS (optional)\n\nassets/wrappers/RegisterAndLoginPage.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  min-height: 100vh;\n  display: grid;\n  align-items: center;\n  .logo {\n    display: block;\n    margin: 0 auto;\n    margin-bottom: 1.38rem;\n  }\n  .form {\n    max-width: 400px;\n    border-top: 5px solid var(--primary-500);\n  }\n\n  h4 {\n    text-align: center;\n    margin-bottom: 1.38rem;\n  }\n  p {\n    margin-top: 1rem;\n    text-align: center;\n    line-height: 1.5;\n  }\n  .btn {\n    margin-top: 1rem;\n  }\n  .member-btn {\n    color: var(--primary-500);\n    letter-spacing: var(--letter-spacing);\n    margin-left: 0.25rem;\n  }\n`;\nexport default Wrapper;\n```\n\n#### Dashboard Pages\n\nApp.jsx\n\n```jsx\n {\n        path: 'dashboard',\n        element: \u003cDashboardLayout /\u003e,\n        children: [\n          {\n            index: true,\n            element: \u003cAddJob /\u003e,\n          },\n          { path: 'stats', element: \u003cStats /\u003e },\n          {\n            path: 'all-jobs',\n            element: \u003cAllJobs /\u003e,\n          },\n\n          {\n            path: 'profile',\n            element: \u003cProfile /\u003e,\n          },\n          {\n            path: 'admin',\n            element: \u003cAdmin /\u003e,\n          },\n        ],\n      },\n```\n\nDashboard.jsx\n\n```jsx\nimport { Outlet } from 'react-router-dom';\n\nconst DashboardLayout = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003cOutlet /\u003e\n    \u003c/div\u003e\n  );\n};\nexport default DashboardLayout;\n```\n\n#### Navbar, BigSidebar and SmallSidebar\n\n- in components create :\n  Navbar.jsx\n  BigSidebar.jsx\n  SmallSidebar.jsx\n\nDashboardLayout.jsx\n\n```jsx\nimport { Outlet } from 'react-router-dom';\n\nimport Wrapper from '../assets/wrappers/Dashboard';\nimport { Navbar, BigSidebar, SmallSidebar } from '../components';\n\nconst Dashboard = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cmain className='dashboard'\u003e\n        \u003cSmallSidebar /\u003e\n        \u003cBigSidebar /\u003e\n        \u003cdiv\u003e\n          \u003cNavbar /\u003e\n          \u003cdiv className='dashboard-page'\u003e\n            \u003cOutlet /\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/main\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Dashboard;\n```\n\n#### Dashboard Layout - CSS (optional)\n\nassets/wrappers/DashboardLayout.jsx\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  .dashboard {\n    display: grid;\n    grid-template-columns: 1fr;\n  }\n  .dashboard-page {\n    width: 90vw;\n    margin: 0 auto;\n    padding: 2rem 0;\n  }\n  @media (min-width: 992px) {\n    .dashboard {\n      grid-template-columns: auto 1fr;\n    }\n    .dashboard-page {\n      width: 90%;\n    }\n  }\n`;\nexport default Wrapper;\n```\n\n#### Dashboard Context\n\n```jsx\nimport { Outlet } from 'react-router-dom';\n\nimport Wrapper from '../assets/wrappers/Dashboard';\nimport { Navbar, BigSidebar, SmallSidebar } from '../components';\n\nimport { useState, createContext, useContext } from 'react';\nconst DashboardContext = createContext();\nconst Dashboard = () =\u003e {\n  // temp\n  const user = { name: 'john' };\n\n  const [showSidebar, setShowSidebar] = useState(false);\n  const [isDarkTheme, setIsDarkTheme] = useState(false);\n\n  const toggleDarkTheme = () =\u003e {\n    console.log('toggle dark theme');\n  };\n\n  const toggleSidebar = () =\u003e {\n    setShowSidebar(!showSidebar);\n  };\n\n  const logoutUser = async () =\u003e {\n    console.log('logout user');\n  };\n  return (\n    \u003cDashboardContext.Provider\n      value={{\n        user,\n        showSidebar,\n        isDarkTheme,\n        toggleDarkTheme,\n        toggleSidebar,\n        logoutUser,\n      }}\n    \u003e\n      \u003cWrapper\u003e\n        \u003cmain className='dashboard'\u003e\n          \u003cSmallSidebar /\u003e\n          \u003cBigSidebar /\u003e\n          \u003cdiv\u003e\n            \u003cNavbar /\u003e\n            \u003cdiv className='dashboard-page'\u003e\n              \u003cOutlet /\u003e\n            \u003c/div\u003e\n          \u003c/div\u003e\n        \u003c/main\u003e\n      \u003c/Wrapper\u003e\n    \u003c/DashboardContext.Provider\u003e\n  );\n};\n\nexport const useDashboardContext = () =\u003e useContext(DashboardContext);\n\nexport default Dashboard;\n```\n\n#### React Icons\n\n[React Icons](https://react-icons.github.io/react-icons/)\n\n```sh\nnpm install react-icons@4.8.0\n```\n\nNavbar.jsx\n\n```jsx\n\nimport {FaHome} from 'react-icons/fa'\nconst Navbar = () =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch2\u003enavbar\u003c/h2\u003e\n      \u003cFaHome\u003e\n    \u003c/div\u003e\n  )\n}\n\n```\n\n#### Navbar - Initial Setup\n\n```jsx\nimport Wrapper from '../assets/wrappers/Navbar';\nimport { FaAlignLeft } from 'react-icons/fa';\nimport Logo from './Logo';\n\nimport { useDashboardContext } from '../pages/DashboardLayout';\nconst Navbar = () =\u003e {\n  const { toggleSidebar } = useDashboardContext();\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv className='nav-center'\u003e\n        \u003cbutton type='button' className='toggle-btn' onClick={toggleSidebar}\u003e\n          \u003cFaAlignLeft /\u003e\n        \u003c/button\u003e\n        \u003cdiv\u003e\n          \u003cLogo /\u003e\n          \u003ch4 className='logo-text'\u003edashboard\u003c/h4\u003e\n        \u003c/div\u003e\n        \u003cdiv className='btn-container'\u003etoggle/logout\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Navbar;\n```\n\n#### Navbar CSS (optional)\n\nassets/wrappers/Navbar.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.nav`\n  height: var(--nav-height);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  box-shadow: 0 1px 0px 0px rgba(0, 0, 0, 0.1);\n  background: var(--background-secondary-color);\n  .logo {\n    display: flex;\n    align-items: center;\n    width: 100px;\n  }\n  .nav-center {\n    display: flex;\n    width: 90vw;\n    align-items: center;\n    justify-content: space-between;\n  }\n  .toggle-btn {\n    background: transparent;\n    border-color: transparent;\n    font-size: 1.75rem;\n    color: var(--primary-500);\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n  }\n  .btn-container {\n    display: flex;\n    align-items: center;\n  }\n\n  .logo-text {\n    display: none;\n  }\n  @media (min-width: 992px) {\n    position: sticky;\n    top: 0;\n\n    .nav-center {\n      width: 90%;\n    }\n    .logo {\n      display: none;\n    }\n    .logo-text {\n      display: block;\n    }\n  }\n`;\nexport default Wrapper;\n```\n\n#### Links\n\n- create src/utils/links.jsx\n\n```jsx\nimport React from 'react';\n\nimport { IoBarChartSharp } from 'react-icons/io5';\nimport { MdQueryStats } from 'react-icons/md';\nimport { FaWpforms } from 'react-icons/fa';\nimport { ImProfile } from 'react-icons/im';\nimport { MdAdminPanelSettings } from 'react-icons/md';\n\nconst links = [\n  { text: 'add job', path: '.', icon: \u003cFaWpforms /\u003e },\n  { text: 'all jobs', path: 'all-jobs', icon: \u003cMdQueryStats /\u003e },\n  { text: 'stats', path: 'stats', icon: \u003cIoBarChartSharp /\u003e },\n  { text: 'profile', path: 'profile', icon: \u003cImProfile /\u003e },\n  { text: 'admin', path: 'admin', icon: \u003cMdAdminPanelSettings /\u003e },\n];\n\nexport default links;\n```\n\n- in a second, we will discuss why '.' in \"add job\"\n\n#### SmallSidebar\n\nSmallSidebar\n\n```jsx\nimport Wrapper from '../assets/wrappers/SmallSidebar';\nimport { FaTimes } from 'react-icons/fa';\n\nimport Logo from './Logo';\nimport { NavLink } from 'react-router-dom';\nimport links from '../utils/links';\nimport { useDashboardContext } from '../pages/DashboardLayout';\n\nconst SmallSidebar = () =\u003e {\n  const { showSidebar, toggleSidebar } = useDashboardContext();\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv\n        className={\n          showSidebar ? 'sidebar-container show-sidebar' : 'sidebar-container'\n        }\n      \u003e\n        \u003cdiv className='content'\u003e\n          \u003cbutton type='button' className='close-btn' onClick={toggleSidebar}\u003e\n            \u003cFaTimes /\u003e\n          \u003c/button\u003e\n          \u003cheader\u003e\n            \u003cLogo /\u003e\n          \u003c/header\u003e\n          \u003cdiv className='nav-links'\u003e\n            {links.map((link) =\u003e {\n              const { text, path, icon } = link;\n\n              return (\n                \u003cNavLink\n                  to={path}\n                  key={text}\n                  className='nav-link'\n                  onClick={toggleSidebar}\n                  // will discuss in a second\n                  end\n                \u003e\n                  \u003cspan className='icon'\u003e{icon}\u003c/span\u003e\n                  {text}\n                \u003c/NavLink\u003e\n              );\n            })}\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default SmallSidebar;\n```\n\n- cover '.' path ,active class and 'end' prop\n\n#### Small Sidebar CSS (optional)\n\nassets/wrappers/SmallSidebar.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.aside`\n  @media (min-width: 992px) {\n    display: none;\n  }\n  .sidebar-container {\n    position: fixed;\n    inset: 0;\n    background: rgba(0, 0, 0, 0.7);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: -1;\n    opacity: 0;\n    transition: var(--transition);\n    visibility: hidden;\n  }\n  .show-sidebar {\n    z-index: 99;\n    opacity: 1;\n    visibility: visible;\n  }\n  .content {\n    background: var(--background-secondary-color);\n    width: var(--fluid-width);\n    height: 95vh;\n    border-radius: var(--border-radius);\n    padding: 4rem 2rem;\n    position: relative;\n    display: flex;\n    align-items: center;\n    flex-direction: column;\n  }\n  .close-btn {\n    position: absolute;\n    top: 10px;\n    left: 10px;\n    background: transparent;\n    border-color: transparent;\n    font-size: 2rem;\n    color: var(--red-dark);\n    cursor: pointer;\n  }\n  .nav-links {\n    padding-top: 2rem;\n    display: flex;\n    flex-direction: column;\n  }\n  .nav-link {\n    display: flex;\n    align-items: center;\n    color: var(--text-secondary-color);\n    padding: 1rem 0;\n    text-transform: capitalize;\n    transition: var(--transition);\n  }\n  .nav-link:hover {\n    color: var(--primary-500);\n  }\n\n  .icon {\n    font-size: 1.5rem;\n    margin-right: 1rem;\n    display: grid;\n    place-items: center;\n  }\n  .active {\n    color: var(--primary-500);\n  }\n`;\nexport default Wrapper;\n```\n\n#### NavLinks\n\n- components/NavLinks.jsx\n\n```jsx\nimport { useDashboardContext } from '../pages/DashboardLayout';\nimport links from '../utils/links';\nimport { NavLink } from 'react-router-dom';\n\nconst NavLinks = () =\u003e {\n  const { user, toggleSidebar } = useDashboardContext();\n\n  return (\n    \u003cdiv className='nav-links'\u003e\n      {links.map((link) =\u003e {\n        const { text, path, icon } = link;\n        // admin user\n        return (\n          \u003cNavLink\n            to={path}\n            key={text}\n            onClick={toggleSidebar}\n            className='nav-link'\n            end\n          \u003e\n            \u003cspan className='icon'\u003e{icon}\u003c/span\u003e\n            {text}\n          \u003c/NavLink\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n};\n\nexport default NavLinks;\n```\n\n#### Big Sidebar\n\n```jsx\nimport NavLinks from './NavLinks';\nimport Logo from '../components/Logo';\nimport Wrapper from '../assets/wrappers/BigSidebar';\nimport { useDashboardContext } from '../pages/DashboardLayout';\n\nconst BigSidebar = () =\u003e {\n  const { showSidebar } = useDashboardContext();\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv\n        className={\n          showSidebar ? 'sidebar-container ' : 'sidebar-container show-sidebar'\n        }\n      \u003e\n        \u003cdiv className='content'\u003e\n          \u003cheader\u003e\n            \u003cLogo /\u003e\n          \u003c/header\u003e\n          \u003cNavLinks isBigSidebar /\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default BigSidebar;\n```\n\n```jsx\nconst NavLinks = ({ isBigSidebar }) =\u003e {\n  const { user, toggleSidebar } = useDashboardContext();\n\n  return (\n    \u003cdiv className='nav-links'\u003e\n      {links.map((link) =\u003e {\n        const { text, path, icon } = link;\n        // admin user\n        return (\n          \u003cNavLink\n            to={path}\n            key={text}\n            onClick={isBigSidebar ? null : toggleSidebar}\n            className='nav-link'\n            end\n          \u003e\n            \u003cspan className='icon'\u003e{icon}\u003c/span\u003e\n            {text}\n          \u003c/NavLink\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n};\n\nexport default NavLinks;\n```\n\n#### BigSidebar CSS (optional)\n\nassets/wrappers/BigSidebar.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.aside`\n  display: none;\n  @media (min-width: 992px) {\n    display: block;\n    box-shadow: 1px 0px 0px 0px rgba(0, 0, 0, 0.1);\n    .sidebar-container {\n      background: var(--background-secondary-color);\n      min-height: 100vh;\n      height: 100%;\n      width: 250px;\n      margin-left: -250px;\n      transition: margin-left 0.3s ease-in-out;\n    }\n    .content {\n      position: sticky;\n      top: 0;\n    }\n    .show-sidebar {\n      margin-left: 0;\n    }\n    header {\n      height: 6rem;\n      display: flex;\n      align-items: center;\n      padding-left: 2.5rem;\n    }\n    .nav-links {\n      padding-top: 2rem;\n      display: flex;\n      flex-direction: column;\n    }\n    .nav-link {\n      display: flex;\n      align-items: center;\n      color: var(--text-secondary-color);\n      padding: 1rem 0;\n      padding-left: 2.5rem;\n      text-transform: capitalize;\n      transition: padding-left 0.3s ease-in-out;\n    }\n    .nav-link:hover {\n      padding-left: 3rem;\n      color: var(--primary-500);\n      transition: var(--transition);\n    }\n\n    .icon {\n      font-size: 1.5rem;\n      margin-right: 1rem;\n      display: grid;\n      place-items: center;\n    }\n    .active {\n      color: var(--primary-500);\n    }\n  }\n`;\nexport default Wrapper;\n```\n\n#### LogoutContainer\n\ncomponents/LogoutContainer.jsx\n\n```jsx\nimport { FaUserCircle, FaCaretDown } from 'react-icons/fa';\nimport Wrapper from '../assets/wrappers/LogoutContainer';\nimport { useState } from 'react';\nimport { useDashboardContext } from '../pages/DashboardLayout';\n\nconst LogoutContainer = () =\u003e {\n  const [showLogout, setShowLogout] = useState(false);\n  const { user, logoutUser } = useDashboardContext();\n\n  return (\n    \u003cWrapper\u003e\n      \u003cbutton\n        type='button'\n        className='btn logout-btn'\n        onClick={() =\u003e setShowLogout(!showLogout)}\n      \u003e\n        {user.avatar ? (\n          \u003cimg src={user.avatar} alt='avatar' className='img' /\u003e\n        ) : (\n          \u003cFaUserCircle /\u003e\n        )}\n\n        {user?.name}\n        \u003cFaCaretDown /\u003e\n      \u003c/button\u003e\n      \u003cdiv className={showLogout ? 'dropdown show-dropdown' : 'dropdown'}\u003e\n        \u003cbutton type='button' className='dropdown-btn' onClick={logoutUser}\u003e\n          logout\n        \u003c/button\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default LogoutContainer;\n```\n\n#### LogoutContainer CSS (optional)\n\nassets/wrappers/LogoutContainer.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  position: relative;\n\n  .logout-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 0 0.5rem;\n  }\n  .img {\n    width: 25px;\n    height: 25px;\n    border-radius: 50%;\n  }\n  .dropdown {\n    position: absolute;\n    top: 45px;\n    left: 0;\n    width: 100%;\n    box-shadow: var(--shadow-2);\n    text-align: center;\n    visibility: hidden;\n    border-radius: var(--border-radius);\n    background: var(--primary-500);\n  }\n  .show-dropdown {\n    visibility: visible;\n  }\n  .dropdown-btn {\n    border-radius: var(--border-radius);\n    padding: 0.5rem;\n    background: transparent;\n    border-color: transparent;\n    color: var(--white);\n    letter-spacing: var(--letter-spacing);\n    text-transform: capitalize;\n    cursor: pointer;\n    width: 100%;\n    height: 100%;\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### ThemeToggle\n\ncomponents/ThemeToggle.jsx\n\n```jsx\nimport { BsFillSunFill, BsFillMoonFill } from 'react-icons/bs';\nimport Wrapper from '../assets/wrappers/ThemeToggle';\nimport { useDashboardContext } from '../pages/DashboardLayout';\n\nconst ThemeToggle = () =\u003e {\n  const { isDarkTheme, toggleDarkTheme } = useDashboardContext();\n  return (\n    \u003cWrapper onClick={toggleDarkTheme}\u003e\n      {isDarkTheme ? (\n        \u003cBsFillSunFill className='toggle-icon' /\u003e\n      ) : (\n        \u003cBsFillMoonFill className='toggle-icon' /\u003e\n      )}\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default ThemeToggle;\n```\n\nNavbar.jsx\n\n```jsx\n\u003cdiv className='btn-container'\u003e\n  \u003cThemeToggle /\u003e\n\u003c/div\u003e\n```\n\n#### ThemeToggle CSS (optional)\n\nassets/wrappers/ThemeToggle.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  background: transparent;\n  border-color: transparent;\n  width: 3.5rem;\n  height: 2rem;\n  display: grid;\n  place-items: center;\n  cursor: pointer;\n\n  .toggle-icon {\n    font-size: 1.15rem;\n    color: var(--text-color);\n  }\n`;\nexport default Wrapper;\n```\n\n#### Dark Theme - Logic\n\nDashboardLayout.jsx\n\n```jsx\nconst toggleDarkTheme = () =\u003e {\n  const newDarkTheme = !isDarkTheme;\n  setIsDarkTheme(newDarkTheme);\n  document.body.classList.toggle('dark-theme', newDarkTheme);\n  localStorage.setItem('darkTheme', newDarkTheme);\n};\n```\n\n#### Access Theme\n\nApp.jsx\n\n```jsx\nconst checkDefaultTheme = () =\u003e {\n  const isDarkTheme =\n    localStorage.getItem('darkTheme') === 'true'\n  document.body.classList.toggle('dark-theme', isDarkTheme);\n  return isDarkTheme;\n};\n\nconst isDarkThemeEnabled = checkDefaultTheme();\n\n{\npath: 'dashboard',\nelement: \u003cDashboardLayout isDarkThemeEnabled={isDarkThemeEnabled} /\u003e,\n}\n```\n\nDashboardLayout.jsx\n\n```jsx\nconst Dashboard = ({ isDarkThemeEnabled }) =\u003e {\n  const [isDarkTheme, setIsDarkTheme] = useState(isDarkThemeEnabled);\n};\n```\n\n#### Dark Theme CSS\n\nindex.css\n\n```css\n:root {\n  /* DARK MODE */\n\n  --dark-mode-bg-color: #333;\n  --dark-mode-text-color: #f0f0f0;\n  --dark-mode-bg-secondary-color: #3f3f3f;\n  --dark-mode-text-secondary-color: var(--grey-300);\n\n  --background-color: var(--grey-50);\n  --text-color: var(--grey-900);\n  --background-secondary-color: var(--white);\n  --text-secondary-color: var(--grey-500);\n}\n\n.dark-theme {\n  --text-color: var(--dark-mode-text-color);\n  --background-color: var(--dark-mode-bg-color);\n  --text-secondary-color: var(--dark-mode-text-secondary-color);\n  --background-secondary-color: var(--dark-mode-bg-secondary-color);\n}\n\nbody {\n  background: var(--background-color);\n  color: var(--text-color);\n}\n```\n\n#### Folder Setup\n\n- IMPORTANT !!!!\n- remove existing .git folder (if any) from client\n\nMac\n\n```sh\nrm -rf .git\n```\n\nWindows\n\n```sh\nrmdir -Force -Recurse .git\n```\n\n```sh\nrd /s /q .git\n```\n\n- Windows commands were shared by students and I have not personally tested them.\n- git status should return :\n  \"fatal: Not a git repository (or any of the parent directories): .git\"\n- create jobify directory\n- copy/paste client\n- move README to root\n\n#### Setup Server\n\n- create package.json\n\n```sh\nnpm init -y\n```\n\n- create and test server.js\n\n```sh\nnode server\n```\n\n#### ES6 Modules\n\npackage.json\n\n```json\n  \"type\": \"module\",\n```\n\nCreate test.js and implement named import\n\ntest.js\n\n```js\nexport const value = 42;\n```\n\nserver.js\n\n```js\nimport { value } from './test.js';\nconsole.log(value);\n```\n\n- don't forget about .js extension\n- for named imports, names must match\n\n#### Source Control\n\n- create .gitignore\n- copy values from client/.gitignore\n- create Github Repo (optional)\n\n#### Install Packages and Setup Install Script\n\n```sh\nnpm install bcryptjs@2.4.3 concurrently@8.0.1 cookie-parser@1.4.6 dayjs@1.11.7 dotenv@16.0.3 express@4.18.2 express-async-errors@3.1.1 express-validator@7.0.1 http-status-codes@2.2.0 jsonwebtoken@9.0.0 mongoose@7.0.5 morgan@1.10.0 multer@1.4.5-lts.1 nanoid@4.0.2 nodemon@2.0.22 cloudinary@1.37.3 dayjs@1.11.9 datauri@4.1.0 helmet@7.0.0 express-rate-limit@6.8.0 express-mongo-sanitize@2.2.0\n\n```\n\npackage.json\n\n```json\n\"scripts\": {\n    \"setup-project\": \"npm i \u0026\u0026 cd client \u0026\u0026 npm i\"\n  },\n```\n\n- install packages in root and client\n\n```sh\nnpm run setup-project\n```\n\n#### Setup Basic Express\n\n- install express and nodemon.\n- setup a basic server which listening on PORT=5100\n- create a basic home route which sends back \"hello world\"\n- setup a script with nodemon package.\n\n[Express Docs](https://expressjs.com/)\n\nExpress is a fast and minimalist web application framework for Node.js. It simplifies the process of building web applications by providing a robust set of features for handling HTTP requests, routing, middleware, and more. Express allows you to create server-side applications and APIs easily, with a focus on simplicity and flexibility.\n\n[Nodemon Docs](https://nodemon.io/)\n\nNodemon is a development tool that improves the developer experience. It monitors your Node.js application for any changes in the code and automatically restarts the server whenever a change is detected. This eliminates the need to manually restart the server after every code modification, making the development process more efficient and productive. Nodemon is commonly used during development to save time and avoid the hassle of manual server restarts.\n\n```sh\nnpm i express@4.18.2 nodemon@2.0.22\n```\n\nserver.js\n\n```js\nimport express from 'express';\nconst app = express();\n\napp.get('/', (req, res) =\u003e {\n  res.send('Hello World');\n});\n\napp.listen(5100, () =\u003e {\n  console.log('server running....');\n});\n```\n\npackage.json\n\n```json\n\"scripts\": {\n    \"dev\": \"nodemon server.js\"\n  },\n```\n\n#### Thunder Client\n\nThunder Client is a popular Visual Studio Code extension that facilitates API testing and debugging. It provides a user-friendly interface for making HTTP requests and viewing the responses, allowing developers to easily test APIs, examine headers, and inspect JSON/XML payloads. Thunder Client offers features such as environment variables, request history, and the ability to save and organize requests for efficient development workflows.\n\n[Thunder Client](https://www.thunderclient.com/)\n\n- install and test home route\n\n#### Accept JSON\n\nSetup express middleware to accept json\n\nserver\n\n```js\napp.use(express.json());\n\napp.post('/', (req, res) =\u003e {\n  console.log(req);\n\n  res.json({ message: 'Data received', data: req.body });\n});\n```\n\n#### Morgan and Dotenv\n\n[Morgan](https://www.npmjs.com/package/morgan)\n\nHTTP request logger middleware for node.js\n\n[Dotenv](https://www.npmjs.com/package/dotenv)\n\nDotenv is a zero-dependency module that loads environment variables from a .env file into process.env.\n\n```sh\nnpm i morgan@1.10.0 dotenv@16.0.3\n```\n\n```js\nimport morgan from 'morgan';\n\napp.use(morgan('dev'));\n```\n\n- create .env file in the root\n- add PORT and NODE_ENV\n- add .env to .gitignore\n\nserver.js\n\n```js\nimport * as dotenv from 'dotenv';\ndotenv.config();\n\nif (process.env.NODE_ENV === 'development') {\n  app.use(morgan('dev'));\n}\n\nconst port = process.env.PORT || 5100;\napp.listen(port, () =\u003e {\n  console.log(`server running on PORT ${port}....`);\n});\n```\n\n#### New Features\n\n- fetch API\n- global await (top-level await)\n- watch mode\n\n```js\ntry {\n  const response = await fetch(\n    'https://www.course-api.com/react-useReducer-cart-project'\n  );\n  const cartData = await response.json();\n  console.log(cartData);\n} catch (error) {\n  console.log(error);\n}\n```\n\npackage.json\n\n```json\n \"scripts\": {\n    \"watch\": \"node --watch server.js \"\n  },\n```\n\n#### Basic CRUD\n\n- create jobs array where each item is an object with following properties\n  id, company, position\n- create routes to handle - create, read, update and delete functionalities\n\n#### Get All Jobs\n\n[Nanoid](https://www.npmjs.com/package/nanoid)\n\nThe nanoid package is a software library used for generating unique and compact identifiers in web applications or databases. It creates short and URL-safe IDs by combining random characters from a set of 64 characters. Nanoid is a popular choice due to its simplicity, efficiency, and collision-resistant nature.\n\n```sh\nnpm i nanoid@4.0.2\n```\n\nserver.js\n\n```js\nimport { nanoid } from 'nanoid';\n\nlet jobs = [\n  { id: nanoid(), company: 'apple', position: 'front-end' },\n  { id: nanoid(), company: 'google', position: 'back-end' },\n];\n\napp.get('/api/v1/jobs', (req, res) =\u003e {\n  res.status(200).json({ jobs });\n});\n```\n\n#### Create, FindOne, Modify and Delete\n\n```js\n// CREATE JOB\n\napp.post('/api/v1/jobs', (req, res) =\u003e {\n  const { company, position } = req.body;\n  if (!company || !position) {\n    return res.status(400).json({ msg: 'please provide company and position' });\n  }\n  const id = nanoid(10);\n  // console.log(id);\n  const job = { id, company, position };\n  jobs.push(job);\n  res.status(200).json({ job });\n});\n\n// GET SINGLE JOB\n\napp.get('/api/v1/jobs/:id', (req, res) =\u003e {\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  res.status(200).json({ job });\n});\n\n// EDIT JOB\n\napp.patch('/api/v1/jobs/:id', (req, res) =\u003e {\n  const { company, position } = req.body;\n  if (!company || !position) {\n    return res.status(400).json({ msg: 'please provide company and position' });\n  }\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n\n  job.company = company;\n  job.position = position;\n  res.status(200).json({ msg: 'job modified', job });\n});\n\n// DELETE JOB\n\napp.delete('/api/v1/jobs/:id', (req, res) =\u003e {\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  const newJobs = jobs.filter((job) =\u003e job.id !== id);\n  jobs = newJobs;\n\n  res.status(200).json({ msg: 'job deleted' });\n});\n```\n\n#### Not Found Middleware\n\n```js\napp.use('*', (req, res) =\u003e {\n  res.status(404).json({ msg: 'not found' });\n});\n```\n\n#### Error Middleware\n\n```js\napp.use((err, req, res, next) =\u003e {\n  console.log(err);\n  res.status(500).json({ msg: 'something went wrong' });\n});\n```\n\n#### Not Found and Error Middleware\n\nThe \"not found\" middleware in Express.js is used when a request is made to a route that does not exist. It catches these requests and responds with a 404 status code, indicating that the requested resource was not found.\n\nOn the other hand, the \"error\" middleware in Express.js is used to handle any errors that occur during the processing of a request. It is typically used to catch unexpected errors or exceptions that are not explicitly handled in the application code. It logs the error and sends a 500 status code, indicating an internal server error.\n\nIn summary, the \"not found\" middleware is specifically designed to handle requests for non-existent routes, while the \"error\" middleware is a catch-all for handling unexpected errors that occur during request processing.\n\n- make a request to \"/jobss\"\n\n```js\n// GET ALL JOBS\napp.get('/api/v1/jobs', (req, res) =\u003e {\n  // console.log(jobss);\n  res.status(200).json({ jobs });\n});\n\n// GET SINGLE JOB\napp.get('/api/v1/jobs/:id', (req, res) =\u003e {\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    throw new Error('no job with that id');\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  res.status(200).json({ job });\n});\n```\n\n#### Controller and Router\n\nsetup controllers and router\n\ncontrollers/jobController.js\n\n```js\nimport { nanoid } from 'nanoid';\n\nlet jobs = [\n  { id: nanoid(), company: 'apple', position: 'front-end developer' },\n  { id: nanoid(), company: 'google', position: 'back-end developer' },\n];\n\nexport const getAllJobs = async (req, res) =\u003e {\n  res.status(200).json({ jobs });\n};\n\nexport const createJob = async (req, res) =\u003e {\n  const { company, position } = req.body;\n\n  if (!company || !position) {\n    return res.status(400).json({ msg: 'please provide company and position' });\n  }\n  const id = nanoid(10);\n  const job = { id, company, position };\n  jobs.push(job);\n  res.status(200).json({ job });\n};\n\nexport const getJob = async (req, res) =\u003e {\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    // throw new Error('no job with that id');\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  res.status(200).json({ job });\n};\n\nexport const updateJob = async (req, res) =\u003e {\n  const { company, position } = req.body;\n  if (!company || !position) {\n    return res.status(400).json({ msg: 'please provide company and position' });\n  }\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n\n  job.company = company;\n  job.position = position;\n  res.status(200).json({ msg: 'job modified', job });\n};\n\nexport const deleteJob = async (req, res) =\u003e {\n  const { id } = req.params;\n  const job = jobs.find((job) =\u003e job.id === id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  const newJobs = jobs.filter((job) =\u003e job.id !== id);\n  jobs = newJobs;\n\n  res.status(200).json({ msg: 'job deleted' });\n};\n```\n\nroutes/jobRouter.js\n\n```js\nimport { Router } from 'express';\nconst router = Router();\n\nimport {\n  getAllJobs,\n  getJob,\n  createJob,\n  updateJob,\n  deleteJob,\n} from '../controllers/jobController.js';\n\n// router.get('/', getAllJobs);\n// router.post('/', createJob);\n\nrouter.route('/').get(getAllJobs).post(createJob);\nrouter.route('/:id').get(getJob).patch(updateJob).delete(deleteJob);\n\nexport default router;\n```\n\nserver.js\n\n```js\nimport jobRouter from './routers/jobRouter.js';\napp.use('/api/v1/jobs', jobRouter);\n```\n\n#### MongoDB\n\n[MongoDb](https://www.mongodb.com/)\n\nMongoDB is a popular NoSQL database that provides a flexible and scalable approach to storing and retrieving data. It uses a document-oriented model, where data is organized into collections of JSON-like documents. MongoDB offers high performance, horizontal scalability, and easy integration with modern development frameworks, making it suitable for handling diverse data types and handling large-scale applications.\n\nMongoDB Atlas is a fully managed cloud database service provided by MongoDB, offering automated deployment, scaling, and monitoring of MongoDB clusters, allowing developers to focus on building their applications without worrying about infrastructure management.\n\n#### Mongoosejs\n\n[Mongoose](https://mongoosejs.com/)\n\nMongoose is an Object Data Modeling (ODM) library for Node.js that provides a straightforward and elegant way to interact with MongoDB. It allows developers to define schemas and models for their data, providing structure and validation. Mongoose also offers features like data querying, middleware, and support for data relationships, making it a powerful tool for building MongoDB-based applications.\n\n```sh\nnpm i mongoose@7.0.5\n```\n\nserver.js\n\n```js\nimport mongoose from 'mongoose';\n\ntry {\n  await mongoose.connect(process.env.MONGO_URL);\n  app.listen(port, () =\u003e {\n    console.log(`server running on PORT ${port}....`);\n  });\n} catch (error) {\n  console.log(error);\n  process.exit(1);\n}\n```\n\n#### Job Model\n\nmodels/JobModel.js\n\nenum - data type represents a field with a predefined set of values\n\n```js\nimport mongoose from 'mongoose';\n\nconst JobSchema = new mongoose.Schema(\n  {\n    company: String,\n    position: String,\n    jobStatus: {\n      type: String,\n      enum: ['interview', 'declined', 'pending'],\n      default: 'pending',\n    },\n    jobType: {\n      type: String,\n      enum: ['full-time', 'part-time', 'internship'],\n      default: 'full-time',\n    },\n    jobLocation: {\n      type: String,\n      default: 'my city',\n    },\n  },\n  { timestamps: true }\n);\n\nexport default mongoose.model('Job', JobSchema);\n```\n\n#### Create Job\n\njobController.js\n\n```js\nimport Job from '../models/JobModel.js';\n\nexport const createJob = async (req, res) =\u003e {\n  const { company, position } = req.body;\n  const job = await Job.create({ company, position });\n ; res.status(201).json({ job });\n}\n```\n\n#### Try / Catch\n\njobController.js\n\n```js\nexport const createJob = async (req, res) =\u003e {\n  const { company, position } = req.body;\n  try {\n    const job = await Job.create('something');\n    res.status(201).json({ job });\n  } catch (error) {\n    res.status(500).json({ msg: 'server error' });\n  }\n};\n```\n\n#### express-async-errors\n\nThe \"express-async-errors\" package is an Express.js middleware that helps handle errors that occur within asynchronous functions. It catches unhandled errors inside async/await functions and forwards them to Express.js's error handling middleware, preventing the Node.js process from crashing. It simplifies error handling in Express.js applications by allowing you to write asynchronous code without worrying about manually catching and forwarding errors.\n\n[Express Async Errors](https://www.npmjs.com/package/express-async-errors)\n\n```sh\nnpm i express-async-errors@3.1.1\n```\n\n- setup import at the top !!!\n\n  server.js\n\n```js\nimport 'express-async-errors';\n```\n\njobController.js\n\n```js\nexport const createJob = async (req, res) =\u003e {\n  const { company, position } = req.body;\n\n  const job = await Job.create({ company, position });\n  res.status(201).json({ job });\n};\n```\n\n#### Get All Jobs\n\njobController.js\n\n```js\nexport const getAllJobs = async (req, res) =\u003e {\n  const jobs = await Job.find({});\n  res.status(200).json({ jobs });\n};\n```\n\n#### Get Single Job\n\n```js\nexport const getJob = async (req, res) =\u003e {\n  const { id } = req.params;\n  const job = await Job.findById(id);\n  if (!job) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  res.status(200).json({ job });\n};\n```\n\n#### Delete Job\n\njobController.js\n\n```js\nexport const deleteJob = async (req, res) =\u003e {\n  const { id } = req.params;\n  const removedJob = await Job.findByIdAndDelete(id);\n\n  if (!removedJob) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  res.status(200).json({ job: removedJob });\n};\n```\n\n#### Update Job\n\n```js\nexport const updateJob = async (req, res) =\u003e {\n  const { id } = req.params;\n\n  const updatedJob = await Job.findByIdAndUpdate(id, req.body, {\n    new: true,\n  });\n\n  if (!updatedJob) {\n    return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n\n  res.status(200).json({ job: updatedJob });\n};\n```\n\n#### Status Codes\n\nA library for HTTP status codes is useful because it provides a comprehensive and standardized set of codes that represent the outcome of HTTP requests. It allows developers to easily understand and handle different scenarios during web development, such as successful responses, client or server errors, redirects, and more. By using a status code library, developers can ensure consistent and reliable communication between servers and clients, leading to better error handling and improved user experience.\n\n[Http Status Codes](https://www.npmjs.com/package/http-status-codes)\n\n```sh\nnpm i http-status-codes@2.2.0\n\n```\n\n200 OK OK\n201 CREATED Created\n\n400 BAD_REQUEST Bad Request\n401 UNAUTHORIZED Unauthorized\n\n403 FORBIDDEN Forbidden\n404 NOT_FOUND Not Found\n\n500 INTERNAL_SERVER_ERROR Internal Server Error\n\n- refactor 200 response in all controllers\n\njobController.js\n\n```js\nres.status(StatusCodes.OK).json({ jobs });\n```\n\ncreateJob\n\n```js\nres.status(StatusCodes.CREATED).json({ job });\n```\n\n#### Custom Error Class\n\njobController\n\n```js\nexport const getJob = async (req, res) =\u003e {\n  ....\n  if (!job) {\n    throw new Error('no job with that id');\n    // return res.status(404).json({ msg: `no job with id ${id}` });\n  }\n  ...\n};\n\n```\n\nerrors/customErrors.js\n\n```js\nimport { StatusCodes } from 'http-status-codes';\nexport class NotFoundError extends Error {\n  constructor(message) {\n    super(message);\n    this.name = 'NotFoundError';\n    this.statusCode = StatusCodes.NOT_FOUND;\n  }\n}\n```\n\nThis code defines a custom error class NotFoundError that extends the built-in Error class in JavaScript. The NotFoundError class is designed to be used when a requested resource is not found, and it includes a status code of 404 to indicate this.\n\nHere's a breakdown of the code:\n\nclass NotFoundError extends Error: This line defines a new class NotFoundError that extends the built-in Error class. This means that NotFoundError inherits all of the properties and methods of the Error class, and can also define its own properties and methods.\n\nconstructor(message): This is the constructor method for the NotFoundError class, which is called when a new instance of the class is created. The message parameter is the error message that will be displayed when the error is thrown.\n\nsuper(message): This line calls the constructor of the Error class and passes the message parameter to it. This sets the error message for the NotFoundError instance.\n\nthis.name = \"NotFoundError\": This line sets the name property of the NotFoundError instance to \"NotFoundError\". This is a built-in property of the Error class that specifies the name of the error.\n\nthis.statusCode = 404: This line sets the statusCode property of the NotFoundError instance to 404. This is a custom property that is specific to the NotFoundError class and indicates the HTTP status code that should be returned when this error occurs.\n\nBy creating a custom error class like NotFoundError, you can provide more specific error messages and properties to help with debugging and error handling in your application.\n\n#### Custom Error\n\njobController.js\n\n```js\nimport { NotFoundError } from '../customErrors.js';\n\nif (!job) throw new NotFoundError(`no job with id : ${id}`);\n```\n\nmiddleware/errorHandlerMiddleware.js\n\n```js\nimport { StatusCodes } from 'http-status-codes';\nconst errorHandlerMiddleware = (err, req, res, next) =\u003e {\n  console.log(err);\n  const statusCode = err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR;\n  const msg = err.message || 'Something went wrong, try again later';\n\n  res.status(statusCode).json({ msg });\n};\n\nexport default errorHandlerMiddleware;\n```\n\nserver.js\n\n```js\nimport errorHandlerMiddleware from './middleware/errorHandlerMiddleware.js';\n\napp.use(errorHandlerMiddleware);\n```\n\n#### Bad Request Error\n\n400 BAD_REQUEST Bad Request\n401 UNAUTHORIZED Unauthorized\n403 FORBIDDEN Forbidden\n404 NOT_FOUND Not Found\n\ncustomErrors.js\n\n```js\nexport class BadRequestError extends Error {\n  constructor(message) {\n    super(message);\n    this.name = 'BadRequestError';\n    this.statusCode = StatusCodes.BAD_REQUEST;\n  }\n}\nexport class UnauthenticatedError extends Error {\n  constructor(message) {\n    super(message);\n    this.name = 'UnauthenticatedError';\n    this.statusCode = StatusCodes.UNAUTHORIZED;\n  }\n}\nexport class UnauthorizedError extends Error {\n  constructor(message) {\n    super(message);\n    this.name = 'UnauthorizedError';\n    this.statusCode = StatusCodes.FORBIDDEN;\n  }\n}\n```\n\n#### Validation Layer\n\n[Express Validator](https://express-validator.github.io/docs/)\n\n```sh\nnpm i express-validator@7.0.1\n```\n\n#### Test Route\n\nserver.js\n\n```js\napp.post('/api/v1/test', (req, res) =\u003e {\n  const { name } = req.body;\n  res.json({ msg: `hello ${name}` });\n});\n```\n\n#### Express Validator\n\n```js\nimport { body, validationResult } from 'express-validator';\n\napp.post(\n  '/api/v1/test',\n  [body('name').notEmpty().withMessage('name is required')],\n  (req, res) =\u003e {\n    const errors = validationResult(req);\n    if (!errors.isEmpty()) {\n      const errorMessages = errors.array().map((error) =\u003e error.msg);\n      return res.status(400).json({ errors: errorMessages });\n    }\n    next();\n  },\n  (req, res) =\u003e {\n    const { name } = req.body;\n    res.json({ msg: `hello ${name}` });\n  }\n);\n```\n\n#### Validation Middleware\n\nmiddleware/validationMiddleware.js\n\n```js\nimport { body, validationResult } from 'express-validator';\nimport { BadRequestError } from '../errors/customErrors';\nconst withValidationErrors = (validateValues) =\u003e {\n  return [\n    validateValues,\n    (req, res, next) =\u003e {\n      const errors = validationResult(req);\n      if (!errors.isEmpty()) {\n        const errorMessages = errors.array().map((error) =\u003e error.msg);\n        throw new BadRequestError(errorMessages);\n      }\n      next();\n    },\n  ];\n};\n\nexport const validateTest = withValidationErrors([\n  body('name')\n    .notEmpty()\n    .withMessage('name is required')\n    .isLength({ min: 3, max: 50 })\n    .withMessage('name must be between 3 and 50 characters long')\n    .trim(),\n]);\n```\n\n#### Remove Test Case From Server\n\n#### Setup Constants\n\nutils/constants.js\n\n```js\nexport const JOB_STATUS = {\n  PENDING: 'pending',\n  INTERVIEW: 'interview',\n  DECLINED: 'declined',\n};\n\nexport const JOB_TYPE = {\n  FULL_TIME: 'full-time',\n  PART_TIME: 'part-time',\n  INTERNSHIP: 'internship',\n};\n\nexport const JOB_SORT_BY = {\n  NEWEST_FIRST: 'newest',\n  OLDEST_FIRST: 'oldest',\n  ASCENDING: 'a-z',\n  DESCENDING: 'z-a',\n};\n```\n\nmodels/JobModel.js\n\n```js\nimport mongoose from 'mongoose';\nimport { JOB_STATUS, JOB_TYPE } from '../utils/constants';\nconst JobSchema = new mongoose.Schema(\n  {\n    company: String,\n    position: String,\n    jobStatus: {\n      type: String,\n      enum: Object.values(JOB_STATUS),\n      default: JOB_STATUS.PENDING,\n    },\n    jobType: {\n      type: String,\n      enum: Object.values(JOB_TYPE),\n      default: JOB_TYPE.FULL_TIME,\n    },\n    jobLocation: {\n      type: String,\n      default: 'my city',\n    },\n  },\n  { timestamps: true }\n);\n```\n\n#### Validate Create Job\n\nvalidationMiddleware.js\n\n```js\nimport { JOB_STATUS, JOB_TYPE } from '../utils/constants.js';\n\nexport const validateJobInput = withValidationErrors([\n  body('company').notEmpty().withMessage('company is required'),\n  body('position').notEmpty().withMessage('position is required'),\n  body('jobLocation').notEmpty().withMessage('job location is required'),\n  body('jobStatus')\n    .isIn(Object.values(JOB_STATUS))\n    .withMessage('invalid status value'),\n  body('jobType').isIn(Object.values(JOB_TYPE)).withMessage('invalid job type'),\n]);\n```\n\n```js\nimport { validateJobInput } from '../middleware/validationMiddleware.js';\n\nrouter.route('/').get(getAllJobs).post(validateJobInput, createJob);\nrouter\n  .route('/:id')\n  .get(getJob)\n  .patch(validateJobInput, updateJob)\n  .delete(deleteJob);\n```\n\n- create job request\n\n```json\n{\n  \"company\": \"coding addict\",\n  \"position\": \"backend-end\",\n  \"jobStatus\": \"pending\",\n  \"jobType\": \"full-time\",\n  \"jobLocation\": \"florida\"\n}\n```\n\n#### Validate ID Parameter\n\nvalidationMiddleware.js\n\n```js\nimport mongoose from 'mongoose';\n\nimport { param } from 'express-validator';\n\nexport const validateIdParam = withValidationErrors([\n  param('id')\n    .custom((value) =\u003e mongoose.Types.ObjectId.isValid(value))\n    .withMessage('invalid MongoDB id'),\n]);\n```\n\n```js\nexport const validateIdParam = withValidationErrors([\n  param('id').custom(async (value) =\u003e {\n    const isValidId = mongoose.Types.ObjectId.isValid(value);\n    if (!isValidId) throw new BadRequestError('invalid MongoDB id');\n    const job = await Job.findById(value);\n    if (!job) throw new NotFoundError(`no job with id : ${value}`);\n  }),\n]);\n```\n\n```js\nimport { body, param, validationResult } from 'express-validator';\nimport { BadRequestError, NotFoundError } from '../errors/customErrors.js';\nimport { JOB_STATUS, JOB_TYPE } from '../utils/constants.js';\nimport mongoose from 'mongoose';\nimport Job from '../models/JobModel.js';\n\nconst withValidationErrors = (validateValues) =\u003e {\n  return [\n    validateValues,\n    (req, res, next) =\u003e {\n      const errors = validationResult(req);\n      if (!errors.isEmpty()) {\n        const errorMessages = errors.array().map((error) =\u003e error.msg);\n        if (errorMessages[0].startsWith('no job')) {\n          throw new NotFoundError(errorMessages);\n        }\n        throw new BadRequestError(errorMessages);\n      }\n      next();\n    },\n  ];\n};\n```\n\n- remove NotFoundError from getJob, updateJob, deleteJob controllers\n\n#### Clean DB\n\n#### User Model\n\nmodels/UserModel.js\n\n```js\nimport mongoose from 'mongoose';\n\nconst UserSchema = new mongoose.Schema({\n  name: String,\n  email: String,\n  password: String,\n  lastName: {\n    type: String,\n    default: 'lastName',\n  },\n  location: {\n    type: String,\n    default: 'my city',\n  },\n  role: {\n    type: String,\n    enum: ['user', 'admin'],\n    default: 'user',\n  },\n});\n\nexport default mongoose.model('User', UserSchema);\n```\n\n#### User Controller and Router\n\ncontrollers/authController.js\n\n```js\nexport const register = async (req, res) =\u003e {\n  res.send('register');\n};\nexport const login = async (req, res) =\u003e {\n  res.send('register');\n};\n```\n\nrouters/authRouter.js\n\n```js\nimport { Router } from 'express';\nimport { register, login } from '../controllers/authController.js';\nconst router = Router();\n\nrouter.post('/register', register);\nrouter.post('/login', login);\n\nexport default router;\n```\n\nserver.js\n\n```js\nimport authRouter from './routers/authRouter.js';\n\napp.use('/api/v1/auth', authRouter);\n```\n\n#### Create User - Initial Setup\n\nauthController.js\n\n```js\nimport { StatusCodes } from 'http-status-codes';\nimport User from '../models/UserModel.js';\n\nexport const register = async (req, res) =\u003e {\n  const user = await User.create(req.body);\n  res.status(StatusCodes.CREATED).json({ user });\n};\n```\n\n- register user request\n\n```json\n{\n  \"name\": \"john\",\n  \"email\": \"john@gmail.com\",\n  \"password\": \"secret123\",\n  \"lastName\": \"smith\",\n  \"location\": \"my city\"\n}\n```\n\n#### Validate User\n\nvalidationMiddleware.js\n\n```js\nimport User from '../models/UserModel.js';\n\nexport const validateRegisterInput = withValidationErrors([\n  body('name').notEmpty().withMessage('name is required'),\n  body('email')\n    .notEmpty()\n    .withMessage('email is required')\n    .isEmail()\n    .withMessage('invalid email format')\n    .custom(async (email) =\u003e {\n      const user = await User.findOne({ email });\n      if (user) {\n        throw new BadRequestError('email already exists');\n      }\n    }),\n  body('password')\n    .notEmpty()\n    .withMessage('password is required')\n    .isLength({ min: 8 })\n    .withMessage('password must be at least 8 characters long'),\n  body('location').notEmpty().withMessage('location is required'),\n  body('lastName').notEmpty().withMessage('last name is required'),\n]);\n```\n\nauthRouter.js\n\n```js\nimport { validateRegisterInput } from '../middleware/validationMiddleware.js';\n\nrouter.post('/register', validateRegisterInput, register);\n```\n\n#### Admin Role\n\nauthController.js\n\n```js\n// first registered user is an admin\nconst isFirstAccount = (await User.countDocuments()) === 0;\nreq.body.role = isFirstAccount ? 'admin' : 'user';\n\nconst user = await User.create(req.body);\n```\n\n#### Hash Passwords\n\n[bcryptjs](https://www.npmjs.com/package/bcryptjs)\n\n```sh\nnpm i bcryptjs@2.4.3\n\n```\n\nauthController.js\n\n```js\nimport bcrypt from 'bcryptjs';\n\nconst register = async (req, res) =\u003e {\n  // a random value that is added to the password before hashing\n  const salt = await bcrypt.genSalt(10);\n  const hashedPassword = await bcrypt.hash(req.body.password, salt);\n  req.body.password = hashedPassword;\n\n  const user = await User.create(req.body);\n};\n```\n\nconst salt = await bcrypt.genSalt(10);\nThis line generates a random \"salt\" value that will be used to hash the password. A salt is a random value that is added to the password before hashing, which helps to make the resulting hash more resistant to attacks like dictionary attacks and rainbow table attacks. The genSalt() function in bcrypt generates a random salt value using a specified \"cost\" value. The cost value determines how much CPU time is needed to calculate the hash, and higher cost values result in stronger hashes that are more resistant to attacks.\n\nIn this example, a cost value of 10 is used to generate the salt. This is a good default value that provides a good balance between security and performance. However, you may need to adjust the cost value based on the specific needs of your application.\n\nconst hashedPassword = await bcrypt.hash(password, salt);\nThis line uses the generated salt value to hash the password. The hash() function in bcrypt takes two arguments: the password to be hashed, and the salt value to use for the hash. It then calculates the hash value using a one-way hash function and the specified salt value.\n\nThe resulting hash value is a string that represents the hashed password. This string can then be stored in a database or other storage mechanism to be compared against the user's password when they log in.\n\nBy using a salt value and a one-way hash function, bcrypt helps to ensure that user passwords are stored securely and are resistant to attacks like password cracking and brute-force attacks.\n\n##### BCRYPT VS BCRYPTJS\n\nbcrypt and bcryptjs are both popular libraries for hashing passwords in Node.js applications. However, bcryptjs is considered to be a better choice for a few reasons:\n\nCross-platform compatibility: bcrypt is a native Node.js module that uses C++ bindings, which can make it difficult to install and use on some platforms. bcryptjs, on the other hand, is a pure JavaScript implementation that works on any platform.\n\nSecurity: While both bcrypt and bcryptjs use the same underlying algorithm for hashing passwords, bcryptjs is designed to be more resistant to certain types of attacks, such as side-channel attacks.\n\nEase of use: bcryptjs has a simpler and more intuitive API than bcrypt, which can make it easier to use and integrate into your application.\n\nOverall, while bcrypt and bcryptjs are both good choices for hashing passwords in Node.js applications, bcryptjs is considered to be a better choice for its cross-platform compatibility, improved security, ease of use, and ongoing maintenance.\n\n#### Setup Password Utils\n\nutils/passwordUtils.js\n\n```js\nimport bcrypt from 'bcryptjs';\n\nexport async function hashPassword(password) {\n  const salt = await bcrypt.genSalt(10);\n  const hashedPassword = await bcrypt.hash(password, salt);\n  return hashedPassword;\n}\n```\n\nauthController.js\n\n```js\nimport { hashPassword } from '../utils/passwordUtils.js';\n\nconst register = async (req, res) =\u003e {\n  const hashedPassword = await hashPassword(req.body.password);\n  req.body.password = hashedPassword;\n\n  const user = await User.create(req.body);\n  res.status(StatusCodes.CREATED).json({ msg: 'user created' });\n};\n```\n\n#### Login User\n\n- login user request\n\n```json\n{\n  \"email\": \"john@gmail.com\",\n  \"password\": \"secret123\"\n}\n```\n\nvalidationMiddleware.js\n\n```js\nexport const validateLoginInput = withValidationErrors([\n  body('email')\n    .notEmpty()\n    .withMessage('email is required')\n    .isEmail()\n    .withMessage('invalid email format'),\n  body('password').notEmpty().withMessage('password is required'),\n]);\n```\n\nauthRouter.js\n\n```js\nimport { validateLoginInput } from '../middleware/validationMiddleware.js';\n\nrouter.post('/login', validateLoginInput, login);\n```\n\n#### Unauthenticated Error\n\nauthController.js\n\n```js\nimport { UnauthenticatedError } from '../errors/customErrors.js';\n\nconst login = async (req, res) =\u003e {\n  // check if user exists\n  // check if password is correct\n\n  const user = await User.findOne({ email: req.body.email });\n  if (!user) throw new UnauthenticatedError('invalid credentials');\n\n  res.send('login route');\n};\n```\n\n#### Compare Password\n\npasswordUtils.js\n\n```js\nexport async function comparePassword(password, hashedPassword) {\n  const isMatch = await bcrypt.compare(password, hashedPassword);\n  return isMatch;\n}\n```\n\nauthController.js\n\n```js\nimport { hashPassword, comparePassword } from '../utils/passwordUtils.js';\n\nconst login = async (req, res) =\u003e {\n  // check if user exists\n  // check if password is correct\n\n  const user = await User.findOne({ email: req.body.email });\n\n  if (!user) throw new UnauthenticatedError('invalid credentials');\n\n  const isPasswordCorrect = await comparePassword(\n    req.body.password,\n    user.password\n  );\n\n  if (!isPasswordCorrect) throw new UnauthenticatedError('invalid credentials');\n  res.send('login route');\n};\n```\n\nRefactor\n\n```js\nconst isValidUser = user \u0026\u0026 (await comparePassword(password, user.password));\nif (!isValidUser) throw new UnauthenticatedError('invalid credentials');\n```\n\n#### JSON Web Token\n\nA JSON Web Token (JWT) is a compact and secure way of transmitting data between parties. It is often used to authenticate and authorize users in web applications and APIs. JWTs contain information about the user and additional metadata, and can be used to securely transmit this information\n\n[Useful Resource](https://jwt.io/introduction)\n\n```sh\nnpm i jsonwebtoken@9.0.0\n```\n\nutils/tokenUtils.js\n\n```js\nimport jwt from 'jsonwebtoken';\n\nexport const createJWT = (payload) =\u003e {\n  const token = jwt.sign(payload, process.env.JWT_SECRET, {\n    expiresIn: process.env.JWT_EXPIRES_IN,\n  });\n  return token;\n};\n```\n\nJWT_SECRET represents the secret key used to sign the JWT. When creating a JWT, the payload (data) is signed with this secret key to generate a unique token. The secret key should be kept secure and should not be disclosed to unauthorized parties.\n\nJWT_EXPIRES_IN specifies the expiration time for the JWT. It determines how long the token remains valid before it expires. The value of JWT_EXPIRES_IN is typically provided as a duration, such as \"1h\" for one hour or \"7d\" for seven days. Once the token expires, it is no longer considered valid and can't be used for authentication or authorization purposes.\n\nThese environment variables (JWT_SECRET and JWT_EXPIRES_IN) are read from the system environment during runtime, allowing for flexibility in configuration without modifying the code.\n\nauthController.js\n\n```js\nimport { createJWT } from '../utils/tokenUtils.js';\n\nconst token = createJWT({ userId: user._id, role: user.role });\nconsole.log(token);\n```\n\n#### Test JWT (optional)\n\n[JWT](https://jwt.io/)\n\n#### ENV Variables\n\n- RESTART SERVER!!!!\n\n.env\n\n```js\nJWT_SECRET=\nJWT_EXPIRES_IN=\n```\n\n#### HTTP Only Cookie\n\nAn HTTP-only cookie is a cookie that can't be accessed by JavaScript running in the browser. It is designed to help prevent cross-site scripting (XSS) attacks, which can be used to steal cookies and other sensitive information.\n\n##### HTTP Only Cookie VS Local Storage\n\nAn HTTP-only cookie is a type of cookie that is designed to be inaccessible to JavaScript running in the browser. It is primarily used for authentication purposes and is a more secure way of storing sensitive information like user tokens. Local storage, on the other hand, is a browser-based storage mechanism that is accessible to JavaScript, and is used to store application data like preferences or user-generated content. While local storage is convenient, it is not a secure way of storing sensitive information as it can be accessed and modified by JavaScript running in the browser.\n\nauthControllers.js\n\n```js\nconst oneDay = 1000 * 60 * 60 * 24;\n\nres.cookie('token', token, {\n  httpOnly: true,\n  expires: new Date(Date.now() + oneDay),\n  secure: process.env.NODE_ENV === 'production',\n});\n\nres.status(StatusCodes.CREATED).json({ msg: 'user logged in' });\n```\n\n```js\nconst oneDay = 1000 * 60 * 60 * 24;\n```\n\nThis line defines a constant oneDay that represents the number of milliseconds in a day. This value is used later to set the expiration time for the cookie.\n\n```js\nres.cookie('token', token, {...});:\n```\n\nThis line sets a cookie with the name \"token\" and a value of token, which is the JWT that was generated for the user. The ... represents an object containing additional options for the cookie.\n\nhttpOnly: true: This option makes the cookie inaccessible to JavaScript running in the browser. This helps to prevent cross-site scripting (XSS) attacks, which can be used to steal cookies and other sensitive information.\n\nexpires: new Date(Date.now() + oneDay): This option sets the expiration time for the cookie. In this case, the cookie will expire one day from the current time (as represented by Date.now() + oneDay).\n\nsecure: process.env.NODE_ENV === 'production': This option determines whether the cookie should be marked as secure or not. If the NODE_ENV environment variable is set to \"production\", then the cookie is marked as secure, which means it can only be transmitted over HTTPS. This helps to prevent man-in-the-middle (MITM) attacks, which can intercept and modify cookies that are transmitted over unsecured connections.\n\njobsController.js\n\n```js\nexport const getAllJobs = async (req, res) =\u003e {\n  console.log(req);\n  const jobs = await Job.find({});\n  res.status(StatusCodes.OK).json({ jobs });\n};\n```\n\n#### Clean DB\n\n#### Connect User and Job\n\nmodels/User.js\n\n```js\nconst JobSchema = new mongoose.Schema(\n  {\n    ....\n    createdBy: {\n      type: mongoose.Types.ObjectId,\n      ref: 'User',\n    },\n  },\n  { timestamps: true }\n);\n```\n\n#### Auth Middleware\n\nmiddleware/authMiddleware.js\n\n```js\nexport const authenticateUser = async (req, res, next) =\u003e {\n  console.log('auth middleware');\n  next();\n};\n```\n\nserver.js\n\n```js\nimport { authenticateUser } from './middleware/authMiddleware.js';\n\napp.use('/api/v1/jobs', authenticateUser, jobRouter);\n```\n\n##### Cookie Parser\n\n[Cookie Parser](https://www.npmjs.com/package/cookie-parser)\n\n```sh\nnpm i cookie-parser@1.4.6\n```\n\nserver.js\n\n```js\nimport cookieParser from 'cookie-parser';\napp.use(cookieParser());\n```\n\n#### Access Token\n\nauthMiddleware.js\n\n```js\nimport { UnauthenticatedError } from '../customErrors.js';\n\nexport const authenticateUser = async (req, res, next) =\u003e {\n  const { token } = req.cookies;\n  if (!token) {\n    throw new UnauthenticatedError('authentication invalid');\n  }\n  next();\n};\n```\n\n#### Verify Token\n\nutils/tokenUtils.js\n\n```js\nexport const verifyJWT = (token) =\u003e {\n  const decoded = jwt.verify(token, process.env.JWT_SECRET);\n  return decoded;\n};\n```\n\nauthMiddleware.js\n\n```js\nimport { UnauthenticatedError } from '../customErrors.js';\nimport { verifyJWT } from '../utils/tokenUtils.js';\n\nexport const authenticateUser = async (req, res, next) =\u003e {\n  const { token } = req.cookies;\n  if (!token) {\n    throw new UnauthenticatedError('authentication invalid');\n  }\n\n  try {\n    const { userId, role } = verifyJWT(token);\n    req.user = { userId, role };\n    next();\n  } catch (error) {\n    throw new UnauthenticatedError('authentication invalid');\n  }\n};\n```\n\njobController.js\n\n```js\nexport const getAllJobs = async (req, res) =\u003e {\n  console.log(req.user);\n  const jobs = await Job.find({ createdBy: req.user.userId });\n  res.status(StatusCodes.OK).json({ jobs });\n};\n```\n\n#### Refactor Create Job\n\njobController.js\n\n```js\nexport const createJob = async (req, res) =\u003e {\n  req.body.createdBy = req.user.userId;\n  const job = await Job.create(req.body);\n  res.status(StatusCodes.CREATED).json({ job });\n};\n```\n\n#### Check Permissions\n\nvalidationMiddleware.js\n\n```js\nconst withValidationErrors = (validateValues) =\u003e {\n  return [\n    validateValues,\n    (req, res, next) =\u003e {\n      const errors = validationResult(req);\n      if (!errors.isEmpty()) {\n       ...\n        if (errorMessages[0].startsWith('not authorized')) {\n          throw new UnauthorizedError('not authorized to access this route');\n        }\n\n        throw new BadRequestError(errorMessages);\n      }\n      next();\n    },\n  ];\n};\n```\n\n```js\nimport {\n  BadRequestError,\n  NotFoundError,\n  UnauthorizedError,\n} from '../errors/customErrors.js';\n\nexport const validateIdParam = withValidationErrors([\n  param('id').custom(async (value, { req }) =\u003e {\n    const isValidMongoId = mongoose.Types.ObjectId.isValid(value);\n    if (!isValidMongoId) throw new BadRequestError('invalid MongoDB id');\n    const job = await Job.findById(value);\n    if (!job) throw new NotFoundError(`no job with id ${value}`);\n    const isAdmin = req.user.role === 'admin';\n    const isOwner = req.user.userId === job.createdBy.toString();\n    if (!isAdmin \u0026\u0026 !isOwner)\n      throw UnauthorizedError('not authorized to access this route');\n  }),\n]);\n```\n\n#### Logout User\n\ncontrollers/authController.js\n\n```js\nconst logout = (req, res) =\u003e {\n  res.cookie('token', 'logout', {\n    httpOnly: true,\n    expires: new Date(Date.now()),\n  });\n  res.status(StatusCodes.OK).json({ msg: 'user logged out!' });\n};\n```\n\nroutes/authRouter.js\n\n```js\nimport { Router } from 'express';\nconst router = Router();\nimport { logout } from '../controllers/authController.js';\n\nrouter.get('/logout', logout);\n\nexport default router;\n```\n\n#### User Routes\n\ncontrollers/userController.js\n\n```js\nimport { StatusCodes } from 'http-status-codes';\nimport User from '../models/User.js';\nimport Job from '../models/Job.js';\n\nexport const getCurrentUser = async (req, res) =\u003e {\n  res.status(StatusCodes.OK).json({ msg: 'get current user' });\n};\n\nexport const getApplicationStats = async (req, res) =\u003e {\n  res.status(StatusCodes.OK).json({ msg: 'application stats' });\n};\n\nexport const updateUser = async (req, res) =\u003e {\n  res.status(StatusCodes.OK).json({ msg: 'update user' });\n};\n```\n\nroutes/userRouter.js\n\n```js\nimport { Router } from 'express';\nconst router = Router();\n\nimport {\n  getCurrentUser,\n  getApplicationStats,\n  updateUser,\n} from '../controllers/userController.js';\n\nrouter.get('/current-user', getCurrentUser);\nrouter.get('/admin/app-stats', getApplicationStats);\nrouter.patch('/update-user', updateUser);\nexport default router;\n```\n\nserver.js\n\n```js\nimport userRouter from './routers/userRouter.js';\n\napp.use('/api/v1/users', authenticateUser, userRouter);\n```\n\n#### Get Current User\n\n```js\nexport const getCurrentUser = async (req, res) =\u003e {\n  const user = await User.findOne({ _id: req.user.userId });\n  res.status(StatusCodes.OK).json({ user });\n};\n```\n\n#### Remove Password\n\nmodels/UserModel.js\n\n```js\nUserSchema.methods.toJSON = function () {\n  var obj = this.toObject();\n  delete obj.password;\n  return obj;\n};\n```\n\n```js\nexport const getCurrentUser = async (req, res) =\u003e {\n  const user = await User.findOne({ _id: req.user.userId });\n  const userWithoutPassword = user.toJSON();\n  res.status(StatusCodes.OK).json({ user: userWithoutPassword });\n};\n```\n\n#### Update User\n\nmiddleware/validationMiddleware.js\n\n```js\nconst validateUpdateUserInput = withValidationErrors([\n  body('name').notEmpty().withMessage('name is required'),\n  body('email')\n    .notEmpty()\n    .withMessage('email is required')\n    .isEmail()\n    .withMessage('invalid email format')\n    .custom(async (email, { req }) =\u003e {\n      const user = await User.findOne({ email });\n      if (user \u0026\u0026 user._id.toString() !== req.user.userId) {\n        throw new Error('email already exists');\n      }\n    }),\n  body('lastName').notEmpty().withMessage('last name is required'),\n  body('location').notEmpty().withMessage('location is required'),\n]);\n```\n\n```js\nexport const updateUser = async (req, res) =\u003e {\n  const updatedUser = await User.findByIdAndUpdate(req.user.userId, req.body);\n  res.status(StatusCodes.OK).json({ msg: 'user updated' });\n};\n```\n\n```json\n{\n  \"name\": \"john\",\n  \"email\": \"john@gmail.com\",\n  \"lastName\": \"smith\",\n  \"location\": \"florida\"\n}\n```\n\n#### Application Stats\n\n```js\nexport const getApplicationStats = async (req, res) =\u003e {\n  const users = await User.countDocuments();\n  const jobs = await Job.countDocuments();\n  res.status(StatusCodes.OK).json({ users, jobs });\n};\n```\n\n```js\nexport const authorizePermissions = (...roles) =\u003e {\n  return (req, res, next) =\u003e {\n    if (!roles.includes(req.user.role)) {\n      throw new UnauthorizedError('Unauthorized to access this route');\n    }\n    next();\n  };\n};\n```\n\n```js\nimport { authorizePermissions } from '../middleware/authMiddleware.js';\n\nrouter.get('/admin/app-stats', [\n  authorizePermissions('admin'),\n  getApplicationStats,\n]);\n```\n\n#### Setup Proxy\n\n- only in dev env\n- a must since cookies are sent back to the same server\n- spin up both servers (our own and vite dev)\n\n- server\n\n```sh\nnpm run dev\n```\n\n- vite dev server\n\n```sh\ncd client \u0026\u0026 npm run dev\n```\n\nserver.js\n\n```js\napp.get('/api/v1/test', (req, res) =\u003e {\n  res.json({ msg: 'test route' });\n});\n```\n\nclient/src/main.jsx\n\n```js\nfetch('http://localhost:5100/api/v1/test')\n  .then((res) =\u003e res.json())\n  .then((data) =\u003e console.log(data));\n```\n\nclient/vite.config.js\n\n```js\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    proxy: {\n      '/api': {\n        target: 'http://localhost:5100/api',\n        changeOrigin: true,\n        rewrite: (path) =\u003e path.replace(/^\\/api/, ''),\n      },\n    },\n  },\n});\n```\n\nmain.jsx\n\n```js\nfetch('/api/v1/test')\n  .then((res) =\u003e res.json())\n  .then((data) =\u003e console.log(data));\n```\n\nThis code configures a proxy rule for the development server, specifically for requests that start with /api. Let's go through each property:\n\n'/api': This is the path to match. If a request is made to the development server with a path that starts with /api, the proxy rule will be applied.\ntarget: 'http://localhost:5100/api': This specifies the target URL where the requests will be redirected. In this case, any request that matches the /api path will be forwarded to http://localhost:5100/api.\n\nchangeOrigin: true: When set to true, this property changes the origin of the request to match the target URL. This can be useful when working with CORS (Cross-Origin Resource Sharing) restrictions.\n\nrewrite: (path) =\u003e path.replace(/^\\/api/, ''): This property allows you to modify the path of the request before it is forwarded to the target. In this case, the rewrite function uses a regular expression (/^\\/api/) to remove the /api prefix from the path. For example, if a request is made to /api/users, the rewritten path will be /users.\n\nTo summarize, these lines of code configure a proxy rule for requests starting with /api on the development server. The requests will be redirected to http://localhost:5100/api, with the /api prefix removed from the path.\n\n#### Concurrently\n\nThe concurrently npm package is a utility that allows you to run multiple commands concurrently in the same terminal window. It provides a convenient way to execute multiple tasks or processes simultaneously.\n\n```sh\nnpm i concurrently@8.0.1\n```\n\n```json\n\"scripts\": {\n    \"setup-project\": \"npm i \u0026\u0026 cd client \u0026\u0026 npm i\",\n    \"server\": \"nodemon server\",\n    \"client\": \"cd client \u0026\u0026 npm run dev\",\n    \"dev\": \"concurrently --kill-others-on-fail \\\" npm run server\\\" \\\" npm run client\\\"\"\n  },\n```\n\nBy default, when a command fails, concurrently continues running the remaining commands. However, when --kill-others-on-fail is specified, if any of the commands fail, concurrently will immediately terminate all the other running commands.\n\n#### Axios\n\nAxios is a popular JavaScript library that simplifies the process of making HTTP requests from web browsers or Node.js. It provides a simple and elegant API for performing asynchronous HTTP requests, supporting features such as making GET, POST, PUT, and DELETE requests, handling request and response headers, handling request cancellation, and more.\n\n[Axios Docs](https://axios-http.com/docs/intro)\n\n```sh\nnpm i axios@1.3.6\n```\n\nmain.jsx\n\n```js\nimport axios from 'axios';\n\nconst data = await axios.get('/api/v1/test');\nconsole.log(data);\n```\n\n#### Custom Instance\n\nutils/customFetch.js\n\n```js\nimport axios from 'axios';\nconst customFetch = axios.create({\n  baseURL: '/api/v1',\n});\n\nexport default customFetch;\n```\n\nmain.jsx\n\n```js\nimport customFetch from './utils/customFetch.js';\n\nconst data = await customFetch.get('/test');\nconsole.log(data);\n```\n\n#### Typical Form Submission\n\n```js\nimport { useState } from 'react';\nimport axios from 'axios';\nconst MyForm = () =\u003e {\n  const [value, setValue] = useState('');\n\n  const handleSubmit = async (event) =\u003e {\n    event.preventDefault();\n    const data = await axios.post('url', { value });\n  };\n\n  return \u003cform onSubmit={handleSubmit}\u003e.....\u003c/form\u003e;\n};\n\nexport default MyForm;\n```\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\nRegister.jsx\n\n```js\nimport { Form, redirect, useNavigation, Link } from 'react-router-dom';\nimport Wrapper from '../assets/wrappers/RegisterAndLoginPage';\nimport { FormRow, Logo } from '../components';\n\nconst Register = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        ...\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Register;\n```\n\nApp.jsx\n\n```jsx\n{\n  path: 'register',\n  element: \u003cRegister /\u003e,\n  action: () =\u003e {\n   console.log('hello there');\n   return null;\n    },\n},\n```\n\n#### Register User\n\n- FormData API\n\n[FormData API - JS Nuggets](https://youtu.be/5-x4OUM-SP8)\n[FormData API - React ](https://youtu.be/WrX5RndZIzw)\n\nRegister.jsx\n\n```js\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n  try {\n    await customFetch.post('/auth/register', data);\n    return redirect('/login');\n  } catch (error) {\n    return error;\n  }\n};\n```\n\nApp.jsx\n\n```jsx\nimport { action as registerAction } from './pages/Register';\n\n{\n  path: 'register',\n  element: \u003cRegister /\u003e,\n  action:registerAction\n},\n```\n\n#### useNavigation() and navigation.state\n\nThis hook tells you everything you need to know about a page navigation to build pending navigation indicators and optimistic UI on data mutations. Things like:\n\n- Global loading indicators\n- Adding busy indicators to submit buttons\n\nNavigation State\n\nidle - There is no navigation pending.\nsubmitting - A route action is being called due to a form submission using POST, PUT, PATCH, or DELETE\nloading - The loaders for the next routes are being called to render the next page\n\nRegister.jsx\n\n```js\nconst Register = () =\u003e {\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        ....\n        \u003cbutton type='submit' className='btn btn-block' disabled={isSubmitting}\u003e\n          {isSubmitting ? 'submitting...' : 'submit'}\n        \u003c/button\u003e\n        ...\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Register;\n```\n\n#### React-Toastify\n\nImport and set up the react-toastify library.\n\n[React Toastify](https://fkhadra.github.io/react-toastify/introduction)\n\n```sh\nnpm i react-toastify@9.1.2\n```\n\nmain.jsx\n\n```js\nimport 'react-toastify/dist/ReactToastify.css';\nimport { ToastContainer } from 'react-toastify';\nReactDOM.createRoot(document.getElementById('root')).render(\n  \u003cReact.StrictMode\u003e\n    \u003cApp /\u003e\n    \u003cToastContainer position='top-center' /\u003e\n  \u003c/React.StrictMode\u003e\n);\n```\n\nRegister.jsx\n\n```js\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    await customFetch.post('/auth/register', data);\n    toast.success('Registration successful');\n    return redirect('/login');\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n```\n\n#### Login User\n\n```js\nimport { Link, Form, redirect, useNavigation } from 'react-router-dom';\nimport Wrapper from '../assets/wrappers/RegisterAndLoginPage';\nimport { FormRow, Logo } from '../components';\nimport customFetch from '../utils/customFetch';\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    await customFetch.post('/auth/login', data);\n    toast.success('Login successful');\n    return redirect('/dashboard');\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n\nconst Login = () =\u003e {\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        \u003cLogo /\u003e\n        \u003ch4\u003elogin\u003c/h4\u003e\n        \u003cFormRow type='email' name='email' defaultValue='john@gmail.com' /\u003e\n        \u003cFormRow type='password' name='password' defaultValue='secret123' /\u003e\n        \u003cbutton type='submit' className='btn btn-block' disabled={isSubmitting}\u003e\n          {isSubmitting ? 'submitting...' : 'submit'}\n        \u003c/button\u003e\n        \u003cbutton type='button' className='btn btn-block'\u003e\n          explore the app\n        \u003c/button\u003e\n        \u003cp\u003e\n          Not a member yet?\n          \u003cLink to='/register' className='member-btn'\u003e\n            Register\n          \u003c/Link\u003e\n        \u003c/p\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Login;\n```\n\n#### Access Action Data (optional)\n\n```js\nimport { useActionData } from 'react-router-dom';\n\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n  const errors = { msg: '' };\n  if (data.password.length \u003c 3) {\n    errors.msg = 'password too short';\n    return errors;\n  }\n  try {\n    await customFetch.post('/auth/login', data);\n    toast.success('Login successful');\n    return redirect('/dashboard');\n  } catch (error) {\n    // toast.error(error?.response?.data?.msg);\n    errors.msg = error.response.data.msg;\n    return errors;\n  }\n};\n\nconst Login = () =\u003e {\n  const errors = useActionData();\n\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        ...\n        {errors \u0026\u0026 \u003cp style={{ color: 'red' }}\u003e{errors.msg}\u003c/p\u003e}\n        ...\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Login;\n```\n\n#### Get Current User\n\nEach route can define a \"loader\" function to provide data to the route element before it renders.\n\n- must return a value\n\nDashboardLayout.jsx\n\n```jsx\nimport { Outlet, redirect, useLoaderData } from 'react-router-dom';\nimport customFetch from '../utils/customFetch';\n\nexport const loader = async () =\u003e {\n  try {\n    const { data } = await customFetch('/users/current-user');\n    return data;\n  } catch (error) {\n    return redirect('/');\n  }\n};\n\n\nconst DashboardLayout = ({ isDarkThemeEnabled }) =\u003e {\n  const { user } = useLoaderData();\n\n  return (\n    \u003cDashboardContext.Provider\n      value={{\n        user,\n        showSidebar,\n        isDarkTheme,\n        toggleDarkTheme,\n        toggleSidebar,\n        logoutUser,\n      }}\n    \u003e\n      \u003cWrapper\u003e\n        \u003cmain className='dashboard'\u003e\n         ...\n            \u003cdiv className='dashboard-page'\u003e\n              \u003cOutlet context={{ user }} /\u003e\n            \u003c/div\u003e\n          \u003c/div\u003e\n        \u003c/main\u003e\n      \u003c/Wrapper\u003e\n    \u003c/DashboardContext.Provider\u003e\n  );\n};\nexport const useDashboardContext = () =\u003e useContext(DashboardContext);\nexport default DashboardLayout;\n\n```\n\n#### Logout User\n\nDashboardLayout.jsx\n\n```js\nimport { useNavigate } from 'react-router-dom';\nimport { toast } from 'react-toastify';\n\nconst DashboardLayout = () =\u003e {\n  const navigate = useNavigate();\n\n  const logoutUser = async () =\u003e {\n    navigate('/');\n    await customFetch.get('/auth/logout');\n    toast.success('Logging out...');\n  };\n};\n```\n\n#### AddJob - Structure\n\npages/AddJob.jsx\n\n```js\nimport { FormRow } from '../components';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { useOutletContext } from 'react-router-dom';\nimport { JOB_STATUS, JOB_TYPE } from '../../../utils/constants';\nimport { Form, useNavigation, redirect } from 'react-router-dom';\nimport { toast } from 'react-toastify';\nimport customFetch from '../utils/customFetch';\n\nconst AddJob = () =\u003e {\n  const { user } = useOutletContext();\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        \u003ch4 className='form-title'\u003eadd job\u003c/h4\u003e\n        \u003cdiv className='form-center'\u003e\n          \u003cFormRow type='text' name='position' /\u003e\n          \u003cFormRow type='text' name='company' /\u003e\n          \u003cFormRow\n            type='text'\n            labelText='job location'\n            name='jobLocation'\n            defaultValue={user.location}\n          /\u003e\n\n          \u003cbutton\n            type='submit'\n            className='btn btn-block form-btn '\n            disabled={isSubmitting}\n          \u003e\n            {isSubmitting ? 'submitting...' : 'submit'}\n          \u003c/button\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default AddJob;\n```\n\n#### Select Input\n\n```js\n\u003cdiv className='form-row'\u003e\n  \u003clabel htmlFor='jobStatus' className='form-label'\u003e\n    job status\n  \u003c/label\u003e\n  \u003cselect\n    name='jobStatus'\n    id='jobStatus'\n    className='form-select'\n    defaultValue={JOB_TYPE.FULL_TIME}\n  \u003e\n    {Object.values(JOB_TYPE).map((itemValue) =\u003e {\n      return (\n        \u003coption key={itemValue} value={itemValue}\u003e\n          {itemValue}\n        \u003c/option\u003e\n      );\n    })}\n  \u003c/select\u003e\n\u003c/div\u003e\n```\n\n#### FormRowSelect Component\n\ncomponents/FormRowSelect.jsx\n\n```js\nconst FormRowSelect = ({ name, labelText, list, defaultValue = '' }) =\u003e {\n  return (\n    \u003cdiv className='form-row'\u003e\n      \u003clabel htmlFor={name} className='form-label'\u003e\n        {labelText || name}\n      \u003c/label\u003e\n      \u003cselect\n        name={name}\n        id={name}\n        className='form-select'\n        defaultValue={defaultValue}\n      \u003e\n        {list.map((itemValue) =\u003e {\n          return (\n            \u003coption key={itemValue} value={itemValue}\u003e\n              {itemValue}\n            \u003c/option\u003e\n          );\n        })}\n      \u003c/select\u003e\n    \u003c/div\u003e\n  );\n};\nexport default FormRowSelect;\n```\n\npages/AddJob.jsx\n\n```js\n\u003cFormRowSelect\n  labelText='job status'\n  name='jobStatus'\n  defaultValue={JOB_STATUS.PENDING}\n  list={Object.values(JOB_STATUS)}\n  /\u003e\n\u003cFormRowSelect\n  name='jobType'\n  labelText='job type'\n  defaultValue={JOB_TYPE.FULL_TIME}\n  list={Object.values(JOB_TYPE)}\n  /\u003e\n```\n\n#### Create Job\n\nAddJob.jsx\n\n```js\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n\n  try {\n    await customFetch.post('/jobs', data);\n    toast.success('Job added successfully');\n    return null;\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n```\n\n#### Pending Class and Redirect\n\nwrappers/BigSidebar.js\n\n```css\n.pending {\n  background: var(--background-color);\n}\n```\n\nAddJob.jsx\n\n```js\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n\n  try {\n    await customFetch.post('/jobs', data);\n    toast.success('Job added successfully');\n    return redirect('all-jobs');\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n```\n\n#### Add Job - CSS(optional)\n\nwrappers/DashboardFormPage.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  border-radius: var(--border-radius);\n  width: 100%;\n  background: var(--background-secondary-color);\n  padding: 3rem 2rem 4rem;\n  box-shadow: var(--shadow-2);\n  .form-title {\n    margin-bottom: 2rem;\n  }\n\n  .form {\n    margin: 0;\n    border-radius: 0;\n    box-shadow: none;\n    padding: 0;\n    max-width: 100%;\n    width: 100%;\n  }\n  .form-row {\n    margin-bottom: 0;\n  }\n  .form-center {\n    display: grid;\n    row-gap: 1rem;\n  }\n  .form-btn {\n    align-self: end;\n    margin-top: 1rem;\n    display: grid;\n    place-items: center;\n  }\n\n  @media (min-width: 992px) {\n    .form-center {\n      grid-template-columns: 1fr 1fr;\n      align-items: center;\n      column-gap: 1rem;\n    }\n  }\n  @media (min-width: 1120px) {\n    .form-center {\n      grid-template-columns: 1fr 1fr 1fr;\n    }\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### All Jobs - Structure\n\n- create JobsContainer and SearchContainer (export)\n- handle loader in App.jsx\n\n```js\nimport { toast } from 'react-toastify';\nimport { JobsContainer, SearchContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useLoaderData } from 'react-router-dom';\nimport { useContext, createContext } from 'react';\n\nexport const loader = async ({ request }) =\u003e {\n  try {\n    const { data } = await customFetch.get('/jobs');\n    return {\n      data,\n    };\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n    return error;\n  }\n};\n\nconst AllJobs = () =\u003e {\n  const { data } = useLoaderData();\n\n  return (\n    \u003c\u003e\n      \u003cSearchContainer /\u003e\n      \u003cJobsContainer /\u003e\n    \u003c/\u003e\n  );\n};\nexport default AllJobs;\n```\n\n#### Setup All Jobs Context\n\n```js\nconst AllJobsContext = createContext();\n\nconst AllJobs = () =\u003e {\n  const { data } = useLoaderData();\n\n  return (\n    \u003cAllJobsContext.Provider value={{ data }}\u003e\n      \u003cSearchContainer /\u003e\n      \u003cJobsContainer /\u003e\n    \u003c/AllJobsContext.Provider\u003e\n  );\n};\n\nexport const useAllJobsContext = () =\u003e useContext(AllJobsContext);\n```\n\n#### Render Jobs\n\n- create Job.jsx\n\nJobsContainer.jsx\n\n```js\nimport Job from './Job';\nimport Wrapper from '../assets/wrappers/JobsContainer';\n\nimport { useAllJobsContext } from '../pages/AllJobs';\n\nconst JobsContainer = () =\u003e {\n  const { data } = useAllJobsContext();\n  const { jobs } = data;\n  if (jobs.length === 0) {\n    return (\n      \u003cWrapper\u003e\n        \u003ch2\u003eNo jobs to display...\u003c/h2\u003e\n      \u003c/Wrapper\u003e\n    );\n  }\n\n  return (\n    \u003cWrapper\u003e\n      \u003cdiv className='jobs'\u003e\n        {jobs.map((job) =\u003e {\n          return \u003cJob key={job._id} {...job} /\u003e;\n        })}\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default JobsContainer;\n```\n\n#### JobsContainer - CSS (optional)\n\nwrappers/JobsContainer.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  margin-top: 4rem;\n  h2 {\n    text-transform: none;\n  }\n  \u0026 \u003e h5 {\n    font-weight: 700;\n    margin-bottom: 1.5rem;\n  }\n  .jobs {\n    display: grid;\n    grid-template-columns: 1fr;\n    row-gap: 2rem;\n  }\n  @media (min-width: 1120px) {\n    .jobs {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 2rem;\n    }\n  }\n`;\nexport default Wrapper;\n```\n\n#### Dayjs\n\n```sh\nnpm i dayjs@1.11.7\n```\n\n[Dayjs Docs](https://day.js.org/docs/en/installation/installation)\n\n#### Job Component\n\n- create JobInfo component\n\n```js\nimport { FaLocationArrow, FaBriefcase, FaCalendarAlt } from 'react-icons/fa';\nimport { Link, Form } from 'react-router-dom';\nimport Wrapper from '../assets/wrappers/Job';\nimport JobInfo from './JobInfo';\nimport day from 'dayjs';\nimport advancedFormat from 'dayjs/plugin/advancedFormat';\nday.extend(advancedFormat);\n\nconst Job = ({\n  _id,\n  position,\n  company,\n  jobLocation,\n  jobType,\n  createdAt,\n  jobStatus,\n}) =\u003e {\n  const date = day(createdAt).format('MMM Do, YYYY');\n\n  return (\n    \u003cWrapper\u003e\n      \u003cheader\u003e\n        \u003cdiv className='main-icon'\u003e{company.charAt(0)}\u003c/div\u003e\n        \u003cdiv className='info'\u003e\n          \u003ch5\u003e{position}\u003c/h5\u003e\n          \u003cp\u003e{company}\u003c/p\u003e\n        \u003c/div\u003e\n      \u003c/header\u003e\n      \u003cdiv className='content'\u003e\n        \u003cdiv className='content-center'\u003e\n          \u003cJobInfo icon={\u003cFaLocationArrow /\u003e} text={jobLocation} /\u003e\n          \u003cJobInfo icon={\u003cFaCalendarAlt /\u003e} text={date} /\u003e\n          \u003cJobInfo icon={\u003cFaBriefcase /\u003e} text={jobType} /\u003e\n          \u003cdiv className={`status ${jobStatus}`}\u003e{jobStatus}\u003c/div\u003e\n        \u003c/div\u003e\n\n        \u003cfooter className='actions'\u003e\n          \u003cLink className='btn edit-btn'\u003eEdit\u003c/Link\u003e\n          \u003cForm\u003e\n            \u003cbutton type='submit' className='btn delete-btn'\u003e\n              Delete\n            \u003c/button\u003e\n          \u003c/Form\u003e\n        \u003c/footer\u003e\n      \u003c/div\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Job;\n```\n\n#### JobInfo Component\n\n```js\nimport Wrapper from '../assets/wrappers/JobInfo';\n\nconst JobInfo = ({ icon, text }) =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cspan className='job-icon'\u003e{icon}\u003c/span\u003e\n      \u003cspan className='job-text'\u003e{text}\u003c/span\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default JobInfo;\n```\n\n#### JobInfo - CSS (optional)\n\nwrappers/JobInfo.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n\n  .job-icon {\n    font-size: 1rem;\n    margin-right: 1rem;\n    display: flex;\n    align-items: center;\n    svg {\n      color: var(--text-secondary-color);\n    }\n  }\n  .job-text {\n    text-transform: capitalize;\n    letter-spacing: var(--letter-spacing);\n  }\n`;\nexport default Wrapper;\n```\n\n#### Job - CSS (optional)\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.article`\n  background: var(--background-secondary-color);\n  border-radius: var(--border-radius);\n  display: grid;\n  grid-template-rows: 1fr auto;\n  box-shadow: var(--shadow-2);\n\n  header {\n    padding: 1rem 1.5rem;\n    border-bottom: 1px solid var(--grey-100);\n    display: grid;\n    grid-template-columns: auto 1fr;\n    align-items: center;\n  }\n  .main-icon {\n    width: 60px;\n    height: 60px;\n    display: grid;\n    place-items: center;\n    background: var(--primary-500);\n    border-radius: var(--border-radius);\n    font-size: 1.5rem;\n    font-weight: 700;\n    text-transform: uppercase;\n    color: var(--white);\n    margin-right: 2rem;\n  }\n  .info {\n    h5 {\n      margin-bottom: 0.5rem;\n    }\n    p {\n      margin: 0;\n      text-transform: capitalize;\n      color: var(--text-secondary-color);\n      letter-spacing: var(--letter-spacing);\n    }\n  }\n\n  .content {\n    padding: 1rem 1.5rem;\n  }\n  .content-center {\n    display: grid;\n    margin-top: 1rem;\n    margin-bottom: 1.5rem;\n    grid-template-columns: 1fr;\n    row-gap: 1.5rem;\n    align-items: center;\n    @media (min-width: 576px) {\n      grid-template-columns: 1fr 1fr;\n    }\n  }\n\n  .status {\n    border-radius: var(--border-radius);\n    text-transform: capitalize;\n    letter-spacing: var(--letter-spacing);\n    text-align: center;\n    width: 100px;\n    height: 30px;\n    display: grid;\n    align-items: center;\n  }\n  .actions {\n    margin-top: 1rem;\n    display: flex;\n    align-items: center;\n  }\n  .edit-btn,\n  .delete-btn {\n    height: 30px;\n    font-size: 0.85rem;\n    display: flex;\n    align-items: center;\n  }\n  .edit-btn {\n    margin-right: 0.5rem;\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### Edit Job - Setup\n\nJob.jsx\n\n```js\n\u003cLink to={`../edit-job/${_id}`} className='btn edit-btn'\u003e\n  Edit\n\u003c/Link\u003e\n```\n\npages/EditJob.jsx\n\n```js\nimport { FormRow, FormRowSelect } from '../components';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { useLoaderData } from 'react-router-dom';\nimport { JOB_STATUS, JOB_TYPE } from '../../../utils/constants';\nimport { Form, useNavigation, redirect } from 'react-router-dom';\nimport { toast } from 'react-toastify';\nimport customFetch from '../utils/customFetch';\n\nexport const loader = async () =\u003e {\n  return null;\n};\nexport const action = async () =\u003e {\n  return null;\n};\n\nconst EditJob = () =\u003e {\n  return \u003ch1\u003eEditJob Page\u003c/h1\u003e;\n};\nexport default EditJob;\n```\n\n- import EditJob page\n  App.jsx\n\n```js\nimport { loader as editJobLoader } from './pages/EditJob';\nimport { action as editJobAction } from './pages/EditJob';\n\n\n{\n  path: 'edit-job/:id',\n  element: \u003cEditJob /\u003e,\n  loader: editJobLoader,\n  action: editJobAction,\n},\n```\n\npages/EditJob.jsx\n\n```js\nexport const loader = async ({ params }) =\u003e {\n  try {\n    const { data } = await customFetch.get(`/jobs/${params.id}`);\n    return data;\n  } catch (error) {\n    toast.error(error.response.data.msg);\n    return redirect('/dashboard/all-jobs');\n  }\n};\nexport const action = async () =\u003e {\n  return null;\n};\n\nconst EditJob = () =\u003e {\n  const params = useParams();\n  console.log(params);\n  const { job } = useLoaderData();\n\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n  return \u003ch1\u003eEditJob Page\u003c/h1\u003e;\n};\nexport default EditJob;\n```\n\n#### Edit Job - Complete\n\n```js\nexport const action = async ({ request, params }) =\u003e {\n  const formData = await request.formData();\n  const data = Object.fromEntries(formData);\n\n  try {\n    await customFetch.patch(`/jobs/${params.id}`, data);\n    toast.success('Job edited successfully');\n    return redirect('/dashboard/all-jobs');\n  } catch (error) {\n    toast.error(error.response.data.msg);\n    return error;\n  }\n};\n\nconst EditJob = () =\u003e {\n  const { job } = useLoaderData();\n\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        \u003ch4 className='form-title'\u003eedit job\u003c/h4\u003e\n        \u003cdiv className='form-center'\u003e\n          \u003cFormRow type='text' name='position' defaultValue={job.position} /\u003e\n          \u003cFormRow type='text' name='company' defaultValue={job.company} /\u003e\n          \u003cFormRow\n            type='text'\n            labelText='job location'\n            name='jobLocation'\n            defaultValue={job.jobLocation}\n          /\u003e\n\n          \u003cFormRowSelect\n            name='jobStatus'\n            labelText='job status'\n            defaultValue={job.jobStatus}\n            list={Object.values(JOB_STATUS)}\n          /\u003e\n          \u003cFormRowSelect\n            name='jobType'\n            labelText='job type'\n            defaultValue={job.jobType}\n            list={Object.values(JOB_TYPE)}\n          /\u003e\n          \u003cbutton\n            type='submit'\n            className='btn btn-block form-btn '\n            disabled={isSubmitting}\n          \u003e\n            {isSubmitting ? 'submitting...' : 'submit'}\n          \u003c/button\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default EditJob;\n```\n\n#### Delete Job\n\nJob.jsx\n\n```js\n\u003cForm method='post' action={`../delete-job/${_id}`}\u003e\n  \u003cbutton type='submit' className='btn delete-btn'\u003e\n    Delete\n  \u003c/button\u003e\n\u003c/Form\u003e\n```\n\npages/DeleteJob.jsx\n\n```js\nimport { redirect } from 'react-router-dom';\nimport customFetch from '../utils/customFetch';\nimport { toast } from 'react-toastify';\n\nexport async function action({ params }) {\n  try {\n    await customFetch.delete(`/jobs/${params.id}`);\n    toast.success('Job deleted successfully');\n  } catch (error) {\n    toast.error(error.response.data.msg);\n  }\n  return redirect('/dashboard/all-jobs');\n}\n```\n\nApp.jsx\n\n```js\nimport { action as deleteJobAction } from './pages/DeleteJob';\n\n { path: 'delete-job/:id', action: deleteJobAction },\n```\n\n#### Admin Page\n\npages/Admin.jsx\n\n```js\nimport { FaSuitcaseRolling, FaCalendarCheck } from 'react-icons/fa';\n\nimport { useLoaderData, redirect } from 'react-router-dom';\nimport customFetch from '../utils/customFetch';\nimport Wrapper from '../assets/wrappers/StatsContainer';\nimport { toast } from 'react-toastify';\nexport const loader = async () =\u003e {\n  try {\n    const response = await customFetch.get('/users/admin/app-stats');\n    return response.data;\n  } catch (error) {\n    toast.error('You are not authorized to view this page');\n    return redirect('/dashboard');\n  }\n};\n\nconst Admin = () =\u003e {\n  const { users, jobs } = useLoaderData();\n\n  return (\n    \u003cWrapper\u003e\n      \u003ch2\u003eadmin page\u003c/h2\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Admin;\n```\n\nApp.jsx\n\n```js\nimport { loader as adminLoader } from './pages/Admin';\n\n{\n  path: 'admin',\n  element: \u003cAdmin /\u003e,\n  loader: adminLoader,\n},\n\n```\n\nNavLinks.jsx\n\n```js\n{\n  links.map((link) =\u003e {\n    const { text, path, icon } = link;\n    const { role } = user;\n    if (role !== 'admin' \u0026\u0026 path === 'admin') return;\n  });\n}\n```\n\n#### StatItem Component\n\n- create StatItem.jsx\n- import/export\n\n  StatItem.jsx\n\n```js\nimport Wrapper from '../assets/wrappers/StatItem';\n\nconst StatItem = ({ count, title, icon, color, bcg }) =\u003e {\n  return (\n    \u003cWrapper color={color} bcg={bcg}\u003e\n      \u003cheader\u003e\n        \u003cspan className='count'\u003e{count}\u003c/span\u003e\n        \u003cspan className='icon'\u003e{icon}\u003c/span\u003e\n      \u003c/header\u003e\n      \u003ch5 className='title'\u003e{title}\u003c/h5\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default StatItem;\n```\n\nAdmin.jsx\n\n```js\nimport { StatItem } from '../components';\n\nconst Admin = () =\u003e {\n  const { users, jobs } = useLoaderData();\n\n  return (\n    \u003cWrapper\u003e\n      \u003cStatItem\n        title='current users'\n        count={users}\n        color='#e9b949'\n        bcg='#fcefc7'\n        icon={\u003cFaSuitcaseRolling /\u003e}\n      /\u003e\n      \u003cStatItem\n        title='total jobs'\n        count={jobs}\n        color='#647acb'\n        bcg='#e0e8f9'\n        icon={\u003cFaCalendarCheck /\u003e}\n      /\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Admin;\n```\n\n#### Admin - CSS (optional)\n\nwrappers/StatsContainer.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  display: grid;\n  row-gap: 2rem;\n  @media (min-width: 768px) {\n    grid-template-columns: 1fr 1fr;\n    column-gap: 1rem;\n  }\n  @media (min-width: 1120px) {\n    grid-template-columns: 1fr 1fr 1fr;\n    column-gap: 1rem;\n  }\n`;\nexport default Wrapper;\n```\n\nwrappers/StatItem.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.article`\n  padding: 2rem;\n  background: var(--background-secondary-color);\n  border-radius: var(--border-radius);\n  border-bottom: 5px solid ${(props) =\u003e props.color};\n  header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n  .count {\n    display: block;\n    font-weight: 700;\n    font-size: 50px;\n    color: ${(props) =\u003e props.color};\n    line-height: 2;\n  }\n  .title {\n    margin: 0;\n    text-transform: capitalize;\n    letter-spacing: var(--letter-spacing);\n    text-align: left;\n    margin-top: 0.5rem;\n    font-size: 1.25rem;\n  }\n  .icon {\n    width: 70px;\n    height: 60px;\n    background: ${(props) =\u003e props.bcg};\n    border-radius: var(--border-radius);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    svg {\n      font-size: 2rem;\n      color: ${(props) =\u003e props.color};\n    }\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### Avatar Image\n\n- get two images from pexels\n\n[pexels](https://www.pexels.com/search/person/)\n\n#### Setup Public Folder\n\nserver.js\n\n```js\nimport { dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport path from 'path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\napp.use(express.static(path.resolve(__dirname, './public')));\n```\n\n- http://localhost:5100/imageName\n\n#### Profile Page - Initial Setup\n\n- remove jobs,users from DB\n- add avatar property in the user model\n\nmodels/UserModel.js\n\n```js\nconst UserSchema = new mongoose.Schema({\n  avatar: String,\n  avatarPublicId: String,\n});\n```\n\n#### Profile Page - Structure\n\npages/Profile.jsx\n\n```js\nimport { FormRow } from '../components';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { useOutletContext } from 'react-router-dom';\nimport { useNavigation, Form } from 'react-router-dom';\nimport customFetch from '../utils/customFetch';\nimport { toast } from 'react-toastify';\n\nconst Profile = () =\u003e {\n  const { user } = useOutletContext();\n  const { name, lastName, email, location } = user;\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form' encType='multipart/form-data'\u003e\n        \u003ch4 className='form-title'\u003eprofile\u003c/h4\u003e\n\n        \u003cdiv className='form-center'\u003e\n          \u003cdiv className='form-row'\u003e\n            \u003clabel htmlFor='image' className='form-label'\u003e\n              Select an image file (max 0.5 MB):\n            \u003c/label\u003e\n            \u003cinput\n              type='file'\n              id='avatar'\n              name='avatar'\n              className='form-input'\n              accept='image/*'\n            /\u003e\n          \u003c/div\u003e\n          \u003cFormRow type='text' name='name' defaultValue={name} /\u003e\n          \u003cFormRow\n            type='text'\n            labelText='last name'\n            name='lastName'\n            defaultValue={lastName}\n          /\u003e\n          \u003cFormRow type='email' name='email' defaultValue={email} /\u003e\n          \u003cFormRow type='text' name='location' defaultValue={location} /\u003e\n          \u003cbutton\n            className='btn btn-block form-btn'\n            type='submit'\n            disabled={isSubmitting}\n          \u003e\n            {isSubmitting ? 'submitting...' : 'save changes'}\n          \u003c/button\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default Profile;\n```\n\n#### Profile Page - Action\n\n- import/export action (App.jsx)\n\n```js\nexport const action = async ({ request }) =\u003e {\n  const formData = await request.formData();\n\n  const file = formData.get('avatar');\n  if (file \u0026\u0026 file.size \u003e 500000) {\n    toast.error('Image size too large');\n    return null;\n  }\n\n  try {\n    await customFetch.patch('/users/update-user', formData);\n    toast.success('Profile updated successfully');\n  } catch (error) {\n    toast.error(error?.response?.data?.msg);\n  }\n  return null;\n};\n```\n\n#### Update User - Server\n\n```sh\nnpm i multer@1.4.5\n```\n\nMulter is a popular middleware package for handling multipart/form-data in Node.js web applications. It is commonly used for handling file uploads. Multer simplifies the process of accepting and storing files submitted through HTTP requests by providing an easy-to-use API. It integrates seamlessly with Express.js and allows developers to define upload destinations, file size limits, and other configurations.\n\n- create middleware/multerMiddleware.js\n- setup multer\n\n```js\nimport multer from 'multer';\n\nconst storage = multer.diskStorage({\n  destination: (req, file, cb) =\u003e {\n    // set the directory where uploaded files will be stored\n    cb(null, 'public/uploads');\n  },\n  filename: (req, file, cb) =\u003e {\n    const fileName = file.originalname;\n    // set the name of the uploaded file\n    cb(null, fileName);\n  },\n});\nconst upload = multer({ storage });\n\nexport default upload;\n```\n\nroutes/userRouter.js\n\n```js\nimport upload from '../middleware/multerMiddleware.js';\n\nrouter.patch(\n  '/update-user',\n  upload.single('avatar'),\n  validateUpdateUserInput,\n  updateUser\n);\n```\n\nFirst, the multer package is imported.\n\nThen, a storage object is created using multer.diskStorage(). This object specifies the configuration for storing uploaded files. In this case, the destination function determines the directory where the uploaded files will be saved, which is set to 'public/uploads'. The filename function defines the name of the uploaded file, which is set to the original filename.\n\nNext, a multer middleware is created by passing the storage object as a configuration option. This multer middleware will be used to handle file uploads in the application.\n\nIn this case, upload is an instance of the Multer middleware that was created earlier. The .single() method is called on this instance to indicate that only one file will be uploaded. The argument 'avatar' specifies the name of the field in the HTTP request that corresponds to the uploaded file.\n\nWhen this middleware is used in an HTTP route handler, it will process the incoming request and extract the file attached to the 'avatar' field. Multer will then save the file according to the specified storage configuration, which includes the destination directory and filename logic defined earlier. The uploaded file can be accessed in the route handler using req.file.\n\n#### Cloudinary - Create Account/Get API Keys\n\n[Cloudinary](https://cloudinary.com/)\n\nCloudinary is a cloud-based media management platform that helps businesses store, optimize, and deliver images and videos across the web. It provides developers with an easy way to upload, manipulate, and serve media assets, enabling faster and more efficient delivery of visual content on websites and applications. Cloudinary also offers features like automatic resizing, format conversion, and responsive delivery to ensure optimal user experiences across different devices and network conditions.\n\n.env\n\n```sh\nCLOUD_NAME=\nCLOUD_API_KEY=\nCLOUD_API_SECRET=\n```\n\n#### Cloudinary - Setup Instance\n\n```sh\nnpm i cloudinary@1.37.3\n```\n\nserver\n\n```js\nimport cloudinary from 'cloudinary';\n\ncloudinary.config({\n  cloud_name: process.env.CLOUD_NAME,\n  api_key: process.env.CLOUD_API_KEY,\n  api_secret: process.env.CLOUD_API_SECRET,\n});\n```\n\n#### Update User Controller\n\ncontrollers/userController.js\n\n```js\nimport cloudinary from 'cloudinary';\nimport { promises as fs } from 'fs';\n\nexport const updateUser = async (req, res) =\u003e {\n  const newUser = { ...req.body };\n  delete newUser.password;\n  if (req.file) {\n    const response = await cloudinary.v2.uploader.upload(req.file.path);\n    await fs.unlink(req.file.path);\n    newUser.avatar = response.secure_url;\n    newUser.avatarPublicId = response.public_id;\n  }\n\n  const updatedUser = await User.findByIdAndUpdate(req.user.userId, newUser);\n\n  if (req.file \u0026\u0026 updatedUser.avatarPublicId) {\n    await cloudinary.v2.uploader.destroy(updatedUser.avatarPublicId);\n  }\n  res.status(StatusCodes.OK).json({ msg: 'update user' });\n};\n```\n\n#### Logout Container\n\n```js\n{\n  user.avatar ? (\n    \u003cimg src={user.avatar} alt='avatar' className='img' /\u003e\n  ) : (\n    \u003cFaUserCircle /\u003e\n  );\n}\n```\n\n#### Submit Btn Component\n\n- create component SubmitBtn (export/import)\n- add all classes, including'.form-btn'\n- setup in Register,Login, AddJob, EditJob, Profile\n- make sure to add formBtn prop\n\n```js\nimport { useNavigation } from 'react-router-dom';\nconst SubmitBtn = ({ formBtn }) =\u003e {\n  const navigation = useNavigation();\n  const isSubmitting = navigation.state === 'submitting';\n  return (\n    \u003cbutton\n      type='submit'\n      className={`btn btn-block ${formBtn \u0026\u0026 'form-btn'}`}\n      disabled={isSubmitting}\n    \u003e\n      {isSubmitting ? 'submitting...' : 'submit'}\n    \u003c/button\u003e\n  );\n};\nexport default SubmitBtn;\n```\n\n#### Test User\n\n- create test user\n- feel free to use one of the chatGPT options\n\n```json\n{\n  \"name\": \"Zippy\",\n  \"email\": \"test@test.com\",\n  \"password\": \"secret123\",\n  \"lastName\": \"ShakeAndBake\",\n  \"location\": \"Codeville\"\n}\n{\n  \"name\": \"Chuckleberry\",\n  \"email\": \"test@test.com\",\n  \"password\": \"secret123\",\n  \"lastName\": \"Gigglepants\",\n  \"location\": \"Laughterland\"\n}\n\n{\n  \"name\": \"Bubbles McLaughster\",\n  \"email\": \"test@test.com\",\n  \"password\": \"secret123\",\n  \"lastName\": \"Ticklebottom\",\n  \"location\": \"Giggle City\"\n}\n\n\n{\n  \"name\": \"Gigglesworth\",\n  \"email\": \"test@test.com\",\n  \"password\": \"secret123\",\n  \"lastName\": \"Snickerdoodle\",\n  \"location\": \"Chuckleburg\"\n}\n```\n\n#### Test User - Login Page\n\n```js\nimport { useNavigate } from 'react-router-dom';\n\nconst Login = () =\u003e {\n  const navigate = useNavigate();\n  const loginDemoUser = async () =\u003e {\n    const data = {\n      email: 'test@test.com',\n      password: 'secret123',\n    };\n    try {\n      await customFetch.post('/auth/login', data);\n      toast.success('take a test drive');\n      navigate('/dashboard');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n    }\n  };\n  return (\n    \u003cWrapper\u003e\n      ...\n        \u003cbutton type='button' className='btn btn-block' onClick={loginDemoUser}\u003e\n          explore the app\n        \u003c/button\u003e\n        ...\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default Login;\n```\n\n#### Test User - Restrict Access\n\nauthMiddleware\n\n```js\nimport {\n  BadRequestError,\n} from '../errors/customErrors.js';\n\nexport const authenticateUser = (req, res, next) =\u003e {\n  ...\n  try {\n    const { userId, role } = verifyJWT(token);\n    const testUser = userId === 'testUserId';\n    req.user = { userId, role, testUser };\n    next();\n  }\n  ....\n};\n\nexport const checkForTestUser = (req, res, next) =\u003e {\n  if (req.user.testUser) {\n    throw new BadRequestError('Demo User. Read Only!');\n  }\n  next();\n};\n\n```\n\n- add to updateUser, createJob, updateJob, deleteJob\n\n#### Mock Data\n\n[Mockaroo ](https://www.mockaroo.com/)\n\n```json\n{\n  \"company\": \"Cogidoo\",\n  \"position\": \"Help Desk Technician\",\n  \"jobLocation\": \"Vyksa\",\n  \"jobStatus\": \"pending\",\n  \"jobType\": \"part-time\",\n  \"createdAt\": \"2022-07-25T21:26:23Z\"\n}\n```\n\n- rename and save json in utils\n\n#### Populate DB\n\n- create populate.js\n- setup for test user and admin\n\n```js\nimport { readFile } from 'fs/promises';\nimport mongoose from 'mongoose';\nimport dotenv from 'dotenv';\ndotenv.config();\n\nimport Job from './models/JobModel.js';\nimport User from './models/UserModel.js';\ntry {\n  await mongoose.connect(process.env.MONGO_URL);\n  // const user = await User.findOne({ email: 'john@gmail.com' });\n  const user = await User.findOne({ email: 'test@test.com' });\n\n  const jsonJobs = JSON.parse(\n    await readFile(new URL('./utils/mockData.json', import.meta.url))\n  );\n  const jobs = jsonJobs.map((job) =\u003e {\n    return { ...job, createdBy: user._id };\n  });\n  await Job.deleteMany({ createdBy: user._id });\n  await Job.create(jobs);\n  console.log('Success!!!');\n  process.exit(0);\n} catch (error) {\n  console.log(error);\n  process.exit(1);\n}\n```\n\n#### Stats - Setup\n\n- create controller\n- setup route and thunder client\n- install/setup dayjs on the server\n\njobController.js\n\n```js\nimport mongoose from 'mongoose';\nimport day from 'dayjs';\n\nexport const showStats = async (req, res) =\u003e {\n  const defaultStats = {\n    pending: 22,\n    interview: 11,\n    declined: 4,\n  };\n\n  let monthlyApplications = [\n    {\n      date: 'May 23',\n      count: 12,\n    },\n    {\n      date: 'Jun 23',\n      count: 9,\n    },\n    {\n      date: 'Jul 23',\n      count: 3,\n    },\n  ];\n  res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications });\n};\n```\n\n#### Stats - Complete Server Functionality\n\n[MongoDB Docs](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/)\n\nThe MongoDB aggregation pipeline is like a factory line for data. Data enters, it goes through different stages like cleaning, sorting, or grouping, and comes out at the end changed in some way. It's a way to process data inside MongoDB.\n\njobController.js\n\n```js\nexport const showStats = async (req, res) =\u003e {\n  let stats = await Job.aggregate([\n    { $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } },\n    { $group: { _id: '$jobStatus', count: { $sum: 1 } } },\n  ]);\n  stats = stats.reduce((acc, curr) =\u003e {\n    const { _id: title, count } = curr;\n    acc[title] = count;\n    return acc;\n  }, {});\n\n  const defaultStats = {\n    pending: stats.pending || 0,\n    interview: stats.interview || 0,\n    declined: stats.declined || 0,\n  };\n\n  let monthlyApplications = await Job.aggregate([\n    { $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } },\n    {\n      $group: {\n        _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } },\n        count: { $sum: 1 },\n      },\n    },\n    { $sort: { '_id.year': -1, '_id.month': -1 } },\n    { $limit: 6 },\n  ]);\n  monthlyApplications = monthlyApplications\n    .map((item) =\u003e {\n      const {\n        _id: { year, month },\n        count,\n      } = item;\n\n      const date = day()\n        .month(month - 1)\n        .year(year)\n        .format('MMM YY');\n      return { date, count };\n    })\n    .reverse();\n\n  res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications });\n};\n```\n\n#### Commentary\n\n```js\nlet stats = await Job.aggregate([\n  { $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } },\n  { $group: { _id: '$jobStatus', count: { $sum: 1 } } },\n]);\n```\n\nlet stats = await Job.aggregate([ ... ]); This line says we're going to perform an aggregation operation on the Job collection in MongoDB and save the result in a variable called stats. The await keyword is used to wait for the operation to finish before continuing, as the operation is asynchronous (i.e., it runs in the background).\n\n{ $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } } This is the first stage of the pipeline. It filters the jobs so that only the ones created by the user specified by req.user.userId are passed to the next stage. The new mongoose.Types.ObjectId(req.user.userId) part converts req.user.userId into an ObjectId (which is the format MongoDB uses for ids).\n\n{ $group: { _id: '$jobStatus', count: { $sum: 1 } } } This is the second stage of the pipeline. It groups the remaining jobs by their status (the jobStatus field). For each group, it calculates the count of jobs by adding 1 for each job ({ $sum: 1 }), and stores this in a field called count.\n\n```js\nlet monthlyApplications = await Job.aggregate([\n  { $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } },\n  {\n    $group: {\n      _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } },\n      count: { $sum: 1 },\n    },\n  },\n  { $sort: { '_id.year': -1, '_id.month': -1 } },\n  { $limit: 6 },\n]);\n```\n\nlet monthlyApplications = await Job.aggregate([ ... ]); This line indicates that an aggregation operation will be performed on the Job collection in MongoDB. The result will be stored in the variable monthlyApplications. The await keyword ensures that the code waits for this operation to complete before proceeding, as it is an asynchronous operation.\n\n{ $match: { createdBy: new mongoose.Types.ObjectId(req.user.userId) } } This is the first stage of the pipeline. It filters the jobs to only those created by the user identified by req.user.userId.\n\n{ $group: { _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } }, count: { $sum: 1 } } } This is the second stage of the pipeline. It groups the remaining jobs based on the year and month when they were created. For each group, it calculates the count of jobs by adding 1 for each job in the group.\n\n{ $sort: { '\\_id.year': -1, '\\_id.month': -1 } } This is the third stage of the pipeline. It sorts the groups by year and month in descending order. The -1 indicates descending order. So it starts with the most recent year and month.\n\n{ $limit: 6 } This is the fourth and last stage of the pipeline. It limits the output to the top 6 groups, after sorting. This is effectively getting the job count for the last 6 months.\n\nSo, monthlyApplications will be an array with up to 6 elements, each representing the number of jobs created by the user in a specific month and year. The array will be sorted by year and month, starting with the most recent.\n\n#### Stats - Front-End Setup\n\n- create four components\n- StatsContainer and ChartsContainer (import/export)\n- AreaChart, BarChart (local)\n\npages/Stats.jsx\n\n```js\nimport { ChartsContainer, StatsContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useLoaderData } from 'react-router-dom';\nexport const loader = async () =\u003e {\n  try {\n    const response = await customFetch.get('/jobs/stats');\n    return response.data;\n  } catch (error) {\n    return error;\n  }\n};\n\nconst Stats = () =\u003e {\n  const { defaultStats, monthlyApplications } = useLoaderData();\n  return (\n    \u003c\u003e\n      \u003cStatsContainer defaultStats={defaultStats} /\u003e\n      {monthlyApplications?.length \u003e 0 \u0026\u0026 (\n        \u003cChartsContainer data={monthlyApplications} /\u003e\n      )}\n    \u003c/\u003e\n  );\n};\nexport default Stats;\n```\n\n#### Stats Container\n\n```js\nimport { FaSuitcaseRolling, FaCalendarCheck, FaBug } from 'react-icons/fa';\nimport Wrapper from '../assets/wrappers/StatsContainer';\nimport StatItem from './StatItem';\nconst StatsContainer = ({ defaultStats }) =\u003e {\n  const stats = [\n    {\n      title: 'pending applications',\n      count: defaultStats?.pending || 0,\n      icon: \u003cFaSuitcaseRolling /\u003e,\n      color: '#f59e0b',\n      bcg: '#fef3c7',\n    },\n    {\n      title: 'interviews scheduled',\n      count: defaultStats?.interview || 0,\n      icon: \u003cFaCalendarCheck /\u003e,\n      color: '#647acb',\n      bcg: '#e0e8f9',\n    },\n    {\n      title: 'jobs declined',\n      count: defaultStats?.declined || 0,\n      icon: \u003cFaBug /\u003e,\n      color: '#d66a6a',\n      bcg: '#ffeeee',\n    },\n  ];\n  return (\n    \u003cWrapper\u003e\n      {stats.map((item) =\u003e {\n        return \u003cStatItem key={item.title} {...item} /\u003e;\n      })}\n    \u003c/Wrapper\u003e\n  );\n};\nexport default StatsContainer;\n```\n\n#### ChartsContainer\n\n```js\nimport { useState } from 'react';\n\nimport BarChart from './BarChart';\nimport AreaChart from './AreaChart';\nimport Wrapper from '../assets/wrappers/ChartsContainer';\n\nconst ChartsContainer = ({ data }) =\u003e {\n  const [barChart, setBarChart] = useState(true);\n\n  return (\n    \u003cWrapper\u003e\n      \u003ch4\u003eMonthly Applications\u003c/h4\u003e\n      \u003cbutton type='button' onClick={() =\u003e setBarChart(!barChart)}\u003e\n        {barChart ? 'Area Chart' : 'Bar Chart'}\n      \u003c/button\u003e\n      {barChart ? \u003cBarChart data={data} /\u003e : \u003cAreaChart data={data} /\u003e}\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default ChartsContainer;\n```\n\n#### Charts\n\n[recharts](https://recharts.org/en-US/)\n\n- in the client\n\n```sh\nnpm i recharts@2.5.0\n```\n\n#### Area Chart\n\n```js\nimport {\n  ResponsiveContainer,\n  AreaChart,\n  Area,\n  XAxis,\n  YAxis,\n  CartesianGrid,\n  Tooltip,\n} from 'recharts';\n\nconst AreaChartComponent = ({ data }) =\u003e {\n  return (\n    \u003cResponsiveContainer width='100%' height={300}\u003e\n      \u003cAreaChart data={data} margin={{ top: 50 }}\u003e\n        \u003cCartesianGrid strokeDasharray='3 3' /\u003e\n        \u003cXAxis dataKey='date' /\u003e\n        \u003cYAxis allowDecimals={false} /\u003e\n        \u003cTooltip /\u003e\n        \u003cArea type='monotone' dataKey='count' stroke='#2cb1bc' fill='#bef8fd' /\u003e\n      \u003c/AreaChart\u003e\n    \u003c/ResponsiveContainer\u003e\n  );\n};\n\nexport default AreaChartComponent;\n```\n\n#### Bar Chart\n\n```js\nimport {\n  BarChart,\n  Bar,\n  XAxis,\n  YAxis,\n  CartesianGrid,\n  Tooltip,\n  ResponsiveContainer,\n} from 'recharts';\n\nconst BarChartComponent = ({ data }) =\u003e {\n  return (\n    \u003cResponsiveContainer width='100%' height={300}\u003e\n      \u003cBarChart data={data} margin={{ top: 50 }}\u003e\n        \u003cCartesianGrid strokeDasharray='3 3 ' /\u003e\n        \u003cXAxis dataKey='date' /\u003e\n        \u003cYAxis allowDecimals={false} /\u003e\n        \u003cTooltip /\u003e\n        \u003cBar dataKey='count' fill='#2cb1bc' barSize={75} /\u003e\n      \u003c/BarChart\u003e\n    \u003c/ResponsiveContainer\u003e\n  );\n};\n\nexport default BarChartComponent;\n```\n\n#### Charts CSS (optional)\n\nwrappers/ChartsContainer.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  margin-top: 4rem;\n  text-align: center;\n  button {\n    background: transparent;\n    border-color: transparent;\n    text-transform: capitalize;\n    color: var(--primary-500);\n    font-size: 1.25rem;\n    cursor: pointer;\n  }\n  h4 {\n    text-align: center;\n    margin-bottom: 0.75rem;\n  }\n`;\n\nexport default Wrapper;\n```\n\n#### Get All Jobs - Server\n\njobController.js\n\nQuery parameters, also known as query strings or URL parameters, are used to pass information to a web server through the URL of a webpage. They are typically appended to the end of a URL after a question mark (?) and separated by ampersands (\u0026). Query parameters consist of a key-value pair, where the key represents the parameter name and the value represents the corresponding data being passed. They are commonly used in web applications to provide additional context or parameters for server-side processing or to filter and sort data.\n\n```js\nexport const getAllJobs = async (req, res) =\u003e {\n  const { search, jobStatus, jobType, sort } = req.query;\n\n  const queryObject = {\n    createdBy: req.user.userId,\n  };\n\n  if (search) {\n    queryObject.$or = [\n      { position: { $regex: search, $options: 'i' } },\n      { company: { $regex: search, $options: 'i' } },\n    ];\n  }\n  if (jobStatus \u0026\u0026 jobStatus !== 'all') {\n    queryObject.jobStatus = jobStatus;\n  }\n  if (jobType \u0026\u0026 jobType !== 'all') {\n    queryObject.jobType = jobType;\n  }\n\n  const sortOptions = {\n    newest: '-createdAt',\n    oldest: 'createdAt',\n    'a-z': 'position',\n    'z-a': '-position',\n  };\n\n  const sortKey = sortOptions[sort] || sortOptions.newest;\n\n  // setup pagination\n  const page = Number(req.query.page) || 1;\n  const limit = Number(req.query.limit) || 10;\n  const skip = (page - 1) * limit;\n\n  const jobs = await Job.find(queryObject)\n    .sort(sortKey)\n    .skip(skip)\n    .limit(limit);\n\n  const totalJobs = await Job.countDocuments(queryObject);\n  const numOfPages = Math.ceil(totalJobs / limit);\n\n  res\n    .status(StatusCodes.OK)\n    .json({ totalJobs, numOfPages, currentPage: page, jobs });\n};\n```\n\n#### Search Container\n\n- setup log in AllJobs loader\n\n```js\nimport { FormRow, FormRowSelect, SubmitBtn } from '.';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { Form, useSubmit, Link } from 'react-router-dom';\nimport { JOB_TYPE, JOB_STATUS, JOB_SORT_BY } from '../../../utils/constants';\nimport { useAllJobsContext } from '../pages/AllJobs';\n\nconst SearchContainer = () =\u003e {\n  return (\n    \u003cWrapper\u003e\n      \u003cForm className='form'\u003e\n        \u003ch5 className='form-title'\u003esearch form\u003c/h5\u003e\n        \u003cdiv className='form-center'\u003e\n          {/* search position */}\n\n          \u003cFormRow type='search' name='search' defaultValue='a' /\u003e\n          \u003cFormRowSelect\n            labelText='job status'\n            name='jobStatus'\n            list={['all', ...Object.values(JOB_STATUS)]}\n            defaultValue='all'\n          /\u003e\n          \u003cFormRowSelect\n            labelText='job type'\n            name='jobType'\n            list={['all', ...Object.values(JOB_TYPE)]}\n            defaultValue='all'\n          /\u003e\n          \u003cFormRowSelect\n            name='sort'\n            defaultValue='newest'\n            list={[...Object.values(JOB_SORT_BY)]}\n          /\u003e\n\n          \u003cLink to='/dashboard/all-jobs' className='btn form-btn delete-btn'\u003e\n            Reset Search Values\n          \u003c/Link\u003e\n          {/* TEMP!!!! */}\n          \u003cSubmitBtn formBtn /\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default SearchContainer;\n```\n\n#### All Jobs Loader\n\nAllJobs.jsx\n\n```js\nimport { toast } from 'react-toastify';\nimport { JobsContainer, SearchContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useLoaderData } from 'react-router-dom';\nimport { useContext, createContext } from 'react';\nconst AllJobsContext = createContext();\nexport const loader = async ({ request }) =\u003e {\n  try {\n    const params = Object.fromEntries([\n      ...new URL(request.url).searchParams.entries(),\n    ]);\n\n    const { data } = await customFetch.get('/jobs', {\n      params,\n    });\n\n    return {\n      data,\n      searchValues: { ...params },\n    };\n  } catch (error) {\n    toast.error(error.response.data.msg);\n    return error;\n  }\n};\n\nconst AllJobs = () =\u003e {\n  const { data, searchValues } = useLoaderData();\n\n  return (\n    \u003cAllJobsContext.Provider value={{ data, searchValues }}\u003e\n      \u003cSearchContainer /\u003e\n      \u003cJobsContainer /\u003e\n    \u003c/AllJobsContext.Provider\u003e\n  );\n};\nexport default AllJobs;\n\nexport const useAllJobsContext = () =\u003e useContext(AllJobsContext);\n```\n\n```js\nconst params = Object.fromEntries([\n  ...new URL(request.url).searchParams.entries(),\n]);\n```\n\nnew URL(request.url): This creates a new URL object by passing the request.url to the URL constructor. The URL object provides various methods and properties to work with URLs.\n\n.searchParams: The searchParams property of the URL object gives you access to the query parameters in the URL. It is an instance of the URLSearchParams class, which provides methods to manipulate and access the parameters.\n\n.entries(): The entries() method of searchParams returns an iterator containing arrays of key-value pairs for each query parameter. Each array contains two elements: the parameter name and its corresponding value.\n\n([...new URL(request.url).searchParams.entries()]): The spread operator ... is used to convert the iterator obtained from searchParams.entries() into an array. This allows us to pass the array to the Object.fromEntries() method.\n\nObject.fromEntries(): This static method creates an object from an array of key-value pairs. It takes an iterable (in this case, the array of parameter key-value pairs) and returns a new object where the keys and values are derived from the iterable.\n\nPutting it all together, the code retrieves the URL from the request.url property, extracts the search parameters using the searchParams property, converts them into an array of key-value pairs using entries(), and finally uses Object.fromEntries() to create an object with the parameter names as keys and their corresponding values. The resulting object, params, contains all the search parameters from the URL.\n\n#### Submit Form Programmatically\n\n- setup default values from the context\n- remove SubmitBtn\n- add onChange to FormRow, FormRowSelect and all inputs\n\nSearchContainer.js\n\n```js\nimport { FormRow, FormRowSelect } from '.';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { Form, useSubmit, Link } from 'react-router-dom';\nimport { JOB_TYPE, JOB_STATUS, JOB_SORT_BY } from '../../../utils/constants';\nimport { useAllJobsContext } from '../pages/AllJobs';\nconst SearchContainer = () =\u003e {\n  const { searchValues } = useAllJobsContext();\n  const { search, jobStatus, jobType, sort } = searchValues;\n\n  const submit = useSubmit();\n\n  return (\n    \u003cWrapper\u003e\n      \u003cForm className='form'\u003e\n        \u003ch5 className='form-title'\u003esearch form\u003c/h5\u003e\n        \u003cdiv className='form-center'\u003e\n          {/* search position */}\n\n          \u003cFormRow\n            type='search'\n            name='search'\n            defaultValue={search}\n            onChange={(e) =\u003e {\n              submit(e.currentTarget.form);\n            }}\n          /\u003e\n          \u003cFormRowSelect\n            labelText='job status'\n            name='jobStatus'\n            list={['all', ...Object.values(JOB_STATUS)]}\n            defaultValue={jobStatus}\n            onChange={(e) =\u003e {\n              submit(e.currentTarget.form);\n            }}\n          /\u003e\n          \u003cFormRowSelect\n            labelText='job type'\n            name='jobType'\n            defaultValue={jobType}\n            list={['all', ...Object.values(JOB_TYPE)]}\n            onChange={(e) =\u003e {\n              submit(e.currentTarget.form);\n            }}\n          /\u003e\n          \u003cFormRowSelect\n            name='sort'\n            defaultValue={sort}\n            list={[...Object.values(JOB_SORT_BY)]}\n            onChange={(e) =\u003e {\n              submit(e.currentTarget.form);\n            }}\n          /\u003e\n          \u003cLink to='/dashboard/all-jobs' className='btn form-btn delete-btn'\u003e\n            Reset Search Values\n          \u003c/Link\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default SearchContainer;\n```\n\n#### Debounce\n\n[JS Nuggets - Debounce](https://youtu.be/tYx6pXdvt1s)\n\nIn JavaScript, debounce is a way to limit how often a function gets called. It helps prevent rapid or repeated function executions by introducing a delay. This is useful for tasks like handling user input, where you want to wait for a pause before triggering an action to avoid unnecessary processing.\n\n```js\nconst debounce = (onChange) =\u003e {\n  let timeout;\n  return (e) =\u003e {\n    const form = e.currentTarget.form;\n    clearTimeout(timeout);\n    timeout = setTimeout(() =\u003e {\n      onChange(form);\n    }, 2000);\n  };\n};\n\u003cFormRow\n  type='search'\n  name='search'\n  defaultValue={search}\n  onChange={debounce((form) =\u003e {\n    submit(form);\n  })}\n/\u003e;\n```\n\n#### Pagination - Setup\n\n- create PageBtnContainer\n\nJobsContainer.jsx\n\n```js\nimport Job from './Job';\nimport Wrapper from '../assets/wrappers/JobsContainer';\nimport PageBtnContainer from './PageBtnContainer';\nimport { useAllJobsContext } from '../pages/AllJobs';\n\nconst JobsContainer = () =\u003e {\n  const { data } = useAllJobsContext();\n  const { jobs, totalJobs, numOfPages } = data;\n  if (jobs.length === 0) {\n    return (\n      \u003cWrapper\u003e\n        \u003ch2\u003eNo jobs to display...\u003c/h2\u003e\n      \u003c/Wrapper\u003e\n    );\n  }\n\n  return (\n    \u003cWrapper\u003e\n      \u003ch5\u003e\n        {totalJobs} job{jobs.length \u003e 1 \u0026\u0026 's'} found\n      \u003c/h5\u003e\n      \u003cdiv className='jobs'\u003e\n        {jobs.map((job) =\u003e {\n          return \u003cJob key={job._id} {...job} /\u003e;\n        })}\n      \u003c/div\u003e\n      {numOfPages \u003e 1 \u0026\u0026 \u003cPageBtnContainer /\u003e}\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default JobsContainer;\n```\n\n#### Basic PageBtnContainer\n\n```js\nimport { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi';\nimport Wrapper from '../assets/wrappers/PageBtnContainer';\nimport { useLocation, Link, useNavigate } from 'react-router-dom';\nimport { useAllJobsContext } from '../pages/AllJobs';\n\nconst PageBtnContainer = () =\u003e {\n  const {\n    data: { numOfPages, currentPage },\n  } = useAllJobsContext();\n  const { search, pathname } = useLocation();\n  const navigate = useNavigate();\n  const pages = Array.from({ length: numOfPages }, (_, index) =\u003e index + 1);\n\n  const handlePageChange = (pageNumber) =\u003e {\n    const searchParams = new URLSearchParams(search);\n    searchParams.set('page', pageNumber);\n    navigate(`${pathname}?${searchParams.toString()}`);\n  };\n\n  return (\n    \u003cWrapper\u003e\n      \u003cbutton\n        className='btn prev-btn'\n        onClick={() =\u003e {\n          let prevPage = currentPage - 1;\n          if (prevPage \u003c 1) prevPage = numOfPages;\n          handlePageChange(prevPage);\n        }}\n      \u003e\n        \u003cHiChevronDoubleLeft /\u003e\n        prev\n      \u003c/button\u003e\n      \u003cdiv className='btn-container'\u003e\n        {pages.map((pageNumber) =\u003e (\n          \u003cbutton\n            className={`btn page-btn ${pageNumber === currentPage \u0026\u0026 'active'}`}\n            key={pageNumber}\n            onClick={() =\u003e handlePageChange(pageNumber)}\n          \u003e\n            {pageNumber}\n          \u003c/button\u003e\n        ))}\n      \u003c/div\u003e\n      \u003cbutton\n        className='btn next-btn'\n        onClick={() =\u003e {\n          let nextPage = currentPage + 1;\n          if (nextPage \u003e numOfPages) nextPage = 1;\n          handlePageChange(nextPage);\n        }}\n      \u003e\n        next\n        \u003cHiChevronDoubleRight /\u003e\n      \u003c/button\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default PageBtnContainer;\n```\n\n#### Complex - PageBtnContainer\n\n```js\nimport { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi';\nimport Wrapper from '../assets/wrappers/PageBtnContainer';\nimport { useLocation, Link, useNavigate } from 'react-router-dom';\nimport { useAllJobsContext } from '../pages/AllJobs';\n\nconst PageBtnContainer = () =\u003e {\n  const {\n    data: { numOfPages, currentPage },\n  } = useAllJobsContext();\n  const { search, pathname } = useLocation();\n  const navigate = useNavigate();\n\n  const handlePageChange = (pageNumber) =\u003e {\n    const searchParams = new URLSearchParams(search);\n    searchParams.set('page', pageNumber);\n    navigate(`${pathname}?${searchParams.toString()}`);\n  };\n\n  const addPageButton = ({ pageNumber, activeClass }) =\u003e {\n    return (\n      \u003cbutton\n        className={`btn page-btn ${activeClass \u0026\u0026 'active'}`}\n        key={pageNumber}\n        onClick={() =\u003e handlePageChange(pageNumber)}\n      \u003e\n        {pageNumber}\n      \u003c/button\u003e\n    );\n  };\n\n  const renderPageButtons = () =\u003e {\n    const pageButtons = [];\n\n    // Add the first page button\n    pageButtons.push(\n      addPageButton({ pageNumber: 1, activeClass: currentPage === 1 })\n    );\n    // Add the dots before the current page if there are more than 3 pages\n    if (currentPage \u003e 3) {\n      pageButtons.push(\n        \u003cspan className='page-btn dots' key='dots-1'\u003e\n          ....\n        \u003c/span\u003e\n      );\n    }\n    // one before current page\n    if (currentPage !== 1 \u0026\u0026 currentPage !== 2) {\n      pageButtons.push(\n        addPageButton({ pageNumber: currentPage - 1, activeClass: false })\n      );\n    }\n\n    // Add the current page button\n    if (currentPage !== 1 \u0026\u0026 currentPage !== numOfPages) {\n      pageButtons.push(\n        addPageButton({ pageNumber: currentPage, activeClass: true })\n      );\n    }\n\n    // one after current page\n    if (currentPage !== numOfPages \u0026\u0026 currentPage !== numOfPages - 1) {\n      pageButtons.push(\n        addPageButton({ pageNumber: currentPage + 1, activeClass: false })\n      );\n    }\n    if (currentPage \u003c numOfPages - 2) {\n      pageButtons.push(\n        \u003cspan className=' page-btn dots' key='dots+1'\u003e\n          ....\n        \u003c/span\u003e\n      );\n    }\n\n    // Add the last page button\n    pageButtons.push(\n      addPageButton({\n        pageNumber: numOfPages,\n        activeClass: currentPage === numOfPages,\n      })\n    );\n\n    return pageButtons;\n  };\n\n  return (\n    \u003cWrapper\u003e\n      \u003cbutton\n        className='prev-btn'\n        onClick={() =\u003e {\n          let prevPage = currentPage - 1;\n          if (prevPage \u003c 1) prevPage = numOfPages;\n          handlePageChange(prevPage);\n        }}\n      \u003e\n        \u003cHiChevronDoubleLeft /\u003e\n        prev\n      \u003c/button\u003e\n      \u003cdiv className='btn-container'\u003e{renderPageButtons()}\u003c/div\u003e\n      \u003cbutton\n        className='btn next-btn'\n        onClick={() =\u003e {\n          let nextPage = currentPage + 1;\n          if (nextPage \u003e numOfPages) nextPage = 1;\n          handlePageChange(nextPage);\n        }}\n      \u003e\n        next\n        \u003cHiChevronDoubleRight /\u003e\n      \u003c/button\u003e\n    \u003c/Wrapper\u003e\n  );\n};\n\nexport default PageBtnContainer;\n```\n\n#### PageBtnContainer CSS (optional)\n\nwrappers/PageBtnContainer.js\n\n```js\nimport styled from 'styled-components';\n\nconst Wrapper = styled.section`\n  height: 6rem;\n  margin-top: 2rem;\n  display: flex;\n  align-items: center;\n  justify-content: end;\n  flex-wrap: wrap;\n  gap: 1rem;\n  .btn-container {\n    background: var(--background-secondary-color);\n    border-radius: var(--border-radius);\n    display: flex;\n  }\n  .page-btn {\n    background: transparent;\n    border-color: transparent;\n    width: 50px;\n    height: 40px;\n    font-weight: 700;\n    font-size: 1.25rem;\n    color: var(--primary-500);\n    border-radius: var(--border-radius);\n    cursor:pointer:\n  }\n  .active{\n    background:var(--primary-500);\n        color: var(--white);\n\n  }\n  .prev-btn,.next-btn{\n    background: var(--background-secondary-color);\n    border-color: transparent;\n        border-radius: var(--border-radius);\n\n    width: 100px;\n    height: 40px;\n        color: var(--primary-500);\ntext-transform:capitalize;\nletter-spacing:var(--letter-spacing);\ndisplay:flex;\nalign-items:center;\njustify-content:center;\ngap:0.5rem;\ncursor:pointer;\n  }\n  .prev-btn:hover,.next-btn:hover{\n    background:var(--primary-500);\n        color: var(--white);\n        transition:var(--transition);\n  }\n.dots{\n  display:grid;\n  place-items:center;\n  cursor:text;\n}\n`;\nexport default Wrapper;\n```\n\n#### Local Build\n\n- remove default values from inputs in Register and Login\n- navigate to client and build front-end\n\n```sh\ncd client \u0026\u0026 npm run build\n```\n\n- copy/paste all the files/folders\n\n  - from client/dist\n  - to server(root)/public\n\n- in server.js point to index.html\n\n```js\napp.get('*', (req, res) =\u003e {\n  res.sendFile(path.resolve(__dirname, './public', 'index.html'));\n});\n```\n\n#### Deploy On Render\n\n[Render](https://render.com/)\n\n- sign up of for account\n- create git repository\n\n#### Build Front-End on Render\n\n- add script\n- change path\n\npackage.json\n\n```js\n \"scripts\": {\n    \"setup-production-app\": \"npm i \u0026\u0026 cd client \u0026\u0026 npm i \u0026\u0026 npm run build\",\n  },\n```\n\nserver.js\n\n```js\napp.use(express.static(path.resolve(__dirname, './client/dist')));\n\napp.get('*', (req, res) =\u003e {\n  res.sendFile(path.resolve(__dirname, './client/dist', 'index.html'));\n});\n```\n\n#### Test Locally\n\n- remove client/dist and client/node_modules\n- remove node_modules and package-lock.json (optional)\n- run \"npm run setup-production-app\", followed by \"node server\"\n\n#### Test in Production\n\n- change build command on render\n\n```sh\nnpm run setup-production-app\n```\n\n- push up to github\n\n#### Upload Image As Buffer\n\n- remove public folder\n\n```sh\nnpm i datauri@4.1.0\n```\n\nmiddleware/multerMiddleware.js\n\n```js\nimport multer from 'multer';\nimport DataParser from 'datauri/parser.js';\nimport path from 'path';\n\nconst storage = multer.memoryStorage();\nconst upload = multer({ storage });\n\nconst parser = new DataParser();\n\nexport const formatImage = (file) =\u003e {\n  const fileExtension = path.extname(file.originalname).toString();\n  return parser.format(fileExtension, file.buffer).content;\n};\n\nexport default upload;\n```\n\ncontroller/userController.js\n\n```js\nimport { formatImage } from '../middleware/multerMiddleware.js';\n\nexport const updateUser = async (req, res) =\u003e {\n  const newUser = { ...req.body };\n  delete newUser.password;\n  if (req.file) {\n    const file = formatImage(req.file);\n    const response = await cloudinary.v2.uploader.upload(file);\n    newUser.avatar = response.secure_url;\n    newUser.avatarPublicId = response.public_id;\n  }\n  const updatedUser = await User.findByIdAndUpdate(req.user.userId, newUser);\n\n  if (req.file \u0026\u0026 updatedUser.avatarPublicId) {\n    await cloudinary.v2.uploader.destroy(updatedUser.avatarPublicId);\n  }\n  res.status(StatusCodes.OK).json({ msg: 'update user' });\n};\n```\n\n#### Setup Global Loading\n\n- create loading component (import/export)\n- check for loading in DashboardLayout page\n\ncomponents/Loading.jsx\n\n```js\nconst Loading = () =\u003e {\n  return \u003cdiv className='loading'\u003e\u003c/div\u003e;\n};\n\nexport default Loading;\n```\n\nDashboardLayout.jsx\n\n```js\nimport { useNavigation } from 'react-router-dom';\nimport { Loading } from '../components';\n\nconst DashboardLayout = ({ isDarkThemeEnabled }) =\u003e {\n  const navigation = useNavigation();\n  const isPageLoading = navigation.state === 'loading';\n\n  return (\n    \u003cWrapper\u003e\n      ...\n      \u003cdiv className='dashboard-page'\u003e\n        {isPageLoading ? \u003cLoading /\u003e : \u003cOutlet context={{ user }} /\u003e}\n      \u003c/div\u003e\n      ...\n    \u003c/Wrapper\u003e\n  );\n};\n```\n\n#### React Query\n\nReact Query is a powerful library that simplifies data fetching, caching, and synchronization in React applications. It provides a declarative and intuitive way to manage remote data by abstracting away the complex logic of fetching and caching data from APIs. React Query offers features like automatic background data refetching, optimistic updates, pagination support, and more, making it easier to build performant and responsive applications that rely on fetching and manipulating data.\n\n[React Query Docs](https://tanstack.com/query/v4/docs/react/overview)\n\n- in the client\n\n```sh\nnpm i @tanstack/react-query@4.29.5 @tanstack/react-query-devtools@4.29.6\n```\n\nApp.jsx\n\n```js\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};\n```\n\n#### Page Error Element\n\n- create components/ErrorElement\n\n```js\nimport { useRouteError } from 'react-router-dom';\n\nconst Error = () =\u003e {\n  const error = useRouteError();\n  console.log(error);\n  return \u003ch4\u003eThere was an error...\u003c/h4\u003e;\n};\nexport default ErrorElement;\n```\n\nStats.jsx\n\n```js\nexport const loader = async () =\u003e {\n  const response = await customFetch.get('/jobs/stats');\n  return response.data;\n};\n```\n\nApp.jsx\n\n```js\n{\n  path: 'stats',\n  element: \u003cStats /\u003e,\n  loader: statsLoader,\n  errorElement: \u003ch4\u003eThere was an error...\u003c/h4\u003e\n},\n```\n\n```js\n{\n  path: 'stats',\n  element: \u003cStats /\u003e,\n  loader: statsLoader,\n  errorElement: \u003cErrorElement /\u003e,\n},\n```\n\n#### First Query\n\n- navigate to stats\n\nStats.jsx\n\n```js\nimport { ChartsContainer, StatsContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useLoaderData } from 'react-router-dom';\nimport { useQuery } from '@tanstack/react-query';\n\nexport const loader = async () =\u003e {\n  return null;\n};\n\nconst Stats = () =\u003e {\n  const response = useQuery({\n    queryKey: ['stats'],\n    queryFn: () =\u003e customFetch.get('/jobs/stats'),\n  });\n  console.log(response);\n  if (response.isLoading) {\n    return \u003ch1\u003eLoading...\u003c/h1\u003e;\n  }\n  return \u003ch1\u003ereact query\u003c/h1\u003e;\n  return (\n    \u003c\u003e\n      \u003cStatsContainer defaultStats={defaultStats} /\u003e\n      {monthlyApplications?.length \u003e 1 \u0026\u0026 (\n        \u003cChartsContainer data={monthlyApplications} /\u003e\n      )}\n    \u003c/\u003e\n  );\n};\nexport default Stats;\n```\n\n```js\nconst data = useQuery({\n  queryKey: ['stats'],\n  queryFn: () =\u003e customFetch.get('/jobs/stats'),\n});\n```\n\nconst data = useQuery({ ... });: This line declares a constant variable named data and assigns it the result of the useQuery hook. The useQuery hook is provided by React Query and is used to perform data fetching.\n\nqueryKey: ['stats'],: The queryKey property is an array that serves as a unique identifier for the query. In this case, the query key is set to ['stats'], indicating that this query is fetching statistics related to jobs.\n\nqueryFn: () =\u003e customFetch.get('/jobs/stats'),: The queryFn property specifies the function that will be executed when the query is triggered. In this case, it uses an arrow function that calls customFetch.get('/jobs/stats'). The customFetch object is likely a custom wrapper around the fetch function or an external HTTP client library, used to make the actual API request to retrieve job statistics.In React Query, the queryFn property expects a function that returns a promise. The promise should resolve with the data you want to fetch and store in the query cache.\n\ncustomFetch.get('/jobs/stats'): This line is making an HTTP GET request to the /jobs/stats endpoint, which is the API route that provides the job statistics data.\n\n#### Get Stats with React Query\n\n```js\nconst statsQuery = {\n  queryKey: ['stats'],\n  queryFn: async () =\u003e {\n    const response = await customFetch.get('/jobs/stats');\n    return response.data;\n  },\n};\n\nexport const loader = async () =\u003e {\n  return null;\n};\n\nconst Stats = () =\u003e {\n  const { isLoading, isError, data } = useQuery(statsQuery);\n\n  if (isLoading) return \u003ch4\u003eLoading...\u003c/h4\u003e;\n  if (isError) return \u003ch4\u003eError...\u003c/h4\u003e;\n  // after loading/error or ?.\n  const { defaultStats, monthlyApplications } = data;\n\n  return (\n    \u003c\u003e\n      \u003cStatsContainer defaultStats={defaultStats} /\u003e\n      {monthlyApplications?.length \u003e 1 \u0026\u0026 (\n        \u003cChartsContainer data={monthlyApplications} /\u003e\n      )}\n    \u003c/\u003e\n  );\n};\nexport default Stats;\n```\n\n#### React Query in Stats Loader\n\nApp.jsx\n\n```js\n{\n  path: 'stats',\n  element: \u003cStats /\u003e,\n  loader: statsLoader(queryClient),\n  errorElement: \u003cErrorElement /\u003e,\n},\n```\n\nStats.jsx\n\n```js\nimport { ChartsContainer, StatsContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useQuery } from '@tanstack/react-query';\n\nconst statsQuery = {\n  queryKey: ['stats'],\n  queryFn: async () =\u003e {\n    const response = await customFetch.get('/jobs/statss');\n    return response.data;\n  },\n};\n\nexport const loader = (queryClient) =\u003e async () =\u003e {\n  const data = await queryClient.ensureQueryData(statsQuery);\n  return data;\n};\n\nconst Stats = () =\u003e {\n  const { data } = useQuery(statsQuery);\n  const { defaultStats, monthlyApplications } = data;\n\n  return (\n    \u003c\u003e\n      \u003cStatsContainer defaultStats={defaultStats} /\u003e\n      {monthlyApplications?.length \u003e 1 \u0026\u0026 (\n        \u003cChartsContainer data={monthlyApplications} /\u003e\n      )}\n    \u003c/\u003e\n  );\n};\nexport default Stats;\n```\n\n#### React Query for Current User\n\nDashboardLayout.jsx\n\n```js\nconst userQuery = {\n  queryKey: ['user'],\n  queryFn: async () =\u003e {\n    const { data } = await customFetch('/users/current-user');\n    return data;\n  },\n};\n\nexport const loader = (queryClient) =\u003e async () =\u003e {\n  try {\n    return await queryClient.ensureQueryData(userQuery);\n  } catch (error) {\n    return redirect('/');\n  }\n};\n\nconst Dashboard = ({ prefersDarkMode, queryClient }) =\u003e {\n  const { user } = useQuery(userQuery)?.data;\n};\n```\n\n#### Invalidate Queries\n\nLogin.jsx\n\n```js\nexport const action =\n  (queryClient) =\u003e\n  async ({ request }) =\u003e {\n    const formData = await request.formData();\n    const data = Object.fromEntries(formData);\n    try {\n      await axios.post('/api/v1/auth/login', data);\n      queryClient.invalidateQueries();\n      toast.success('Login successful');\n      return redirect('/dashboard');\n    } catch (error) {\n      toast.error(error.response.data.msg);\n      return error;\n    }\n  };\n```\n\nDashboardLayout.jsx\n\n```js\nconst logoutUser = async () =\u003e {\n  navigate('/');\n  await customFetch.get('/auth/logout');\n  queryClient.invalidateQueries();\n  toast.success('Logging out...');\n};\n```\n\nProfile.jsx\n\n```js\nexport const action =\n  (queryClient) =\u003e\n  async ({ request }) =\u003e {\n    const formData = await request.formData();\n    const file = formData.get('avatar');\n    if (file \u0026\u0026 file.size \u003e 500000) {\n      toast.error('Image size too large');\n      return null;\n    }\n    try {\n      await customFetch.patch('/users/update-user', formData);\n      queryClient.invalidateQueries(['user']);\n      toast.success('Profile updated successfully');\n      return redirect('/dashboard');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n      return null;\n    }\n  };\n```\n\n#### All Jobs Query\n\nAllJobs.jsx\n\n```js\nimport { toast } from 'react-toastify';\nimport { JobsContainer, SearchContainer } from '../components';\nimport customFetch from '../utils/customFetch';\nimport { useLoaderData } from 'react-router-dom';\nimport { useContext, createContext } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nconst AllJobsContext = createContext();\n\nconst allJobsQuery = (params) =\u003e {\n  const { search, jobStatus, jobType, sort, page } = params;\n  return {\n    queryKey: [\n      'jobs',\n      search ?? '',\n      jobStatus ?? 'all',\n      jobType ?? 'all',\n      sort ?? 'newest',\n      page ?? 1,\n    ],\n    queryFn: async () =\u003e {\n      const { data } = await customFetch.get('/jobs', {\n        params,\n      });\n      return data;\n    },\n  };\n};\n\nexport const loader =\n  (queryClient) =\u003e\n  async ({ request }) =\u003e {\n    const params = Object.fromEntries([\n      ...new URL(request.url).searchParams.entries(),\n    ]);\n\n    await queryClient.ensureQueryData(allJobsQuery(params));\n    return { searchValues: { ...params } };\n  };\n\nconst AllJobs = () =\u003e {\n  const { searchValues } = useLoaderData();\n  const { data } = useQuery(allJobsQuery(searchValues));\n  return (\n    \u003cAllJobsContext.Provider value={{ data, searchValues }}\u003e\n      \u003cSearchContainer /\u003e\n      \u003cJobsContainer /\u003e\n    \u003c/AllJobsContext.Provider\u003e\n  );\n};\nexport default AllJobs;\n\nexport const useAllJobsContext = () =\u003e useContext(AllJobsContext);\n```\n\n#### Invalidate Jobs\n\nAddJob.jsx\n\n```js\nexport const action =\n  (queryClient) =\u003e\n  async ({ request }) =\u003e {\n    const formData = await request.formData();\n    const data = Object.fromEntries(formData);\n    try {\n      await customFetch.post('/jobs', data);\n      queryClient.invalidateQueries(['jobs']);\n      toast.success('Job added successfully ');\n      return redirect('all-jobs');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n      return error;\n    }\n  };\n```\n\nEditJob.jsx\n\n```js\nexport const action =\n  (queryClient) =\u003e\n  async ({ request, params }) =\u003e {\n    const formData = await request.formData();\n    const data = Object.fromEntries(formData);\n    try {\n      await customFetch.patch(`/jobs/${params.id}`, data);\n      queryClient.invalidateQueries(['jobs']);\n      toast.success('Job edited successfully');\n      return redirect('/dashboard/all-jobs');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n      return error;\n    }\n  };\n```\n\nDeleteJob.jsx\n\n```js\nexport const action =\n  (queryClient) =\u003e\n  async ({ params }) =\u003e {\n    try {\n      await customFetch.delete(`/jobs/${params.id}`);\n      queryClient.invalidateQueries(['jobs']);\n      toast.success('Job deleted successfully');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n    }\n    return redirect('/dashboard/all-jobs');\n  };\n```\n\n#### Edit Job Loader\n\n```js\nimport { FormRow, FormRowSelect, SubmitBtn } from '../components';\nimport Wrapper from '../assets/wrappers/DashboardFormPage';\nimport { useLoaderData, useParams } from 'react-router-dom';\nimport { JOB_STATUS, JOB_TYPE } from '../../../utils/constants';\nimport { Form, redirect } from 'react-router-dom';\nimport { toast } from 'react-toastify';\nimport customFetch from '../utils/customFetch';\nimport { useQuery } from '@tanstack/react-query';\n\nconst singleJobQuery = (id) =\u003e {\n  return {\n    queryKey: ['job', id],\n    queryFn: async () =\u003e {\n      const { data } = await customFetch.get(`/jobs/${id}`);\n      return data;\n    },\n  };\n};\n\nexport const loader =\n  (queryClient) =\u003e\n  async ({ params }) =\u003e {\n    try {\n      await queryClient.ensureQueryData(singleJobQuery(params.id));\n      return params.id;\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n      return redirect('/dashboard/all-jobs');\n    }\n  };\n\nexport const action =\n  (queryClient) =\u003e\n  async ({ request, params }) =\u003e {\n    const formData = await request.formData();\n    const data = Object.fromEntries(formData);\n    try {\n      await customFetch.patch(`/jobs/${params.id}`, data);\n      queryClient.invalidateQueries(['jobs']);\n\n      toast.success('Job edited successfully');\n      return redirect('/dashboard/all-jobs');\n    } catch (error) {\n      toast.error(error?.response?.data?.msg);\n      return error;\n    }\n  };\n\nconst EditJob = () =\u003e {\n  const id = useLoaderData();\n\n  const {\n    data: { job },\n  } = useQuery(singleJobQuery(id));\n\n  return (\n    \u003cWrapper\u003e\n      \u003cForm method='post' className='form'\u003e\n        \u003ch4 className='form-title'\u003eedit job\u003c/h4\u003e\n        \u003cdiv className='form-center'\u003e\n          \u003cFormRow type='text' name='position' defaultValue={job.position} /\u003e\n          \u003cFormRow type='text' name='company' defaultValue={job.company} /\u003e\n          \u003cFormRow\n            type='text'\n            name='jobLocation'\n            labelText='job location'\n            defaultValue={job.jobLocation}\n          /\u003e\n          \u003cFormRowSelect\n            name='jobStatus'\n            labelText='job status'\n            defaultValue={job.jobStatus}\n            list={Object.values(JOB_STATUS)}\n          /\u003e\n          \u003cFormRowSelect\n            name='jobType'\n            labelText='job type'\n            defaultValue={job.jobType}\n            list={Object.values(JOB_TYPE)}\n          /\u003e\n          \u003cSubmitBtn formBtn /\u003e\n        \u003c/div\u003e\n      \u003c/Form\u003e\n    \u003c/Wrapper\u003e\n  );\n};\nexport default EditJob;\n```\n\n#### Axios Interceptors\n\nDashboardLayout.jsx\n\n```js\nconst DashboardContext = createContext();\n\nconst DashboardLayout = ({ isDarkThemeEnabled }) =\u003e {\n  const [isAuthError, setIsAuthError] = useState(false);\n\n  const logoutUser = async () =\u003e {\n    await customFetch.get('/auth/logout');\n    toast.success('Logging out...');\n    navigate('/');\n  };\n\n  customFetch.interceptors.response.use(\n    (response) =\u003e {\n      return response;\n    },\n    (error) =\u003e {\n      if (error?.response?.status === 401) {\n        setIsAuthError(true);\n      }\n      return Promise.reject(error);\n    }\n  );\n  useEffect(() =\u003e {\n    if (!isAuthError) return;\n    logoutUser();\n  }, [isAuthError]);\n  return (\n    ...\n  )\n};\n\n```\n\n#### Security\n\n```sh\nnpm install helmet express-mongo-sanitize express-rate-limit\n\n```\n\nPackage: helmet\nDescription: helmet is a security package for Express.js applications that helps protect them by setting various HTTP headers to enhance security, prevent common web vulnerabilities, and improve overall application security posture.\nNeed: The package is needed to safeguard web applications from potential security threats, such as cross-site scripting (XSS) attacks, clickjacking, and other security exploits.\n\nPackage: express-mongo-sanitize\nDescription: express-mongo-sanitize is a middleware for Express.js that sanitizes user-supplied data coming from request parameters, body, and query strings to prevent potential NoSQL injection attacks on MongoDB databases.\nNeed: The package addresses the need to protect MongoDB databases from malicious attempts to manipulate data and helps ensure the integrity of data storage and retrieval.\n\nPackage: express-rate-limit\nDescription: express-rate-limit is an Express.js middleware that helps control and limit the rate of incoming requests from a specific IP address or a set of IP addresses to protect the server from abuse, brute-force attacks, and potential denial-of-service (DoS) attacks.\nNeed: This package is necessary to manage and regulate the number of requests made to the server within a given time frame, preventing excessive usage and improving the overall stability and performance of the application.\n\nserver.js\n\n```js\nimport helmet from 'helmet';\nimport mongoSanitize from 'express-mongo-sanitize';\n\napp.use(helmet());\napp.use(mongoSanitize());\n```\n\nroutes/authRouter.js\n\n```js\nimport rateLimiter from 'express-rate-limit';\n\nconst apiLimiter = rateLimiter({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 15,\n  message: { msg: 'IP rate limit exceeded, retry in 15 minutes.' },\n});\nrouter.post('/register', apiLimiter, validateRegisterInput, register);\nrouter.post('/login', apiLimiter, validateLoginInput, login);\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbourgui07%2Fjobify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbourgui07%2Fjobify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbourgui07%2Fjobify/lists"}