How to Build a GitHub Repo Explorer with React and TypeScript
Rifki Andriyanto
Posted on July 3, 2023
GitHub is a popular platform for hosting and managing Git repositories. In this tutorial, we will build a GitHub repository explorer using React and TypeScript. We will leverage the GitHub API to fetch user data and display their repositories. So, let's get started!
Installation
First, let's set up our project. Open your terminal and run the following command to create a new React project using Vite:
yarn create vite
Follow the prompts and choose React and TypeScript as the template.
Project Structure
Our project structure will consist of several files and directories. Here's an overview:
- api: Contains the API functions for fetching data from GitHub.
- components: Contains reusable React components.
- styles: Contains CSS module files for styling the components.
Creating the App
Component
Let's start by creating the main component of our application, App.tsx
. This component will handle user input, fetch data from the GitHub API, and display the results.
import { useEffect, useState } from "react";
import { fetchUsers } from "@api/users";
import { isErrorWithMessage } from "@components/helpers";
import Loading from "@components/loading";
import styles from "@styles/App.module.css";
import Dropdown from "@components/dropdown";
import RepositoryCard from "@components/repository-card";
import Snackbar from "@components/snackbar";
import type { KeyboardEventHandler } from "react";
import type { UserDataType } from "@api/users";
import type { SnackbarPropsType } from "@components/snackbar";
function App() {
const [username, setUsername] = useState("");
const [users, setUsers] = useState<UserDataType[] | never[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [showData, setShowData] = useState(false);
const [snackbar, setSnackbar] = useState<
SnackbarPropsType & { show: boolean }
>({
variant: "success",
message: "",
show: false,
});
const getUsers = async () => {
setShowData(false);
setIsLoading(true);
const result = await fetchUsers(username);
if (isErrorWithMessage(result)) {
setSnackbar({
variant: "error",
message: result.message,
show: true,
});
} else {
setUsers(result);
setShowData(true);
}
setIsLoading(false);
};
useEffect(() => {
if (snackbar.show) {
setTimeout(() => {
setShowData(false);
setSnackbar({
variant: "success",
message: "",
show: false,
});
}, 2000);
}
}, [snackbar.show]);
const handlePress: KeyboardEventHandler<HTMLInputElement> | undefined = (
e
) => {
if (e.key === "Enter") {
getUsers();
}
};
return (
<div className={styles.container}>
<div className={styles.searchBar}>
<input
type="text"
placeholder="Enter Username"
aria-label="username-input"
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={handlePress}
/>
{isLoading ? (
<Loading />
) : (
<button disabled={username === ""} onClick={getUsers}>
Search
</button>
)}
</div>
{showData && (
<div className={styles.userSection}>
{users.length > 0 ? (
users.map(
({ login: githubUsername,
repositories, id: userId }) => (
<Dropdown key={userId} label={githubUsername}>
{repositories.length > 0 &&
repositories.map(
({
id,
name: title,
description,
stargazers_count: stargazersCount,
}) => (
<RepositoryCard
key={id}
title={title}
description={description}
stargazerCount={stargazersCount}
/>
)
)}
</Dropdown>
)
)
) : (
<div className={styles.notFound}>Ooop username not found </div>
)}
</div>
)}
{snackbar.show && (
<Snackbar variant={snackbar.variant} message={snackbar.message} />
)}
</div>
);
}
export default App;
The App
component consists of a search bar where the user can enter a GitHub username. On pressing Enter or clicking the Search button, the getUsers
function is called, which fetches the user data from the GitHub API using the fetchUsers
function. The fetched data is then displayed in the UI using the Dropdown
and RepositoryCard
components.
Fetching Repositories from the GitHub API
Next, let's create the API functions for fetching data from the GitHub API. Create a new file called repositories.ts
inside the api
directory and add the following code:
export type RepositoryDataType = {
id: number;
name: string;
description: string;
stargazers_count: number;
};
export const fetchRepositories = async (username: string) => {
const response = await fetch(
`https://api.github.com/users/${username}/repos`
);
const data: Array<RepositoryDataType> = await response.json();
return data;
};
This file exports the fetchRepositories
function, which takes a username
as input and fetches the repositories for that user from the GitHub API.
Next, create another file called users.ts
inside the api
directory and add the following code:
import {
type RepositoryDataType,
fetchRepositories,
} from "./repositories";
import { ErrorWithMessageType, getErrorMessage } from "@components/helpers";
export type UserDataType = {
id: number;
repositories: Array<RepositoryDataType>;
login: string;
};
export type UserDataResponseType = {
incomplete_results: boolean;
items: Array<UserDataType>;
total_count: number;
};
export const fetchUsers = async (
username: string
): Promise<Array<UserDataType> | ErrorWithMessageType> => {
try {
const response = await fetch(
`https://api.github.com/search/users?q=${username}`
);
if (response.ok) {
const { items, total_count: totalCount }: UserDataResponseType =
await response.json();
const total = totalCount < 5 ? totalCount : 5;
const users: Array<UserDataType> = [];
for (let i = 0; i < total; i++) {
const repositories = await fetchRepositories(items[i].login);
if (repositories) {
users.push({ ...items[i], repositories });
}
}
return users;
} else {
throw new Error("Something went wrong..");
}
} catch (error) {
return {
isInstanceOfError: true,
message: getErrorMessage(error),
};
}
};
In this file, we define the fetchUsers
function, which takes a username
as input and fetches the user data from the GitHub API. It also calls the fetchRepositories
function to fetch the repositories for each user.
The function returns an array of UserDataType
or an ErrorWithMessageType
object in case of an error.
Creating Additional Components
Now let's create some additional components that will be used in the App
component.
Create a new file called dropdown.tsx
inside the components
directory and add the following code:
import { PropsWithChildren, useState } from "react";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
import styles from "@styles/dropdown.module.css";
type DropdownPropsType = {
label: string;
};
const Dropdown = ({
label,
children,
}: PropsWithChildren<DropdownPropsType>) => {
const [open, setOpen] = useState(false);
return (
<div className={styles.dropdown} role="listbox">
<div
className={styles.label}
onClick={() => setOpen((openState) => !openState)}
data-testid="dropdown-label"
>
<p>{label}</p>
{open ? <FaChevronUp /> : <FaChevronDown />}
</div>
{open && <div>{children}</div>}
</div>
);
};
export default Dropdown;
This component represents a dropdown menu. It takes a label
prop and renders it as the dropdown header. Clicking on the header toggles the dropdown menu's visibility. The children
prop is the content of the dropdown menu.
Next, create a file called helpers.ts
inside the components
directory and add the following code:
export type ErrorWithMessageType = {
isInstanceOfError: boolean;
message: string;
};
export const isErrorWithMessage = (
error: any
): error is ErrorWithMessageType => {
return (error as ErrorWithMessageType).isInstanceOfError !== undefined;
};
export function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message;
return "Unknown error occurred";
}
This file defines some helper functions. The isErrorWithMessage
function checks if an error object is an instance of ErrorWithMessageType
. The getErrorMessage
function extracts the error message from an error object.
Create a file called loading.tsx
inside the components
directory and add the following code:
import styles from "@styles/loading.module.css";
const Loading = () => {
return (
<div className={styles.loading} role="progressbar">
<div></div>
</div>
);
};
export default Loading;
This component represents a loading indicator.
Finally, create a file called repository-card.tsx
inside the components
directory and add the following code:
import { FaStar } from "react-icons/fa";
import styles from "@styles/repo-card.module.css";
type RepositoryCardPropsType = {
title: string;
description: string;
stargazerCount: number;
};
const RepositoryCard = ({
title,
description,
stargazerCount,
}: RepositoryCardPropsType) => {
return (
<div className={styles.repositoryCard}>
<div className={styles.header}>
<h5>{title}</h5>
<div className={styles.stargazerCount}>
<p>{stargazerCount}</p>
<FaStar />
</div>
</div>
<p>{description}</p>
</div>
);
};
export default RepositoryCard;
This component represents a card displaying repository information. It takes title
, description
, and stargazerCount
props and renders them in the card.
Finalizing the Snackbar Component
Finally
, let's create the snackbar.tsx
component. Create a file called snackbar.tsx
inside the components
directory and add the following code:
import styles from "@styles/snackbar.module.css";
export type SnackbarPropsType = {
variant: "success" | "error";
message: string;
};
const Snackbar = ({ variant, message }: SnackbarPropsType) => {
return (
<div
className={`${styles.snackbar} ${
variant === "success" ? styles.success : styles.error
}`}
>
{message}
</div>
);
};
export default Snackbar;
This component represents a snackbar notification. It takes a variant
prop with values "success" or "error" to determine the styling and a message
prop to display the notification message.
Styling the Components
Now let's add some styles to our components. Create a file called App.module.css
inside the styles
directory and add the following CSS code:
.container {
width: 100%;
}
.searchBar {
bottom: 50%;
left: 50%;
display: flex;
flex-direction: column;
max-width: min(768px, 100vw - 64px);
margin: 3rem 30% 1rem 30%;
}
.searchBar input {
background-color: var(--gray);
padding: 16px 24px;
border: 1px solid var(--border-gray);
border-radius: 6px;
width: 100%;
height: 3rem;
font-size: 1rem;
margin-bottom: 1.5rem;
}
.searchBar input::placeholder {
font-size: 1rem;
}
.searchBar input:focus,
.searchBar input:focus-visible {
background-color: var(--white);
border-color: var(--black);
}
.searchBar button {
background-color: var(--blue);
border-radius: 0.3rem;
padding: 1rem;
border: none;
color: var(--white);
width: 100%;
height: 3rem;
cursor: pointer;
font-size: 1rem;
padding: 0.3rem;
}
.userSection {
padding: 32px 0;
overflow: auto;
max-width: min(768px, 100vw - 64px);
margin: 3rem 30% 1rem 30%;
}
.notFound {
display: flex;
align-items: center;
justify-content: center;
padding: 128px;
font-size: 2rem;
font-weight: 400;
}
@media only screen and (max-width: 640px) {
.searchBar {
margin: 1rem auto;
}
.userSection {
margin: 0 auto;
}
.searchBar input {
font-size: 1rem;
width: 100%;
}
.searchBar button {
font-size: 1rem;
width: 100%;
}
.notFound {
padding: 64px;
font-size: 1.5rem;
font-weight: 400;
}
}
This CSS code styles the various components in the application.
Wrapping Up
With these components and API functions in place, you should have a functioning GitHub repository search application in React. You can further customize and enhance the components and styles to fit your needs.
Other CSS styles are not available in this post, and if there are some incomplete components, you can visit GitHub at https://github.com/rifkiandriyanto/github-repo-explorer.
Posted on July 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.