paint-brush
How to Build a Github User Finder App With Next.js & Tailwind CSSby@raivikas
876 reads
876 reads

How to Build a Github User Finder App With Next.js & Tailwind CSS

by Vikas RaiApril 29th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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 Nextjs as a framework. The app will use Github API to pull profile data and display it. It will be fully responsive and also have dark mode functionality. The project is a [frontendmentorio.io/challenges/github-user-search-app-Q09YOgaH6) challenge, and our goal is to make this design as close as possible to the given design.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Build a Github User Finder App With Next.js & Tailwind CSS
Vikas Rai HackerNoon profile picture


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:


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

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

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


  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 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.

To fetch the Github API and Display the data

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.

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


  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

Also Published Here