Github Profile Finder Project: A Walkthrough

eyesaidyo

John Rasine Irem

Posted on January 9, 2023

Github Profile Finder Project: A Walkthrough

Part 1: The Basics

So, it was the end of my second semester at AltSchool Africa and I had an exam project to create a site showing my github profile and details about my repositories. In this article, I will explain how I went about implementing this project.

Tooling

As far as tooling goes, this project was made using Replit, which is an online IDE and I did this so I could work anywhere and on any device on my project. Replit comes with React tooling already set up (via Vite) so you can set up a new React project in seconds and install packages using the GUI.

Structure

All of the code was initially written in a single App component to make it so that I could get a working prototype as quickly as possible. All refactoring will be done in a more advanced portion of this article.

Code

I identified the most important steps to get my project up and running to be:

  • Fetching my account from the github API using the Fetch API.
  • Creating some basic state to assign the fetch results to.

Fetching data from an API and setting that data to a state (using a setter function) will often reuquire the use of the useEffect hook. This is done in order to avoid an infinite re-rendering cycle in React as the JSON objects fetched cannot be read as the same result even if they contain the exact same content.
I hence created an asynchronus getUser() function to handle all my API calls and promises. In that function, I implemented the fetch as follows:

const totalRepos= await fetch(`https://api.github.com/users/eyesaidyo`)
    .then(res=>res.json())
    .then(data=>data.public_repos)
Enter fullscreen mode Exit fullscreen mode

Creating state to assign the data to went as follows:
const [currentPage, setCurrentPage]= useState('')
This state is what will be used to track and change paginated data coming from the API.

A second fetch was then implemented in the getUser function this time, to get the actual repositories and their data:

fetch(`https://api.github.com/users/eyesaidyo/repos?per_page=${reposPerPage}&page=${currentPage}`)
.then(res=>res.json())
    .then(res=>{
      toSetRepos(res)
      // console.log(res)
      toSetPageCount(Math.ceil(totalRepos/reposPerPage))
    })
Enter fullscreen mode Exit fullscreen mode

toSetRepos is a setter function that gets a repos array for whatever the current page searched from the API is.
toSetPageCount calculates the number of pages needed to accomodate all the repos in my profile based on a fixed reposPerPage variable.

To make sure that I am able to get a certain list of repos based on which page in the fetch request is specified(currentPage):

useEffect(()=>{
getUser()
}, [currentPage])
Enter fullscreen mode Exit fullscreen mode

Now, whenever the component is mounted or there is a change to currentPage via a setter function, my getUser function runs.

In order to display the repos, a Cards component was made:

export const Cards=({repos})=>{

    return (
      <div>
        {repos.map((repo)=>{
        return(
          <Link className='repo-item-link' to={repo.name} key={repo.id}>
          <h3 className='repo-item' >{repo.name}</h3>
            </Link>
        )
        })}
        </div>
    )
  }
Enter fullscreen mode Exit fullscreen mode

This way, each repo item on the currentPage will be rendered as Link components which link to a new pathway, repo.name that will in turn show more specific details partaining to the particular repository.

Routing

The first thing is to import Routes and Route from react-router-dom
Routes acts as a container for each individual Route and what it links to.Note that Route needs a path property and an element property. path specifies the directory the webpage goes to in relation to its homepage or parent page. element specifies a component to be displayed based on what directory the webpage is in. An example is as follows:

<Routes>
        <Route path='/' element={<Nav/>}>
          <Route index element={<Home/>}/>
        </Route>
<Routes>
Enter fullscreen mode Exit fullscreen mode

I created a <Route> for each repository Link (existing in the Cards component) by using the map function and looping through the state that is repos:

{
            repos.map(repo=>{
          return(
           <Route path={`/${repo.name}`} key={repo.id}               element={<Card 
          name={repo.name}
          forks={repo.forks}
          visibility={repo.visibility}              
          createdAt={repo.created_at}  
          language={repo.language}                                           watchers={repo.watchers}
          url={repo.html_url}
                                                                                />}>
           </Route>

          )
          })}
Enter fullscreen mode Exit fullscreen mode

A route was also created to show a <NotFound/> component anytime the user goes to a wrong directory (*)

<Route path='*' element={<NotFound/>}> </Route>
Enter fullscreen mode Exit fullscreen mode

Pagination

As things stand currently, this webpage can only display one small piece of the data fetched from the API at a time, This is because the fetch is paginated and only serves a few repos at a time (reposPerPage) on whatever the currentPage is.
We need a way to dynamically set the currentPage to whatever we want; this is where pagination comes in .
In this project, I created my own basic pagination without any external libraries. My general idea was to use the calculated pageCount gotten from the following snippet in the getUser function: toSetPageCount(Math.ceil(totalRepos/reposPerPage)); this pageCount lets me know the total number of pages I'd need to accomodate all the repositories in my profile. I would then generate a button representing each page and assign an onClick() which calls the setCurrentPage function. Of course, showing every single button at the same time could make the UI bloated so I decided to show only 5 buttons at a time and then make a 'prev' and 'next' button to show the previous or the next 5 buttons if any. This previous and next buttons were set to disable at the beginning and at the end of available pages.
The one thing missing is a way to map through all the pages since the pageCount is not an array. I generated the array using a for loop as follows:

