How to build a Github Portfolio using Github API, React JS and TailwindCSS

How to build a Github Portfolio using Github API, React JS and TailwindCSS

React JS has made creating user interfaces easier and faster, so I built a GitHub portfolio web page (a second-semester project for Frontend devs by AltSchool ) using React JS. In this article, we'll learn how to build a GitHub portfolio website featuring your profile, a list of your repositories, brief details of each repository, implementing a search functionality for GitHub users, and using TailwindCSS to beautify the web page.

Prerequisites

  • GitHub account

  • Web Browser

  • Code Editor (Visual Studio Code, Sublime Text... etc)

  • Knowledge of JavaScript, TailwindCSS

  • Basic knowledge of React JS

  • A glass of water 😜

Initializing a react app

  • Run the command on your terminal to initialize a new react app
npx create-react-app project-name
  • change into your project directory

      cd project-name
    

    Install TailwindCSS

  • Install tailwindcss via npm, and then run the init command to generate your tailwind.config.js file.

      npm install -D tailwindcss
      npx tailwindcss init
    

    Configure your template paths

  • Add the paths to all of your template files in your tailwind.config.js file.

      /** @type {import('tailwindcss').Config} */
      module.exports = {
        content: [
          "./src/**/*.{js,jsx,ts,tsx}",
        ],
        theme: {
          extend: {},
        },
        plugins: [],
      }
    

    Add the tailwind directives to your CSS

  • Add the @tailwind directives for each of Tailwind’s layers to your ./src/index.css file.

      @tailwind base;
      @tailwind components;
      @tailwind utilities;
    
  • Run your build process with npm run start.

      npm run start
    

Installation of Project Dependencies

Run the following commands on your terminals to install the necessary dependencies for this project

  • react-router-dom: A lightweight, fully-featured routing library for the React JavaScript library.

      npm install react-router-dom
      or
      yarn add react-router-dom
    
  • react-error-boundary: React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.

      npm install react-error-boundary
      or
      yarn add react-error-boundary
    

Folder Structure

The nested routes are stored in the pages folder while the components are stored in the components folder

index.js

Here, we'll be importing the 'ErrorBoundary' component from the react-error-boundary package that we installed earlier. After which, we simply wrap our <App /> component with the ErrorBoundary component and pass in our ErrorFallback component to the Fallback prop so that if there’s an error (that can be caught and handled by react-error-boundary), our fallback component will be rendered; otherwise, our component will be rendered.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { ErrorBoundary } from "react-error-boundary";
import App from "./App";
import ErrorFallback from "./pages/ErrorFallback";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <ErrorBoundary fallback={<ErrorFallback />}>
      <App />
    </ErrorBoundary>
  </React.StrictMode>
);

[clears throat] That should be about that. Now, on to the next

Getting Data from GitHub API

Declaring state variables with the useState hook

  • Firstly, in our App.js we'll declare state variables for storing our data from GitHub.

  • Here, we declare the user state variable with the useState hook to store the GitHub username of the user (your GitHub username).

  • We'll also create a state variable for the data we're getting from GitHub API (Here, I named it 'items') which we'll set as an empty array as you can see from the image below.

  • After that, we'll declare a loading state variable with a 'true' value to control the page displayed (this will help us display the loading page at first, then display the repository page when data is gotten from GitHub using ternary operators).

      const [user, setUser] = useState("Jay035");
      const [items, setItems] = useState([]);
      const [loading, setLoading] = useState(true);
    

Fetch function

Here, we'll write an async/await function wrapped in a useEffect hook with an empty array as its dependency(to cause the function to run only once when the page loads) where we'll make an API call to GitHub and update the state variables with the data gotten.

useEffect(() => {
    const fetchRepos = async () => {
      const res = await fetch(
        `https://api.github.com/users/${user}/repos?page=1&per_page=30&sort=updated`
      );
      const data = await res.json();
      setItems(data);
      setLoading(false);
    };

    fetchRepos();
  }, []);

Routing

Let's import the necessary components from react-router-dom in our App.js

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

Now, wrap your JSX code with the <Router> component. Here, I added the <main> tag which serves as my container for the web page and added some stylings (TailwindCSS style😊); also pass the state variables we declared earlier as props to the <Repo /> and <FullRepoDetails /> components as shown below

