Table of Contents

Older VersionHomepageSkillsBlogsProjectsCurrent VersionSetup ProjectCreate ProjectInstall Tailwind CSSAdd the Tailwind directives to your CSSConfigure your template pathsStart the serverSetup `next.config.js`Setup `jsconfig.json`Disabling some ESLint rulesAdding DarkMode SupportCreating LayoutCreating NavbarAdding Scroll to top ButtonCreating FooterInstalling SWRCreating `fetcher` functionCreating Social Media LinksAdding Now Playing Spotify RouteAdding QRCodeContainerInstalling dependenciesCreating useWindowLocation HookAdding NProgressbarInstalling nprogressUsing nprogressAdding FontsDownloading FontsLink the fontsAdd fonts in `_document.js`Add fonts in `global.css`Adding fonts to `vercel.json`Adding StylingAdding SEO SupportCreate MetaDataCreating Meta Data ComponentHomepageUser ProfileSkillsCreating `getPinnedSkills`Creating `SkillSection` ComponentRecent PostsContactInstalling dependenciesCreating Contact ComponentCreating ContactForm ComponentStats PageBlog and GitHub StatsDev.to StatsGithub StatsTop StreamsTop ArtistsCertificates PageCreating Certificate DataCreating Certificate PageProjects PageCreating Projects DataCreating Project PageUtilities PageCreating Utility DataCustom SVG IconsDittoFluxMicrosoftToDoRainDropShareXUPICreating Utilities PageBlogs PageInstalling DependenciesGet the MDX ContentCreating Blogs pageCreating Blog ComponentAdding Bookmark SupportCreating Bookmark PageAdding RSS feed for blogCreating Individual Blog PagesCreating `PageNotFound` ComponentCreating `BlogLayout` ComponentCreate Table of ContentsCreating `ScrollProgressBar` ComponentNewsletter ComponentShare Blog on Social MediaMDXComponentsCodeSandboxCodeTitleCodepenDangerFigcaptionNextAndPreviousButtonPreStepTipWarningYouTubeExport MDXComponentsAbout me PageCreating `Support` componentFramerMotion Custom ComponentsAnimatedButtonAnimatedDivAnimatedHeadingAnimatedInputAnimatedLinkAnimatedTextAnimatedTextAreaAdding SitemapGenerating PWAAdding ScreenshotsAdding ShortcutsWrapping up

How I Made My Portfolio with Next.js

Oct 12, 2022
126 min read
25106 words

Everyone needs to establish a portfolio or personal website so that potential employers can get a better sense of your work. I believe everyone should make one, even if it's very simple or doesn't have much to show. Having one is always a good idea.

We will be looking at my portfolio built with Next.js, which includes many things (which we will explore later). Without further ado, let's get started.

Initially, I didn't have a great design in mind. So I just started coding and it turned out better than I expected (trust me, I thought worse). You will see two versions of my portfolio. There is an older one and a newer one.

Older Version

Previously, I had only four main pages: Homepage (About me), Skills, Blog, and Projects. I was using Dev.to API to fetch my blogs and their data.

Homepage

homepage

There were seven sections on the old homepage: About me, Top Skills, Recent blogs, Certification, Projects, FAQ, and Contact. Rather than showing all blogs and projects, it just shows 10 cards.

Skills

skills

The skill page had a progress bar showing how well I knew the language or skill, along with a brief description.

Blogs

blogs

There are a lot of things going on in the blog. First, here is a search bar and then the Blog sorting system. After that, you have a blog card.

sortingsystem

When hovering over a blog card, you see two options, view and dev.to, along with a brief description of the blog.

blog

As you click on the View Button, then it would have taken you to the Blog. Which looks like this:

blog

Projects

projects

In Projects, there are only three buttons through which you can just go to the GitHub link Live Project, and can share it.

This was my old portfolio. You can also share your insights about this portfolio. I look forward to hearing what you have to say.

Current Version

The current version of the portfolio has many things. We will be looking at them one by one along with how I implemented them. Lets' just build it.

Take a look at the current version of my portfolio by vising j471n.in

Setup Project

Create Project

Start by creating a new Next.js project if you don’t have one set up already. The most common approach is to use Create Next App.

terminal

npx create-next-app my-project
cd my-project

Install Tailwind CSS

Install tailwindcss and its peer dependencies via npm, and then run the init command to generate both tailwind.config.js and postcss.config.js.

terminal

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Add the Tailwind directives to your CSS

Add the @tailwind directives for each of Tailwind's layers to your ./styles/globals.css file.

styles/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Configure your template paths

Add the paths to all of your template files in your tailwind.config.js file.

tailwind.config.js

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
    "./layout/*.{js,jsx,ts,tsx}",
  ],
  darkMode: "class", // to setup dark mode
  theme: {
    // Adding New Fonts
    fontFamily: {
      inter: ["Inter", "sans-serif"],
      sarina: ["Sarina", "cursive"],
      barlow: ["Barlow", "sans-serif"],
      mono: ["monospace"],
    },
    extend: {
      colors: {
        darkPrimary: "#181A1B",
        darkSecondary: "#25282A",
        darkWhite: "#f2f5fa",
      },
      listStyleType: {
        square: "square",
        roman: "upper-roman",
      },
      animation: {
        wiggle: "wiggle 1s ease-in-out infinite",
        "photo-spin": "photo-spin 2s 1 linear forwards",
      },
      keyframes: {
        wiggle: {
          "0%, 100%": { transform: "rotate(-3deg)" },
          "50%": { transform: "rotate(3deg)" },
        },
        "photo-spin": {
          "0%": { transform: "rotate(0deg)" },
          "100%": { transform: "rotate(360deg)" },
        },
      },
      screens: {
        // Custom Screen styles
        "3xl": "2000px",
        xs: "480px",
      },
    },
    // Adding Tailwind Plugins
    plugins: [
      require("@tailwindcss/line-clamp"),
      require("@tailwindcss/typography"),
      require("tailwind-scrollbar-hide"),
    ],
  },
};

I am going to use pnpm as the package manager instead of npm. You can use npm or yarn according to your preference.

Now we need to install the tailwindcss plugins that we just added to tailwind.config.js

terminal

pnpm install @tailwindcss/line-clamp @tailwindcss/typography tailwind-scrollbar-hide

@tailwindcss/typography will be used in styling the blog.

Start the server

Run your build process with pnpm run dev. And your project will be available at http://localhost:3000

terminal

npm run dev

Setup next.config.js

Installing @next/bundle-analyzer

terminal

pnpm i @next/bundle-analyzer next-pwa

next.config.js

const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});
 
const withPWA = require("next-pwa");
 
module.exports = withBundleAnalyzer(
  withPWA({
    webpack: true,
    webpack: (config) => {
      // Fixes npm packages that depend on `fs` module
      config.resolve.fallback = { fs: false };
      return config;
    },
    reactStrictMode: true,
    images: {
      domains: [
        "cdn.buymeacoffee.com",
        "res.cloudinary.com",
        "imgur.com",
        "i.imgur.com",
        "cutt.ly",
        "activity-graph.herokuapp.com",
        "i.scdn.co", // images from spotify
        "images.unsplash.com",
      ],
    },
 
    // Pwa Setting
    pwa: {
      dest: "public",
      register: true,
      skipWaiting: true,
      disable: process.env.NODE_ENV === "development",
      publicExcludes: ["!resume.pdf"], // don't cache pdf which I'll add later
    },
  })
);

Setup jsconfig.json

Setting up jsconfig.json to make import easier. You don't need to use ../../../.. anymore, you can just use @ to refer to any folder.

json

jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["components/*"],
      "@lib/*": ["lib/*"],
      "@utils/*": ["utils/*"],
      "@content/*": ["content/*"],
      "@styles/*": ["styles/*"],
      "@context/*": ["context/*"],
      "@layout/*": ["layout/*"],
      "@hooks/*": ["hooks/*"]
    }
  }
}

Disabling some ESLint rules

.eslintrc.json

{
  "extends": "next/core-web-vitals",
  "rules": {
    "react/no-unescaped-entities": 0,
    "@next/next/no-img-element": "off"
  }
}

Adding DarkMode Support

This is just to create a Dark mode Support using Context API:

context/darkModeContext.js

import React, { useState, useContext, useEffect, createContext } from "react";
const DarkModeContext = createContext(undefined);
 
export function DarkModeProvider({ children }) {
  const [isDarkMode, setDarkMode] = useState(false);
 
  function updateTheme() {
    const currentTheme = localStorage.getItem("isDarkMode") || "false";
    if (currentTheme === "true") {
      document.body.classList.add("dark");
      setDarkMode(true);
    } else {
      document.body.classList.remove("dark");
      setDarkMode(false);
    }
  }
  useEffect(() => {
    updateTheme();
  }, []);
  function changeDarkMode(value) {
    localStorage.setItem("isDarkMode", value.toString());
    // setDarkMode(value);
    updateTheme();
  }
 
  return (
    <DarkModeContext.Provider value={{ isDarkMode, changeDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
}
 
export const useDarkMode = () => {
  const context = useContext(DarkModeContext);
  if (context === undefined) {
    throw new Error("useAuth can only be used inside AuthProvider");
  }
  return context;
};

Now Wrap the _app.js with DarkModeProvider:

_app.js

import "@styles/globals.css";
import { DarkModeProvider } from "@context/darkModeContext";
 
function MyApp({ Component, pageProps }) {
  return (
   {/* Adding Darkmode Provider */}
    <DarkModeProvider>
        <Component {...pageProps} />
    </DarkModeProvider>
  );
}

Creating Layout

layout/Layout.js

import { useState } from "react";
import TopNavbar from "../components/TopNavbar";
import ScrollToTopButton from "../components/ScrollToTopButton";
import Footer from "../components/Footer";
import QRCodeContainer from "@components/QRCodeContainer";
 
export default function Layout({ children }) {
  const [showQR, setShowQR] = useState(false);
  return (
    <>
      <TopNavbar />
      <main>{children}</main>
      <Footer setShowQR={setShowQR} showQR={showQR} />
      <ScrollToTopButton />
      <QRCodeContainer showQR={showQR} setShowQR={setShowQR} />
    </>
  );
}

Don't worry we will create all of them one by one:

Creating Navbar

This is what we are going to create: navbar

Before we build Navbar we need to install some dependencies first:

terminal

pnpm install react-icons framer-motion

Let's set up routes first. I am using a static Array where I can add a new route and it will automatically be visible to the navbar:

utils/utils.js

export const navigationRoutes = [
  "home",
  "about",
  "stats",
  "utilities",
  "blogs",
  "certificates",
  "projects",
  "newsletter",
  "rss",
];

These are the routes we are going to create. Let's look at the TopNavbar component now:

components/TopNavbar.js

/* Importing modules */
 
import React, { useEffect, useState, useRef, useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { motion, useAnimation, AnimatePresence } from "framer-motion";
import {
  FadeContainer,
  hamFastFadeContainer,
  mobileNavItemSideways,
  popUp,
} from "../content/FramerMotionVariants";
import { useDarkMode } from "../context/darkModeContext";
import { navigationRoutes } from "../utils/utils";
import { FiMoon, FiSun } from "react-icons/fi";

We imported many things, but we have not created FramerMotionVariants yet. So let's create them:

content/FramerMotionVariants.js

export const FadeContainer = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { delayChildren: 0, staggerChildren: 0.1 },
  },
};
 
export const hamFastFadeContainer = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      delayChildren: 0,
      staggerChildren: 0.1,
    },
  },
};
 
export const mobileNavItemSideways = {
  hidden: { x: -40, opacity: 0 },
  visible: {
    x: 0,
    opacity: 1,
  },
};
 
export const popUp = {
  hidden: { scale: 0, opacity: 0 },
  visible: {
    opacity: 1,
    scale: 1,
  },
  transition: {
    type: "spring",
  },
};

These are the variants for Navbar Animation for desktop and mobile devices as well.

Learn about Framer motion

Now, back to the TopNavbar:

components/TopNavbar.js

