Building a simple website that shows my GitHub Repositories (React + GitHub API)

Building a simple website that shows my GitHub Repositories (React + GitHub API)

Article on how I built my AltSchool second semester exam

Table of contents

No heading

No headings in the article.

Coming from just building apps with Vanilla Javascript, CSS and HTML, React looked like the perfect framework to learn as it simplifies things like DOM manipulation. React is a well-documented framework for JavaScript that eases the process of building web applications with a lot of extended and awesome features.

This article will explain the process of building a website that displays a user's GitHub repositories with:

  • React (useState, useEffect)

  • CSS

  • GitHub API

Note: Familiarity with HTML, CSS, React and JavaScript is required.

Run npm create react-app from your terminal or powershell and cd into the folder. Create a pages folder for all the respective pages on the website. The pages folder should be inside the src folder.

npm create react-app my-github-repo
cd my-github-repo

In the pages folder, create these files:

  • repos.jsx

  • homepage.jsx

  • error-404.jsx

  • errorbound.jsx

In the Components folder, create these files:

  • navbar.jsx

  • shared-navbar.jsx

  • repos-details.jsx

Note: Kegilka fontface was created because no CDN was hosting was found for the font. The font was implemented in the CSS class "kegilka".

@font-face {
  font-family: "Kegilka",;  
  src: local("Kegilka"),
    url("./Kegilka.otf") format("opentype");
}

.Kegilka {
  font-family: "Kegilka", 'Clash Display', sans-serif;
  font-size: 6.4rem;
  line-height: 5.9rem;
  font-weight:400;
}

Install the react-router-dom package to be able to utilize React router.

Install react-helmet-async package for SEO functionality on all pages. Import react-helmet-async in index.jsx file and wrap the App import inside it.

Install react-error-boundary to provide a safety cushion for when our code breaks. A page will also be created to test the error boundary.

npm i react-router-dom
npm i react-helmet-async
npm i react-error-boundary
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { HelmetProvider } from 'react-helmet-async'

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode> 
    <HelmetProvider>
      <App />
    </HelmetProvider>
    </React.StrictMode>
)

The error-fallback component will be used as the fallback component of the react-error-boundary which will display the error message instead of breaking our codes.

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

export const ErrorFallback = () => {
  return <div role="alert">
    <h1 className="Kegilka">Something went wrong!!! Why not go back home</h1>
    <a className="back-home" href="/" >Go Back home</a>
  </div>
}

In the errorbound component, we create a fake counter using useState with maxCount set to 2. Once count is greater than 2, it throws an error which will be gotten from the error-fallback component.

import {useState} from 'react';
import {useErrorHandler} from 'react-error-boundary'
import { Helmet } from 'react-helmet-async';

const maxCount = 2;

const ErrorBound = () => {

  const [count, setCount] = useState(0)
  const handleError = useErrorHandler()
  const handleClick = () => {
   try  {
     if (count === maxCount) {
      throw new Error('Count limit Exceeded')
    } 
    else {
      setCount((prev) => prev+1)
    }
  } 
    catch (e) {
      handleError(e)
    }
  }

  return (
    <>
      <Helmet>
        <title>ErrorBoundary Test Page</title>
        <meta
          name="description"
          content="This page is specifically for testing error boundary"
        />
        <link rel="canonical" href="/errorboundary" />
      </Helmet>
      <div className="error-bound">
        <h1 className="Kegilka error-head">{count} </h1>
        <button className="back-home error-link" onClick={handleClick} >Counter</button>
      </div>
    </> 
  )
}

export default ErrorBound

The error-boundary will then be implemented in the App.jsx file by wrapping the whole code in the App.jsx file inside the ErrorBoundary wrapper which takes two props, with ErrorFallback being the fallback component FallbackComponent={ErrorFallback} onError={errorHandler}.

In the Navbar component, import NavLink from 'react-router-dom'. NavLink gives us access to different states of the link which can be styled accordingly without adding extra classes.

import { NavLink } from 'react-router-dom';

export default function Navbar() {
  return (
    <nav className='navbar'>
      <div className="larmideh">Larmideh</div>
      <div>
        <NavLink className='nav-items' end to="/">Home</NavLink>
        <NavLink className='nav-items' to="/repos">Repos</NavLink>
        <NavLink className='nav-items nav-items3' to="/errorboundary">Error Test</NavLink>     
      </div>
    </nav>
  )
}

To share the Navbar across all pages, a SharedNavbar component is implemented wherein Outlet is imported from react-router-dom. Outlet shares whatever component or codes inside the function to all routes.

import { Outlet } from 'react-router-dom';
import Navbar from "./navbar.jsx";

export default function SharedNavbar() {
  return (
    <>
      <Navbar />
      <Outlet />
    </>
  )
}

In the App.jsx file, import all pages including the error-404 page and Setup react router and also the individual routes.

