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:
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;
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;
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>
);
}
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;
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 };
};
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>
);
}
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}   {' '}
<span>Last Update:</span>{' '}
{new Date(data.updated_at).toLocaleString()}
</p>
<p>
<span>Followers:</span> {data.followers}   {' '}
<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;
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 };
};
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;
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;
The app is wrapped with the SEO(HelmetProvider
from react-helmet-async
) and error-boundary components in the index.js
file:
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>
);
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.
Top comments (5)
Awesome work!
great project👍
Welcome to DEV and congrats to your first post!
Nice article. Would you link to the school as well, please?
It's altschoolafrica.com/
Good example