DEV Community

Cover image for Implementing A GitHub Repository API Call.
missbaah
missbaah

Posted on

Implementing A GitHub Repository API Call.

As part of the second-semester examination for Alt School Africa Frontend Engineering track. We were tasked to implement key features such as API fetch, routing, error boundary, and SEO using React.js. Check out the live site here. Below reads the task

Task

Implement an API fetch of your GitHub portfolio, show a page with a list of all your repositories on GitHub(the page should implement pagination for the repo list), and create another page showing data for a single repo clicked from the list of repos using nested routes while using all the necessary tools in react. Implement the proper SEO, Error Boundary (show a page to test the error boundary), and 404 pages. Good UI and Designs are important.

Approach

I split the task into components, i.e. ;

  • Implement an API fetch of my GitHub Portfolio
  • Show a page with a list of all my repositories
  • Implement pagination for the repo list
  • Show a page for single repo clicked from the list of repos using nested routes.
  • Implement proper SEO
  • Show a page to test boundary error
  • Create a 404 page

Project Setup

To set up my project I used the online IDE Replit and selected a react template. In the src file, I set up react-router to dynamically access all the pages necessary for this task in the App.js file.

For this project, I create 5 pages namely home, repos, repo-project, not found, and error boundary page. The repo project in a nested route within the repos page from a single repository clicked and the error page is the 404 page for a URL not found.

import './App.css'
import { Routes, Route, Link } from "react-router-dom"
import { Repos, Home, NotFound, ErrorPage, Project } from "./components"

