{"id":15767810,"url":"https://github.com/eliasafara/apihub","last_synced_at":"2026-04-06T08:01:49.380Z","repository":{"id":133855676,"uuid":"342219192","full_name":"EliasAfara/ApiHub","owner":"EliasAfara","description":"Showcasing projects made using free APIs","archived":false,"fork":false,"pushed_at":"2021-03-16T12:01:45.000Z","size":580,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-10-05T13:41:23.421Z","etag":null,"topics":["api","express","githubjobs","hackernews-api","hackernewsclone","newsapi","nodejs","react","redux"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EliasAfara.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2021-02-25T11:16:28.000Z","updated_at":"2021-03-16T12:01:47.000Z","dependencies_parsed_at":null,"dependency_job_id":"c9decf7d-72b1-4263-8de3-66e5aaf3e9b9","html_url":"https://github.com/EliasAfara/ApiHub","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/EliasAfara/ApiHub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliasAfara%2FApiHub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliasAfara%2FApiHub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliasAfara%2FApiHub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliasAfara%2FApiHub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EliasAfara","download_url":"https://codeload.github.com/EliasAfara/ApiHub/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliasAfara%2FApiHub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31464102,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T21:22:52.476Z","status":"online","status_checked_at":"2026-04-06T02:00:07.287Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["api","express","githubjobs","hackernews-api","hackernewsclone","newsapi","nodejs","react","redux"],"created_at":"2024-10-04T13:41:15.780Z","updated_at":"2026-04-06T08:01:49.359Z","avatar_url":"https://github.com/EliasAfara.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ApiHub\n\n\u003e Showcasing projects made using free APIs\n\nThis project purpose is to combine to some of the educational projects I do related to APIs\nTechnology Stack Used: React, Redux, Nodejs, Express\n\n# Google Job Search\n\nAPI Site: https://jobs.github.com/api\n\nGitHub jobs API does not allow accessing jobs from client side apps. That's why I have used Node.js for making API call.\n\n## Features\n\n- Used React Context API for sharing data between components\n- Applied lazy loading to the images\n- Displayed a placeholder loading image while the actual image is downloading\n- Implemented load more functionality\n\n### The reason for adding `encodeURIComponent` for each input field in [GithubJobs.js](server/routes/api/GithubJobs.js) is to convert special characters if any like space to %20.\n```js\ndescription = description ? encodeURIComponent(description) : '';\nlocation = location ? encodeURIComponent(location) : '';\n```\n\n### By default, the API gives a list of the latest 50 jobs only but we can get more jobs by sending page query parameter with values 1, 2, 3, etc.\nSo we are validating the page query parameter by the following code\n\n```js\nif (page) {\n  page = parseInt(page);\n  page = isNaN(page) ? '' : `\u0026page=${page}`;\n}\n```\n\n### Creating the API URL by combining all parameter values:\n\n```js\nconst jobs = await axios.get(\n  `/api/jobs/github?description=${description}\u0026location=${location}${full_time}${page}`\n);\n```\nThe `description` and `location` are optional parameters\n\n\n### The data is sorted by creation date in the [initiateGetJobs](client/src/actions/jobs.js) function:\n```js\nconst sortedJobs = jobs.data.sort(\n      (a, b) =\u003e moment(new Date(b.created_at)) - moment(new Date(a.created_at))\n);\n```\n\n\u003e **React does not directly display the HTML content when used inside the JSX Expression to avoid the Cross Site Scripting (XSS) attacks. React escapes all the html content provided in the JSX Expression which is written in curly brackets so it will be printed as it is.**\n\nThe `description` and `how to apply` fields that we get from the API response contains the HTML content and and inorder to display the HTML content if its the requirement as in our case, we need to use a special prop called `dangerouslySetInnerHTML` and pass it the HTML in the `__html` field inside [JobDetails.js](client/src/components/JobDetails.js) as shown below:\n```html\n\u003cdiv className=\"job-description\" dangerouslySetInnerHTML={{ __html: description }}\u003e\u003c/div\u003e\n```\nand\n```html\n\u003cdiv dangerouslySetInnerHTML={{ __html: how_to_apply }}\u003e\u003c/div\u003e\n```\n### Using Context API to Avoid Prop Drilling\n\nCreated a [context](client/src/context/jobs.js) which we can use to access data in other components\n\n```js\nimport React from 'react';\n\nconst JobsContext = React.createContext();\n\nexport default JobsContext;\n```\nInside the [Jobs.js](client/src/pages/Jobs.js) I've imported the JobsContext at the top of the file\nand created a value object with the data we want to access in other components\n\n```js\nimport JobsContext from '../context/jobs';\n\nconst value = {\n  details: jobDetails,\n  onSearch: handleSearch,\n  onResetPage: handleResetPage,\n};\n```\nthen returned the JobContext provider with the value created\n\n```js\n\u003cJobsContext.Provider value={value}\u003e...\u003c/JobsContext.Provider\u003e\n```\nTo access the data from value object for example inside the [Search.js](client/src/components/Search.js) form component\nwe need to import `useContext` hook at the top inorder to destruct the passed `handleSearch` function in the context provider.\n\n```js\nimport React, { useState, useContext } from 'react';\n\nconst { onSearch } = useContext(JobsContext);\n```\n### Reset Scroll Position\n\nWhen the user clicks on any of the displayed jobs, the JobDetails component will automatically be displayed at the top of the page.\n\n```js\nconst handleItemClick = (jobId) =\u003e {\n  ...\n  window.scrollTo(0, 0);\n};\n```\n\n### Custom Loader Component For Overlay using React Portal\n\nThis custom loader using React Portal is used to display an overlay so the user will not be able to click on any of the job when loading and we will also see a clear indication of loading.\n\nInside [index.html](client/public/index.html) and alongside the div with id `root` I've added another div with id `loader`\n\n```html\n\u003cdiv id=\"root\"\u003e\u003c/div\u003e\n\u003cdiv id=\"loader\"\u003e\u003c/div\u003e\n```\nThe `ReactDOM.createPortal` method which we have used in [Loader.js](client/src/components/Loader.js) will create a loader inside the div with id `loader` so it will be outside out `React` application DOM hierarchy and hence we can use it to provide an overlay for our entire application. This is the primary reason for using the `React Portal` for creating a loader.\n\nSo even if we will include the [Loader.js](client/src/components/Loader.js) component in [Jobs.js](client/src/pages/Jobs.js) file, it will be rendered outside all the divs but inside the div with id `loader`.\n\nIn the [Loader.js](client/src/components/Loader.js) file, we have first created a div where will add a loader message\n\n```js\nconst [node] = useState(document.createElement('div'));\n```\nThen, we are adding the `message class` to that div and adding that div to the div added in `index.html`\n\n```js\ndocument.querySelector('#loader').appendChild(node).classList.add('message');\n```\nand based on the show prop passed from the [Jobs.js](client/src/pages/Jobs.js) component, we will add or remove the `hide class` and then finally we will render the `Loader component` using\n\n```js\nReactDOM.createPortal(props.children, node);\n```\nThen we add or remove the `loader-open class` to the body tag of the page which will disable or enable the scrolling of the page\n\n```js\ndocument.body.classList.add('loader-open');\ndocument.body.classList.remove('loader-open');\n```\n\nHere, the data we will pass in between the opening and closing `Loader` tag will be available inside `props.children` so we can display a simple loading message or we can include an image to be shown as a loader.\n\n```js\nimport Loader from '../components/Loader';\n\u003cLoader show={isLoading}\u003eLoading...\u003c/Loader\u003e\n```\nI also imported the Loader component inside [App.js](client/src/App.js) since we have multiple pages and we don't want the loader to show on pages which does not contain the loader and passes false value through props to not display it.\n\n```js\n\u003cLoader show={false} /\u003e\n```\n\n### Lazy Loading Images Functionality\n\nLazy loading images: until the user does not scroll to the job in the list, the image will not be downloaded. This will load the page faster and save internet bandwidth.\n\nAs you are aware now when we are requesting from Jobs API, we are getting a list of 50 jobs initially and as we are showing the company logo on the list page, the browser has to download those 50 images which may take time so you might see the blank area sometimes before the image is fully loaded.\n\nAlso if you are browsing the application on a mobile device and you are using a slow network connection, it may take more time to download the images and those much `MB` of unnecessary images browser may download even if you are not scrolling the page to see other jobs listing which is not good user experience.\n\nI've created an [observer.js](client/src/custom-hooks/observer.js) in which I am using an Intersection Observer API to identify which area of the page is currently displayed and only images in that area will be downloaded.\n\n\u003e [Intersection Observer Helpful Article](https://levelup.gitconnected.com/what-is-so-special-about-intersection-observer-api-in-javascript-f2430a159fa7)\n\nSo in the [observer.js](client/src/custom-hooks/observer.js) file, we are taking a `ref` and adding that `ref` to be observed by the observer\n\n```js\nobserver.observe(current);\n```\n\nIf the image with added `ref` is displayed on screen then we are calling `setIsVisible(true);` and we are returning the `isVisible` value from this custom hook and based on the `isVisible` flag we can decide if we want to display the image or not.\n\nTo use this costume hook I've imported the [useObserver](client/src/custom-hooks/observer.js) and `useRef hook` inside the [ItemCard.js](client/src/components/ItemCard.js)\n\n```js\nimport React, { useRef } from 'react';\nimport useObserver from '../custom-hooks/observer';\n```\nThen created a `ref` which we can assign to the image and call the custom hook and get the isVisible value\n\n```js\nconst imageRef = useRef();\nconst [isVisible] = useObserver(imageRef);\n\n\u003cdiv className='Company__Logo' ref={imageRef}\u003e\n  {isVisible \u0026\u0026 (\n    \u003cImage\n      className='mr-3'\n      src={company_logo}\n      alt={company}\n      height='150'\n      width='150'\n      draggable='false'\n    /\u003e\n  )}\n\u003c/div\u003e\n```\n### Default Loading Image\n\nDefault loading image is an alternative image which will be replaced by the original image once it's completely downloaded.\n\nThis way we can avoid the empty space and is a widely used way of not showing the empty image area.\n\nThe website used for creating the image is [placeholder](https://placeholder.com/).\n\nYou can specify the `width`, `height`, and `text` of the image you want.\n\nThe URL used to generate that loading image is this\n\n```text\nhttps://via.placeholder.com/150x150?text=Loading\n```\n\nI've created [Image.js](client/src/components/Image.js) component in which we are initially displaying the loading image instead of the actual image.\n\nThe `img` tag has `onLoad` handler added which will be triggered when the image is completely loaded where we set the `isVisible` flag to true and once it's true we are displaying that image and hiding the previous loading image by using display CSS property.\n\n```js\n\u003cimg\n  src={src}\n  alt={alt}\n  width='150'\n  height='150'\n  onLoad={changeVisibility}\n  style={{ display: isVisible ? 'inline' : 'none' }}\n  {...props}\n/\u003e\n```\n\n---\n\n# Hacker News Clone\n\nWe will be using the Hackernews API from [this url](https://github.com/HackerNews/API).\n\nAPI                                   | Link\n--------------------------------------| --------------------------------------------------------------\nAPI to get top stories, use this URL  | https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty\nAPI to get new stories, use this URL  | https://hacker-news.firebaseio.com/v0/newstories.json?print=pretty\nAPI to get best stories, use this URL | https://hacker-news.firebaseio.com/v0/beststories.json?print=pretty\n\nEach of the above stories API returns only an array of IDs representing a story.\n\nSo to get the details of that particular story, we need to make another API call.\n\nAPI to get story details, use this URL: https://hacker-news.firebaseio.com/v0/item/story_id.json?print=pretty\n\nFor example: https://hacker-news.firebaseio.com/v0/item/26061935.json?print=pretty\n\nIn the [getStories function](client/src/utils/hackerNewsApis.js) we pass the type of story we want (`top`, `new` or `best`). Then we make an API call to the respective `.json` URL provided at the start of this article.\n\nNote that we have declared the function as `async` so we can use the `await` keyword to call the API and wait for the response to come.\n\nAs the `axios` library always returns the result in the `.data` property of the response, we take out that property and rename it to `storyIds` because the API returns an array of story IDs.\n\n```js\nconst res = await axios.get(`${BASE_API_URL}/${type}stories.json`);\nconst storyIds = res.data;\n);\n```\nSince we get an array of story IDs back, instead of making separate API calls for each `id` and then waiting for the previous one to finish, we use the `Promise.all` method to make API calls simultaneously for all the story ids.\n\n```js\nconst stories = await Promise.all(storyIds.slice(0, 30).map(getStory));\n\n// .map(getStory) is a simplified version of .map((storyId) =\u003e getStory(storyId))\n```\nHere, we use the Array slice method to take only the first 30 story ids so the data will load faster.\n\nThen we use the Array map method to call the [getStory function](client/src/utils/hackerNewsApis.js) to make an API call to the individual story item by passing the `storyId` to it.\n\nIn the API response, we get the time of the story in seconds. So in the [Story component](client/src/components/HackerNews/Story.js), we multiply it by 1000 to convert it to milliseconds so we can display the correct date in proper format using JavaScript's `toLocaleDateString` method:\n\n```js\n{new Date(time * 1000).toLocaleDateString('en-US', {\n  hour: 'numeric',\n  minute: 'numeric',\n})}\n```\n\n---\n\n# Credits\n\nProject                        | Article Link\n-------------------------------| --------------------------------------------------------------\nGithub Job Search              | https://dev.to/myogeshchavan97/build-an-amazing-job-search-app-using-react-42p\nHacker News Clone 01           | https://www.freecodecamp.org/news/how-to-build-a-hacker-news-clone-using-react/\nHacker News Clone 02           | https://yogeshchavan.hashnode.dev/how-to-implement-caching-for-hacker-news-app-in-react\n\n---\n\n# Quick Start 🚀\n\n### Install server dependencies\n\n```bash\ncd server\nnpm install\n```\n\n### Install client dependencies\n\n```bash\ncd client\nnpm install\n```\n\n### Run both Express \u0026 React inside server directory\n\n```bash\ncd server\nnpm run dev\n```\n\n---\n\n## App Info\n\n### Author\n\nTheGrindev\n[Elias Afara](https://eliasafara.github.io/)\n\n\n### License\n\nThis project is licensed under the MIT License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feliasafara%2Fapihub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feliasafara%2Fapihub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feliasafara%2Fapihub/lists"}