GitHub User Profile (An Update to the AltSchool Exam Project)

Photo by Andrew Neel on Unsplash

GitHub User Profile (An Update to the AltSchool Exam Project)

React is a popular JavaScript library for building user interfaces, and the GitHub API allows developers to access information about repositories and users on GitHub. In this article, we'll learn how to use the GitHub API and react-router to build a simple React program that searches for GitHub user profiles.

Before we start, you'll need to have a basic understanding of React and the GitHub API. You should also have Node.js and npm (the Node.js package manager) installed on your machine.

To begin, create a new React project using the create-react-app command:

Copy codenpx create-react-app github-search

Next, navigate to the project directory and install the react-router package:

cd github-search
npm install react-router-dom

Next, you'll need to install the Bootstrap CSS library in your project. You can do this by running the following command in your project directory:

npm install bootstrap

Now that we have our dependencies installed, let's start building our program.

First, let's create the main component that ties everything together. In the src directory, create a new file called App.js and add the following code:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ErrorPage from "./components/ErrorPage";
import Header from "./components/Header";
import Home from "./components/Home";
import Repo from "./components/Repo";
import Profile from "./components/Profile";
import Search from "./components/Search";
import ErrorBoundry from "./components/ErrorBoundry";
import TestError from "./components/TestError";

function App() {
  return (
    <Router>
      <Header />
      <Routes>
        <Route exact path="/" element={<Home />} />
        <Route path="profile" element={<Profile />}></Route>
        <Route path="search" element={<Search />}></Route>
        <Route path="search/:profile" element={<Profile />} />
        <Route path="search/profile/:repo" element={<Repo />} />
        <Route
          path="testerror"
          element={
            <ErrorBoundry>
              <TestError />
            </ErrorBoundry>
          }
        />
        <Route path="*" element={<ErrorPage />} />
      </Routes>
    </Router>
  );
}
export default App;

Basically, this component act as a container for all other components in the program.

Here, I used setup the react-router to enable the program to navigate to other pages in the program.

Next, create the SearchUsers component. This is the primary page for the GitHub search. This component contains a form, where the user enters the search word or phrase.

The form in the SearchUsers component has a submit method that calls on a search function from the Search component that uses Axios to search the GitHub API and pull user information to the client side. The information is then passed to the GetUsers component where it is now displayed in the browser.

import React, { useState, Fragment } from "react";

function SearchUsers({ searchUser }) {
  const [input, setInput] = useState("");

  const onsubmit = (e) => {
    e.preventDefault();
    searchUser(input);
    setInput("");
  };
  const onChange = (e) => {
    setInput(e.target.value);
  };
  return (
    <Fragment>
      <main className="  mt-5 ">
        <form
          style={{ maxWidth: "100%" }}
          onSubmit={onsubmit}
          className="input-group mb-2">
          <input
            type="text"
            className="form-control  search-input me-2    col"
            placeholder="Enter text to Search"
            aria-label="Enter text to Search"
            aria-describedby="button-addon2"
            value={input}
            onChange={onChange}
          />
          <input
            className="btn btn-primary btn-block "
            type="submit"
            id="button-addon2"
            value="Search"
          />
        </form>
      </main>
    </Fragment>
  );
}
export default SearchUsers;
import React, { Fragment, useState } from "react";
import axios from "axios";
import GetUsers from "./GetUsers";
import SearchUsers from "./SearchUsers";

let githubClientId;
let githubClientSecret;
if (process.env.NODE_ENV !== "production") {
  githubClientId = process.env.REACT_APP_GITHUB_CLIENT_ID;
  githubClientSecret = process.env.REACT_APP_GITHUB_CLIENT_SECRET;
} else {
  githubClientId = process.env.CLIENT_ID;
  githubClientSecret = process.env.CLIENT_SECRET;
}

function Search() {
  const [usersData, setUsersData] = useState([]);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
  const [clearButton, setClearButton] = useState(false);
  const [displayWarning, setDisplayWarning] = useState(false);

  const searchUser = async (input) => {
    if (input === "") {
      setDisplayWarning(true);
      setTimeout(() => {
        setDisplayWarning(false);
      }, 5000);
    } else {
      setLoading(true);
      try {
        const res = await axios.get(
          `https://api.github.com/search/users?q=${input}&client_id=${githubClientId}&secret=${githubClientSecret}`
        );
        setUsersData(res.data.items);
      } catch (error) {
        setError(error);
      }
      setLoading(false);
      setClearButton(true);
    }
  };

  const clearUsers = () => {
    setUsersData([]);
    setClearButton(false);
  };

  return (
    <Fragment>
      <button className="btn btn-primary ms-5 mt-4">Back</button>
      <div className="container">
        <SearchUsers searchUser={searchUser} />
        {displayWarning && (
          <p className="text-light bg-danger p-2 mt-4 mb-4">Input is empty</p>
        )}
        {clearButton && (
          <button
            onClick={clearUsers}
            className="btn btn-secondary btn-block mb-3 mt-3 w-100">
            Clear
          </button>
        )}
        <GetUsers loading={loading} usersData={usersData} error={error} />
      </div>
    </Fragment>
  );
}
export default Search;