export default function App() {
  return (
      <Routes>
        <Route index element={<Home />} />
        <Route path="home" element={<Home />} />
        <Route path="repos/*" element={<Repos />} />
        <Route path="repos/:projectId" element={<Project />} />
        <Route path="error" element={<ErrorPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

The project started with a nav bar with links that routed to 3 pages i.e. the home, repos, and error boundary page.

    <main>
      <nav className="main-nav">
        <Link className="nav-link" to="/home">Home</Link>
        <Link className="nav-link" to="/repos">Repos</Link>
        <Link className="nav-link" to="/error">Error Boundary</Link>
      </nav>
Enter fullscreen mode Exit fullscreen mode

I created a components folder in src to carry the component created in this project.

Home Page

Image of the home page
I wanted the home page to show details such as my name, photo, number of repositories, followers, and following from my GitHub profile. To handle the data, I used useState to be able to set the user and set the loading state. To be able to display the data I need, I needed to fetch it from the GitHub API to render it. I used useEffectwith the help of Axios to fetch the data and set it to setUser. From there, the data needed could be obtained using dot notation. Find the code below

import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { LinearProgress } from "@mui/material";
import "../App.css"


const Home = () => {
  const [user, setUser] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(
    () => {
      const fetchUser = async () => {
        setLoading(true)
        const response = await axios.get('https://api.github.com/users/missbaah')
        setUser(response.data);
        setLoading(false)
      }
      fetchUser()
    }, []
  )

  if (loading) {
    return <div>
      <h2>Loading</h2>
      <LinearProgress />
    </div>
  }

  return (
    <section>
      <Helmet>
        <title>Home</title>
        <meta name="description" content="This page is the home page containing introductory info" />
        <link rel="canonical" href="/home" />
      </Helmet>


      <div className="intro-container">
        <div className="box-A">

          <img src={user.avatar_url} alt="image of the user" loading="eager" title="Image of Adwoa Baah Addo-Brako" width="460" height="460" />
          <h1 className="intro">Hello 👋,  I'm {user.name} </h1>
        </div>
        <section className="box-B">
          <div className="user-bio">{user.bio}</div>
          <div className="stats">
            <div className="stat"><span className="bold">{user.public_repos}</span> repositories</div>
            <div className="stat"><span className="bold">{user.followers}</span> followers</div>
            <div className="stat"><span className="bold">{user.following}</span> following</div>
          </div>
        </section>
      </div>
    </section>
  )
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

GitHub API Fetch (Repos Page)

The next component was the reposcomponent which will show the list of all my GitHub repos. Similar to the home page, useStatewas used to handle the dynamic data, and useEffectwas used to fetch the data and set it to a state. To render every individual repo, I used the .map() method and returned a list of the repos with the specific data I wanted to display.

import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
import { Routes, Route, Link } from "react-router-dom"
import '../App.css';

const Repos = () => {
  // setting states 
  const [repos, setRepos] = useState([]);
  const [loading, setLoading] = useState(false);

  // fetching the data from the API using axios
  useEffect(
    () => {
      const fetchRepos = async () => {
        setLoading(true)
        const response = await axios.get('https://api.github.com/users/missbaah/repos')
        setRepos(response.data);
        setLoading(false)
      }
      fetchRepos()
    }, []
  )

 // looping throught the repos to create a list item of each repo
  const listOfProjects = repos.map((repo) => {
    return (
      <li className="card" key={repo.id}>
        <Link className="card-link" to={`/repos/${repo.id}`}>
          <div className="name">
            {repo.name}
          </div>
          <br />
          <div className="lang" >
            {repo.language ? `Prominent Languge: ${repo.language}` : "Language: None"}
          </div>
          <br />
          <div className="contributors" >
            Contributors: @{repo.owner.login}
          </div>
        </Link>
      </li>)
  });

return (
    <>
      <h2>GitHub Repositories</h2>
      {/*renders an unordered list of repos*/}
      <ul>
        {listOfProjects}
      </ul>
      <Routes>
        <Route path=":id" element={<Project />} />
      </Routes>

    </>
  )

}

export default Repos;

Enter fullscreen mode Exit fullscreen mode

Pagination

After getting the list of repos on my GitHub profile, I implemented pagination for the page. To do that, I created a pagination component that takes 3 props; reposPerPage, totalRepos, and paginate. The pageNumvariable was created in the pagination component to track page numbers and set to an empty array. A for loop was used to loop over the totalRepos/reposPerPage and pushed into the pageNumarray. To render each page number individually, a list of the pageNumarray was created using the .map() method.

import React from "react";

const Pagination = ({reposPerPage, totalRepos, paginate}) => {
  // set the page number to an empty array
 const pageNum = [];

  // loop throw the total over repos per each page
  for (let i=1; i <= Math.ceil(totalRepos/reposPerPage); i++){
    // add the numbers unto the the pageNum array
  pageNum.push(i)
  }

  // Create a list of the array of page numbers 
  const listOfPageNums = pageNum.map((num)=>{ 
    return <li className="list-box" key={num}>
      {/*each page number is a link that accepts a prop called paginate*/}
            <a className="paginate-link" href="#" onClick={()=>paginate(num)}>{num}</a>
          </li> 
  })

  return (
    <nav >
       {/*render an unorder list of page numbers*/}
      <ul className="paginate-box">
        {listOfPageNums}
      </ul>
    </nav>
  )
}

export default Pagination;
Enter fullscreen mode Exit fullscreen mode

The pagination component was then imported into repos component. To be able to render only the repos needed per page, I found the index of the last and first repos and created a variable called currentReposwhich uses .slice() to store the repos needed for each page. The variable currentReposreplaces the reposarray to be looped over to create a list of repos with the specific data needed. A paginate function was created in repos with accepts numas a parameter and sets the currentPageto num. After adding pagination, the code for the repos component will look like this;

import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
import { Routes, Route, Link } from "react-router-dom"
import '../App.css';
import Pagination from "./Pagination.jsx"
import Project from "./Project.jsx"
import { Helmet } from "react-helmet-async"
import { LinearProgress } from "@mui/material";

const Repos = () => {
  // setting states 
  const [repos, setRepos] = useState([]);
  const [loading, setLoading] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const [reposPerPage] = useState(3);

  // fetching the data from the API using axios
  useEffect(
    () => {
      const fetchRepos = async () => {
        setLoading(true)
        const response = await axios.get('https://api.github.com/users/missbaah/repos')
        setRepos(response.data);
        setLoading(false)
      }
      fetchRepos()
    }, []
  )



  const indexOfLastRepo = currentPage * reposPerPage;
  const indexOfFirstRepo = indexOfLastRepo - reposPerPage;
  // limiting the number of repos per page
  const currentRepos = repos.slice(indexOfFirstRepo, indexOfLastRepo);


  // looping throught the repos to create a list item of each repo
  const listOfProjects = currentRepos.map((repo) => {
    return (
      <li className="card" key={repo.id}>
        <Link className="card-link" to={`/repos/${repo.id}`}>
          <div className="name">
            {repo.name}
          </div>
          <br />
          <div className="lang" >
            {repo.language ? `Prominent Languge: ${repo.language}` : "Language: None"}
          </div>
          <br />
          <div className="contributors" >
            Contributors: @{repo.owner.login}
          </div>
        </Link>
      </li>)
  });

  // Create paginate function
  const paginate = (num) => setCurrentPage(num);
  if (loading) {
    return <div >
      <h2>Loading</h2>
      <LinearProgress/>
    </div>

  }

  return (
    <>
      <Helmet>
        <title>Repos</title>
        <meta name="description" content="Here is a list of Repos for the user's github profile" />
        <link rel="canonical" href="/repos" />
      </Helmet>

      <h2>GitHub Repositories</h2>
      {/*renders an unordered list of repos*/}
      <ul>
        {listOfProjects}
      </ul>
      {/*renders the pagination component*/}
      <Pagination reposPerPage={reposPerPage} totalRepos={repos.length} paginate={paginate} />
      {/*nested route to individual repos*/}
      <Routes>
        <Route path=":id" element={<Project />} />
      </Routes>

    </>
  )

}

export default Repos;
Enter fullscreen mode Exit fullscreen mode

Viewing Data From A Single Repo

For a single repo clicked
When a single repo is clicked it should open up to another page with details of the repo. To achieve this I used nested routes. In the App.jsfile, I nested repos/:projectId to represent a single project clicked on and linked it to the project component.

  <Route path="repos/*" element={<Repos />} />
        <Route path="repos/:projectId" element={<Project />} />
Enter fullscreen mode Exit fullscreen mode

To build the project component, the data for the clicked repo was fetched using the API and then destructed projectId using useParams. I created a variable named project which tracked which if the id of the single repo clicked matched with the id project id. Project was then destructured to obtained the data need from the API call.

import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { Helmet } from "react-helmet-async";

const Project = () => {
  const [repos, setRepos] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(
    () => {
      const fetchRepos = async () => {
        setLoading(true)
        const response = await axios.get('https://api.github.com/users/missbaah/repos')
        setRepos(response.data);
        setLoading(false)
      }
      fetchRepos()
    }, []
  )

  const { projectId } = useParams();
  // console.log(repos)
  const project = repos.find(repo => repo.id == projectId);
  const {name, html_url, forks, stargazers_count, updated_at} = project || {};

  return (
    <>
      <Helmet>
        <title>Project</title>
        <meta name="description" content="This page contains the data for a single repo clicked on"/>
        <link rel="canonical" href="/repos/:projectId" />
      </Helmet>

      <h2>{name}</h2>
      <section className="data-box">
      <div className="details">Stars: <span className="deets">{stargazers_count}</span></div>
      <div className="details">Forks: <span className="deets">{forks}</span></div>
      <div className="details">Last Update: <span className="deets">{updated_at}</span></div>
      <a  className="details" href={html_url} target="_blank">Visit Source Code</a>
        </section>
      <Link className="home" to="/repos">Back to Repos</Link>
    </>
  )
}

export default Project;
Enter fullscreen mode Exit fullscreen mode

Error Boundary Page

Error Boundary Page
The next page tackled was error boundary. To create the error boundary page , I created an errorpage component which had a heading, a text body and an errorbutton component nested between an errorboundarycomponent.

import React from "react";
import ErrorButton from "./ErrorButton.jsx"
import ErrorBoundary from "./ErrorBoundary.jsx"
import {Helmet} from "react-helmet-async"
import "../App.css"

const ErrorPage = () => {
  return (
    <div>
      <Helmet>
        <title>ErrorPage</title>
        <meta name="description" content="This page displays the error boundary test setup"/>
        <link rel="canonical" href="/error" />
      </Helmet>
      <h2>Error Boundary Test</h2>
      <p className="p-body">When the button is clicked, it shows throws and error and shows a fallback UI to prevent the entire component tree from unmounting</p>
      <ErrorBoundary>
        <ErrorButton />
      </ErrorBoundary>
    </div>
  )
}

export default ErrorPage;
Enter fullscreen mode Exit fullscreen mode

The errorbutton component used useState to handle if the error data was true or false and returned a button with an onClick event listener which accepted the function handleClick. The fuction handleClick was used to set the state to true.

import React from "react";
import { useState } from "react"
import "../App.css"


const ErrorButton = () => {
// setting the state of the error
  const [error, setError] = useState(false);

 // handling error button click event 
  const handleClick = () => {
    setError(true);
  }

  // throw string if error is successful
  if (error) throw new Error("Error Boundary Test Successful!");

  // render the error buttom
  return (
    <div className="err-button">
    <button  onClick={handleClick}>Throw Error</button>
      </div>
  )
}

export default ErrorButton;
Enter fullscreen mode Exit fullscreen mode

The errorboundary component code is down below.

import React from "react";
import { Component } from "react";
import "../App.css"

 export default class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }


  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo
    })
  }

  render() {
    // if there is an error render...
    if (this.state.errorInfo) {
      return (
        <div>
          {/*a phrase and details of the error */}
          <h2>Something went wrong.</h2>
          <details>
            {this.state.error && this.state.error.toString()}
            <br/>
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      );
 }
    return this.props.children;
  }  
}
Enter fullscreen mode Exit fullscreen mode