/* Importing Modules */
import React, { useEffect, useState, useRef, useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { motion, useAnimation, AnimatePresence } from "framer-motion";
import {
  FadeContainer,
  hamFastFadeContainer,
  mobileNavItemSideways,
  popUp,
} from "../content/FramerMotionVariants";
import { useDarkMode } from "../context/darkModeContext";
import { navigationRoutes } from "../utils/utils";
import { FiMoon, FiSun } from "react-icons/fi";
 
/* TopNavbar Component */
export default function TopNavbar() {
  const router = useRouter();
  const navRef = useRef(null);
 
  /*  Using to control animation as I'll show the name to the mobile navbar when you scroll a bit
   * demo: https://i.imgur.com/5LKI5DY.gif
   */
  const control = useAnimation();
  const [navOpen, setNavOpen] = useState(false);
  const { isDarkMode, changeDarkMode } = useDarkMode();
 
  // Adding Shadow, backdrop to the navbar as user scroll the screen
  const addShadowToNavbar = useCallback(() => {
    if (window.pageYOffset > 10) {
      navRef.current.classList.add(
        ...[
          "shadow",
          "backdrop-blur-xl",
          "bg-white/70",
          "dark:bg-darkSecondary",
        ]
      );
 
      control.start("visible");
    } else {
      navRef.current.classList.remove(
        ...[
          "shadow",
          "backdrop-blur-xl",
          "bg-white/70",
          "dark:bg-darkSecondary",
        ]
      );
      control.start("hidden");
    }
  }, [control]);
 
  useEffect(() => {
    window.addEventListener("scroll", addShadowToNavbar);
    return () => {
      window.removeEventListener("scroll", addShadowToNavbar);
    };
  }, [addShadowToNavbar]);
 
  // to lock the scroll when mobile is open
  function lockScroll() {
    const root = document.getElementsByTagName("html")[0];
    root.classList.toggle("lock-scroll"); // class is define in the global.css
  }
 
  /* To Lock  the Scroll when user visit the mobile nav page */
  function handleClick() {
    lockScroll();
    setNavOpen(!navOpen);
  }
 
  return (
    <div
      className="fixed w-full dark:text-white top-0 flex items-center justify-between px-4 py-[10px] sm:p-4 sm:px-6 z-50 print:hidden"
      ref={navRef}
    >
      {/* Mobile Navigation Hamburger and MobileMenu */}
      <HamBurger open={navOpen} handleClick={handleClick} />
      <AnimatePresence>
        {navOpen && (
          <MobileMenu links={navigationRoutes} handleClick={handleClick} />
        )}
      </AnimatePresence>
 
      <Link href="/" passHref>
        <div className="flex gap-2 items-center cursor-pointer z-50">
          <motion.a
            initial="hidden"
            animate="visible"
            variants={popUp}
            className="relative hidden sm:inline-flex mr-3"
          >
            <h1 className="font-sarina text-xl">JS</h1>
          </motion.a>
          <motion.p
            initial="hidden"
            animate={control}
            variants={{
              hidden: { opacity: 0, scale: 1, display: "none" },
              visible: { opacity: 1, scale: 1, display: "inline-flex" },
            }}
            className="absolute sm:!hidden w-fit left-0 right-0 mx-auto flex justify-center  text-base font-sarina"
          >
            Jatin Sharma
          </motion.p>
        </div>
      </Link>
 
      {/* Top Nav list */}
      <motion.nav className="hidden sm:flex z-10 md:absolute md:inset-0 md:justify-center">
        <motion.div
          initial="hidden"
          animate="visible"
          variants={FadeContainer}
          className="flex items-center md:gap-2"
        >
          {navigationRoutes.slice(0, 7).map((link, index) => {
            return (
              <NavItem
                key={index}
                href={`/${link}`}
                text={link}
                router={router}
              />
            );
          })}
        </motion.div>
      </motion.nav>
 
      {/* DarkMode Container */}
      <motion.div
        initial="hidden"
        animate="visible"
        variants={popUp}
        className="cursor-pointer rounded-full z-30 transition active:scale-75"
        title="Toggle Theme"
        onClick={() => changeDarkMode(!isDarkMode)}
      >
        {isDarkMode ? (
          <FiMoon className="h-6 w-6 sm:h-7 sm:w-7 select-none transition active:scale-75" />
        ) : (
          <FiSun className="h-6 w-6 sm:h-7 sm:w-7 select-none transition active:scale-75" />
        )}
      </motion.div>
    </div>
  );
}
 
// NavItem Container
function NavItem({ href, text, router }) {
  const isActive = router.asPath === (href === "/home" ? "/" : href);
  return (
    <Link href={href === "/home" ? "/" : href} passHref>
      <motion.a
        variants={popUp}
        className={`${
          isActive
            ? "font-bold text-gray-800 dark:text-gray-100"
            : " text-gray-600 dark:text-gray-300"
        } sm:inline-block transition-all text-[17px] hidden px-2 md:px-3 py-[3px] hover:bg-gray-100 dark:hover:bg-neutral-700/50 rounded-md`}
      >
        <span className="capitalize">{text}</span>
      </motion.a>
    </Link>
  );
}
 
// Hamburger Button
function HamBurger({ open, handleClick }) {
  return (
    <motion.div
      style={{ zIndex: 1000 }}
      initial="hidden"
      animate="visible"
      variants={popUp}
      className="sm:hidden"
    >
      {!open ? (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="h-6 w-6 cursor-pointer select-none transform duration-300 rounded-md active:scale-50"
          onClick={handleClick}
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
          strokeWidth={2}
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M4 6h16M4 12h16M4 18h16"
          />
        </svg>
      ) : (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="h-6 w-6 cursor-pointer select-none transform duration-300  rounded-md active:scale-50"
          onClick={handleClick}
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
          strokeWidth={2}
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M6 18L18 6M6 6l12 12"
          />
        </svg>
      )}
    </motion.div>
  );
}
 
// Mobile navigation menu
const MobileMenu = ({ links, handleClick }) => {
  return (
    <motion.div
      className="absolute font-normal bg-white dark:bg-darkPrimary w-screen h-screen top-0 left-0 z-10 sm:hidden"
      variants={hamFastFadeContainer}
      initial="hidden"
      animate="visible"
      exit="hidden"
    >
      <motion.nav className="mt-28 mx-8 flex flex-col">
        {links.map((link, index) => {
          const navlink =
            link.toLowerCase() === "home" ? "/" : `/${link.toLowerCase()}`;
          return (
            <Link href={navlink} key={`mobileNav-${index}`} passHref>
              <motion.a
                href={navlink}
                className="border-b border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100 font-semibold flex w-auto py-4 capitalize text-base cursor-pointer"
                variants={mobileNavItemSideways}
                onClick={handleClick}
              >
                {link === "rss" ? link.toUpperCase() : link}
              </motion.a>
            </Link>
          );
        })}
      </motion.nav>
    </motion.div>
  );
};
  • This is the whole file for the TopNavbar I used. Where I added a scroll event to the window just to add shadow to Navbar and show my name if you are on a mobile device.
  • I also locked the scroll by calling the lockScroll function when Mobile Navbar is active/open. So that user won't be able to scroll.
  • I am only showing only 7 navigation routes (navigationRoutes.slice(0, 7)) so that navbar looks simple and clean. I'll add the remaining in the footer section.
  • There is also a button on the right side of the navigation bar that switches from dark mode to light mode.

Results Now when you'll scroll , then the name will be shown on mobile devices, this is because of that control we added earlier:

name

After doing that when you open mobile navigation it will animate like this:

side navbar animation

TopNavbar have four components:

  • TopNavbar: Main Navigation Panel
  • Hamburger: Top-left hamburger button
  • MobileMenu: Mobile Navigation list or menu
  • NavItem: Navigation link/item for desktop mode

We have already added TopNavbar to Layout Component.

Adding Scroll to top Button

components/ScrollToTopButton.js

import { IoIosArrowUp } from "react-icons/io";
import { useEffect, useState } from "react";
import useScrollPercentage from "@hooks/useScrollPercentage";
 
export default function ScrollToTopButton() {
  const [showButton, setShowButton] = useState(false);
  const scrollPercentage = useScrollPercentage();
 
  useEffect(() => {
    if (scrollPercentage < 95 && scrollPercentage > 10) {
      setShowButton(true);
    } else {
      setShowButton(false);
    }
  }, [scrollPercentage]);
 
  // This function will scroll the window to the top
  const scrollToTop = () => {
    window.scrollTo({
      top: 0,
      behavior: "smooth", // for smoothly scrolling
    });
  };
 
  return (
    <>
      {showButton && (
        <button
          onClick={scrollToTop}
          aria-label="Scroll To Top"
          className="fixed bottom-20 right-8 md:bottom-[50px] md:right-[20px]  z-40 print:hidden"
        >
          <IoIosArrowUp className="bg-black dark:bg-gray-200 dark:text-darkPrimary text-white rounded-lg shadow-lg text-[45px] md:mr-10" />
        </button>
      )}
    </>
  );
}

ScrollToTopButton won't show when you are at the top or at the bottom of the page. It will only show when your viewport is somewhere between 10%-95%. After doing this it will look something like this:

Scroll Top top button

I have also made an article with an explanation you can read that too:

blog image

Scroll to the top with JS

The footer could be a little bit complex as it uses Spotify API. This is what we are going to build:

footer

Spotify Integration shows if I am currently playing any song or not. The Footer changed on that basis. The following image shows both states. Which we will create in a moment.

footer design

There is a QR Code Button at the bottom of the Footer which I'll cover in the next section.

creating components/Footer.js:

components/Footer.js

import Link from "next/link";
import Image from "next/image";
import { FadeContainer, popUp } from "../content/FramerMotionVariants";
import { navigationRoutes } from "../utils/utils";
import { motion } from "framer-motion";
import { SiSpotify } from "react-icons/si";
import { HiOutlineQrcode } from "react-icons/hi";
import useSWR from "swr"; // not installed yet
 
// Not create yet
import fetcher from "../lib/fetcher";
import socialMedia from "../content/socialMedia";

Installing SWR

First, we need to install swr. SWR is a strategy to first return the data from the cache (stale), then send the fetch request (revalidate), and finally, come up with up-to-date data.

terminal

pnpm i swr

SWR uses the fetcher function which is just a wrapper of the native fetch. You can visit their docs

Creating fetcher function

lib/fetcher.js

export default async function fetcher(url) {
  return fetch(url).then((r) => r.json());
}

As you can see in the footer we have social media links as well which have not been created yet. Let's do it then:

content/socialMedia.js

import { AiOutlineInstagram, AiOutlineTwitter } from "react-icons/ai";
import { BsFacebook, BsGithub, BsLinkedin } from "react-icons/bs";
import { FaDev } from "react-icons/fa";
import { HiMail } from "react-icons/hi";
import { SiCodepen } from "react-icons/si";
 
export default [
  {
    title: "Twitter",
    Icon: AiOutlineTwitter,
    url: "https://twitter.com/intent/follow?screen_name=j471n_",
  },
  {
    title: "LinkedIn",
    Icon: BsLinkedin,
    url: "https://www.linkedin.com/in/j471n/",
  },
  {
    title: "Github",
    Icon: BsGithub,
    url: "https://github.com/j471n",
  },
  {
    title: "Instagram",
    Icon: AiOutlineInstagram,
    url: "https://www.instagram.com/j471n_",
  },
  {
    title: "Dev.to",
    Icon: FaDev,
    url: "https://dev.to/j471n",
  },
  {
    title: "Codepen",
    Icon: SiCodepen,
    url: "https://codepen.io/j471n",
  },
  {
    title: "Facebook",
    Icon: BsFacebook,
    url: "https://www.facebook.com/ja7in/",
  },
  {
    title: "Mail",
    Icon: HiMail,
    url: "mailto:jatinsharma8669@gmail.com",
  },
];

I am importing react icons as well , maybe you want to add Icons along with the text. It's up to you if you don't want then you can remove the icon part.

Now we have done almost everything, let's continue with Footer.js:

components/Footer.js

/*......previous code......*/
 
export default function Footer({ setShowQR, showQR }) {}

Taking setShowQR and showQR as props to trigger the bottom QR Code button at the bottom.

components/Footer.js

/*......previous code......*/
 
export default function Footer({ setShowQR, showQR }) {
  const { data: currentSong } = useSWR("/api/now-playing", fetcher);
}

Now we will use useSWR to fetch the Next.js API route that will return the currently playing song if the user is playing otherwise simple false

API Response

// when not playing
{
  "isPlaying": false
}
 
// when playing
{
  "album": "See You Again (feat. Charlie Puth)",
  "albumImageUrl": "https://i.scdn.co/image/ab67616d0000b2734e5df11b17b2727da2b718d8",
  "artist": "Wiz Khalifa, Charlie Puth",
  "isPlaying": true,
  "songUrl": "https://open.spotify.com/track/2JzZzZUQj3Qff7wapcbKjc",
  "title": "See You Again (feat. Charlie Puth)"
}

It will return with which song I am playing along with details. But we have not created /api/now-playing route. Did we? Let's create it then.

Adding Now Playing Spotify Route

If you are not familiar with Nextjs API routes then I'll highly recommend you to look at the docs first.

Create pages/api/now-playing.js file:

pages/api/now-playing.js

export default async function handler(req, res) {}

Now the most complex part is Spotify integration. But don't worry about that I have a blog on that in detail about how you can implement that so you can read that here (this is a must):

blog image

How to use Spotify API with Next.js

After implementing Spotify API integration you will have three things:

  • SPOTIFY_CLIENT_ID=
  • SPOTIFY_CLIENT_SECRET=
  • SPOTIFY_REFRESH_TOKEN=

Add these to your .env.local along with their values and restart the server.

Now we will create a function that will fetch the data on the server.

Create lib/spotify.js:

lib/spotify.js

/* This function use "refresh_token" to get the "access_token" that will be later use for authorization   */
const getAccessToken = async () => {
  const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN;
 
  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`
      ).toString("base64")}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token,
    }),
  });
 
  return response.json();
};
 
/* Uses "access_token" to get the current playing song */
export const currentlyPlayingSong = async () => {
  const { access_token } = await getAccessToken();
 
  return fetch("https://api.spotify.com/v1/me/player/currently-playing", {
    headers: {
      Authorization: `Bearer ${access_token}`,
    },
  });
};

In the above code we just added two functions:

  • getAccessToken: This will use the refresh_token and return the access_token to authorization for other API endpoints.
  • currentlyPlayingSong: This is fetch the current playing song and returns the JSON data.

You can learn more about Spotify Authorization in their documentation.

Now, we have created a function to fetch the data. Let's call it in our API Routes.

pages/api/now-playing.js

import { currentlyPlayingSong } from "@lib/spotify";
 
export default async function handler(req, res) {
  const response = await currentlyPlayingSong();
 
  if (response.status === 204 || response.status > 400) {
    return res.status(200).json({ isPlaying: false });
  }
 
  const song = await response.json();
 
  /* Extracting the main info that we need */
  const isPlaying = song.is_playing;
  const title = song.item.name;
  const artist = song.item.artists.map((_artist) => _artist.name).join(", ");
  const album = song.item.album.name;
  const albumImageUrl = song.item.album.images[0].url;
  const songUrl = song.item.external_urls.spotify;
 
  /* Return the data as JSON */
  return res.status(200).json({
    album,
    albumImageUrl,
    artist,
    isPlaying,
    songUrl,
    title,
  });
}

It was simple Right? Now, we have added the API route (/api/now-playing). Let's call it in Footer and fetch the currently playing song.

components/Footer.js

/*......previous code......*/
 
export default function Footer({ setShowQR, showQR }) {
  // we just implemented this line in API Routes now it will work as expected
  const { data: currentSong } = useSWR("/api/now-playing", fetcher);
}

If you go to http://localhost:3000/api/now-playing you will see some data is returned. If you didn't mess up. (If you did then try reading the about part again)

components/Footer.js

/*......previous code......*/
 
export default function Footer({ setShowQR, showQR }) {
  const { data: currentSong } = useSWR("/api/now-playing", fetcher);
 
  return (
    <footer className=" text-gray-600 dark:text-gray-400/50 w-screen font-inter mb-20 print:hidden">
      <motion.div
        initial="hidden"
        whileInView="visible"
        variants={FadeContainer}
        viewport={{ once: true }}
        className="max-w-4xl 2xl:max-w-5xl 3xl:max-w-7xl p-5 border-t-2 border-gray-200  dark:border-gray-400/10 mx-auto text-sm sm:text-base flex flex-col gap-5"
      >
        <div>
          {currentSong?.isPlaying ? (
            <WhenPlaying song={currentSong} />
          ) : (
            <NotPlaying />
          )}
        </div>
 
        <section className="grid grid-cols-3 gap-10">
          <div className="flex flex-col gap-4 capitalize">
            {navigationRoutes.slice(0, 4).map((text, index) => {
              return (
                <FooterLink key={index} id={index} route={text} text={text} />
              );
            })}
          </div>
          <div className="flex flex-col gap-4 capitalize">
            {navigationRoutes
              .slice(4, navigationRoutes.length)
              .map((route, index) => {
                let text = route;
                if (route === "rss") text = "RSS";
                return <FooterLink key={index} route={route} text={text} />;
              })}
          </div>
          <div className="flex flex-col gap-4 capitalize">
            {socialMedia.slice(0, 4).map((platform, index) => {
              return (
                <Link key={index} href={platform.url} passHref>
                  <motion.a
                    className="hover:text-black dark:hover:text-white w-fit"
                    variants={popUp}
                    target="_blank"
                    rel="noopener noreferrer"
                    href={platform.url}
                  >
                    {platform.title}
                  </motion.a>
                </Link>
              );
            })}
          </div>
        </section>
      </motion.div>
 
      <div className="w-full flex justify-center">
        <div
          onClick={() => setShowQR(!showQR)}
          className="bg-gray-700 text-white p-4 rounded-full cursor-pointer transition-all active:scale-90 hover:scale-105"
        >
          <HiOutlineQrcode className="w-6 h-6 " />
        </div>
      </div>
    </footer>
  );
}

In the about Code, we have not implemented three things yet:

  • FooterLink: To show the link in the footer
  • NotPlaying: when I am not playing a song then show this component
  • WhenPlaying: Show this component when I am playing the song

Let's add these components inside components/Footer.js. You can make separate components for them, it's up to you. I've added those inside the Footercomponents:

components/Footer.js

/*......previous code (Footer Component)......*/
 
function FooterLink({ route, text }) {
  return (
    <Link href={`/${route}`} passHref>
      <motion.a
        className="hover:text-black dark:hover:text-white w-fit"
        variants={popUp}
        href={`/${route}`}
      >
        {text}
      </motion.a>
    </Link>
  );
}
 
function NotPlaying() {
  return (
    <div className="flex items-center gap-2 flex-row-reverse sm:flex-row justify-between sm:justify-start">
      <SiSpotify className="w-6 h-6" />
      <div className="flex flex-col sm:flex-row sm:items-center sm:gap-3">
        <div className="font-semibold md:text-lg text-black dark:text-white">
          Not Playing
        </div>
        <span className="hidden md:inline-flex">—</span>
        <p className="text-gray-500 text-xs sm:text-sm">Spotify</p>
      </div>
    </div>
  );
}
 
function WhenPlaying({ song }) {
  return (
    <div className="flex flex-col gap-4">
      <h4 className="text-lg font-semibold">Now Playing</h4>
      <Link href={song.songUrl} passHref>
        <a
          href={song.songUrl}
          className="flex items-center justify-between bg-gray-200 dark:bg-darkSecondary  p-3 sm:p-4 rounded-sm"
        >
          <div className=" flex items-center gap-2">
            <div className="w-10 h-10">
              <Image
                alt={song.title}
                src={song.albumImageUrl}
                width={40}
                height={40}
                layout="fixed"
                quality={50}
                placeholder="blur"
                blurDataURL={song.albumImageUrl}
              />
            </div>
            <div className="flex flex-col sm:flex-row sm:items-center sm:gap-3">
              <h3 className="font-semibold md:text-lg text-black dark:text-white animate-">
                {song.title}
              </h3>
              <span className="hidden md:inline-flex">—</span>
              <p className="text-gray-600 text-xs sm:text-sm">{song.artist}</p>
            </div>
          </div>
          <div className="flex items-center gap-2">
            <SiSpotify className="w-6 h-6 text-green-500 animate-[spin_2s_linear_infinite]" />
          </div>
        </a>
      </Link>
    </div>
  );
}

If you are confused that how will it turn out , then the following is the full code of the footer:

components/Footer.js

import Link from "next/link";
import Image from "next/image";
import socialMedia from "../content/socialMedia";
import { FadeContainer, popUp } from "../content/FramerMotionVariants";
import { navigationRoutes } from "../utils/utils";
import { motion } from "framer-motion";
import { SiSpotify } from "react-icons/si";
import useSWR from "swr";
import fetcher from "../lib/fetcher";
import { HiOutlineQrcode } from "react-icons/hi";
 
export default function Footer({ setShowQR, showQR }) {
  const { data: currentSong } = useSWR("/api/now-playing", fetcher);
 
  return (
    <footer className=" text-gray-600 dark:text-gray-400/50 w-screen font-inter mb-20 print:hidden">
      <motion.div
        initial="hidden"
        whileInView="visible"
        variants={FadeContainer}
        viewport={{ once: true }}
        className="max-w-4xl 2xl:max-w-5xl 3xl:max-w-7xl p-5 border-t-2 border-gray-200  dark:border-gray-400/10 mx-auto text-sm sm:text-base flex flex-col gap-5"
      >
        <div>
          {currentSong?.isPlaying ? (
            <WhenPlaying song={currentSong} />
          ) : (
            <NotPlaying />
          )}
 
          <div></div>
        </div>
 
        <section className="grid grid-cols-3 gap-10">
          <div className="flex flex-col gap-4 capitalize">
            {navigationRoutes.slice(0, 4).map((text, index) => {
              return (
                <FooterLink key={index} id={index} route={text} text={text} />
              );
            })}
          </div>
          <div className="flex flex-col gap-4 capitalize">
            {navigationRoutes
              .slice(4, navigationRoutes.length)
              .map((route, index) => {
                let text = route;
                if (route === "rss") text = "RSS";
                return <FooterLink key={index} route={route} text={text} />;
              })}
          </div>
          <div className="flex flex-col gap-4 capitalize">
            {socialMedia.slice(0, 4).map((platform, index) => {
              return (
                <Link key={index} href={platform.url} passHref>
                  <motion.a
                    className="hover:text-black dark:hover:text-white w-fit"
                    variants={popUp}
                    target="_blank"
                    rel="noopener noreferrer"
                    href={platform.url}
                  >
                    {platform.title}
                  </motion.a>
                </Link>
              );
            })}
          </div>
        </section>
      </motion.div>
 
      <div className="w-full flex justify-center">
        <div
          onClick={() => setShowQR(!showQR)}
          className="bg-gray-700 text-white p-4 rounded-full cursor-pointer transition-all active:scale-90 hover:scale-105"
        >
          <HiOutlineQrcode className="w-6 h-6 " />
        </div>
      </div>
    </footer>
  );
}
 
function FooterLink({ route, text }) {
  return (
    <Link href={`/${route}`} passHref>
      <motion.a
        className="hover:text-black dark:hover:text-white w-fit"
        variants={popUp}
        href={`/${route}`}
      >
        {text}
      </motion.a>
    </Link>
  );
}
 
function NotPlaying() {
  return (
    <div className="flex items-center gap-2 flex-row-reverse sm:flex-row justify-between sm:justify-start">
      <SiSpotify className="w-6 h-6" />
      <div className="flex flex-col sm:flex-row sm:items-center sm:gap-3">
        <div className="font-semibold md:text-lg text-black dark:text-white">
          Not Playing
        </div>
        <span className="hidden md:inline-flex">—</span>
        <p className="text-gray-500 text-xs sm:text-sm">Spotify</p>
      </div>
    </div>
  );
}
 
function WhenPlaying({ song }) {
  return (
    <div className="flex flex-col gap-4">
      <h4 className="text-lg font-semibold">Now Playing</h4>
      <Link href={song.songUrl} passHref>
        <a
          href={song.songUrl}
          className="flex items-center justify-between bg-gray-200 dark:bg-darkSecondary  p-3 sm:p-4 rounded-sm"
        >
          <div className=" flex items-center gap-2">
            <div className="w-10 h-10">
              <Image
                alt={song.title}
                src={song.albumImageUrl}
                width={40}
                height={40}
                layout="fixed"
                quality={50}
                placeholder="blur"
                blurDataURL={song.albumImageUrl}
              />
            </div>
            <div className="flex flex-col sm:flex-row sm:items-center sm:gap-3">
              <h3 className="font-semibold md:text-lg text-black dark:text-white animate-">
                {song.title}
              </h3>
              <span className="hidden md:inline-flex">—</span>
 
              <p className="text-gray-600 text-xs sm:text-sm">{song.artist}</p>
            </div>
          </div>
          <div className="flex items-center gap-2">
            <SiSpotify className="w-6 h-6 text-green-500 animate-[spin_2s_linear_infinite]" />
          </div>
        </a>
      </Link>
    </div>
  );
}

Adding QRCodeContainer

Till now you might be wondering why there was showQR in Footer.js and the QR Code icon at the bottom. It was because that button will toggle QRCodeContainer to show the QR Code of the current URL. So let's just create it.

Installing dependencies

We need to install react-qr-code and react-ripples to generate QR Code and show the Ripple effect respectively.

terminal

pnpm i react-qr-code react-ripples

Creating useWindowLocation Hook

We need to create a hook that will return the current window location.

Create hooks/useWindowLocation.js

hooks/useWindowLocation.js

import { useEffect, useState } from "react";
import { useRouter } from "next/router";
 
export default function useWindowLocation() {
  const [currentURL, setCurrentURL] = useState("");
  const router = useRouter();
 
  useEffect(() => {
    setCurrentURL(window.location.href);
  }, [router.asPath]);
 
  return { currentURL };
}

After creating useWindowLocation hook. Let's create QRCodeContainer:

components/QRCodeContainer.js

/* Importing Modules */
import QRCode from "react-qr-code";
import Ripples from "react-ripples";
import useWindowLocation from "@hooks/useWindowLocation";
import { CgClose } from "react-icons/cg";
import { AnimatePresence, motion } from "framer-motion";
import { useDarkMode } from "@context/darkModeContext";
 
export default function QRCodeContainer({ showQR, setShowQR }) {
  /* Get the Current URL from the hook that we have just created */
  const { currentURL } = useWindowLocation();
  const { isDarkMode } = useDarkMode();
 
  /* As I have added download QR Code button then we need to create it as image
   * I am just creating a 2D canvas and drawing then Converting that canvas to image/png and generating a download link
   */
  function downloadQRCode() {
    const svg = document.getElementById("QRCode");
    const svgData = new XMLSerializer().serializeToString(svg);
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const img = new Image();
    img.onload = () => {
      canvas.width = img.width;
      canvas.height = img.height;
      ctx.drawImage(img, 0, 0);
      const pngFile = canvas.toDataURL("image/png");
      const downloadLink = document.createElement("a");
      downloadLink.download = "QRCode";
      downloadLink.href = `${pngFile}`;
      downloadLink.click();
    };
    img.src = `data:image/svg+xml;base64,${btoa(svgData)}`;
  }
 
  return (
    <>
      <AnimatePresence>
        {showQR && (
          <motion.div
            initial="hidden"
            whileInView="visible"
            exit="hidden"
            variants={{
              hidden: { y: "100vh", opacity: 0 },
              visible: {
                y: 0,
                opacity: 1,
              },
            }}
            transition={{
              type: "spring",
              bounce: 0.15,
            }}
            className="bg-white dark:bg-darkSecondary fixed inset-0  grid place-items-center"
            style={{ zIndex: 10000 }}
          >
            <button
              className="outline-none absolute right-5 top-5 text-black dark:text-white"
              onClick={() => setShowQR(false)}
            >
              <CgClose className="w-8 h-8" />
            </button>
 
            <div className="text-black dark:text-white flex flex-col gap-2">
              <h1 className="font-semibold text-xl">Share this page</h1>
              <QRCode
                id="QRCode"
                value={currentURL}
                bgColor={isDarkMode ? "#25282a" : "white"}
                fgColor={isDarkMode ? "white" : "#25282a"}
              />
 
              <Ripples
                className="mt-2"
                color={
                  isDarkMode ? "rgba(0,0,0, 0.2)" : "rgba(225, 225, 225, 0.2)"
                }
              >
                <button
                  className="w-full  px-3 py-2 font-medium bg-darkPrimary dark:bg-gray-100 text-white dark:text-darkPrimary rounded text-sm"
                  onClick={downloadQRCode}
                >
                  Download
                </button>
              </Ripples>
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}

After implementing all the things I mentioned above You have created a footer and it will look something like this:

footer gif

Our Layout is complete now. If you haven't add this layout to _app.js then do that now:

pages/_app.js

/* previous code */
import Layout from "@layout/Layout";
 
function MyApp({ Component, pageProps }) {
  return (
    <DarkModeProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </DarkModeProvider>
  );
}
 
export default MyApp;

Adding NProgressbar

NProgress is a tiny JavaScript plugin for creating a slim and nanoscopic progress bar that features realistic trickle animations to convince your users that something is happening. This will show the progress bar at the top of the webpage.

Installing nprogress

Run the following command to install the nprogress

terminal

pnpm i nprogress

Using nprogress

After installing nprogress we need to use that in our pages/_app.js:

pages/_app.js

/* ............Previous Code......... */
 
import { useEffect } from "react";
import { useRouter } from "next/router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
 
/* Progressbar Configurations */
NProgress.configure({
  easing: "ease",
  speed: 800,
  showSpinner: false,
});
 
function MyApp({ Component, pageProps }) {
  const router = useRouter();
 
  useEffect(() => {
    const start = () => {
      NProgress.start();
    };
    const end = () => {
      NProgress.done();
    };
    router.events.on("routeChangeStart", start);
    router.events.on("routeChangeComplete", end);
    router.events.on("routeChangeError", end);
    return () => {
      router.events.off("routeChangeStart", start);
      router.events.off("routeChangeComplete", end);
      router.events.off("routeChangeError", end);
    };
  }, [router.events]);
 
  return (
    <DarkModeProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </DarkModeProvider>
  );
}
 
export default MyApp;

That's all we need to do to make that work. The above useEffect will run as the route changes and according to that, it will handle the NProgress.

Adding Fonts

In the starting of the project we added some fonts to tailwind.config.js

tailwind.config.js

/* ...... */
  fontFamily: {
      inter: ["Inter", "sans-serif"],
      sarina: ["Sarina", "cursive"],
      barlow: ["Barlow", "sans-serif"],
      mono: ["monospace"],
  },
/* ...... */

Now we need to install them. There are many ways you can use these fonts such as-

  • by using @import
  • by using link

But we are not going to use them in this way. We are going to download these fonts locally and then we'll use them. This is because it will take time if your browser fetches the fonts every time users visit your website. It takes time and by using them locally , we can use Cache-Control. Which will cache the fonts and load faster.

I am using Vercel to host my website and I guess you should too because it can provide Cache-Control which we will implement in just a moment.

Downloading Fonts

Good for you, you don't have to download each one of them individually. Follow the following steps:

  • Download these fonts from Here as zip
  • Extract the zip
  • Create a fonts folder inside the public folder if you don't have any
  • Put all the fonts inside that folder like this:

Files

my-project/
├── components
├── pages
└── public/
    └── fonts/
        ├── Barlow/
        │   ├── Barlow-400.woff2
        │   ├── Barlow-500.woff2
        │   ├── Barlow-600.woff2
        │   ├── Barlow-700.woff2
        │   └── Barlow-800.woff2
        ├── Sarina/
        │   └── Sarina-400.woff2
        └── Inter-var.woff2

Now you have downloaded and saved fonts in the right place. Let's add them now.

Add fonts in _document.js

Now we need to link or add the fonts in pages/_document.js. Create one if you don't have one:

pages/_document.js

import { Html, Head, Main, NextScript } from "next/document";
 
export default function Document() {
  return (
    <Html lang="en">
      <Head>
        {/* Barlow */}
        <link
          rel="preload"
          href="/fonts/Barlow/Barlow-400.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        <link
          rel="preload"
          href="/fonts/Barlow/Barlow-500.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        <link
          rel="preload"
          href="/fonts/Barlow/Barlow-600.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        <link
          rel="preload"
          href="/fonts/Barlow/Barlow-700.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        <link
          rel="preload"
          href="/fonts/Barlow/Barlow-800.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
 
        {/* Inter */}
        <link
          rel="preload"
          href="/fonts/Inter-var.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        {/* Sarina */}
        <link
          rel="preload"
          href="/fonts/Sarina/Sarina-400.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
 
        {/* Tailwind CSS Typography  */}
        <link
          rel="stylesheet"
          href="https://unpkg.com/@tailwindcss/typography@0.4.x/dist/typography.min.css"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Add fonts in global.css

We need to use @font-face for font optimization:

styles/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-400.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-500.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-600.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-700.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-800.woff2) format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/fonts/Inter-var.woff2) format("woff2");
}
@font-face {
  font-family: "Sarina";
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url(/fonts/Sarina/Sarina-400.woff2) format("woff2");
}

Adding fonts to vercel.json

This step is important as we are using Vercel as a hosting platform and we need to use the Cache-Control. So vercel.json is like the following code:

vercel.json

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    {
      "source": "/fonts/Barlow/Barlow-400.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Barlow/Barlow-500.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Barlow/Barlow-600.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Barlow/Barlow-700.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Barlow/Barlow-800.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Inter-var.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/fonts/Sarina/Sarina-400.woff2",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ],
}

That's all we need to do to add fonts in the project.

Adding Styling

I am using TailwindCSS to style the projects, but we need some custom styling which will take place in styles/global.css. The following styles and classes will be used throughout the project. So just Copy Paste the following in your styles/global.css (Don't add duplicates):

styles/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-400.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-500.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-600.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-700.woff2) format("woff2");
}
@font-face {
  font-family: "Barlow";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/fonts/Barlow/Barlow-800.woff2) format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/fonts/Inter-var.woff2) format("woff2");
}
@font-face {
  font-family: "Sarina";
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url(/fonts/Sarina/Sarina-400.woff2) format("woff2");
}
 
body,
html {
  overflow-x: hidden;
  scroll-behavior: auto;
}
body::-webkit-scrollbar {
  width: 6px;
}
 
/* Adding Scroll Margin for top */
* {
  scroll-margin-top: 80px;
}
@media screen and (max-width: 640px) {
  * {
    scroll-margin-top: 60px;
  }
  body::-webkit-scrollbar {
    width: 2px;
  }
}
 
pre::-webkit-scrollbar {
  display: none;
}
 
body.dark {
  background-color: #181a1b;
}
 
::-webkit-scrollbar {
  width: 6px;
}
 
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background-color: #b3b3b3;
}
 
.dark::-webkit-scrollbar-thumb {
  background-color: #393e41;
}
 
.no-scrollbar::-webkit-scrollbar {
  display: none;
}
 
.lock-scroll {
  overflow: hidden !important;
}
 
/* For preventing the blue highlight color box on tap(click) */
* {
  -webkit-tap-highlight-color: transparent;
}
 
.truncate-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
 
.truncate-3 {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
 
.auto-row {
  -webkit-margin-before: auto;
  margin-block-start: auto;
}
 
/* Code Line Highlighting START */
code {
  counter-reset: line;
}
 
code span.line {
  padding: 2px 12px;
  border-left: 4px solid transparent;
}
 
code > .line::before {
  counter-increment: line;
  content: counter(line);
 
  /* Other styling */
  display: inline-block;
  width: 1rem;
  margin-right: 1rem;
  text-align: right;
  color: gray;
  font-weight: 500;
  border-right: 4px solid transparent;
}
 
.highlighted {
  background: rgba(200, 200, 255, 0.1);
  border-left: 4px solid #3777de !important;
  filter: saturate(1.5);
}
/* Code Line Highlighting ENDS */
 
/* Nprogress bar Custom Styling (force) : STARTS */
#nprogress .bar {
  background-color: rgba(0, 89, 255, 0.7) !important;
  height: 3px !important;
}
.dark #nprogress .bar {
  background: #fff !important;
}
#nprogress .peg {
  box-shadow: none !important;
}
/* Nprogress bar Custom Styling (force) : ENDS */
 
.blogGrid {
  display: grid;
  grid-template-columns: 300px 1fr;
  grid-template-rows: 1fr;
}
 
/* Layers Components or the custom class extends with tailwind */
@layer components {
  .bottom_nav_icon {
    @apply mb-[2px] text-2xl cursor-pointer;
  }
  .top-nav-link {
    @apply list-none mx-1 px-3 py-1 border-black dark:border-white transition-all duration-200 hover:rounded-md hover:bg-gray-100 dark:hover:bg-darkSecondary cursor-pointer text-lg font-semibold select-none sm:text-sm md:text-base;
  }
  .contact_field {
    @apply text-sm font-medium text-black dark:text-white w-full px-4 py-2 m-2 rounded-md border-none outline-none shadow-inner shadow-slate-200 dark:shadow-zinc-800 focus:ring-1 focus:ring-purple-500 dark:bg-darkPrimary dark:placeholder-gray-500;
  }
  .title_of_page {
    @apply text-center text-xl font-bold  dark:bg-darkPrimary dark:text-gray-100;
  }
  .icon {
    @apply text-2xl sm:text-3xl m-1 transform duration-200 lg:hover:scale-150 text-zinc-500 hover:text-zinc-800 dark:hover:text-white cursor-pointer;
  }
 
  .page_container {
    @apply p-5 md:px-24 pb-10 dark:bg-darkPrimary dark:text-gray-200 grid gap-6 grid-cols-1 sm:grid-cols-2  lg:grid-cols-3 xl:grid-cols-4 3xl:grid-cols-5;
  }
 
  .blog_bottom_icon {
    @apply text-3xl p-1 bg-gray-100 dark:bg-darkSecondary sm:bg-transparent ring-1 dark:ring-gray-500 ring-gray-300 sm:hover:bg-gray-100 rounded-md cursor-pointer ml-1;
  }
  .blog_bottom_button {
    @apply block sm:hidden py-1 w-full lg:hover:bg-gray-300 cursor-pointer bg-gray-200 rounded-md transform duration-100 active:scale-90 select-none;
  }
  .user_reaction {
    @apply flex font-semibold items-center cursor-pointer w-full justify-center sm:justify-start sm:w-auto space-x-1 text-base;
  }
  .project_link {
    @apply text-center bg-gray-200 p-2 my-1 rounded-full dark:bg-darkSecondary dark:text-white cursor-pointer shadow dark:shadow-gray-500;
  }
  .clickable_button {
    @apply transform duration-100 active:scale-90 lg:hover:scale-105;
  }
 
  .home-section-container {
    @apply flex gap-2 overflow-x-scroll p-5 md:px-24 w-full min-h-[200px] select-none snap-x lg:snap-none;
  }
  .home-content-section {
    @apply relative min-w-[250px] xl:min-w-[300px] break-words shadow shadow-black/20 dark:shadow-white/20 dark:bg-darkSecondary ring-gray-400 rounded-xl p-3 cursor-pointer select-none  lg:hover:scale-105 scale-95 transition bg-white snap-center lg:snap-align-none md:first:ml-24 md:last:mr-24;
  }
  .blog-hover-button {
    @apply flex items-center space-x-2 border-2 border-white dark:border-zinc-600 px-3 py-1 font-semibold w-min text-white dark:text-white hover:bg-white dark:hover:bg-zinc-600 hover:text-black;
  }
  .hover-slide-animation {
    @apply relative overflow-hidden before:absolute before:h-full before:w-40 before:bg-stone-900 dark:before:bg-gray-50 before:opacity-10 dark:before:opacity-5 before:-right-10 before:-z-10 before:rotate-[20deg] before:scale-y-150 before:top-4 hover:before:scale-[7] before:duration-700;
  }
  .pageTop {
    @apply mt-[44px] md:mt-[60px] max-w-4xl 2xl:max-w-5xl 3xl:max-w-7xl relative mx-auto p-4 mb-10 text-neutral-900 dark:text-neutral-200;
  }
  .utilities-svg {
    @apply !pointer-events-none mb-4 w-8 h-8;
  }
  .card {
    @apply bg-white dark:bg-darkSecondary p-5 sm:p-10 flex flex-col sm:flex-row gap-8 items-center max-w-2xl shadow-md rounded-lg mt-[30%] sm:mt-8 transition-all;
  }
  .blog-container {
    @apply !w-full dark:text-neutral-400 my-5 font-medium;
  }
}
 
@layer base {
  body {
    @apply font-inter bg-darkWhite;
  }
  button {
    @apply outline-none;
  }
  hr {
    @apply !mx-auto !w-1/2 h-0.5 !bg-gray-700 dark:!bg-gray-300 border-0 !rounded-full;
  }
 
  table {
    @apply !border-collapse text-left;
  }
 
  table thead tr > th,
  table tbody tr > td {
    @apply !p-2 border border-gray-400 align-middle;
  }
 
  table thead tr > th {
    @apply text-black dark:text-white;
  }
 
  table thead tr {
    @apply align-text-top;
  }
  table th {
    @apply font-bold;
  }
  table a {
    @apply !text-blue-500 dark:!text-blue-400;
  }
 
  strong {
    @apply !text-black dark:!text-white !font-bold;
  }
 
  /* For Blog page to remove the underline  */
  h2 > a,
  h3 > a,
  h4 > a,
  h5 > a,
  h6 > a {
    @apply !text-black dark:!text-white !font-bold !no-underline;
  }
}
 
@layer utilities {
  /* Hiding the arrows in the input number */
  input[type="number"]::-webkit-inner-spin-button,
  input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }
}

Adding SEO Support

We need to use metadata to improve SEO. So Before Creating pages and portfolio, let's add support for SEO.

Create MetaData

We need metadata to add that to the website to improve SEO.So I have created a static Data file that contains the title, description, image (when the page is shared), and keywords. Copy and paste the following code inside content/meta.js and update this with your information.

content/meta.js

export default {
  home: {
    title: "",
    description:
      "Hey, I am Jatin Sharma. A Front-end Developer/React Developer from India who loves to design and code. I use React.js or Next.js to build the web application interfaces and the functionalities. At the moment, I am pursuing my Bachelor's degree in Computer Science.",
    image: "https://imgur.com/KeJgIVl.png",
    keywords: "portfolio jatin, portfolio j471n, jatin blogs",
  },
 
  stats: {
    title: "Statistics -",
    description:
      "These are my personal statistics about me. It includes My Blogs and github Stats and top music stats.",
    image: "https://imgur.com/9scFfW5.png",
    keywords: "stats, Statistics",
  },
  utilities: {
    title: "Utilities - ",
    description:
      "In case you are wondering What tech I use, Here's the list of what tech I'm currently using for coding on the daily basis. This list is always changing.",
    image: "https://imgur.com/MpfymCd.png",
    keywords: "Utilities, what i use?, utils, setup, uses,",
  },
  blogs: {
    title: "Blogs -",
    description:
      "I've been writing online since 2021, mostly about web development and tech careers. In total, I've written more than 50 articles till now.",
    image: "https://imgur.com/nbNLLZk.png",
    keywords: "j471n blog, blog, webdev, react",
  },
 
  bookmark: {
    title: "Bookmarks -",
    description: "Bookmarked Blogs of Jatin Sharma's blogs by you",
    image: "https://imgur.com/5XkrVPq.png",
    keywords: "bookmark, blogs, ",
  },
  certificates: {
    title: "Certificates -",
    description:
      "I've participated in many contests, courses and test and get certified in many skills. You can find the certificates below.",
    image: "https://imgur.com/J0q1OdT.png",
    keywords: "Certificates, verified",
  },
  projects: {
    title: "Projects -",
    description:
      "I've been making various types of projects some of them were basics and some of them were complicated.",
    image: "https://imgur.com/XJqiuNK.png",
    keywords: "projects, work, side project,",
  },
  about: {
    title: "About -",
    description:
      "Hey, I am Jatin Sharma. A Front-end Developer/React Developer from India who loves to design and code. I use React.js or Next.js to build the web application interfaces and the functionalities. At the moment, I am pursuing my Bachelor's degree in Computer Science.",
    image: "https://imgur.com/b0HRaPv.png",
    keywords: "about",
  },
};

Creating Meta Data Component

We need a Component that we can use on every page that uses the above metadata and add that to the head of the page. There is one more reason we need that we'll create this app as PWA. Copy & Paste the above code inside components/MetaData.js

components/MetaData.js

import Head from "next/head";
import useWindowLocation from "@hooks/useWindowLocation";
 
export default function MetaData({
  title,
  description,
  previewImage,
  keywords,
}) {
  const { currentURL } = useWindowLocation();
 
  return (
    <Head>
      <meta charSet="utf-8" />
      <meta
        name="viewport"
        content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"
      />
      <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
      <meta name="description" content={description || "Jatin Sharma"} />
      <title>{`${title || ""} Jatin Sharma`}</title>
      <meta name="theme-color" content="#000" />
      <link rel="shortcut icon" href="/favicon.ico" />
      <link rel="manifest" href="/manifest.json" />
      <link rel="apple-touch-icon" href="/icons/icon-192x192.png"></link>
      <meta httpEquiv="Content-Type" content="text/html; charset=UTF-8" />
      <meta name="author" content="Jatin Sharma"></meta>
      <meta name="robots" content="index,follow" />
      <meta
        name="keywords"
        content={`${keywords || ""} Jatin, Jatin sharma, j471n, j471n_`}
      />
 
      {/* Og */}
      <meta property="og:title" content={`${title || ""} Jatin Sharma`} />
      <meta property="og:description" content={description || "Jatin Sharma"} />
      <meta property="og:site_name" content="Jatin Sharma" />
      <meta property="og:url" content={currentURL} key="ogurl" />
      <meta property="og:image" content={previewImage || ""} />
 
      {/* Twitter */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:creator" content="@j471n_" />
      <meta name="twitter:title" content={`${title || ""} Jatin Sharma`} />
      <meta name="twitter:description" content={description} />
      <meta name="twitter:image" content={previewImage || ""} />
      <meta name="twitter:image:alt" content={title || "Jatin Sharma"}></meta>
      <meta name="twitter:domain" content={currentURL} />
    </Head>
  );
}

Homepage

We have added almost everything we need to start the project. Let's Create our Homepage First. It will have four sections:

  • User Profile
  • Skills
  • Recent Posts
  • Contact (Get in Touch)

We will create them one by one.

User Profile

This is the first section of the homepage. which should look like this:

profile section

pages/index.js

import Image from "next/image";
import {
  FadeContainer,
  headingFromLeft,
  opacityVariant,
  popUp,
} from "@content/FramerMotionVariants";
import { homeProfileImage } from "@utils/utils"; // not created yet
import { motion } from "framer-motion";
import { FiDownload } from "react-icons/fi";
import Ripples from "react-ripples";
import Metadata from "@components/MetaData";
import pageMeta from "@content/meta";
 
export default function Home() {
  return (
    <>
      <Metadata
        description={pageMeta.home.description}
        previewImage={pageMeta.home.image}
        keywords={pageMeta.home.keywords}
      />
    </>
  );
}

The above code just adds the meta-data to the page. One thing we haven't created yet is homeProfileImage. Let's create it quick:

Inside utils/utils.js add your profile image URL (It should be in png or jpg format)

utils/utils.js

export const homeProfileImage = "https://imgur.com/mKrXwWF.png";

Now we are good. We just need to add JSX to the index.js let's add that too.

pages/index.js

/* ...........Previous code.......... */
 
export default function Home({ blogs, skills }) {
  return (
    <>
      <Metadata
        description={pageMeta.home.description}
        previewImage={pageMeta.home.image}
        keywords={pageMeta.home.keywords}
      />
 
      {/* Following is the new Code */}
      <div className="relative dark:bg-darkPrimary dark:text-gray-100 max-w-4xl 2xl:max-w-5xl 3xl:max-w-7xl mx-auto">
        <motion.section
          initial="hidden"
          whileInView="visible"
          variants={FadeContainer}
          viewport={{ once: true }}
          className="grid place-content-center py-20  min-h-screen"
        >
          <div className="w-full relative mx-auto flex flex-col items-center gap-10">
            <motion.div
              variants={popUp}
              className="relative w-44 h-44 xs:w-52 xs:h-52 flex justify-center items-center rounded-full p-3 before:absolute before:inset-0 before:border-t-4 before:border-b-4 before:border-black before:dark:border-white before:rounded-full before:animate-photo-spin"
            >
              <Image
                src={homeProfileImage}
                className="rounded-full shadow filter saturate-0"
                width={400}
                height={400}
                alt="cover Profile Image"
                quality={75}
                priority={true}
              />
            </motion.div>
 
            <div className="w-full flex flex-col p-5 gap-3 select-none text-center ">
              <div className="flex flex-col gap-1">
                <motion.h1
                  variants={opacityVariant}
                  className="text-5xl lg:text-6xl font-bold font-sarina"
                >
                  Jatin Sharma
                </motion.h1>
                <motion.p
                  variants={opacityVariant}
                  className="font-medium text-xs md:text-sm lg:text-lg text-gray-500"
                >
                  React Developer, Competitive Programmer
                </motion.p>
              </div>
 
              <motion.p
                variants={opacityVariant}
                className=" text-slate-500 dark:text-gray-300 font-medium text-sm md:text-base text-center"
              >
                I am currently perusing my Bachelor Degree in Computer Science.
                I can code in Python, C, C++, etc.
              </motion.p>
            </div>
 
            <motion.div className="rounded-md overflow-hidden" variants={popUp}>
              <Ripples className="w-full" color="rgba(0, 0, 0, 0.5)">
                <button
                  className="flex items-center gap-2 px-5 py-2 border rounded-md border-gray-500 dark:border-gray-400 select-none  hover:bg-gray-100 dark:hover:bg-neutral-800 outline-none"
                  onClick={() => window.open("/resume")}
                >
                  <FiDownload />
                  <p>Resume</p>
                </button>
              </Ripples>
            </motion.div>
          </div>
        </motion.section>
      </div>
    </>
  );
}

In the above code, when the resume button is Clicked, we are directing the user to /resume route which we have not created yet. We won't create it because we are going to use vercel.json to redirect the user. Let's see how we do that:

vercel.json

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    /* ......Fonts...... */
  ],
  "redirects": [
    {
      "source": "/home",
      "destination": "/"
    },
    {
      "source": "/resume",
      "destination": "/resume.pdf"
    },
  ]
}

The above code shows that we redirect the user from /home to / and /resume to /resume.pdf. You just need to put your resume as a pdf in the public directory. which will later look like this:

Files

my-project/
├── components
├── pages
└── public/
    ├── fonts/
    └── resume.pdf

Skills

This is the second section of the homepage. which should look like this: skills

For Skills, I have also used static data. which you can manipulate.

Create content/skillsData.js

content/skillsData.js

module.exports = [
  {
    name: "HTML",
    level: 100,
    pinned: false,
  },
  {
    name: "CSS",
    level: 95,
    pinned: true,
  },
  {
    name: "Javascript",
    level: 80,
    pinned: true,
  },
  {
    name: "SASS",
    level: 80,
    pinned: false,
  },
  {
    name: "React.js",
    level: 80,
    pinned: true,
  },
  {
    name: "Next.js",
    level: 80,
    pinned: true,
  },
  {
    name: "Tailwind CSS",
    level: 100,
    pinned: true,
  },
 
  /* .....Add more..... */
];

The above code returns the array of objects and each object contains three keys:

  • name: name of the skill
  • level: how well you know the skill (optional)
  • pinned: (true/false) skill will show on the home screen if it is true

We will need to create two things:

  • getPinnedSkills: function that returns only pinned skills
  • SkillSection: Component for skills

Creating getPinnedSkills

Create lib/dataFetch.js and inside that add the following code:

lib/dataFetch.js

import skills from "@content/skillsData";
 
export function getPinnedSkills() {
  return skills.filter((skill) => skill.pinned);
}

That's it, now we need to implement them in pages/index.js:

pages/index.js

/* ...............Previous Code........... */
import { getPinnedSkills } from "@lib/dataFetch";
 
 
/* HomePage Component */
- export default function Home() {
+ export default function Home({ skills ) {
 
/* .....New Code........ */
export async function getStaticProps() {
  const skills = getPinnedSkills();
  return {
    props: { skills },
  };
}

Add getStaticProps at the bottom of the index.js and call getPinnedSkills it returns skills as props which we can use inside the Home component.

Here + shows what is added and - shows what is deleted.

Creating SkillSection Component

Now we are creating a separate component for Skills.

components/Home/SkillSection.js

import { FadeContainer, popUp } from "@content/FramerMotionVariants";
import { HomeHeading } from "../../pages"; // ----> not created yet
import { motion } from "framer-motion";
import {
  SiHtml5,
  SiCss3,
  SiJavascript,
  SiNextdotjs,
  SiTailwindcss,
  SiPython,
  SiGit,
  SiMysql,
  SiFirebase,
} from "react-icons/si";
import { FaReact } from "react-icons/fa";
import { useDarkMode } from "@context/darkModeContext";
import * as WindowsAnimation from "@lib/windowsAnimation"; //-----> not created yet
 
export default function SkillSection({ skills }) {
  const { isDarkMode } = useDarkMode();
 
  return (
    <section className="mx-5">
      <HomeHeading title="My Top Skills" />
 
      <motion.div
        initial="hidden"
        whileInView="visible"
        variants={FadeContainer}
        viewport={{ once: true }}
        className="grid my-10 gap-4 grid-cols-3"
      >
        {skills.map((skill, index) => {
          const Icon = chooseIcon(skill.name.toLowerCase());
          return (
            <motion.div
              variants={popUp}
              key={index}
              title={skill.name}
              onMouseMove={(e) =>
                WindowsAnimation.showHoverAnimation(e, isDarkMode)
              }
              onMouseLeave={(e) => WindowsAnimation.removeHoverAnimation(e)}
              className="p-4 flex items-center justify-center sm:justify-start gap-4 bg-gray-50 hover:bg-white dark:bg-darkPrimary hover:dark:bg-darkSecondary border rounded-sm border-gray-300 dark:border-neutral-700 transform origin-center md:origin-top group"
            >
              <div className="relative transition group-hover:scale-110 sm:group-hover:scale-100 select-none pointer-events-none">
                <Icon className="w-8 h-8" />
              </div>
              <p className="hidden sm:inline-flex text-sm md:text-base font-semibold select-none pointer-events-none">
                {skill.name}
              </p>
            </motion.div>
          );
        })}
      </motion.div>
    </section>
  );
}
 
/* To choose the Icon according to the Name */
function chooseIcon(title) {
  let Icon;
  switch (title) {
    case "python":
      Icon = SiPython;
      break;
    case "javascript":
      Icon = SiJavascript;
      break;
    case "html":
      Icon = SiHtml5;
      break;
    case "css":
      Icon = SiCss3;
      break;
    case "next.js":
      Icon = SiNextdotjs;
      break;
    case "react.js":
      Icon = FaReact;
      break;
    case "tailwind css":
      Icon = SiTailwindcss;
      break;
    case "firebase":
      Icon = SiFirebase;
      break;
    case "git":
      Icon = SiGit;
      break;
    case "git":
      Icon = SiGit;
      break;
    case "mysql":
      Icon = SiMysql;
      break;
    default:
      break;
  }
  return Icon;
}

In the above code, we are creating a grid that shows the skills with their icon. as we did not add icons to the content/skillData.js. So here we are choosing the Icon (I know that's not the best way). We add the windows hover animation onMouseMove and remove it onMouseLeave. We will create that in a moment. We have not created two things in the above code:

  • HomeHeading: Animated Heading for the Project
  • WindowsAnimation: Windows Hover animation for Skill Card

Creating HomeHeading Component

You can create HomeHeading Component in your components folder, but I create inside pages/index.js at the bottom:

pages/index.js

/* ......Home page Component....... */
 
export function HomeHeading({ title }) {
  return (
    <AnimatedHeading
      className="w-full font-bold text-3xl text-left my-2 font-inter"
      variants={headingFromLeft}
    >
      {title}
    </AnimatedHeading>
  );
}
 
/* ......getStaticProps()....... */

In the above code, We haven't created AnimatedHeading yet. You can find that here

Creating WindowsAnimation

We will create a window's hover animation for the Skill card. For that Create a file lib/WindowsAnimation.js:

lib/WindowsAnimation.js

export function showHoverAnimation(e, isDarkMode) {
  const rect = e.target.getBoundingClientRect();
  const x = e.clientX - rect.left; // x position within the element.
  const y = e.clientY - rect.top; // y position within the element.
 
  if (isDarkMode) {
    e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.2),rgba(255,255,255,0) )`;
    e.target.style.borderImage = `radial-gradient(20% 75% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 1 / 1px / 0px stretch `;
  }
}
 