Creating the User's Details Page

First, we begin by importing several dependencies, including React, the "ErrorPage" and "Loading" components, and the "GetRepo" component which is used to display the user's repositories. The "Fragment" component is also imported to group a set of children without adding extra nodes to the DOM.

The "GetUser" component receives several props as input, including a loading flag, data object, error object, and a URL for the user's profile. The data object contains information about the user such as their hireability status, bio, contact information, and the number of followers and following.

The component uses a conditional statement to check the value of the loading flag. If the flag is true, the component returns the "Loading" component, which is used to show a loading animation while the data is being fetched. If the loading flag is false and the error object is not null, the component will render the ErrorPage component, which will show the error message.

If the loading flag is false and the error object is null, the component will render the user's information. The information is displayed in a visually appealing manner using Bootstrap classes and some custom CSS styles. The user's hireability status, bio, contact information, and the number of followers and following are displayed in various elements using JSX, which is the syntax used by React for rendering components.

Finally, the "GetRepo" component is rendered, which is used to display the user's repositories. It receives the URL prop as input which is used to construct the URL for making the API call for the repositories.

import React, { Fragment } from "react";
import ErrorPage from "./ErrorPage";
// import { Link } from "react-router-dom";
import GetRepo from "./GetRepo";
import Loading from "./Loading";

function GetUser({ loading, data, error, url }) {
  const {
    hireable,
    bio,
    email,
    blog,
    avatar_url,
    name,
    login,
    followers,
    following,
    location,
    twitter_username,
  } = data;

  if (loading) return <Loading />;

  if (!loading && error !== null) {
    return <ErrorPage error={error.message} />;
  }

  return (
    <div className="container ">
      <h1 className="h3">Github User Profile</h1>
      <div className="card p-5 m-2 grid   shadow">
        <p className="p">
          Hireable{" "}
          <span>
            <i
              className={
                hireable
                  ? "fa-solid fa-check text-primary"
                  : "text-danger fa-solid fa-xmark"
              }
            ></i>
          </span>{" "}
        </p>

        <div className=" p-1">
          <div className="d-flex  flex-column flex-md-row justify-content-evenly">
            <div className="flex-shrink-0 text-center">
              <img
                className="img-thumbnail img-fluid  mx-auto rounded-circle "
                src={avatar_url}
                alt="profile "
                style={{ maxWidth: "10rem", aspectRatio: "1" }}
              />
              <h3 className="h3 text-primary">{name}</h3>
              <p className="mb-1">@{login}</p>
              <p className="location p">{location}</p>
            </div>
            <div className="flex-grow-1 ms-4 ">
              {bio && (
                <div className="grid">
                  <span className="fw-bold">Bio:</span> <p>{bio}</p>{" "}
                </div>
              )}

              {blog && (
                <div className="grid pb-2">
                  <span className="fw-bold">Website:</span>{" "}
                  <a className="link" href={blog}>
                    {blog}
                  </a>{" "}
                </div>
              )}
              {email && (
                <div className="grid pb-2">
                  <span className="fw-bold">Website:</span>{" "}
                  <a className="link" href={`mailto:${email}`}>
                    {email}
                  </a>
                </div>
              )}

              <div className="follows flex">
                <div className="d-flex flex-sm-row ">
                  <p className=" px-3  bg-secondary text-white bg-gradient">
                    <span className=" ">{followers}</span>∘ Followers
                  </p>
                  <p className="px-3 mx-4 bg-primary text-white bg-gradient">
                    <span className="">{following}</span>∘ Following
                  </p>
                </div>
              </div>

              <p style={{ maxWidth: "150px" }} className="text-white bg-info  ">
                <span>
                  <i className="fa-brands fa-twitter mx-3 "></i>
                </span>
                {twitter_username}
              </p>
            </div>
          </div>
          <Fragment>
            <GetRepo url={url} />
          </Fragment>
        </div>
      </div>
    </div>
  );
}

export default GetUser;

The GetRepo Component

In this component, we'll import the "Pagination" component which we will be discussing later, and a "Loading" component to show a loading animation while data is being fetched. The useEffect and useState Hooks are also imported to handle the side-effects and state management.

The "GetRepo" component receives a prop called "url" that is used to construct the API endpoint to fetch the repositories. The component has several state variables: repos, loading, error, currentPage, and usersPerPage. repos is an array that holds the data of the repositories, loading is a Boolean value that shows if the API request is still being made or it's done, error is an object that contains the error message if an error occurs, currentPage is a number that holds the current page number and usersPerPage is the number of the repositories to be shown per page.

