Vikas Rai
Nextjs Dev

Nextjs Dev

Let's Build Github User Finder App with Next.js & Tailwind CSS.

Let's Build Github User Finder App with Next.js & Tailwind CSS.

Vikas Rai's photo
Vikas Rai
ยทJun 30, 2022ยท

9 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Table of contents

  • Create Next.js App with Tailwind CSS
  • To Create the Components and add the dark mode Functionality
  • To fetch the Github API and Display the data
  • Conclusion

Originally Publist at Nextjs Dev

Hello everyone, In this project, we are going to build 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.

Demo Link of the Project

Github Link of the Project

So let's start building ๐Ÿš€ :

Create 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:

  1. Install @heroicons/react
  2. Install next-themes

    npm install next-themes @heroicons/react

To Create the Components and add the dark mode Functionality

First of all, create a components folder in the root of the directory and add these files:

  1. Avatar.js
  2. Loading.js
  3. Logo.js
  4. Navbar.js
  5. GithubUser.js
  6. SearchBar.js
  7. UserBio.js
  8. UserData.js
  9. UserProfile.js
  10. UserStats.js

Don,t get worried by seeing so many components, I have purposely created these components so that it will be easy for you to manage 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 also which 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.

To fetch the Github API and Display the data

Now the final step has come, go to index.js inside the pages directory and delete all the code inside it and then paste this code inside it:

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 and restart the server and visit localhost:3000 and you will the application working.

You can type any valid GitHub username and you will see the data get displayed on the front-end.

Conclusion

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 like 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 and 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 not able to find the solution you can check out my completed Github Repo here.

Thanks for your time to read this project, if you like this please share it on Twitter and Facebook or any other social media and tag me there.

I will see you in my next blog โœŒ๏ธ. Till then take care and keep building projects.

Some Useful Link:

  1. Next.js and Tailwind Installation Docs
  2. Github link for project

Connect with me:

  1. Twitter Link
  2. LinkedIn link
  3. Facebook Link
  4. Github Link

Did you find this article valuable?

Support Vikas Rai by becoming a sponsor. Any amount is appreciated!

See recent sponsors |ย Learn more about Hashnode Sponsors