Using GitHub API to fetch and display a GitHub user profile

falanatolu

Falana Tolulope

Posted on January 11, 2023

Using GitHub API to fetch and display a GitHub user profile

Github API can be used to programmatically interact with GitHub. It gives you access to publicly available user-generated information (like public repositories, user profiles, etc.) and with an authenticated access token can even grant access to private information (my guess though, is that if you are here, you already knew all that).

We are creating an app to access publicly available information with the Github API and display it in react. We are essentially transforming information provided by the Github API's endpoints from the traditional JSON format into aesthetically-more-pleasing to-look-at format (you could almost say we're redesigning Github's user interface).

Here are the features we want to implement in this app:

  • A user-friendly UI to display a GitHub user's profile.

  • A proper SEO for each page in the app.

  • An error boundary and a page to test the error boundary.

  • Routing.

  • Pagination for fetched results retrieved as lists.

Requirements

Code editor — preferably Visual Studio Code
Node.js
Basic knowledge of React

The code for the app can be found in this GitHub repository.
A live demonstration can be found here.

Creating and setting up a React app

Type the following command in the terminal to create a new React app called "myapp":
npx create-react-app myapp

After that, all that is needed is to type the command to install the npm packages we will be using for the project:

npm install react-router-dom framer-motion prop-types react-helmet-async react-icons react-loader-spinner

react-helmet-async was installed to handle the SEO, framer-motion to animate page transitions(to which prop-types was a dependency), react-router-dom handled the routing of the application. react-icons provided the icons used in the app.
react-loader-spinner was for the loading component.

For the color scheme, I used envato.com.

To run the react app, in the terminal type:
cd myapp && npm start

Implementation

My final folder structure looked something like this:

Final Folder Structure

The first thing I do is to create files for the main pages of the app and store them in a folder called pages. We have decided that the main pages should the home, about, profile and test pages.

The home page Home.js is the app's root page, it welcomes users to the app and redirects to the profile page and the about page.