The component makes use of the useEffect Hook to invoke an async function to fetch the repositories data once the component mount, this function makes use of axios to make the API request to the endpoint with the user’s URL and it sets the repos and loading state variables accordingly.

The component also makes use of useEffect Hook to handle the highlighting of the current page number. It selects all elements with class page-link and it checks if the inner html of each element equals the currentPage, if so, it adds the class "active" to the element, otherwise, it removes it.

There are two other functions, prev() and next(), that are used to decrement or increment the currentPage state variable respectively and an updatePage() function that sets the currentPage to the clicked page number.

A "Pagination" component is rendered, which takes several props to handle the pagination logic. These props include totalPageCount, updatePage, prev, next, and currentPage. totalPageCount is the total number of pages, updatePage is the function that updates the currentPage, prev is a function that decrements the currentPage and next is a function that increments the currentPage.

The component uses a conditional statement to check the value of the loading flag. If the flag is true, the component returns the "Loading" component, which is used to show a loading animation while the data is being fetched. If the loading flag is false and the error object is not null, the component will render an error message

If the loading flag is false and the error object is null, the component will render a list of the user's repositories, each repository is wrapped in a "Link" component, which is used to navigate to a specific repository's page. Each repository is wrapped in a "li" element and there is a key prop to ensure that React can keep track of the list items.

Finally, the component renders the "Pagination" component which will show the pagination links.

Overall, the "GetRepo" component provides a way to display a list of a user's repositories and allow the user to navigate through them.

import React, { Fragment, useEffect, useState } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import Pagination from "./Pagination";
import Loading from "./Loading";

function GetRepo({ url }) {
  const [repos, setRepos] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [currentPage, setCurrentPage] = useState(1);
  const [usersPerPage] = useState(5);

  const indexOfLastRepo = currentPage * usersPerPage;
  const skip = indexOfLastRepo - usersPerPage;
  const currentPosts = repos?.slice(skip, indexOfLastRepo);

  let totalUsers = repos?.length;

  //  Get Total Page Count
  const totalPageCount = Math.ceil(totalUsers / usersPerPage);

  const updatePage = (e) => {
    setCurrentPage(Number(e.target.innerHTML));
  };
  const prev = () =>
    currentPage <= 1 ? setCurrentPage(1) : setCurrentPage(currentPage - 1);

  const next = () =>
    currentPage >= totalPageCount
      ? setCurrentPage(totalPageCount)
      : setCurrentPage(currentPage + 1);

  useEffect(() => {
    const getRepo = async () => {
      setLoading(true);

      try {
        const res = await axios.get(
          `https://api.github.com/users/${url}/repos?page=1&per_page=150&client_id=${process.env.REACT_APP_CLIENT_ID}&secret=${process.env.REACT_APP_CLIENT_SECRET}`
        );
        setRepos(res.data);
      } catch (error) {
        setError(error);
      }
      setLoading(false);
    };

    getRepo();
  }, [url]);

  useEffect(() => {
    let list = document.querySelectorAll(".page-link");

    list.forEach((item) => {
      item.classList.remove("active");
      if (Number(item.innerHTML) === Number(currentPage)) {
        item.classList.add("active");
      } else {
        item.classList.remove("active");
      }
    });
  });

  if (loading) return <Loading />;

  if (!loading && error !== null) return <p className="p">{error.message}</p>;

  return (
    <Fragment>
      <div className="container shadow-sm card">
        <h3 className="p text-danger">Repositories</h3>
        <ol className="repo-list m-2">
          {currentPosts?.map((repo) => {
            return (
              <li className=" text-success" key={repo.id}>
                <Link
                  className=" link link-success"
                  to=":repo"
                  state={{ repoLink: repo?.url }}
                >
                  {repo.name}
                </Link>
              </li>
            );
          })}
        </ol>
        <Pagination
          totalPageCount={totalPageCount}
          updatePage={updatePage}
          prev={prev}
          next={next}
          currentPage={currentPage}
        />
      </div>
    </Fragment>
  );
}

export default GetRepo;

In conclusion, creating an app that utilizes React, React-Router, the GitHub API, and Bootstrap allows for a highly functional and visually appealing user experience. The ability to navigate between different routes within the app is greatly enhanced by using React-Router, making it easy for the user to switch between different pages or views of the app. Incorporating the GitHub API into the application allows for seamless integration of data, while Bootstrap ensures that the app is visually consistent and responsive across different devices. The ability to search for specific GitHub users and display their information in a user-friendly manner is a key feature of the application, made possible by combining all these technologies. Overall, using React, React-Router, the GitHub API, and Bootstrap provides a powerful and efficient way to create a fully-featured GitHub user search and information display app.