import './App.css'
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Repos from './pages/repos.jsx'
import Homepage from './pages/homepage.jsx'
import Error from './pages/error-404.jsx'
import SingleRepo from './pages/singlerepo.jsx'
import SharedNavbar from './components/shared-navbar.jsx'
import ErrorBound from './components/errorbound.jsx';
export default function App() {
  return (
    <main>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<SharedNavbar />} >
              <Route index element={<Homepage />} />
              <Route path="/errorboundary" element={<ErrorBound />} />
              <Route path="/repos" element={<Repos />} />
              <Route path="/repos/:repoId" element={<SingleRepo />} />
              <Route path="*" element={<Error />} />
            </Route>
          </Routes>
        </BrowserRouter>
    </main>
  )
}

Next is to write the codes for individual pages. Helmet is imported from 'react-helmet-async' in all pages. SEO descriptions are written inside the Helmet wrapper.

Homepage.jsx

The homepage is simple with just a navbar, title and contact details at the bottom.

import { Helmet } from 'react-helmet-async';

export default function Homepage () {
  return (
    <>
        <Helmet>
          <title>Homepage</title>
          <meta 
            name="description"
            content="This is the homepage for AltSchool second semester exam, Question 1 developed by Abdulhameed Busari"
            />
          <link rel="canonical" href="/" />
        </Helmet>
      <div className="homepage-container">
        <h1 className="Kegilka headline">a flashy ninja weaving codes</h1>
        <div className="flex">
          <div className="personal-details">
            <p>ABDULHAMEED BUSARI</p>
            <p>LARMIDEH@GMAIL.COM</p>
          </div>
          <div className="personal-details personal-details2">
            <p>FRONTEND DEVELOPER</p>
            <p>ALMOST PRACTICING ARCHITECT</p>
            <p>ALTSCHOOL STUDENT (CLASS 22')</p> 
          </div>
        </div>

    </div>
    </>
  )
}

Create a card component to display each GitHub repository inside the repo-details.jsx . The component accepts props which will eventually be the data gotten from the GitHub API. Import Link from 'react-router-dom' to link to the single repository page.

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

export default function RepoDetails (props) {
  return (
    <div className="repo-container">
      <div>
        <div className="repo-title">{props.title}</div>
        <div className="owner">{props.owner}</div>
      </div>
      <div className="index">0{props.index+1}</div>
      <Link to={`/repos/${props.id}`} className="links">more info</Link>  
    </div>
  )
}

In the Repos.jsx file, we will be implementing useState for state management of API data and pagination, useEffect for API call and GitHub API. First is to import useState and useEffect, then call the GitHub API inside the useEffect hook. The API endpoint returns an array of objects with each object containing details about a repository Create state for the initial repos data and also the isLoading boolean that shows the loading state. Implement pagination by firstly creating state for current page and the number of items per page (perPage).

Remember we passed in props inside the components, the props will get data from the API result. The API endpoint returns an array of objects with individual objects containin details of each repository. The data is stored in the repos state and a new array currentRepos is created to slice the data for each page. A new array reposMapped is created which returns the jsx format of the data from the currentRepos array.

Write the jsx codes and also the page buttons with disabled state implemented. In the jsx, a ternary condition is written first to ascertain the state has gotten the data from the useEffect hook when rendering. Initially, we had set the value of isLoading to true which then shows a spinning logo on the homepage whilst waiting from the return data from the API. Once the data is gotten, the other condition of the ternary is achieved and the HTML content is displayed proper.

import { useEffect, useState } from "react"
import RepoDetails from '../components/repo-details.jsx'
import { Helmet } from 'react-helmet-async';

export default function Repos() {
  const [repos, setRepos] = useState([]);
  const [currentPage] = useState(1);
  const [perPage] = useState(4);
  const [isLoading, setIsLoading] = useState(true);

  // Call Github API
  useEffect(() =>  {
    const url = "https://api.github.com/users/0xlarmideh/repos";
    const fetchUsers = async () => {
    const res = await fetch(url);
    const data = await res.json();      
    setRepos(data);
    setIsLoading(false);
    };
    fetchUsers();
  }, []);

  // Pagination
  // Get Current Repos
  const length = repos.length
  const indexOfLastRepo = currentPage * perPage;
  const indexOfFirstRepo = indexOfLastRepo - perPage;
  const currentRepos = repos.slice(indexOfFirstRepo, indexOfLastRepo)

   // Mapping Repo details
  const reposMapped = currentRepos.map(((item, index) => <RepoDetails key={item.id} title={item.name} index={index} owner={item.owner.login} id={item.name} 
  />))

  // Create page array
  const pageNumbersArr = [];
  let reposLength = Math.ceil(length/perPage)
  for(let i=1; i<=reposLength; i++) {
    pageNumbersArr.push(i)
  }

  // Map over Page Array and Change page
  const pageNumbers = pageNumbersArr.map(number => {
     return <button key={number} onClick={(e) => setCurrentPage(number)} className="page-link">{number}</button>
  })

  return (
    <>
        <Helmet>
          <title>0xlarmideh's Repositories</title>
          <meta 
            name="description"
            content="This page displays all repositories details from 0xlarmideh's Github"
            rel="/repos"
            />
          <link rel="canonical" href="/repos" />
        </Helmet>

     { isLoading ? (<div className="loading-gif"><img src="/loading.gif"></img></div>) : (
    <div className="repos-container">
      <h1 className="Kegilka headline repo-headline">{'<' + 'flashy' + '>'} repos by larmideh </h1>
      <div className="repo-details">{reposMapped} </div>
      <div className="current-page">Page <span className="strong">{currentPage} </span> of {reposLength} </div><br></br>
        <section className="pagination-container">
          <button className="page-link" disabled={currentPage <= 1} aria-disabled={currentPage <= 1} onClick={() => setCurrentPage(prev => prev-1)}>Prev</button>
          <div className="pagination">{pageNumbers}</div>
          <button className="page-link" disabled={currentPage >= reposLength} aria-disabled={currentPage >= 1} onClick={() => setCurrentPage(prev => prev+1)}>Next</button>
        </section>
    </div>
  )}
    </> 
  )
}

The SingleRepo component is similar to the Repos page but with some extended functions. useParams import from the 'react-router-dom' is to be utilized to get the id of each repository once the learn more button is clicked. GitHub provides another endpoint that returns a single repository with its ID being an attribute. Once the button is clicked, the useParams gets the id and stores it in the repoId variable which is then fed into the url for a fresh API call. Data is then displayed in the jsx.

import { useEffect, useState } from "react"
import { Link, useParams } from 'react-router-dom'
import { Helmet } from 'react-helmet-async';

const SingleRepo = () => {
  const [repos, setRepos] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const { repoId } = useParams();
  const githubUrl = `https://github.com/0xlarmideh/${repoId}`;
  useEffect(() => {
    const url = `https://api.github.com/repos/0xlarmideh/${repoId}`;
    const fetchUsers = async () => {
      const res = await fetch(url)
      const data = await res.json();
      setRepos(data)
      setIsLoading(false)
    };
    fetchUsers();
  }, []);

  let dateObj = new Date(repos.created_at);
  let myDate = (dateObj.getUTCFullYear()) + "/" + (dateObj.getMonth() + 1) + "/" + (dateObj.getUTCDate());

  return isLoading ? (<img className="loading-gif" src="/loading.gif"></img>) : (
    <div>
      <Helmet>
        <title>Single Repository Page</title>
        <meta
          name="description"
          content="This page displays single repository details from 0xlarmideh's Github"
        />
          <link rel="canonical" href="/repos/:repoId" />
      </Helmet>

      <div>
        <div className="repo-top">
          <p className="singrepo-language">{repos.language} </p>
          <h1 className="singrepo-title"> {repos.name} </h1>
          <p className="singrepo-date">{myDate}</p>
        </div>
        <div className="desc-container">
          <div className="repo-desc-head">Descriptions</div>
          <div className="repo-description">{repos.description === null ? <div>No descriptions available</div> : repos.description} </div>
          <div className="singrepo-links">
            <a className="back-home" href={githubUrl} target="_blank" rel="noopener noreferrer"  >Check on Github</a>
            <Link className="back-home" to="/repos">Back to repos</Link>
          </div>
        </div>
      </div>
    </div>)
}

export default SingleRepo

error-404.jsx

The jsx of the 404 page is simple and it dislays the heading, a description and a link to go back to the homepage.

import { Link } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';

export default function Error() {
  return (
    <>
        <Helmet>
          <title>Homepage</title>
          <meta 
            name="description"
            content="A page nobody ever wants to visit, but you still got here. Stubborn Kid!"
            />
          <link rel="canonical" href="*" />
        </Helmet>
    <section className='grid-container'>
        <h1 className="Kegilka headline">404</h1>
      <div className='grid-item item1'>
        <div className="desc-joke">How you got here is still a mystery and we're as confused as you are. You made the wrong turn but luckily, you can still fix this.</div>
        <Link className="back-home" to='/'><i className="fa-solid fa-house"></i> Go Home</Link>
      </div>
    </section>
    </>   
  )
}

An error I encountered was when I deployed the codes to Vercel. I created the project with vite and these hosting platforms(Vercel, Netlify....) have an issue with react routing. When I move around the app to different routes, it works peerfectly well but once I input a specific route url, it throws an 404 error. The fix was to create a vercel.json file and input these codes:

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

I also created a sitemap.txt file in the public folder which tells search engines to crawl highlighted urls.

https://0xlarmideh-altschool-exam.vercel.app/
https://0xlarmideh-altschool-exam.vercel.app/repos
https://0xlarmideh-altschool-exam.vercel.app/repos/:repoId

We currently have all the pages implemented without styling. I used Vanilla CSS as it was the only styling solution I knew. Now I know CSS, SCSS, Tailwind and Styled Components.

You can have a look at the deployed site at 0xlarmideh-altschool-exam.vercel.app

GitHub Url: https://github.com/0xlarmideh/github-repo-lists-ALtschool

Resources