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 yourtailwind.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 achangePage
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