Creating A GitHub Portfolio Website Using GitHub API In React
Introduction
Today, we would create a GitHub portfolio website that displays a Github user’s profile information and repositories.
What is GitHub?
GitHub is a platform used by developers for storing and collaborating on code repositories. It can also be used to create a portfolio to showcase a developer’s skills and experience to potential employers. A GitHub portfolio helps developers present their work professionally and stand out to potential employers.
We would build a personalised Github profile that pulls certain selected information about a user from the API (Application Programming Interface) and displays it on the webpage. The website would be built using popular tools such as React, Styled-components, and hosted with Netlify.
Prerequisites
To go through the process of creating our app from start to deployment, you will need to have the following:
- Node.js installed on your computer. Download it at nodejs.org.
- Git installed on your computer. Download it at git-scm.com.
- I would recommend using VS Code as your code editor. Download it at code.visualstudio.com.
- A free Netlify account at netlify.com.
- A free GitHub account at github.com.
- You should also have a good knowledge of ReactJs
Getting Started
The first thing we want to do is to set up our react application, I’ll be using vite which can be installed by running npm create vite@latest
in terminal, of our VS Code environment. Follow the prompts to set it up.
Fetching User Profile Data
Now that our React App is setup, create a functional component MyProfile.jsx. Inside this component, we’ll define a state variable using the useState
Hook where we will store the user’s profile data.
import { useState } from "react";
const MyProfile = () => {
const [data, setData] = useState([]);
return (
)
}
We will install axios
library with the command npm install axios
, to fetch data from the GitHub API.
The useEffect
Hook is then called with an empty dependency array, which means it will only run once when the component mounts. It uses the axios
library to make a GET request to the GitHub API to retrieve the user's profile data and stores the data in the data
state variable.
import { useEffect, useState } from "react";
import axios from "axios";
const MyProfile = () => {
const [data, setData] = useState([]);
useEffect(() => {
const getProfileInfo = async () => {
try {
const res = await axios.get("https://api.github.com/users/ebokes");
setData(res.data);
} catch (error) {
console.error(error);
}
};
getProfileInfo();
}, []);
return (
<Row1>
<h1>My GitHub Profile</h1>
<ProfileCard>
<Col1>
<div>
<img alt="avatar" src={data.avatar_url} />
</div>
</Col1>
<Col2>
<span>
<h2>
<span>{data.name}</span>
<span>@{data.login}</span>
</h2>
</span>
<span>
<p>{data.bio}</p>
</span>
<span>
<HiLink />
<a href={`${data.blog}`} target="_blank">
Personal Porfolio Website
</a>
</span>
<span>
<GoLocation />
<p>{data.location}</p>
</span>
<span>
<GoVerified /> <p>{`${data.created_at}`.slice(0, 10)}</p>
</span>
<Stats>
<div>
<span>Repos: </span>
<span>{data.public_repos}</span>
</div>
<div>
<span>Followers: </span> <span>{data.followers}</span>
</div>
<div>
<span>Following: </span> <span>{data.following}</span>
</div>
</Stats>
<ProfileBtnGroup>
<ViewProfile href={`${data.html_url}`} target="_blank">
<span>
<BsGithub />
<span>View Profile</span>
</span>
</ViewProfile>
<RepoToggleBtn to="/profile/allrepos">View Repos</RepoToggleBtn>
</ProfileBtnGroup>
</Col2>
</ProfileCard>
</Row1>
);
};
export default MyProfile;
We’ll then go on to create the interface where the user information will be displayed.
Also, because we are using styled components for our styling, we would install the styled-components library using npm install styled-components
create and import all created styles.
Fetch All Repositories
We have been able to create a user profile for a Github user in the previous section. Now we make a GET request to the GitHub API to fetch a list of repositories from the ebokes
user on GitHub which will be stored in the repos
state variable.
Pagination
The list of repositories is sliced based on the current page number and the number of repositories per page, specified as the PER_PAGE
constant. The pagination is handled by the ButtonGroup
element, which displays buttons for each page, as well as Prev
and Next
buttons for navigating to the previous and next pages. When a page button or the Prev
or Next
buttons are clicked, the setPage
function updates the current page number.
import axios from "axios";
import React, { useEffect, useState } from "react";
const AllRepos = () => {
const [repos, setRepos] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getAllRepos = async () => {
loading;
try {
const response = await axios.get(
"https://api.github.com/users/ebokes/repos"
);
setRepos(response.data);
setLoading(true);
} catch (error) {
console.error(error);
}
};
getAllRepos();
}, []);
const PER_PAGE = 6;
const total = repos?.length;
const pages = Math.ceil(total / PER_PAGE);
const skip = page * PER_PAGE - PER_PAGE;
return (
<AllReposSection>
{loading && (
<Col>
<div>
<h2>Repositories</h2>
<RepoList>
{repos?.slice(skip, skip + PER_PAGE).map((item) => (
<RepoItem key={item.id}>
<RepoLink to={`/profile/${item.id}`}>
{item.name}
</RepoLink>
<DetailsBtn to={`/profile/${item.id}`}>
View Details
</DetailsBtn>
</RepoItem>
))}
</RepoList>
</div>
</Col>
<Paginate>
<div>
{page} of {pages}
</div>
<ButtonGroup>
<Prev
disabled={page <= 1}
onClick={() => {
setPage((prev) => prev - 1);
}}
>
Prev
</Prev>
{Array.from({ length: pages }, (value, index) => index + 1).map(
(repo) => (
<Pagebtn
key={repo}
onClick={() => {
setPage(repo);
}}
>
{repo}
</Pagebtn>
)
)}
<Next
disabled={page >= pages}
aria-disabled={page >= pages}
onClick={() => {
setPage((prev) => prev + 1);
}}
>
Next
</Next>
</ButtonGroup>
</Paginate>
)}
</Row>
</Container>
</AllReposSection>
);
};
export default AllRepos;
Open Individual Repository
We define a state variable repos
using useState
and a variable params
using useParams
. It then defines a useEffect
hook that makes an HTTP request to the GitHub API using axios
to fetch data for the repository specified by the repoId
route parameter, and stores the data in the repos
state variable. This useEffect
hook will be run every time the params
object changes, which will happen when the route changes to a different repository.
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
const Repo = () => {
const [repos, setRepos] = useState([]);
const params = useParams();
useEffect(() => {
const getRepo = async () => {
try {
const res = await axios.get(
`https://api.github.com/repositories/${params.repoId}`
);
setRepos(res.data);
} catch (error) {
console.log(error);
}
};
getRepo();
}, [params]);
return (
<RepoSection>
<Container>
<RepoContainer>
<BackBtn to="/profile/allrepos">
<HiArrowNarrowLeft />
</BackBtn>
<div>
<h2>Repository Details</h2>
<span>
<h3>Repo Name:</h3> <p>{repos.name}</p>
</span>
<span>
<h3>Language:</h3> <p>{repos.language}</p>
</span>
<span>
<h3>Fork Count:</h3> <p>{repos.forks}</p>
</span>
<span>
<h3>Watchers:</h3> <p>{repos.watchers_count}</p>
</span>
<span>
<h3>Default branch:</h3> <p>{repos.default_branch}</p>
</span>
<span>
<h3>Visibility:</h3> <p>{repos.visibility}</p>
</span>
<span>
<h3>Date Created:</h3> <p>{`${repos.created_at}`.slice(0, 10)}</p>
</span>
<span>
<a href={repos.html_url} target="_blank">
View Repo
</a>
{repos.homepage && (
<a href={repos.homepage} target="_blank">
Live Demo
</a>
)}
</span>
</div>
</RepoContainer>
</Container>
</RepoSection>
);
};
export default Repo;
Creating Search Page
This page allows users to search for Github profiles. It has three state variables: input
, userSearch
, and loading
. input
stores the value of the search input field, userSearch
stores the search results returned by the Github API, and loading
tracks whether a search is in progress. The component has a function called onSubmitHandler
that is called when the user submits the search form. This function makes a request to the Github API with the value of the search input and stores the search results in the userSearch
state variable. It also sets loading
to true
while the search is in progress and false
when the search is complete. The component renders a form with a search input field and search button, and it displays a list of search results if there are any. If a search is in progress, it displays a loading spinner.
import { useState } from "react";
import axios from "axios";
import Loader from "../../components/Loading";
import { Helmet } from "react-helmet-async";
const Search = () => {
const [input, setInput] = useState("");
const [userSearch, setUserSearch] = useState([]);
const [loading, setLoading] = useState(false);
const onSubmitHandler = async (e) => {
e.preventDefault();
try {
setLoading(true);
const response = await axios.get(
`https://api.github.com/search/users?q=${input}`
);
setUserSearch(response.data.items);
setLoading(false);
} catch (error) {
console.error(error);
}
};
return (
<SearchSection>
<Container>
<Helmet>
<title>Search Github Profile</title>
<meta
name="description"
content="Search for any Github user's profile"
/>
<link rel="canonical" href="/search" />
</Helmet>
<SearchContainer>
<SearchBar
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "tween", duration: 0.5 }}
initial={{ scale: 0, opacity: 0 }}
>
<h1>Github Profile Finder</h1>
<p>Enter name or Github username</p>
{/* Search Bar */}
<form>
<input
value={input}
onChange={(e) => {
setInput(e.target.value);
setUserSearch([]);
}}
type="search"
placeholder="Enter Username"
required
/>
<SearchBtn onClick={onSubmitHandler}>Search</SearchBtn>
</form>
</SearchBar>
{loading ? (
<Loader />
) : (
<SearchResultContainer animate={{ y: 0 }} initial={{ y: 30 }}>
<SearchList>
{userSearch.map((user) => {
return (
<div key={user.id}>
<UserCard>
<CardContent>
<CardAvatar>
<img alt="avatar" src={user.avatar_url} />
</CardAvatar>
<Details>
<h1>{user.login}</h1>
<p>{user.location}</p>
<View href={`${user.html_url}`} target="_blank">
<BsGithub />
<p>View Profile</p>
</View>
</Details>
</CardContent>
</UserCard>
</div>
);
})}
</SearchList>
</SearchResultContainer>
)}
</SearchContainer>
</Container>
</SearchSection>
);
};
export default Search;
Implementing SEO
SEO stands for “Search Engine Optimisation.” It is the practice of optimising a website to rank higher in search engine results pages (SERPs), with the goal of increasing the number of visitors to the site. More on SEO
The default title of a react web application is “React App”, to change the title of your application, you either change it manually or install React Helmet. React Helmet is a library that allows you to manage the document head of a React application. You can use it to change the title and other attributes of the document head, such as the meta tags and link tags.
To use React Helmet in a React application, you will need to install the library by running the following command npm install react-helmet-async
import { Helmet } from "react-helmet-async";
const MyProfile = () => {
return(
<div>
<Helmet>
<title>Chibuokem's Github Profile</title>
<meta
name="description"
content="Github Repositories of Chibuokem Egbuchulam"
/>
<link rel="canonical" href="/profile" />
</Helmet>
</div>
)
The Helmet
component allows you to set the title, meta tags, and link tags of the document head. You can nest it anywhere in your component hierarchy, and it will manage the document head for you.
The 404 Error Page
The 404 error page provides a helpful and user-friendly experience for users who encounter an error when trying to access a webpage. It is an important part of a website’s user experience, as it helps to prevent users from becoming frustrated or lost when they encounter an error or navigate to a page that does not exist.
Creating Error Boundary
The ErrorBoundary
component is a higher-order component that is used to catch JavaScript errors anywhere in the component tree below it. If an error is caught, the ErrorBoundary
component will display a fallback UI instead of the component tree that has the error.
import React from "react";
import { ErrorBoundarySection, Heading } from "./styles";
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);
console.log(errorInfo);
}
render() {
if (this.state.hasError) {
return (
<ErrorBoundarySection>
<Heading>Oops! Something went wrong</Heading>
</ErrorBoundarySection>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Conclusion
This is how I created a customised GitHub portfolio that showcases my profile. Thanks for taking out the time to go through.