How to Build a GitHub Repo Explorer with React and TypeScript
Published: 2023-07-03 04:13:52


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xu98lzxb2ino34tsa66h.png)

[Image: TheTerrasque / Reddit](https://www.reddit.com/r/ProgrammerHumor/comments/8op819/how_do_you_do_fellow_devs/)


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:

```bash
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.

```tsx
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:

```ts
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:

```ts
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:

```tsx
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:

```ts
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:

```tsx
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:

```tsx
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:

```tsx
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:

```css
.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.