Hello everyone. In this project, we are going to build a GitHub user search app using Github API.
We will design the UI of the app using Tailwind CSS, with Next.js as a framework.
We will use Github API to pull profile data and display it.
Here is the image of the application that we are going to build:
This is a frontendmentor.io challenge, and our goal is to make this design as close as possible to the given design.
It will be fully responsive, and also have dark mode functionality.
So let's start building 🚀 :
Create a Next.js app with tailwind CSS:
npx create-next-app my-project // without Tailwind CSS installed
or
npx create-next-app -e with-tailwindcss my-project //with Tailwind CSS
Install the necessary npm packages:
Install @heroicons/react.
Install next-themes.
npm install next-themes @heroicons/react
First of all, create a components folder in the root of the directory and add these files:
Don’t be worried about seeing so many components, I have purposely created these components so that it will be easy for you to manage the code.
One important point before that, open the _app.js file inside the pages directory, and add the following code:
Inside _app.js:
import 'tailwindcss/tailwind.css'
import '../styles/globals.css'
import { ThemeProvider } from 'next-themes'
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider attribute="class">
<Component {...pageProps} />
</ThemeProvider>
)
}
export default MyApp
Then, open the tailwind.config.js file, and add the darkMode Key with the value of class like this:
Inside tailwind.config.js:
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
animation: {
wiggle: 'wiggle 1s ease-in-out ',
wiggle_reverse:'wiggle_reverse 0.3s ease-in'
},
keyframes: {
wiggle: {
'0%, 100%': { transform: 'rotate(-60deg)' },
'50%': { transform: 'rotate(60deg)' },
},
wiggle_reverse:{
'0%': { transform: 'rotate(90deg)' },
'100%': { transform: 'rotate(0deg)' },
}
}
},
},
darkMode:"class",
plugins: [],
}
And then next.config.js file, and add the domains for the images like this:
Inside next.config.js:
/** @type {import('next').NextConfig} */
module.exports = {
images: {
domains:["nextjsdev.com","avatars.githubusercontent.com"]
},
reactStrictMode: true,
}
I have added two custom animations that I am using in this application.
Now, paste this code one by one in each component:
Inside Avatar.js:
import Image from "next/image";
import Vercel from "../public/vercel.svg"
const Avatar = ({imageURL}) => {
return (
<div className=" w-[120px] h-[120px] ml-8 ring-[5px] ring-[#3b52d4] dark:ring-[#053bff] rounded-full ">
{imageURL ? (
<Image
src={imageURL ? imageURL : Vercel}
width="120"
height="120"
objectFit="cover"
className="rounded-full "
/>
): (
<p className="text-lg font-bold font-mono text-center pt-8 text-gray-800 dark:text-gray-200">No Image Found</p>
)
}
</div>
)
}
export default Avatar
Inside Loading.js:
export const Loading = () => {
return (
<div className=" w-28 mx-auto mt-40">
<svg className="animate-spin -ml-1 mr-3 h-10 w-10 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
)
}
Inside Logo.js:
import Link from 'next/link'
const Logo = () => {
return (
<Link href="/">
<div className="p-3 rounded-xl cursor-pointer">
<p className="font-mono text-xl font-semibold text-gray-800 dark:text-gray-50 md:text-xl lg:text-2xl">
devfinder
</p>
</div>
</Link>
)
}
export default Logo
Inside Navbar.js:
import { SunIcon } from '@heroicons/react/outline'
import { MoonIcon } from '@heroicons/react/solid'
import Logo from './Logo'
import { useTheme } from 'next-themes'
import { useState, useEffect } from 'react'
const Navbar = () => {
const { systemTheme, theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, [])
const renderThemeChanger = () => {
const currentTheme = theme === 'system' ? systemTheme : theme
if (!mounted) return null;
if (currentTheme === 'dark') {
return (
<div className="flex cursor-pointer items-center ">
<h2 className="text-md font-mono font-semibold uppercase tracking-wider dark:text-gray-50">
Light
<SunIcon
className="ml-1 inline-block h-8 w-8 text-amber-400 animate-wiggle "
onClick={() => setTheme('light')}
/>
</h2>
</div>
)
} else {
return (
<div className="flex cursor-pointer items-center">
<h2 className="text-md font-mono font-semibold uppercase tracking-wider text-slate-500 ">
Dark
<MoonIcon
className="ml-1 inline-block h-8 w-8 text-gray-600 animate-wiggle_reverse"
onClick={() => setTheme('dark')}
/>
</h2>
</div>
)
}
}
return (
<header className="align-items mx-auto mt-4 flex max-w-md justify-between space-x-4 rounded-md p-2 md:max-w-2xl">
<div>
<Logo />
</div>
{renderThemeChanger()}
</header>
)
}
export default Navbar
Inside GithubUser.js:
import UserProfile from './UserProfile'
import UserBio from './UserBio'
import UserStats from './UserStats'
import UserData from './UserData'
const GithubUser = (props) => {
const date = new Date(props.data.created_at)
const newDate = date.toDateString(4, 10).slice(4, 15)
return (
<div className="mx-auto mt-6 flex max-w-md min-h-[470px] flex-col items-end justify-between space-y-4 rounded-lg bg-gray-200 py-6 transition duration-300 ease-in dark:bg-[#2b365e] md:min-h-fit md:max-w-2xl">
<UserProfile
name={props.data.name}
date={newDate}
username={props.data.login}
imageURL={props.data.avatar_url}
/>
<div className=" flex w-full md:max-w-lg flex-col space-y-6 px-6 py-3">
<UserBio bio={props.data.bio} />
<UserStats
repos={props.data.public_repos}
followers={props.data.followers}
following={props.data.following}
/>
<UserData
location={props.data.location}
twitterUsername={props.data.twitter_username}
blog={props.data.blog}
company={props.data.company}
/>
</div>
</div>
)
}
export default GithubUser
Inside SearchBar.js:
import { SearchIcon } from '@heroicons/react/outline'
const SearchBar = ({userName, handleClick , userRef}) => {
return (
<div className=" align-items mx-auto mt-4 flex max-w-md justify-between space-x-2 rounded-lg bg-gray-200 p-2 pb-2 transition duration-300 ease-in dark:bg-[#2b365e] md:max-w-2xl">
<SearchIcon className="mt-3 ml-2 h-6 w-6 text-[#5176ff] dark:text-blue-600" />
<input
name="search"
ref={userRef}
placeholder="Search GitHub username....."
className="text-md mt-1 w-[400px] rounded-md bg-gray-200 px-2 py-2 font-mono leading-6 text-slate-500 placeholder-neutral-400 transition duration-300 ease-in focus:outline-none dark:bg-[#2b365e] dark:text-gray-50 dark:placeholder-slate-500"
/>
<button
onClick={handleClick}
className=" text-md mx-auto h-10 rounded-md bg-gray-50 px-4 font-mono font-medium text-blue-600 shadow-xl transition duration-300 ease-in hover:bg-blue-500 hover:text-blue-100 dark:bg-[#5176ff] dark:text-white dark:hover:bg-blue-600 "
>
Search
</button>
</div>
)
}
export default SearchBar
Inside UserBio.js:
import React from 'react'
const UserBio = ({bio}) => {
return (
<p className="font-mono text-sm font-medium text-gray-800 dark:text-gray-300 text-center ">
Bio-{bio === null ? 'Not Available' :bio}
</p>
)
}
export default UserBio
Inside UserData.js:
import {
LocationMarkerIcon,
LinkIcon,
OfficeBuildingIcon,
} from '@heroicons/react/solid'
const UserData = ({location, twitterUsername,blog, company}) => {
return (
<div className="grid grid-cols-1 gap-6 px-2 py-4 md:grid-cols-2 md:gap-x-10">
<div className="flex items-center space-x-2 font-semibold text-white transition-colors duration-150 hover:text-blue-400">
<LocationMarkerIcon className="h-5 w-5 text-slate-500 dark:text-gray-100" />
<p className="font-mono text-sm font-medium text-gray-900 dark:text-gray-300">
{location ? location : "Not Available"}
</p>
</div>
<div className="flex items-center space-x-2 font-semibold text-white transition-colors duration-150 hover:text-blue-400">
<svg
width="20"
height="20"
fill="currentColor"
className="text-sky-400 opacity-100"
>
<path d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84"></path>
</svg>
<p className="font-mono text-sm font-medium text-gray-900 dark:text-gray-300">
<a
href={`https://twitter.com/${twitterUsername}`}
target="_blank"
>
{twitterUsername ? twitterUsername : "Not Available"}
</a>
</p>
</div>
<div className="flex items-center space-x-2 font-semibold text-white transition-colors duration-150 hover:text-blue-400">
<LinkIcon className="h-5 w-5 text-slate-500 dark:text-gray-100 " />
<p className="decoration-3 font-sm font-mono text-sm font-medium text-gray-900 underline dark:text-gray-300">
<a href={`https://${blog}`} target="_blank">
{blog ? blog :"Not Available"}
</a>
</p>
</div>
<div className="flex items-center space-x-2 font-semibold text-white transition-colors duration-150 hover:text-blue-400">
<OfficeBuildingIcon className="h-5 w-5 text-slate-500 dark:text-gray-100 " />
<p className="font-sm font-mono text-sm font-medium text-gray-900 dark:text-gray-300">
{company ? company : "Not Available"}
</p>
</div>
</div>
)
}
export default UserData
Inside UserProfile.js:
import Avatar from './Avatar'
const UserProfile = ({ name, date, username ,imageURL}) => {
return (
<div className=" flex w-full items-center space-x-4 md:justify-evenly md:space-x-6">
<Avatar imageURL={imageURL} />
<div className="flex flex-1 items-center space-x-6 px-2 md:flex-1 md:items-start md:justify-between">
<h2 className="w-32 md:w-44 font-mono text-lg font-bold text-gray-800 dark:text-gray-50 md:text-2xl">
{name}{' '}
<span className="inline-block font-mono text-sm text-blue-400">
{username && `@${username ? username :'Not Available'}`}
</span>
</h2>
{username && <p className=" md:text-md -mt-2 pl-6 font-mono text-sm font-[400] text-slate-600 dark:text-gray-300 md:mt-0 md:p-6 md:pt-2">
Joined{' '}
<span className="flex font-mono text-xs font-semibold md:inline-block md:text-sm">
{date ? date :"Not Available"}
</span>
</p>}
</div>
</div>
)
}
export default UserProfile
Inside UserStats.js:
import React from 'react'
const UserStats = ({repos,followers ,following}) => {
return (
<div className=" grid grid-cols-3 gap-6 divide-x divide-gray-700 rounded-xl bg-gray-50 py-4 dark:divide-gray-50 dark:bg-[#1e253f]">
<div className="align-items flex flex-col px-4 text-center">
<h4 className="font-mono text-xs font-semibold text-gray-700 dark:text-gray-400 ">
Repos
</h4>
<p className="font-mono text-lg font-extrabold text-gray-700 dark:text-gray-50 ">
{repos ? repos :"Not Available"}
</p>
</div>
<div className="align-items flex flex-col text-center">
<h4 className="font-mono text-xs font-semibold text-gray-700 dark:text-gray-400 ">
Followers
</h4>
<p className="font-mono text-lg font-extrabold text-gray-700 dark:text-gray-50 ">
{followers ? followers :"Not Available"}
</p>
</div>
<div className="align-items flex flex-col text-center">
<h4 className="font-mono text-xs font-semibold text-gray-700 dark:text-gray-400 ">
Following
</h4>
<p className="font-mono text-lg font-extrabold text-gray-700 dark:text-gray-50 ">
{following? following : "Not Available"}
</p>
</div>
</div>
)
}
export default UserStats
Now, open up the terminal, and start the application by running the command:
npm run dev
The application might be working, or it may be showing some error because we haven't completed the application yet, so don't worry about it.
Now, the final step has come. Go to index.js, inside the pages directory, delete all the code inside it, and then paste this code inside:
Inside the index.js:
import Head from 'next/head'
import SearchBar from '../components/SearchBar'
import Navbar from '../components/Navbar'
import GithubUser from '../components/GithubUser'
import { useState, useRef, useEffect } from 'react'
import { Loading } from '../components/Loading'
export default function Home() {
let API = 'https://api.github.com/users/octocat'
const userRef = useRef(null)
const [userName, setUserName] = useState('')
const [data, setData] = useState('')
const [isLoading, setLoading] = useState(false)
function handleClick() {
setUserName(userRef.current.value)
}
useEffect(() => {
setLoading(true)
if (userName) {
API = `https://api.github.com/users/${userName}`
}
fetch(API)
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [userName]);
if(!data) (
<p>No Profile data.</p>
)
return (
<div className="min-h-screen bg-gray-50 py-7 dark:bg-[#1e253f]">
<Head>
<title>GitHub User Finder App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar />
{isLoading ? <Loading /> :
<>
<SearchBar
userName={userName}
handleClick={handleClick}
userRef={userRef}
/>
<GithubUser data={data} />
</>
}
</div>
)
}
After pasting the code, save the application, restart the server, and visit localhost:3000 and you will see the application working.
You can type any valid GitHub username and you will see the data get displayed on the front-end.
Hope you were able to build this amazing Github User finder App for your next project. Feel free to follow me on Twitter and share this if you liked this project 😉.
It took me 4-5 days to build this project, and I would appreciate ✌️ it if you could share this blog post.
If you think that this was helpful, then please do consider visiting my blog website nextjsdev.com and do follow me on Twitter, and connect with me on LinkedIn.
If you were stuck somewhere and were not able to find the solution, you can check out my completed Github Repo here.
I will see you in my next blog ✌️. Till then take care and keep building projects.
Some Useful Links:
Also Published Here