export function removeHoverAnimation(e) {
  e.target.style.background = null;
  e.target.style.borderImage = null;
}

It has two functions showHoverAnimation to show the animation and removeHoverAnimation to remove the animation the cursor is removed. After implementing that it looks something like this (It's for dark mode only):

windows hover animation

Now we just need to import SkillSection into pages/index.js:

pages/index.js

/* ...Other imports.... */
import BlogsSection from "@components/Home/BlogsSection";
 
export default function Home({ blogs, skills }) {
  /*....other code */
  return (
    <>
      {/* previous JSX */}
      <SkillSection skills={skills} />
    </>
  );
}
 
/* ........other code....... */

Recent Posts

To implement this functionality we need to create a Blog. For this, I am using MDX. We are going to cover this later in this article when we will create a Blog page. or you can directly jump on Blog section.

Contact

Now We need to Create a Contact Form through which any person can contact me via Email. We are going to use Email.js to send the mail You can see their docs to set up (it's very simple to setup). If you want you can use something else its totally up to you. Our contact form looks like this:

Contact Section

Installing dependencies

You need to install two dependencies before we start building the form:

terminal

pnpm i react-toastify @emailjs/browser

Creating Contact Component

First Create a file name index.js inside components/Contact folder:

components/Contact/index.js

export { default } from "./Contact";

In the above code, we are importing Contact Component. let's create it:

components/Contact.js

/* importing modules */
import React from "react";
import { popUpFromBottomForText } from "../../content/FramerMotionVariants";
import AnimatedHeading from "../FramerMotion/AnimatedHeading";
import "react-toastify/dist/ReactToastify.css";
 
import ContactForm from "./ContactForm"; // ======>> not created yet
import AnimatedText from "../FramerMotion/AnimatedText"; // ======>> not created yet
 
export default function Contact() {
  return (
    <div id="contact" className="dark:bg-darkPrimary !relative">
      {/* Get in touch top section */}
      <section className="w-full-width text-center pt-6 dark:bg-darkPrimary dark:text-white">
        <AnimatedHeading
          variants={popUpFromBottomForText}
          className="font-bold text-4xl"
        >
          Get in touch
        </AnimatedHeading>
 
        <AnimatedText
          variants={popUpFromBottomForText}
          className="px-4 py-2 font-medium text-slate-400"
        >
          Have a little something, something you wanna talk about? Please feel
          free to get in touch anytime, whether for work or to just Hi 🙋‍♂️.
        </AnimatedText>
      </section>
 
      {/* Wrapper Container */}
      <section className="flex flex-col lg:flex-row w-full mx-auto px-5 dark:bg-darkPrimary dark:text-white lg:pb-10">
        {/* Left Contact form section */}
        <div className="w-full mx-auto mt-10">
          <AnimatedHeading
            variants={popUpFromBottomForText}
            className="text-2xl font-bold w-full text-center my-2"
          >
            Connect with me
          </AnimatedHeading>
 
          <ContactForm />
        </div>
      </section>
    </div>
  );
}

The above code is simple. We just didn't create two things:

  • AnimatedText: Animation paragraph
  • ContactForm: form component

You can find AnimatedText component Here

Creating ContactForm Component

components/Contact/ContactForm.js

import { useState } from "react";
import { AiOutlineLoading } from "react-icons/ai";
import { ToastContainer, toast } from "react-toastify";
import { useDarkMode } from "../../context/darkModeContext";
import emailjs from "@emailjs/browser";
import { motion } from "framer-motion";
import {
  FadeContainer,
  mobileNavItemSideways,
} from "../../content/FramerMotionVariants";
import Ripples from "react-ripples";
 
// initial State of the form
const initialFormState = {
  to_name: "Jatin Sharma",
  first_name: "",
  last_name: "",
  email: "",
  subject: "",
  message: "",
};
 
export default function Form() {
  const [emailInfo, setEmailInfo] = useState(initialFormState);
  const [loading, setLoading] = useState(false);
  const { isDarkMode } = useDarkMode();
 
  /* Here we send an Email using emailjs you can get service_id, template_id, and user_id after sign up on their site  */
  function sendEmail(e) {
    e.preventDefault();
    setLoading(true);
 
    emailjs
      .send(
        process.env.NEXT_PUBLIC_YOUR_SERVICE_ID,
        process.env.NEXT_PUBLIC_YOUR_TEMPLATE_ID,
        emailInfo,
        process.env.NEXT_PUBLIC_YOUR_USER_ID
      )
      .then((res) => {
        setLoading(false);
        setEmailInfo(initialFormState);
        toast.success("Message Sent ✌");
      })
      .catch((err) => {
        console.log(err.text);
        setLoading(false);
        toast.error("😢 " + err.text);
      });
  }
 
  /* For Form Validation I simply check each field should not be empty */
  function validateForm() {
    for (const key in emailInfo) {
      if (emailInfo[key] === "") return false;
    }
    return true;
  }
 
  /* When user is typing and  Press Ctrl+Enter then it will try to send the mail after validating */
  function submitFormOnEnter(event) {
    if ((event.keyCode == 10 || event.keyCode == 13) && event.ctrlKey) {
      if (validateForm()) {
        return sendEmail(event);
      }
      toast.error("Looks like you have not filled the form");
    }
  }
 
  return (
    <>
      <motion.form
        initial="hidden"
        whileInView="visible"
        variants={FadeContainer}
        viewport={{ once: true }}
        className="w-full flex flex-col items-center max-w-xl mx-auto my-10 dark:text-gray-300"
        onSubmit={sendEmail}
        onKeyDown={submitFormOnEnter}
      >
        {/* First Name And Last Name */}
        <div className="w-full grid grid-cols-2 gap-6">
          <motion.div
            variants={mobileNavItemSideways}
            className="relative z-0 w-full mb-6 group"
          >
            <input
              type="text"
              name="first_name"
              id="floating_first_name"
              className="block py-2 mt-2 px-0 w-full text-sm text-white-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-white focus:outline-none focus:ring-0 focus:border-black peer"
              placeholder=" "
              required
              value={emailInfo.first_name}
              onChange={(e) =>
                setEmailInfo({
                  ...emailInfo,
                  [e.target.name]: e.target.value,
                })
              }
            />
            <label
              htmlFor="floating_first_name"
              className="peer-focus:font-medium absolute text-sm text-slate-400 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-black dark:peer-focus:text-white peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
            >
              First name
            </label>
          </motion.div>
          <motion.div
            variants={mobileNavItemSideways}
            className="relative z-0 w-full mb-6 group"
          >
            <input
              type="text"
              name="last_name"
              id="floating_last_name"
              className="block py-2 mt-2 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-white focus:outline-none focus:ring-0 focus:border-black peer"
              placeholder=" "
              required
              value={emailInfo.last_name}
              onChange={(e) =>
                setEmailInfo({
                  ...emailInfo,
                  [e.target.name]: e.target.value,
                })
              }
            />
            <label
              htmlFor="floating_last_name"
              className="peer-focus:font-medium absolute text-sm text-slate-400 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-black dark:peer-focus:text-white peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
            >
              Last name
            </label>
          </motion.div>
        </div>
        <motion.div
          variants={mobileNavItemSideways}
          className="relative z-0 w-full mb-6 group"
        >
          <input
            type="email"
            name="email"
            id="floating_email"
            className="block py-2 mt-2 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 focus:outline-none focus:ring-0 focus:dark:border-white focus:border-black peer"
            placeholder=" "
            required
            value={emailInfo.email}
            onChange={(e) =>
              setEmailInfo({
                ...emailInfo,
                [e.target.name]: e.target.value,
              })
            }
          />
          <label
            htmlFor="floating_email"
            className="peer-focus:font-medium absolute text-sm text-slate-400 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-black dark:peer-focus:text-white peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
          >
            Email address
          </label>
        </motion.div>
        <motion.div
          variants={mobileNavItemSideways}
          className="relative z-0 w-full mb-6 group"
        >
          <input
            type="subject"
            name="subject"
            id="floating_subject"
            className="block py-2 mt-2 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-white focus:outline-none focus:ring-0 focus:border-black peer"
            placeholder=" "
            required
            value={emailInfo.subject}
            onChange={(e) =>
              setEmailInfo({
                ...emailInfo,
                [e.target.name]: e.target.value,
              })
            }
          />
          <label
            htmlFor="floating_subject"
            className="peer-focus:font-medium absolute text-sm text-slate-400 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-black dark:peer-focus:text-white peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
          >
            Subject
          </label>
        </motion.div>
        <motion.div
          variants={mobileNavItemSideways}
          className="relative z-0 w-full mb-6 group"
        >
          <textarea
            name="message"
            id="floating_message"
            className="block py-2 mt-2 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-white focus:outline-none focus:ring-0  peer min-h-[100px] resize-y focus:border-black"
            placeholder=" "
            required
            value={emailInfo.message}
            onChange={(e) =>
              setEmailInfo({
                ...emailInfo,
                [e.target.name]: e.target.value,
              })
            }
          />
          <label
            htmlFor="floating_message"
            className="peer-focus:font-medium absolute text-sm text-slate-400 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-black dark:peer-focus:text-white peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
          >
            Message
          </label>
        </motion.div>
 
        <motion.div
          variants={mobileNavItemSideways}
          className="w-full sm:max-w-sm rounded-lg overflow-hidden "
        >
          <Ripples
            className="flex w-full justify-center"
            color="rgba(225, 225,225,0.2)"
          >
            <button
              type="submit"
              className="text-white bg-neutral-800  dark:bg-darkSecondary font-medium rounded-lg text-sm w-full px-4 py-3 text-center relative overflow-hidden transition duration-300 outline-none active:scale-95"
            >
              <div className="relative w-full flex items-center justify-center">
                <p
                  className={
                    loading ? "inline-flex animate-spin mr-3" : "hidden"
                  }
                >
                  <AiOutlineLoading className="font-bold text-xl" />
                </p>
                <p>{loading ? "Sending..." : "Send"}</p>
              </div>
            </button>
          </Ripples>
        </motion.div>
      </motion.form>
      <ToastContainer
        theme={isDarkMode ? "dark" : "light"}
        style={{ zIndex: 1000 }}
      />
    </>
  );
}

In the above code, I am just rendering the Contact Form and sending the email via emailjs. Every field is required however I am submitting a form when the user press CTRL + Enter as it doesn't care about the required field that's why I have to manually check that every field should be filled using validateForm function. It will give you a toast about if the mail is sent as shown below:

contact demo

This is our homepage that we have just created. It doesn't have Recent Blogs section yet. we will create that when we create Blog Page if you are curious then you jump to Blog section to learn how we are going to implement that.

Stats Page

The stats page contains my personal statistics about my dev.to blog, GitHub and Spotify. Following is the image how it looks like:

stats page

The above image shows that the Stats page has three sections:

  • Blog Stats
  • Top streams
  • Top Artists

we will create those one by one. But first, create a file called stats.js inside the pages folder:

pages/stats.js

import React from "react";
 
export default function Stats() {
  return <></>;
}

Blog and GitHub Stats

In this section, we'll be creating Blog and GitHub statistics cards. which will look like the following:

stats-1

Let's add the Header first. we are going to create a component called PageTop which will be used throughout the project.

Create components/PageTop.js:

components/PageTop

import {
  fromLeftVariant,
  opacityVariant,
} from "../content/FramerMotionVariants"; // ===> not created yet
import AnimatedHeading from "./FramerMotion/AnimatedHeading";
import AnimatedText from "./FramerMotion/AnimatedText";
 
export default function PageTop({ pageTitle, headingClass, children }) {
  return (
    <div className="w-full flex flex-col gap-3 py-5 select-none mb-10">
      <AnimatedHeading
        variants={fromLeftVariant}
        className={`text-4xl  md:text-5xl font-bold text-neutral-900 dark:text-neutral-200 ${headingClass}`}
      >
        {pageTitle}
      </AnimatedHeading>
      <AnimatedText
        variants={opacityVariant}
        className="font-medium text-lg text-gray-400"
      >
        {children}
      </AnimatedText>
    </div>
  );
}

In this, we are going to take the pageTitle as a prop and the children as the description of the project. We haven't created fromLeftVariant and opacityVariant. Let's do it then:

content/FramerMotionVariants.js

export const opacityVariant = {
  hidden: { opacity: 0 },
  visible: { opacity: 1, transition: { delay: 0.2 } },
};
 
export const fromLeftVariant = {
  hidden: { x: -100, opacity: 0 },
  visible: {
    x: 0,
    opacity: 1,
    transition: {
      duration: 0.1,
      type: "spring",
      stiffness: 100,
    },
  },
};

Now add this PageTop component to the pages/stats.js:

pages/stats.js

import React from "react";
import useSWR from "swr";
import { motion } from "framer-motion";
import {
  FadeContainer,
  fromLeftVariant,
  popUpFromBottomForText,
} from "@content/FramerMotionVariants";
import fetcher from "@lib/fetcher";
import MetaData from "@components/MetaData";
import PageTop from "@components/PageTop";
import AnimatedHeading from "@components/FramerMotion/AnimatedHeading";
import AnimatedText from "@components/FramerMotion/AnimatedText";
import pageMeta from "@content/meta";
 
export default function Stats() {
  return (
    <>
      <MetaData
        title={pageMeta.stats.title}
        description={pageMeta.stats.description}
        previewImage={pageMeta.stats.image}
        keywords={pageMeta.stats.keywords}
      />
      <section className="pageTop font-inter">
        <PageTop pageTitle="Statistics">
          These are my personal statistics about my Dev.to Blogs, Github and Top
          Streamed Music on Spotify.
        </PageTop>
      </section>
    </>
  );
}

In the above code, I have imported the modules and rendered the MetaData and PageTop.

Dev.to Stats

Now that we have added the top section of the page, let's implement the Dev.to stats. For this, I am going to use Dev.to API to get information about my blog stats. But this API has some limitations such as that it can only show a maximum of 1000 objects on a single page (not all of them). So we will be tackling that.

I have also written a blog on Dev.to API if you are interested then go to the following link:

blog image

How to use the dev.to API?

I am going to use Next.js API routes as well to fetch the data. So let's create pages/api/stats/devto.js file. This is going to be the API route which will return the stats.

pages/api/stats/devto.js

/* File: pages/api/stats/devto.js */
 
import { allFollowers, allPosts } from "@lib/devto"; // ====> not created yet
 
export default async function handler(req, res) {
  const followers = await allFollowers();
  const posts = await allPosts();
 
  let totalViews = 0;
  let totalLikes = 0;
  let totalComments = 0;
 
  posts.forEach((post) => {
    totalLikes += post.public_reactions_count;
    totalViews += post.page_views_count;
    totalComments += post.comments_count;
  });
 
  res.setHeader(
    "Cache-Control",
    "public, s-maxage=86400, stale-while-revalidate=43200"
  );
 
  return res.status(200).json({
    followers: followers,
    likes: totalLikes,
    views: totalViews,
    comments: totalComments,
    posts: posts.length,
  });
}

We are using two functions allFollowers and allPosts that will return all the followers I have and all the blogs/posts I have published. Then we are iterating over each post and extracting the likes (public reactions), views, and comments. After that, I am setting Cache so that browser doesn't fetch the data again and again. we are revalidating the request every 43200 seconds which is 12 hours.

Let's create allFollowers and allFollowers functions inside lib/devto.js:

lib/devto.js

const PER_PAGE = 1000;
const DEV_API = process.env.NEXT_PUBLIC_BLOGS_API;
 
const getPageOfFollowers = async (page) => {
  const perPageFollowers = await fetch(
    `https://dev.to/api/followers/users?per_page=${PER_PAGE}&page=${page}`,
    {
      headers: {
        api_key: DEV_API,
      },
    }
  )
    .then((response) => response.json())
    .catch((err) => console.log(err));
 
  return perPageFollowers.length;
};
 
export const allFollowers = async () => {
  let numReturned = PER_PAGE;
  let page = 1;
  var totalFollowers = 0;
 
  while (numReturned === PER_PAGE) {
    const followers = await getPageOfFollowers(page);
    totalFollowers += followers;
    numReturned = followers;
    page++;
  }
  return totalFollowers;
};
 
const getPageOfPosts = async (page) => {
  const perPagePosts = await fetch(
    `https://dev.to/api/articles/me?per_page=${PER_PAGE}&page=${page}`,
    {
      headers: {
        api_key: DEV_API,
      },
    }
  )
    .then((response) => response.json())
    .catch((err) => console.log(err));
 
  return perPagePosts;
};
 
export const allPosts = async () => {
  let numReturned = PER_PAGE;
  let page = 1;
  var totalPosts = [];
 
  while (numReturned === PER_PAGE) {
    const posts = await getPageOfPosts(page);
    totalPosts.push(...posts);
    numReturned = posts.length;
    page++;
  }
  return totalPosts;
};

You need to add NEXT_PUBLIC_BLOGS_API to your .env.local and then restart your server. If you don't know how to get the Dev.to API, then click here and go to the bottom of the page and generate the API.

The above code has two extra functions getPageOfFollowers and getPageOfPosts these just help us to get the stats of the one page and request the API until all the data is received.

Now let's fetch it in our pages/stats.js:

pages/stats.js

/* File: pages/stats.js */
 
/* ..............Other modules........... */
import StatsCard from "@components/Stats/StatsCard"; // ====> not created yet
 
export default function Stats() {
  const { data: devto } = useSWR("/api/stats/devto", fetcher);
 
  const stats = [
    {
      title: "Total Posts",
      value: devto?.posts.toLocaleString(),
    },
    {
      title: "Blog Followers",
      value: devto?.followers.toLocaleString(),
    },
    {
      title: "Blog Reactions",
      value: devto?.likes.toLocaleString(),
    },
    {
      title: "Blog Views",
      value: devto?.views.toLocaleString(),
    },
    {
      title: "Blog Comments",
      value: devto?.comments.toLocaleString(),
    },
  ];
 
  return (
    <>
      {/* .......old code...... (<PageTop />) */}
 
      {/* Enter the following code under the `PageTop` component.*/}
      {/* Blogs and github stats */}
      <motion.div
        className="grid xs:grid-cols-2 sm:!grid-cols-3 md:!grid-cols-4 gap-5 my-10"
        variants={FadeContainer}
        initial="hidden"
        whileInView="visible"
        viewport={{ once: true }}
      >
        {stats.map((stat, index) => (
          <StatsCard
            key={index}
            title={stat.title}
            value={
              stat.value === undefined ? (
                <div className="w-28 h-8 rounded-sm bg-gray-300 dark:bg-neutral-700 animate-pulse" />
              ) : (
                stat.value
              )
            }
          />
        ))}
      </motion.div>
      {/* ........other code...... */}
    </>
  );
}

I have created an array to iterate over and then render StatsCard which we will create in a moment. In StatsCard while passing the value prop we check if the value is undefined and then pass another div instead of the value that is just a loader and as soon as we get the value then it will render the value.

Create components/Stats/StatsCard.js

components/Stats/StatsCard.js

import { motion } from "framer-motion";
import { popUp } from "@content/FramerMotionVariants"; // ====> not created yet
 
export default function StatsCard({ title, value }) {
  return (
    <motion.div
      className="flex-col justify-center py-4 px-7 rounded-md select-none transform origin-center  bg-white dark:bg-darkSecondary shadow dark:shadow-md border border-transparent hover:border-gray-400 dark:hover:border-neutral-600 group"
      variants={popUp}
    >
      <h1 className="text-3xl my-2 font-bold text-gray-600 dark:text-gray-300 group-hover:text-black  dark:group-hover:text-white">
        {value}
      </h1>
      <p className="text-base font-medium text-gray-500 group-hover:text-black  dark:group-hover:text-white">
        {title}
      </p>
    </motion.div>
  );
}

Github Stats

Now we have implemented Dev.to Stats then Github stats will be very easy to implement. But first, we need to create an API route for Github Stats.

Create pages/api/stats/github.js

pages/api/stats/github.js

import { fetchGithub, getOldStats } from "@lib/github"; // ====> not created yet
 
export default async function handler(req, res) {
  const {
    public_repos: repos,
    public_gists: gists,
    followers,
  } = await fetchGithub();
 
  // it runs when user's api is exhausted, it gives the old data
  if (repos === undefined && gists === undefined) {
    const {
      public_repos: repos,
      public_gists: gists,
      followers,
    } = getOldStats();
    return res.status(200).json({
      repos,
      gists,
      followers,
    });
  }
 
  res.setHeader(
    "Cache-Control",
    "public, s-maxage=86400, stale-while-revalidate=43200"
  );
 
  return res.status(200).json({
    repos,
    gists,
    followers,
  });
}

The above code has two functions fetchGithub and getOldStats. Github API has limited request. So it might exhausted, then we need to take care of that by using mock response (old static data).

  • fetchGithub: it returns the new data from github
  • getOldStats : it returns the mock response

Create lib/github.js:

lib/github.js

const tempData = {
  login: "j471n",
  id: 55713505,
  /* ..........other keys...... */
};
 
// its for /api/stats/github
export async function fetchGithub() {
  return fetch("https://api.github.com/users/j471n").then((res) => res.json());
}
 
// its for getting temporary old data
export function getOldStats() {
  return tempData;
}

You can get your mock response by typing https://api.github.com/users/<your-username> in the browser.

Now that our API has been created let's call it inside pages/stats.js:

pages/stats.js

/* ..............Other modules........... */
import StatsCard from "@components/Stats/StatsCard"; // ====> not created yet
 
export default function Stats() {
  const { data: devto } = useSWR("/api/stats/devto", fetcher);
  const { data: github } = useSWR("/api/stats/github", fetcher);
 
  const stats = [
    /* ...previous blog stats key...... */
    /* Following code is added new */
    {
      title: "Github Repos",
      value: github?.repos,
    },
    {
      title: "Github Gists",
      value: github?.gists,
    },
    {
      title: "Github Followers",
      value: github?.followers,
    },
  ];
 
  return (
    <>
      {/* .......old code...... */}
      {/* We don't need to change something here it's all good here  */}
    </>
  );
}

That's all you need to Create the First section of the stats Page. Now we will move to the next section which is Top Streams

Top Streams

In this section, I'll add my top Streams from Spotify. I have already given a little intro about Spotify at the beginning of this article in the Footer section where we add the current playing song support. The top stream section will look like this: top streams

pages/stats.js

/* ..............Other modules........... */
import Track from "@components/Stats/Track"; // not created yet
 
export default function Stats() {
  /* ...other api requests.... */
  const { data: topTracks } = useSWR("/api/stats/tracks", fetcher);
 
  return (
    <>
      {/* .......old code...... */}
      {/* .........Blogs and github stats......... */}
 
      {/* Spotify top songs */}
      <div className="font-barlow">
        <AnimatedHeading
          variants={fromLeftVariant}
          className="text-3xl sm:text-4xl capitalize font-bold text-neutral-900 dark:text-neutral-200"
        >
          My Top streams songs
        </AnimatedHeading>
 
        <AnimatedText
          variants={popUpFromBottomForText}
          className="mt-4 text-gray-500"
        >
          <span className="font-semibold">
            {topTracks && topTracks[0].title}
          </span>{" "}
          is the most streamed song of mine. Here's my top tracks on Spotify
          updated daily.
        </AnimatedText>
        <motion.div
          variants={FadeContainer}
          initial="hidden"
          whileInView="visible"
          viewport={{ once: true }}
          className="flex flex-col my-10 gap-0 font-barlow"
        >
          {topTracks?.map((track, index) => (
            <Track
              key={index}
              id={index}
              track={track}
              url={track.url}
              title={track.title}
              coverImage={track.coverImage.url}
              artist={track.artist}
            />
          ))}
        </motion.div>
      </div>
      {/* ............. */}
    </>
  );
}

Add the following code to pages/stats.js. In the above code we just fetch the /api/stats/tracks it returns the top 10 most streamed songs, then we render those by using the Track Component. We have not created them yet. So let's create them.

Create pages/api/stats/tracks.js:

pages/api/stats/tracks.js

import { topTracks } from "../../../lib/spotify";
 
export default async function handler(req, res) {
  const response = await topTracks();
  const { items } = await response.json();
 
  const tracks = items.slice(0, 10).map((track) => ({
    title: track.name,
    artist: track.artists.map((_artist) => _artist.name).join(", "),
    url: track.external_urls.spotify,
    coverImage: track.album.images[1],
  }));
 
  res.setHeader(
    "Cache-Control",
    "public, s-maxage=86400, stale-while-revalidate=43200"
  );
 
  return res.status(200).json(tracks);
}

In the above code, I am getting data from topTracks function and then extracting the main info that I need. Let's create topTracks:

lib/spotify.js

/* .........Other methods........ */
export const topTracks = async () => {
  const { access_token } = await getAccessToken();
 
  return fetch("https://api.spotify.com/v1/me/top/tracks", {
    headers: {
      Authorization: `Bearer ${access_token}`,
    },
  });
};

That's it for this. Now we need to create Track Component:

Create components/Stats/Track.js

components/Stats/Track.js

import Image from "next/image";
import Link from "next/link";
import { fromBottomVariant } from "../../content/FramerMotionVariants";
import { motion } from "framer-motion";
 
export default function Track({ url, title, artist, coverImage, id }) {
  return (
    <Link href={url} passHref>
      <motion.a
        variants={fromBottomVariant}
        href={url}
        className="bg-gray-100 hover:bg-gray-200 dark:bg-darkPrimary hover:dark:bg-darkSecondary border-l first:border-t border-r border-b  border-gray-300 dark:border-neutral-600 p-4 font-barlow flex items-center gap-5 overflow-hidden relative xs:pl-16 md:!pl-20 "
        rel="noreferrer"
        target="_blank"
      >
        <div className="absolute left-4 md:left-6 text-xl text-gray-500 transform origin-center font-inter tracking-wider hidden xs:inline-flex">
          #{id + 1}
        </div>
 
        <div className="relative w-12 h-12 transform origin-center">
          <Image
            src={coverImage}
            width={50}
            height={50}
            layout="fixed"
            alt={title}
            quality={50}
          ></Image>
        </div>
        <div>
          <h2 className="text-base md:text-xl text-gray-900 dark:text-white font-semibold transform origin-left font-barlow">
            {title}
          </h2>
          <p className="transform origin-left text-gray-500 text-xs sm:text-sm md:text-base line-clamp-1">
            {artist}
          </p>
        </div>
      </motion.a>
    </Link>
  );
}

That's all you need to create The Top Stream Section.

Top Artists

This section contains my top stream artists. It looks like this:

top artists

Let's just add the following code in your pages/stats.js

pages/stats.js

/* ..............Other modules........... */
import Artist from "@components/Stats/Artist"; // not created yet
 
export default function Stats() {
  /* ...other api requests.... */
  const { data: artists } = useSWR("/api/stats/artists", fetcher);
 
  return (
    <>
      {/* .........Blogs and github stats......... */}
      {/* ...........Spotify top songs.......... */}
 
      <div className="font-barlow">
        <AnimatedHeading
          variants={fromLeftVariant}
          className="text-3xl sm:text-4xl capitalize font-bold text-neutral-900 dark:text-neutral-200"
        >
          My Top Artists
        </AnimatedHeading>
        <AnimatedText className="mt-4 text-gray-500">
          My most listened Artist is{" "}
          <span className="font-semibold">{artists && artists[0].name}</span> on
          Spotify.
        </AnimatedText>
 
        <motion.div
          variants={FadeContainer}
          initial="hidden"
          whileInView="visible"
          viewport={{ once: true }}
          className="flex flex-col my-10 gap-0 font-barlow"
        >
          {artists?.map((artist, index) => (
            <Artist
              key={index}
              id={index}
              name={artist.name}
              url={artist.url}
              coverImage={artist.coverImage.url}
              followers={artist.followers}
            />
          ))}
        </motion.div>
      </div>
 
      {/* ............. */}
    </>
  );
}

Add the following code to pages/artists.js. In the above code we just fetch the /api/stats/artists it returns the top 5 most streamed artists then we render those by using the Artist Component. We have not created them yet. So let's create them.

Create pages/api/stats/artists.js:

pages/api/stats/artists.js

import { topArtists } from "../../../lib/spotify";
 
export default async function handler(req, res) {
  const response = await topArtists();
  const { items } = await response.json();
 
  const artists = items.slice(0, 5).map((artist) => ({
    name: artist.name,
    url: artist.external_urls.spotify,
    coverImage: artist.images[1],
    followers: artist.followers.total,
  }));
 
  res.setHeader(
    "Cache-Control",
    "public, s-maxage=86400, stale-while-revalidate=43200"
  );
 
  return res.status(200).json(artists);
}

In the above we also fetch the data using topArtists function, let's create it inside lib/spotify.js:

lib/spotify.js

/*.......other functions/modules....... */
 
export const topArtists = async () => {
  const { access_token } = await getAccessToken();
 
  return fetch("https://api.spotify.com/v1/me/top/artists", {
    headers: {
      Authorization: `Bearer ${access_token}`,
    },
  });
};

Now as it has been add we need one more thing to add and that is Artist component:

Create components/Stats/Artist.js

components/Stats/Artist.js

import Image from "next/image";
import Link from "next/link";
import { fromBottomVariant, popUp } from "../../content/FramerMotionVariants";
import { motion } from "framer-motion";
 
export default function Track({ name, url, coverImage, followers, id }) {
  return (
    <Link href={url} passHref>
      <motion.a
        variants={fromBottomVariant}
        href={url}
        className="bg-gray-100 hover:bg-gray-200 dark:bg-darkPrimary hover:dark:bg-darkSecondary border-l first:border-t border-r border-b border-gray-300 dark:border-neutral-600 p-4 font-barlow flex items-center gap-5 overflow-hidden"
        rel="noreferrer"
        target="_blank"
      >
        <div className="text-xl text-gray-500 transform origin-center font-inter tracking-wider hidden xs:inline-flex">
          #{id + 1}
        </div>
        <div
          variants={popUp}
          className="relative w-12 md:w-24 h-12 md:h-24 transform origin-center"
        >
          <Image
            className="rounded-full"
            src={coverImage}
            width={100}
            height={100}
            layout="responsive"
            alt={name}
            quality={50}
          ></Image>
        </div>
        <div>
          <h2
            variants={popUp}
            className="text-base sm:text-lg md:text-xl xl:text-2xl text-gray-900 dark:text-white font-semibold md:font-bold transform origin-left font-barlow"
          >
            {name}
          </h2>
          <p
            variants={popUp}
            className="transform origin-left text-gray-500 text-xs sm:text-sm md:text-base md:font-medium line-clamp-1"
          >
            {followers.toLocaleString()} Followers
          </p>
        </div>
      </motion.a>
    </Link>
  );
}

This is it your stats page is ready to go. It was a lot to take in so let's look at the final result of pages/stats.js:

pages/stats.js

import React from "react";
import useSWR from "swr";
import { motion } from "framer-motion";
import {
  FadeContainer,
  fromLeftVariant,
  popUpFromBottomForText,
} from "@content/FramerMotionVariants";
import fetcher from "@lib/fetcher";
import MetaData from "@components/MetaData";
import PageTop from "@components/PageTop";
import StatsCard from "@components/Stats/StatsCard";
import Track from "@components/Stats/Track";
import Artist from "@components/Stats/Artist";
import AnimatedHeading from "@components/FramerMotion/AnimatedHeading";
import AnimatedText from "@components/FramerMotion/AnimatedText";
import pageMeta from "@content/meta";
 
export default function Stats() {
  const { data: topTracks } = useSWR("/api/stats/tracks", fetcher);
  const { data: artists } = useSWR("/api/stats/artists", fetcher);
  const { data: devto } = useSWR("/api/stats/devto", fetcher);
  const { data: github } = useSWR("/api/stats/github", fetcher);
 
  const stats = [
    {
      title: "Total Posts",
      value: devto?.posts.toLocaleString(),
    },
    {
      title: "Blog Followers",
      value: devto?.followers.toLocaleString(),
    },
    {
      title: "Blog Reactions",
      value: devto?.likes.toLocaleString(),
    },
    {
      title: "Blog Views",
      value: devto?.views.toLocaleString(),
    },
    {
      title: "Blog Comments",
      value: devto?.comments.toLocaleString(),
    },
    {
      title: "Github Repos",
      value: github?.repos,
    },
    {
      title: "Github Gists",
      value: github?.gists,
    },
    {
      title: "Github Followers",
      value: github?.followers,
    },
  ];
 
  return (
    <>
      <MetaData
        title={pageMeta.stats.title}
        description={pageMeta.stats.description}
        previewImage={pageMeta.stats.image}
        keywords={pageMeta.stats.keywords}
      />
 
      <section className="pageTop font-inter">
        <PageTop pageTitle="Statistics">
          These are my personal statistics about my Dev.to Blogs, Github and Top
          Streamed Music on Spotify.
        </PageTop>
 
        {/* Blogs and github stats */}
        <motion.div
          className="grid xs:grid-cols-2 sm:!grid-cols-3 md:!grid-cols-4 gap-5 my-10"
          variants={FadeContainer}
          initial="hidden"
          whileInView="visible"
          viewport={{ once: true }}
        >
          {stats.map((stat, index) => (
            <StatsCard
              key={index}
              title={stat.title}
              value={
                stat.value === undefined ? (
                  <div className="w-28 h-8 rounded-sm bg-gray-300 dark:bg-neutral-700 animate-pulse" />
                ) : (
                  stat.value
                )
              }
            />
          ))}
        </motion.div>
 
        {/* Spotify top songs */}
        <div className="font-barlow">
          <AnimatedHeading
            variants={fromLeftVariant}
            className="text-3xl sm:text-4xl capitalize font-bold text-neutral-900 dark:text-neutral-200"
          >
            My Top streams songs
          </AnimatedHeading>
 
          <AnimatedText
            variants={popUpFromBottomForText}
            className="mt-4 text-gray-500"
          >
            <span className="font-semibold">
              {topTracks && topTracks[0].title}
            </span>{" "}
            is the most streamed song of mine. Here's my top tracks on Spotify
            updated daily.
          </AnimatedText>
          <motion.div
            variants={FadeContainer}
            initial="hidden"
            whileInView="visible"
            viewport={{ once: true }}
            className="flex flex-col my-10 gap-0 font-barlow"
          >
            {topTracks?.map((track, index) => (
              <Track
                key={index}
                id={index}
                track={track}
                url={track.url}
                title={track.title}
                coverImage={track.coverImage.url}
                artist={track.artist}
              />
            ))}
          </motion.div>
        </div>
 
        {/* Spotify top Artists */}
 
        <div className="font-barlow">
          <AnimatedHeading
            variants={fromLeftVariant}
            className="text-3xl sm:text-4xl capitalize font-bold text-neutral-900 dark:text-neutral-200"
          >
            My Top Artists
          </AnimatedHeading>
          <AnimatedText className="mt-4 text-gray-500">
            My most listened Artist is{" "}
            <span className="font-semibold">{artists && artists[0].name}</span>{" "}
            on Spotify.
          </AnimatedText>
 
          <motion.div
            variants={FadeContainer}
            initial="hidden"
            whileInView="visible"
            viewport={{ once: true }}
            className="flex flex-col my-10 gap-0 font-barlow"
          >
            {artists?.map((artist, index) => (
              <Artist
                key={index}
                id={index}
                name={artist.name}
                url={artist.url}
                coverImage=