import React from 'react';
import { motion as m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { FaGithub } from 'react-icons/fa';
import { SEO, Navigation } from '../components';

const Home = () => {
  return (
    <m.section
      initial={{ y: '100%' }}
      animate={{ y: '0%' }}
      transition={{ duration: 1, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="home-section"
    >
      <SEO
        title="Home"
        name="Home Page"
        description="Home page for Github Profile App."
        type="article"
      />
      <Navigation />
      <article className="home-text">
        <div className="home-title-container">
          <m.h1
            initial={{ x: '-250%' }}
            animate={{ x: '0%' }}
            transition={{ delay: 0.5, duration: 2, ease: 'easeOut' }}
          >
            <span className="icon-container">
              <FaGithub />
            </span>{' '}
            GitHub Profile App
          </m.h1>
        </div>
        <p>
          This app was made to retrieve the details of the user's github account
          with the help of the Github API.
        </p>
        <div className="link">
          <Link to="/user">
            <button>Get Started</button>
          </Link>
          <Link to="/about">
            <button>Learn More</button>
          </Link>
        </div>
      </article>
    </m.section>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

The about page About.js gives random information about the app:

import React from 'react';
import { motion as m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { SEO, Navigation } from '../components';

const About = () => {
  return (
    <m.section
      initial={{ y: '100%' }}
      animate={{ y: '0%' }}
      transition={{ duration: 0.8, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="about-section"
    >
      <SEO
        title="About"
        name="About Page"
        description="About page for Github Profile App."
        type="Info Page"
      />
      <Navigation />
      <h1>About Page</h1>
      <p>
        This app displays the user's Github profile information. This was
        achieved by fetching data containing the details of the user's Github
        account from the Github API.
      </p>
      <p>
        The information displayed includes the user's repositories, the user's
        most recent activities, a snapshot of Github accounts following the user
        and accounts the user follows.
      </p>
      <p>
        Only information available publicly through the Github API was used.
      </p>
      <div className="link">
        <Link to="/">
          <button>Back Home</button>
        </Link>
      </div>
      <div className="about-image">
        <img
          src="https://github.githubassets.com/images/modules/site/home-campaign/astrocat.png?width=480&format=webpll"
          alt="astrocat"
        />
        <p>credit: www.github.com</p>
      </div>
    </m.section>
  );
};

export default About;
Enter fullscreen mode Exit fullscreen mode

The test page Test.js is for the error boundary test. It has a button that throws an error so the error boundary can be seen in action(more on the error boundary later).

import React, { useState } from 'react';
import { Navigation } from '../components';

export default function Test() {
  const [error, setError] = useState(false);

  if (error)
    throw new Error(
      'Oh! See! Should have warned you. This page tests the error boundary!'
    );

  return (
    <section>
      <Navigation />
      <div className="link">
        <h1>Hello!</h1>
        <button
          onClick={() => {
            setError(true);
          }}
        >
          Click Me!
        </button>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

The profile page Profile.js is the application's main page.

import React, { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { motion as m } from 'framer-motion';
import { Navigation, SEO, Loader, GetFetch, InputUser } from '../components';
import { ProfileView } from '../pages';
import { useEffect } from 'react';

const Profile = () => {
  const url = `https://api.github.com/users/${user}`;

  const { data, loading, error, fetchUsers } = GetFetch(url);

  useEffect (() => {fetchUsers()},[user])

  if (loading) {
    return <Loader />;
  }

  if (error) {
    return <h2>{`Fetch error: ${error.message}`}</h2>;
  }

  return (
    <m.section
      initial={{ x: '-50%' }}
      animate={{ x: '0%' }}
      transition={{ duration: 1, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="profile-section"
    >
      <SEO
        title="Profile"
        name="Profile Page"
        description="Github profile information is displayed here using the Github API"
        type="App"
      />
      <Navigation />
      <ProfileView data={data} />
      <Outlet context={data} />
    </m.section>
  );
};

export default Profile;
Enter fullscreen mode Exit fullscreen mode

The profile page receives data fetched by a GetFetch.js component. This component uses an async function fetchUsers that is capable of accepting a url parameter to retrieve data from the Github users' endpoint.
The GetFetch.js component can be seen below:

import { useState, useEffect } from 'react';

const GetFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchUsers = async () => {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
      setError(null);
    } catch (err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  return { data, loading, error, fetchUsers };
};
Enter fullscreen mode Exit fullscreen mode

To improve readability, the data retrieved in the profile page is passed as a data prop to a ProfileView.js component.

We have also decided that this app should be able to show not just the user's general information, but also the user's repositories, a single repository, events in the last 30 days, the accounts following the user and the account the user follows. We are creating "sub-pages" for these. The "sub-pages" are created as nested routes in the app that can be accessed as outlets in the profile page.
Data is passed along to these outlets in the form of outlet context(specifically so we can get the total number of items necessary for paginating these sub-pages).
So the App.js file should look like this:

import React from 'react';
import { Route, Routes } from 'react-router-dom';
import {
  Home,
  About,
  NotFound,
  Test,
  Profile,
  Repo,
  Activity,
  Following,
  Followers,
  SingleRepo,
} from './pages';
import './index.css';

export default function App() {
   return (
    <section>
        <Route path="/" element={<Home />} />
        <Route exact path="/user" element={<Profile />}>
          <Route exact path="/user/repo" element={<Repo />}>
            <Route exact path=":repoid" element={<SingleRepo />} />
          </Route>
          <Route exact path="/user/activity" element={<Activity />} />
          <Route exact path="/user/following" element={<Following />} />
          <Route exact path="/user/followers" element={<Followers />} />
        </Route>
        <Route path="about" element={<About />} />
        <Route path="test" element={<Test />} />
        <Route path="*" element={<NotFound />} />
    </section>
  );
}

Enter fullscreen mode Exit fullscreen mode

The ProfileView.js component:

import React from 'react';
import { NavLink } from 'react-router-dom';
import { TfiNewWindow } from 'react-icons/tfi';
import { FaMapMarkerAlt } from 'react-icons/fa';

const ProfileView = ({ data }) => {
  return (
    <article className="profile-container">
      <h2 className="profile-title">Profile</h2>
      <div className="profile">
        <div className="profile-image-container">
          <img src={data.avatar_url} alt={data.name} />
          <div>
            <span className="icon-container">
              <FaMapMarkerAlt />
            </span>{' '}
            {data.location}
          </div>
        </div>
        <div className="profile-text">
          <h3>{data.name}</h3>
          <p>
            <span>Login name:</span> {data.login}
          </p>
          <p>
            <span>Bio:</span> {data.bio}
          </p>
          <p>
            <span>Email:</span> {data.email}
          </p>
          <p>
            <span>Twitter: </span> {data.twitter_username}
          </p>
          <p>
            <span>Joined:</span> {new Date(data.created_at).toLocaleString()}
          </p>
          <p>
            <span>Public Repos:</span> {data.public_repos} &#160;&#160;{' '}
            <span>Last Update:</span>{' '}
            {new Date(data.updated_at).toLocaleString()}
          </p>
          <p>
            <span>Followers:</span> {data.followers} &#160;&#160;{' '}
            <span>Following:</span> {data.following}
          </p>
          <p className="external-link">
            <a href={data.html_url} target="_blank" rel="noreferrer">
              View on Github <TfiNewWindow fill="rgb(145, 145, 145)" />
            </a>
          </p>
        </div>
      </div>
      <h3 className="link-to-outlet">
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/repo"
        >
          Repositories
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/activity"
        >
          Activity
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/following"
        >
          Following
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/followers"
        >
          Followers
        </NavLink>
      </h3>
    </article>
  );
};

export default ProfileView;
Enter fullscreen mode Exit fullscreen mode

Okay, so we have our nested routes. Here is where we run into a bit of a problem though. The Github rest API can only return a maximum of 30 pages of data. To solve this, we create a new GetFetchPages component to create a concatenated list of fetched data using an async function fetchPages:

const GetFetchPages = (baseUrl, totalNum, page, per_page) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const startIndex = page * per_page - per_page;
  const totalPages = Math.ceil(totalNum / per_page);

  const fetchPages = async (page) => {
    const fetchPromise = [];
    try {
      for (page = 1; page < totalPages + 1; page++) {
        const response = await fetch(
          `${baseUrl}?page=${page}&per_page=${per_page}`
        );
        if (!response.ok) {
          throw new Error(`HTTP Error: ${response.status}`);
        }
        fetchPromise.push(response);
      }
      const responses = await Promise.all(fetchPromise);
      const results = await Promise.all(
        responses.map((response) => response.json())
      );
      let dataList = [];
      results.forEach((result) => {
        dataList = dataList.concat(result);
      });
      setData(dataList);
      setError(null);
    } catch (err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPages();
    // eslint-disable-next-line
  }, []);

  return { data, loading, error, startIndex, page, per_page, totalPages };
};
Enter fullscreen mode Exit fullscreen mode

The data from the outlet context and that of the GetFetchPages is used to paginate the sub-pages like in Followers.js:

import React, { useState } from 'react';
import { useOutletContext } from 'react-router-dom';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { Loader, GetFetchPages } from '../components';

const Followers = () => {
  const user = useOutletContext();

  const [page, setPage] = useState(1);

  const totalNum = user.followers;
  const per_page = 10;


  const baseUrl = user.followers_url;

  const { data, loading, error, startIndex, totalPages } = GetFetchPages(
    baseUrl,
    totalNum,
    page,
    per_page
  );

  if (loading) {
    return <Loader />;
  }

  if (error) {
    return <h2>{`Fetch error: ${error.message}`}</h2>;
  }

  return (
    <>
    <div className="list follow">
      {data?.slice(startIndex, startIndex + per_page).map((followers) => {
        return (
          <article className="followers" key={followers.id}>
            <div>
              <div>
                <img src={followers.avatar_url} alt={followers.login} />
              </div>
              <p>
                <span>Login name:</span>{' '}
                <a href={followers.html_url}>{followers.login}</a>
              </p>
              <p className="external-link">
                <a href={followers.html_url} target="_blank"  rel="noreferrer">
                  View Profile
                </a>
              </p>
            </div>
          </article>
        );
      })}
    </div>
    <div className="page-nav">
        <p>
          Pages: {page} of {totalPages}{' '}
        </p>
        <button 
          onClick={() => setPage((prev) => Math.max(prev - 1, 0))}
          disabled={page <= 1}
          aria-disabled={page <= 1}
        >
          <FaChevronLeft />
        </button>
        {Array.from({ length: totalPages }, (value, index) => index + 1).map(
          (each, index) => (
            <button key={index} onClick={() => setPage(each)} id={each === page && "current"}>
              {each}
            </button>
          )
        )}
        <button
          onClick={() => setPage((prev) => prev + 1)}
          disabled={page >= totalPages}
          aria-disabled={page >= totalPages}
        >
          <FaChevronRight />
        </button>
      </div>
      </>
  );
};

export default Followers;
Enter fullscreen mode Exit fullscreen mode

The error boundary is implemented as a class component in ErrorBoundary.js:

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: '', errorInfo: '', hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '20px' }}>
          <h1>Some error don happen.</h1>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <p>{this.state.error && this.state.error.toString()}</p>
            <pre>{this.state.errorInfo.componentStack}</pre>
            {console.log(
              `Some error don happen: ${this.state.errorInfo.componentStack}`
            )}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Enter fullscreen mode Exit fullscreen mode

The app is wrapped with the SEO(HelmetProvider from react-helmet-async) and error-boundary components in the index.jsfile:

import React, { Suspense, StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import ErrorBoundary from "./components/ErrorBoundary";
import { HelmetProvider } from "react-helmet-async";
import { Loader } from "./components";
import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <Router>
    <StrictMode>
      <ErrorBoundary>
        <HelmetProvider>
          <Suspense fallback={<Loader />}>
            <App />
          </Suspense>
        </HelmetProvider>
      </ErrorBoundary>
    </StrictMode>
  </Router>
);

Enter fullscreen mode Exit fullscreen mode

Wrapping Up

All features should work perfectly once implemented in the other subpages.

By the way, this was my first ever write-up and this app was part of an exam conducted by the AltSchool Africa School of Software Engineering. I would appreciate all comments and suggestions for improvement.

The code to my final solution, where I try to display each repo as a modal and implement a user input to change the app's user can be seen here.
You can also check out the three other questions and how I attempted to solve them here.

💖 💪 🙅 🚩
falanatolu
Falana Tolulope

Posted on January 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related