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 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