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>
)
}
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>
I created a components folder in src to carry the component created in this project.
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 useEffect
with 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;
GitHub API Fetch (Repos Page)
The next component was the repos
component which will show the list of all my GitHub repos. Similar to the home page, useState
was used to handle the dynamic data, and useEffect
was 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;
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 pageNum
variable 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 pageNum
array. To render each page number individually, a list of the pageNum
array 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;
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 currentRepos
which uses .slice()
to store the repos needed for each page. The variable currentRepos
replaces the repos
array to be looped over to create a list of repos with the specific data needed. A paginate function was created in repos with accepts num
as a parameter and sets the currentPage
to 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;
Viewing Data From A Single Repo
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.js
file, 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 />} />
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;
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 errorboundary
component.
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;
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;
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;
}
}
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;
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>
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)