import { useState, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Loading from "./pages/Loading";
import Repo from "./pages/Repo";
import FullRepoDetails from "./pages/FullRepoDetails";
import Error404Page from "./pages/Error404Page";

function App() {
  const [user, setUser] = useState("Jay035");
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchRepos = async () => {
      const res = await fetch(
        `https://api.github.com/users/${user}/repos?page=1&per_page=30&sort=updated`
      );
      const data = await res.json();
      setItems(data);
      setLoading(false);
    };

    fetchRepos();
  }, []);

  return (
    <Router>
      <main
        data-aos="fade-in"
        className="font-Barlefair bg-[#1a1c1e] text-[#e7e8e8] h-screen mb-[50px] overflow-x-hidden leading-normal pt-8 px-4"
      >
        <Routes>
          {/* Home page */}
          <Route
            exact
            path="/"
            element={loading ? <Loading /> : <Repo items={items} user={user} />}
          />

          {/* Full repo details */}
          <Route
            path="/repo/:name"
            element={<FullRepoDetails items={items} />}
          />

          {/* when a user goes to a non-existent route */}
          <Route path="*" element={<Error404Page />} />
        </Routes>

      </main>
    </Router>
  );
}

export default App;

Components and Pages

<Repo />

Let's declare some variables that will determine the number of repositories to display at once on the screen using the slice method(See code below)

  • pageNumber: current page we're viewing.

  • repoPerPage: number of repositories you want to be displayed on the screen.

  • pageCount: used to store the number of pages we've viewed.

import { useState } from "react";
import RepoList from "../components/RepoList";
import Footer from "../components/Footer";

export default function Repo({items, user}) {
  const [pageNumber, setPageNumber] = useState(0);
  const repoPerPage = 6;
  const pagesVisited = pageNumber * repoPerPage;
  const pageCount = Math.ceil(items.length / repoPerPage);

  return (
    <>
      <h1 className="text-2xl text-white text-center font-bold mb-4">
        {user}'s GitHub Repositories
      </h1>
      <div className="grid gap-8 my-10 lg:grid-cols-2 xl:grid-cols-3 xl:px-6">
        {items.slice(pagesVisited, pagesVisited + repoPerPage).map((item) => (
          <RepoList key={item.id} {...item} />
        ))}
      </div>

    <Footer />
    </>
  )
}

<RepoList />

import { Link } from "react-router-dom";

export default function RepoList(props) {
  return (
    <section data-aos="fade-in" className="bg-[#282a2e] w-full max-w-[600px] mx-auto p-4 rounded-lg text-base flex flex-col gap-4">
      <div className="flex gap-6 items-center">
        <h1 className="text-xl font-semibold capitalize">{props.name}</h1>
        {props.private ? (
          <p className="bg-[#f46674] text-white text-sm px-2 py-1 w-fit rounded-lg">
            Private
          </p>
        ) : (
          <p className="bg-[#2bc070] text-white text-sm px-2 py-1 w-fit rounded-lg">
            Public
          </p>
        )}
      </div>
{/* Here we'll use the repository name as a parameter to filter out the specific repository we're viewing */}
      <Link to={`/repo/${props.name}`} className="bg-[#4e5051] p-2 w-fit rounded-lg shadow text-sm hover:bg-[#4e5051]/50">View more</Link>
    </section>
  );
}

