How to Build a GitHub Repo Explorer with React and TypeScript

rifkiandriyanto

Rifki Andriyanto

Posted on July 3, 2023

How to Build a GitHub Repo Explorer with React and TypeScript

Image description

Image: TheTerrasque / Reddit

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
rifkiandriyanto
Rifki Andriyanto

Posted on July 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related