Github Profile Finder Project: A Walkthrough
John Rasine Irem
Posted on January 9, 2023
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 and the repository of the project can be found here,
Thanks a lot for reading!
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
November 28, 2024
November 28, 2024