Github Profile Finder Project: A Walkthrough
Tags: javascript, react
Published: 2023-01-09 09:26:34

## 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)
```

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))
    })
```
`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])
```
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>
    )
  }
```
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>
```
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>
              
          )
          })}
```
A route was also created to show a `<NotFound/>` component anytime the user goes to a wrong directory (`*`)
```
<Route path='*' element={<NotFound/>}> </Route>
```
###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()
```
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>)
           }
         )
}
```
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>
```
``` 
<button
            className='pg-btn'
            disabled={lastIndex===pageCount || pageCount<buttonsPerPage}
            onClick={()=>{
              toSetFirstIndex(firstIndex+buttonsPerPage)
              
              toSetLastIndex(lastIndex+buttonsPerPage <=pageCount?lastIndex+buttonsPerPage:pageCount)
              
            }}
            >
        next
      </button>
```
##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:()=>{}
})
```
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
        
    } 
  }
```
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})
  }
```
##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)})
    }
  }
```
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>
  )
```
###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](https://eyesaidyo.netlify.app/) and the repository of the project can be found [here](https://github.com/eyesaidyo/my-repos-2),
Thanks a lot for reading!