Building a simple portfolio using React and GitHub API

·

8 min read

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

Aim of this article

The aim of this article is to help you to consolidate your knowledge on react principles by learning

  1. How to Create a Loading and Landing page,

  2. Use the React Router to navigate through pages

  3. Display a List of our Projects through Pagination,

  4. Implement Errorboundary and test

  5. 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:

  1. Fetch the Data

  2. Paginate

  3. Navigate to the next page

  4. Navigate to the previous page

  5. 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 👋🏽