Building a simple portfolio using React and GitHub API
Portfolios are essential for developers to display their work and projects. In most recruitment processes, having a portfolio sets candidates apart and gives them an edge.
In this article, I will walk you through building a portfolio with Javascript using the React framework and GitHub API. Below is a demo of our portfolio
Let's get started!
Requirement
Basic knowledge of Javascript
Recent version of Nodejs
Aim of this article
The aim of this article is to help you to consolidate your knowledge on react principles by learning
How to Create a Loading and Landing page,
Use the React Router to navigate through pages
Display a List of our Projects through Pagination,
Implement Errorboundary and test
Implement SEO.
Getting Started
To set up your work area you'll need to create react app and install special libraries required for this project which are the React Router Dom and React Helmet Async. Open up your preferred command line, go to the directory of your choice and type the command below in your root directory to set up your work area.
npx create-react-app .
npm start
To install the React Router Dom and the React Helmet Async, run these commands in your project terminal.
npm install react-router-dom
npm install react-helmet-async
Creating the Portfolio
Step 1 —> Creating our Loading and Landing page
This article focuses on the logic and the CSS can be reviewed as the source code will be attached to the end of this blog. Having styled your landing page to your taste, as a placeholder for your data the Loading function can be used by setting a loading state to false
import {useState} from react;
const [loading, setLoading ] = useState(true);
const [repositories, setRepositories] = useState([]);
We can now manipulate our landing page by writing an if statement, see the snippet of code below
const Home = () => {
if (loading) {
return (
<article className="intro">
<h2>Loading...</h2>
</article>
);
}
return (
<main className="into">
<h2>Peace's Github</h2>
</main>
);
};
Step 2 —> Use the React Router to navigate through pages
Each Navbar button created for navigating view components on our landing pages uses the react-router to achieve this. To use the router-dom
Import the Browser Router and wrap it over your app
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Import the Routes, and Route components. Next, use the Route component to specify a path.
import { Routes, Route } from "react-router-dom";
import Navbar from "./Components/Navbar";
import { Home, Repositories, SingleRepository, ErrorPage, TestErrorBoundary } from "./Pages";
function App() {
return (
<section>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="Repositories" element={<Repositories />} />
<Route
path="Repositories/SingleRepositories/:id"
element={<SingleRepository />}
/>
<Route path="*" element={<ErrorPage />} />
<Route path="/testerrorboundary" element={<TestErrorBoundary />}></Route>
</Routes>
</section>
);
}
Import and attach the Navlinks to the routes to HTML elements for easy styling.
import { NavLink } from 'react-router-dom';
const Navbar = () => {
return (
<nav id="nav">
<div className="nav">
<div className="nav-links">
<ul className="links">
<li>
<NavLink to="/" className={({ isActive }) => isActive ? "scrollLink active" : "scrollLink"}
> Home
</NavLink>
</li>
<li>
<NavLink to="/Repositories" className={({ isActive }) =>
isActive ? "scrollLink active" : "scrollLink"}
>Repositories
</NavLink>
</li>
<li>
<NavLink to="/testerrorboundary" className={({ isActive }) =>
isActive ? "scrollLink active" : "scrollLink"}
> TestError
</NavLink>
</li>
<li>
<NavLink to="/error" className={({ isActive }) =>
isActive ? "scrollLink active" : "scrollLink"}
>TestErrorPage
</NavLink>
</li>
</ul>
</div>
Step 3 —> Display a List of our Projects through Pagination
Our portfolio should have the following functionalities:
Fetch the Data
Paginate
Navigate to the next page
Navigate to the previous page
Handle page
Fetch the Data
Firstly, we will Fetch our data by creating a custom hook useFetch
on a file named useFetch.js
with the async
and await
promises as displayed below:
import { useState, useEffect } from "react";
//Fetching data
const useFetch = (url) => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState([]);
const getData = async () => {
try {
const response = await fetch(url);
const rawData = await response.json();
setData(rawData);
setLoading(false);
} catch (error) {
console.log("not loaded")
}
};
useEffect(() => {
getData();
}, []);
return { loading, data };
};
export default useFetch
Paginate
Next, we will set up our initial states, the Apidata
stores a total rundown of all the repositories, while the repositories
stores each array of repositories sliced by the pagination function, this is represented in the code below:
const initialState = {
Apidata: [],
page: 0,
repositories: [],
paginatedRepositories: [],
};
The pagination function is needed to slice the data into a set amount of repositories for each page to fit the number of pages needed.
const paginate = (repos) => {
const itemPerPage = 10;
const numberOfPages = Math.ceil(repos.length / itemPerPage);
const newrepos = Array.from({ length: numberOfPages }, (_, index) => {
const start = index * itemPerPage;
return repos.slice(start, start + itemPerPage);
});
return newrepos;
};
The useFetch
custom hook is used to retrieve the list of repositories and basic GitHub information, the pagination function which takes in the parameter of the API as seen below is called to update the initial states created above through the use of reducers and context API.
import React, { useContext, useEffect, useReducer } from "react";
import useFetch from "./useFetch";
import reducer from "./Reducer";
import paginate from "./Paginate";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { loading, data } = useFetch("<https://api.github.com/users/peaceOffiong>");
const { loading: loading2, data: data2 } = useFetch(
"<https://api.github.com/users/PeaceOffiong/repos>");
useEffect(() => {
if (loading) {
return
} else if(!loading) {
dispatch({ type: "SET_API_DATA", payload: data });
dispatch({ type: "SET_LOADING" });
}
if (!loading2) {
dispatch({ type: "SET_REPOSITORIES", payload: paginate(data2) });
dispatch({ type: "SET_PAGINATED_REPOSITORY", payload: paginate(data2) });
}
}, [loading, state.page, loading2]);
return (
<AppContext.Provider
value={{
...state,
}}
>{children}</AppContext.Provider>
);
};
The state page
is used to determine the index of the repositories
on display, on default the page is set to ‘0’ signifying the first value in an array.
const Reducer = (state, action) => {
switch (action.type) {
case "SET_PAGINATED_REPOSITORY":
return { ...state, paginatedRepositories: action.payload[state.page] };
}
throw new Error("no matching type");
}
export default Reducer
Handle Page
The number of arrays in the state repositories
can be used to determine the number of paginated pages through the index.
{repositories.map((each, index) => {
return (
<button
key={index}
className={`page-btn ${index === page ? "active-btn" : null}`}
onClick={() => handlePage(index)}
>
{index}
</button>
);
})}
The event listener added to each button above performs the function of listening to clicks on each of the buttons and updates the state page
which displays the next array, this is performed by the function handlepage
const handlePage = (index) => {
dispatch({ type: "SET_PAGE", payload: index });
};
We need to add a click Eventlisteners to the next page button and run the function nextpage as well as the prev page button shown on the snippet below.
const nextPage = () => {
const setNextPage = (page) => {
let nextPage = page + 1;
if (nextPage > state.repositories.length - 1) {
nextPage = 0;
}
return nextPage;
}
dispatch({ type: "SET_PAGE", payload: setNextPage(state.page) });
};
const prevPage = () => {
const setPrevPage = (page) => {
let prevPage = page - 1;
if (prevPage < 0) {
prevPage = state.repositories.length - 1;
}
return prevPage
}
dispatch({type: "SET_PAGE", payload: setPrevPage(state.page)})
};
Step 4 —> Use Errorboundary
The error boundary is implemented to catch errors on any child component and have a fallback UI. The Error boundary is written with the class component as seen below
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.log(error);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong!.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
The error boundary needs to wrap the components you want to catch an error in, the standard practice is that it wraps the entire app but for our portfolio, we want to test the error boundary then we will limit it to the component we throw an error in
import React from 'react';
import { ErrorBoundary, Welcome, TestButton } from "../Components";
const TestErrorBoundary = () => {
return (
<div>
<ErrorBoundary>
<Welcome />
<TestButton/>
</ErrorBoundary>
</div>
)
}
export default TestErrorBoundary
let's throw an error in our test button component
const TestButton = () => {
throw new Error("yep there is an error");
return (
<button>TestButton</button>
)
}
export default TestButton
Remember to test our error boundary we have a button on our navbar for this purpose and routes to a new page.
Step 5 —> Implement SEO.
To use our react-helmet async firstly we’ll import the helmet provider component and wrap it around our root component app
import { HelmetProvider } from "react-helmet-async";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<HelmetProvider>
<App />
</HelmetProvider>
);
On each of our pages or page of our choice, we’ll import the helmet and manipulate our metadata
import { Helmet } from "react-helmet-async";
import image from "../Image/CNEM4759.JPG"
const Home = () => {
<main className="into">
<Helmet>
<title>Peace's Github Portfolio</title>
<meta
name="description"
content="A responsive page displaying Peace's Github and repositories"
/>
<link rel="icon" type="image/png" href="favicon.ico" sizes="16x16" />
</Helmet>
<h2>Peace's Github</h2>
</main>
}
Conclusion
Congratulations you’ve successfully created a simple functional portfolio if you made it to this point. I hope you enjoyed building this project as much as I did. You can go ahead and host your portfolio on any hosting platform of your choice.
Here is the link to
Source code: https://github.com/PeaceOffiong/GithubApi-Alt-school-Exams
Hosted Portfolio: https://github-api-alt-school-exams.vercel.app/
See you in the next blog 👋🏽