<Profile />

  • This component shows brief details about (profile of) the GitHub user.

      import { useState, useEffect } from "react";
    
      export default function Profile({ user }) {
        const [data, setData] = useState([]);
    
        useEffect(() => {
          const fetchRepos = async () => {
            const res = await fetch(`https://api.github.com/users/${user}`);
            const data = await res.json();
            setData(data);
          };
    
          fetchRepos();
        }, []);
    
        return (
          <section className="text-[#c9d1d9] flex flex-wrap gap-8 mt-12 md:flex-nowrap lg:flex-wrap lg:flex-col lg:items-start lg:h-fit ">
            <img
              className="w-28 lg:w-48 lg:h-48 shadow-md rounded-full"
              src={data?.avatar_url}
              alt="profile img"
            />
            <div className="">
              <h1 className="flex flex-col">
                <span className="text-2xl font-semibold">{data?.name}</span>
                <span className="text-[#8b949e] text-xl">{data?.login}</span>
              </h1>
              <p className="mt-4 ">{data?.bio}</p>
            </div>
            <div className="flex items-center gap-2">
              <svg
                fill="#8b949e"
                text="muted"
                aria-hidden="true"
                height="16"
                viewBox="0 0 16 16"
                version="1.1"
                width="16"
                data-view-component="true"
              >
                <path
                  fill-rule="evenodd"
                  d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"
                ></path>
              </svg>
              <p>
                {data?.followers} <span className="text-[#8b949e]">followers</span> .{" "}
                {data?.following} <span className="text-[#8b949e]"> following</span>
              </p>
            </div>
    
            <ul>
              {/* user's location */}
              <li className="flex items-center gap-2">
                {/* location icon */}
                <svg
                  fill="#8b949e"
                  viewBox="0 0 16 16"
                  version="1.1"
                  width="16"
                  height="16"
                  aria-hidden="true"
                >
                  <path
                    fillRule="evenodd"
                    d="M11.536 3.464a5 5 0 010 7.072L8 14.07l-3.536-3.535a5 5 0 117.072-7.072v.001zm1.06 8.132a6.5 6.5 0 10-9.192 0l3.535 3.536a1.5 1.5 0 002.122 0l3.535-3.536zM8 9a2 2 0 100-4 2 2 0 000 4z"
                  ></path>
                </svg>
                <span>{data?.location}</span>
              </li>
              {/* user's website / blog */}
              <li className="flex items-center gap-2">
                {/* website icon */}
                <svg
                  fill="#8b949e"
                  aria-hidden="true"
                  height="16"
                  viewBox="0 0 16 16"
                  version="1.1"
                  width="16"
                  data-view-component="true"
                >
                  <path
                    fillRule="evenodd"
                    d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"
                  ></path>
                </svg>
                <span>
                  <a href={data.blog}>{data?.blog}</a>
                </span>
              </li>
    
              {/* user's twitter */}
              <li className="flex items-center gap-2">
                {/* twitter icon */}
                <svg
                  className="w-4 h-4"
                  fill="#8b949e"
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 273.5 222.3"
                  role="img"
                  aria-labelledby="pdyhj9d0k4mznuzb3vdf4zjdz7tf88u"
                >
                  <title id="pdyhj9d0k4mznuzb3vdf4zjdz7tf88u">Twitter</title>
                  <path
                    d="M273.5 26.3a109.77 109.77 0 0 1-32.2 8.8 56.07 56.07 0 0 0 24.7-31 113.39 113.39 0 0 1-35.7 13.6 56.1 56.1 0 0 0-97 38.4 54 54 0 0 0 1.5 12.8A159.68 159.68 0 0 1 19.1 10.3a56.12 56.12 0 0 0 17.4 74.9 56.06 56.06 0 0 1-25.4-7v.7a56.11 56.11 0 0 0 45 55 55.65 55.65 0 0 1-14.8 2 62.39 62.39 0 0 1-10.6-1 56.24 56.24 0 0 0 52.4 39 112.87 112.87 0 0 1-69.7 24 119 119 0 0 1-13.4-.8 158.83 158.83 0 0 0 86 25.2c103.2 0 159.6-85.5 159.6-159.6 0-2.4-.1-4.9-.2-7.3a114.25 114.25 0 0 0 28.1-29.1"
                    fill="currentColor"
                  ></path>
                </svg>
                <a href={`https://twitter.com/${data.twitter_username}`}>
                  {data/.twitter_username}
                </a>
              </li>
            </ul>
          </section>
        );
      }
    

    <Loading />

    This is the page that will be rendered while the data is being gotten from the GitHub API. Here I implemented a simple spin animation using TailwindCSS [See code below]

    
      export default function Loading() {
        return (
          <div className="flex justify-center items-center h-screen">
            <div className="border-[#eee] border-2 border-dashed w-24 h-24 animate-spin rounded-full"></div>
          </div>
        )
      }
    

    <FullRepoDetails />

    This page shows more details about each repository. Here, we're getting the name of the specific repository the user is trying to view more of using the useParams hook. After which, we'll run a filter check using the filter method to get the repository with the same name value as the 'name' we're getting using the useParams hook data and mapping through...getting the object values we need.

    PS: You'll notice we used the ternary operator and the && statement in some parts of the code. This is to only show the respective data only if they have value or are public/private (eg: show if the repository is public or private, display the data only if it isn't null/undefined).

      import { Link, useParams } from "react-router-dom";
      import Footer from "../components/Footer";
      import Loading from "./Loading";
    
      export default function FullRepoDetails({ items, loading }) {
        const { name } = useParams();
    
        return (
          <>
            <Link
              to="/"
              className="flex items-center p-3 rounded text-lg transition-colors outline-none text-white bg-[#2bc070] w-fit hover:bg-[#2bc070]/50"
            >
              <i className="ri-arrow-left-s-line"></i> Go back
            </Link>
            <div className="flex flex-col justify-center items-center h-[80vh]">
              {items
                ?.filter((item) => item.name === name)
                .map((repo, index) => (
                  <section
                    key={index}
                    className="bg-[#282a2e] w-full max-w-[700px] mx-auto p-4 rounded-lg text-base flex flex-col gap-6"
                  >
                    <article className="flex justify-start gap-4">
                      <img
                        className="w-24 h-24 rounded-full shadow"
                        src={repo.owner.avatar_url}
                        alt={repo.owner.login}
                      />
                      <div className="flex flex-col gap-2 ">
                        <h1 className="font-bold text-lg">{repo.owner.login}</h1>
                        <p className="text-sm capitalize">{repo.name}</p>
                        {repo.private ? (
                          <p className="bg-[#f46674] text-white text-sm px-2 py-1 w-fit rounded-lg">
                            Private
                          </p>
                        ) : (
                          <p className="bg-[#2bc070] text-white text-sm px-2 py-1 w-fit rounded-lg">
                            Public
                          </p>
                        )}
                      </div>
                    </article>
                    <div className="">
                      <p>
                        This repository was created on{" "}
                        {new Date(repo.created_at).toLocaleDateString()} by{" "}
                        {repo.owner.login}
                      </p>
                    </div>
    
                    <div className="flex justify-between items-center">
                      <div className="flex flex-col gap-1">
                        <a href={repo.html_url} className="underline cursor-pointer">
                          View Repo
                        </a>
                        <a href={repo.homepage} className="underline cursor-pointer">
                          View Site
                        </a>
                      </div>
                      <ul>
                        <li>{repo.stargazers_count} stars</li>
                        <li>{repo.watchers_count.toLocaleString()} watchers</li>
                      </ul>
                    </div>
    
                    <div className="flex flex-wrap justify-between items-center gap-4">
                      {/* languages */}
                      {repo.language && (
                        <p className="bg-[#4e5051] p-2 inline-block rounded-lg shadow text-xs">
                          {repo.language}
                        </p>
                      )}
                      {repo.topics && (
                        <ul className="flex justify-between items-center gap-2">
                          {repo.topics.map((topic, index) => (
                            <li
                              key={index}
                              className="bg-[#4e5051] p-2 inline-block rounded-lg shadow text-xs"
                            >
                              {topic}
                            </li>
                          ))}
                        </ul>
                      )}
    
                      {repo.open_issues !== 0 && (
                        <p className="text-xs">{repo.open_issues} issues</p>
                      )}
                    </div>
                  </section>
                ))}
            </div>
            <Footer />
          </>
        );
      }
    

    <Error404 /> : This page will be rendered when the user goes to a non-existent route.

      import { Link } from "react-router-dom";
    
      export default function Error404Page() {
        return (
          <div className="mx-auto max-w-[600px] w-full leading-7 flex flex-col gap-6 justify-center min-h-screen">
            <h1 className="text-4xl font-bold">404</h1>
            <h2 className="text-2xl font-bold">Oops!!!....PAGE NOT FOUND</h2>
            <Link
              to="/"
              className="underline uppercase px-7 py-2 bg-[#4e5051] w-fit rounded-lg"
            >
              Go back
            </Link>
          </div>
        );
      }
    

    <ErrorFallback />

    This page renders when an error occurs. Two props are passed:

    • The resetErrorBoundary prop resets the web page after an error has occurred.

    • an error prop that carries the error information.