404 Page

404 Page
The 404 page was created using the NotFound component. Below is the code.

import React from "react";
import { Helmet } from "react-helmet-async";
import {Link} from "react-router-dom"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight} from "@fortawesome/free-solid-svg-icons";
import "../App.css"

const NotFound = () => {
   return (

        <section className="err-container">
      <Helmet>
        <title>NotFound</title>
        <meta name="description" content="This is the 404 error page for this site"/>
        <link rel="canonical" href="*" />
      </Helmet>

     <div className="err">404</div>
      <p className="err-title">Oops !</p>
    <br/>
      <p className="err-body">Looks like the page you are looking for cannont be found</p>
     <Link className="home" to="/home">Back to Home <FontAwesomeIcon icon={faArrowRight} /></Link>

    </section>


  )
}

export default NotFound;
Enter fullscreen mode Exit fullscreen mode

Implementing SEO

To improve the SEO of the site, react-helmet was used to help assign titles, meta information, and links to each page on the site. An example code below

 <Helmet>
        <title>NotFound</title>
        <meta name="description" content="This is the 404 error page for this site"/>
        <link rel="canonical" href="*" />
      </Helmet>
Enter fullscreen mode Exit fullscreen mode

I also made use of the META SEO inspector by viewing the suggestions assigned to each page and implementing them.

Improvements

I made sure to make my site media responsive.

Additional Notes

All styling was done using an external style sheet and the 404 page used Font Awesome icons. Thank you for reading. I hope you found this helpful. Find below the live site and the GitHub repository for this project
Live Site
GitHub Repository

Top comments (0)