const getPagesArray=()=>{
    let ans=[];
    for(let i=1; i<=pageCount; i++){
      ans.push(i)
    }
    return ans;
  }
  const pagesArray=getPagesArray()
Enter fullscreen mode Exit fullscreen mode

Now armed with my pagesArray, I can choose to display only a slice() of it in order to show only buttonsPerPage number of buttons at a time (mine is set to five).
There is also state for firstIndex and lastIndex so I can use the prev and next buttons to show the next 5 and previous 5 buttons. This is how the code was written:

{

           pagesArray.slice(firstIndex, lastIndex).map((num,ind)=> 
          {
             return (
               <button
                 key={ind}
                 onClick={()=>{
           toSetCurrentPage(pagesArray.indexOf(num)+1)
                 }} 
              >{pagesArray.indexOf(num)+1}</button>)
           }
         )
}
Enter fullscreen mode Exit fullscreen mode

The prev and next buttons were written like this:

<button
            className='pg-btn'
            disabled={firstIndex===0}
            onClick={()=>{
              toSetFirstIndex(firstIndex-buttonsPerPage+1 >=0?
                             firstIndex-buttonsPerPage:
                             null)

              toSetLastIndex(lastIndex=== pageCount?
                            lastIndex-(pageCount%buttonsPerPage):lastIndex-buttonsPerPage
                            )

            }}
            >prev</button>
Enter fullscreen mode Exit fullscreen mode
<button
            className='pg-btn'
            disabled={lastIndex===pageCount || pageCount<buttonsPerPage}
            onClick={()=>{
              toSetFirstIndex(firstIndex+buttonsPerPage)

              toSetLastIndex(lastIndex+buttonsPerPage <=pageCount?lastIndex+buttonsPerPage:pageCount)

            }}
            >
        next
      </button>
Enter fullscreen mode Exit fullscreen mode

Part 2: Refactoring and Improvements

The 2 main areas I wanted to refactor and improve were :
-Change the pagination components state manager from useState to useReducer
-Move the pagination component out of the App component and make the state available to the App component using the Context API
It started up as follows:

const INITIAL_STATE={
    pageCount:0,
    firstIndex:0,
    lastIndex:0,
    currentPage:1,
    repos:[]
  }
export const PaginationContext=createContext({
  ...INITIAL_STATE,
  toSetFirstIndex:()=>{},
  toSetLastIndex:()=>{},
  toSetCurrentPage:()=>{},
  toSetPageCount:()=>{},
  toSetRepos:()=>{}
})
Enter fullscreen mode Exit fullscreen mode

Reducers need an intial state for when the hook is called so i created one. Also I included the main functionalities I wanted to access in the App component which are mostly setter functions.
The context provider was created next which includes the paginationReducer itself:

export const PaginationProvider=({children})=>{


  const paginationReducer=(state, action)=>{
    const {type, payload}= action
    switch (type){
      case 'CHANGE_FIRST_INDEX':
        return {
          ...state,
          firstIndex:payload
        }
      case 'CHANGE_LAST_INDEX':
        return {
          ...state,
          lastIndex:payload
        }
      case 'change_page':
        return{
          ...state,
          currentPage:payload
        }
        case 'change_page_count':
        return{
          ...state,
          pageCount:payload
        }
        case 'change_repos':
        return{
          ...state,
          repos:payload
        }
      default:
        return state

    } 
  }
Enter fullscreen mode Exit fullscreen mode

The useReducer is then called and all the setter functions were created in the same format as the toSetFirstIndex below:

const [state, dispatch]= useReducer(paginationReducer, INITIAL_STATE)

  const toSetFirstIndex=(idx)=>{
    dispatch({type:'CHANGE_FIRST_INDEX', payload:idx})
  }
Enter fullscreen mode Exit fullscreen mode

Part 3: New Features

I added a Search page which basically included a search bar with an onChange handler. The handler's job is to fetch any profiles matching the value of the searchbox:

  const handleChange=async (e)=>{
    if( e.target.value){
      await fetch(`https://api.github.com/users/${e.target.value}`)
      .then(res=>res.json())
      .then(res=>{
        if(res.status==404){
          setShowError(true)
        } else 
        { 
          setShowError(false)
          setName(res.name)
                  setImage(res.avatar_url)
      }})
     await fetch(`https://api.github.com/users/${e.target.value}/repos?per_page=${reposPerPage}&page=${currentPage}`)
       .then(res=>res.json())
      .then(res=>{

        setRepos(res)})
    }
  }
Enter fullscreen mode Exit fullscreen mode

In the end if a repository is found to match what is in the searchbox, a div is displayed showing the name and profile picture of the user, also some of the user's repositories are shown if the profile picture is clicked on:

return(
    <div >

      <h1>search page</h1>
      <input type='search' onChange={handleChange} />
      {showError &&<h2>userNotFound</h2>}
      <div className='search-result'>
      <img onClick={handleClick} className="search-img" src={image}/>
      <p>{name}</p>
        {showRepos&&<Cards repos={repos}/> }
      </div>
    </div>
  )
Enter fullscreen mode Exit fullscreen mode

Conclusion

This was quite a challenging project at times despite how simple it looked on paper but I am glad i got it done. The final outcome
can be found here and the repository of the project can be found here,
Thanks a lot for reading!

đź’– đź’Ş đź™… đźš©
eyesaidyo
John Rasine Irem

Posted on January 9, 2023

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

Sign up to receive the latest update from our blog.

Related