```javascript
export default function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert" className=''>
      <h1>An error occured</h1>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}
```

Pagination

We'll be using react-paginate for the pagination functionality of the list of repositories.

  • install the library
npm install react-paginate
or
yarn add react-paginate
  • import into your Repo.jsx

      import ReactPaginate from "react-paginate";
    
  • add this function in Repo.jsx . Here, we're declaring a changePage function, which we'll be passing to the onPageChange property of <ReactPaginate /> . This function sets the pageNumber to the selected page value.

      // change page function
      const changePage = ({ selected }) => {
          setPageNumber(selected);
      };
    
    • add the ReactPaginate component with the necessary props at the end of your code in Repo.jsx

        <ReactPaginate
           breakLabel="..."
           pageRangeDisplayed={1}
           renderOnZeroPageCount={null}
           previousLabel={"Previous"}
           nextLabel={"Next"}
           pageCount={pageCount}
           onPageChange={changePage}
           containerClassName={"paginationBtns"}
           previousLinkClassName={"previousBtn"}
           nextLinkClassName={"nextBtn"}
           disabledClassName={"paginationDisabled"}
           activeClassName={"paginationActive"}
        />
      

      To understand more about the props we passed above, kindly refer to this.

Conclusion

And that's it!

I hope you’ve found this article useful and have learned how to build a GitHub portfolio with React JS and also get data from the GitHub API. Feel free to research more and also customize your styling using TailwindCSS or any CSS framework of your choice.

You can clone this project from this link; also find the API links below

GitHub profile API: api.github.com/users/${user}

GitHub repositories: api.github.com/users/${user}/repos

I’d also love to see what you build with this, so please feel free to share what you’ve built and let me know what you think about the tutorial in the comment section