Building a portfolio website using Nextjs - Part 2 - (building the components for site layout)

In this part, we gonna build all the components, we need to build the layout of our website, for that we need to build the components to be used globally.

So the portfolio site will require this layout components mainly :

  • Header

  • Body

  • Footer

And for the components, we might need to build responsive components for the below :

  • Fixed Navbar with Menu and Dropdown (responsive for mobile)

  • Dark mode Toggle

  • Footer

  • Follow me (social media links)

  • Newsletter

  • Scroll To Top

Todo: (setting up storybook and jest)

For this we are going to utilise Storybook to render the components and Jest to write tests for each component, and also setup a test coverage for making sure that our components pass 100% test coverage.

Getting Started

We will create the components inside components folder, (which will be inside src/components folder and since it will be having all the client side components, we will be using use client to tell Next.js that this are client side components.

Building the Navbar component

src/components/Navbar/Navbar.tsx

"use client";
import Link from "next/link";
import { NavbarProps } from "./Navbar.types"; // import the types
import Icon from "../Icon";
import DarkModeSwitcher from "../DarkModeSwitcher/DarkModeSwitcher";

export const Navbar = ({
  siteTitle = "Next Portfolio",
  routes,
}: NavbarProps) => {
  return (
    <div className="navbar bg-gray-50/80 dark:bg-gray-900/80 border-b border-gray-200 dark:border-gray-800 fixed w-full z-50 backdrop-blur-md">
      <div className="navbar-start container mx-auto">
        <div className="dropdown">
         <!-- menu icon -->
          <div
            tabIndex={0}
            role="button"
            className="btn btn-ghost text-black dark:text-white lg:hidden"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="h-5 w-5"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="M4 6h16M4 12h8m-8 6h16"
              />
            </svg>
          </div>
         <!--- Nav menu for mobile -->
          <ul
            tabIndex={0}
            className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow dark:bg-gray-900 border border-gray-200 dark:border-gray-800"
          >
            {routes.map((route, index) => (
              <li key={index}>
                <Link href={route.path} className="text-black dark:text-white">
                  {route.name}
                </Link>
              </li>
            ))}
          </ul>
        </div>
        <!-- Site Title or Logo -->
        <Link
          className="btn btn-ghost text-xl text-black dark:text-white"
          href={"/"}
        >
          <img
            src="/assets/images/sujay.png"
            alt="Sujay"
            className="w-10 h-10"
          />

          {siteTitle}
        </Link>
      </div>
      <!-- menu for tablet and desktop -->
      <div className="navbar-center hidden lg:flex">
        <ul className="menu menu-horizontal px-1">
          {routes.map((route, index) => (
            <li key={index}>
              <Link
                href={route.path}
                className="text-lg text-black dark:text-white"
              >
                {route.name}
              </Link>
            </li>
          ))}
        </ul>
      </div>
      <!--- CTA Button -->
      <div className="navbar-end">
        <Link className="btn dark:btn-neutral" href="/contact">
          {" "}
          Let&apos;s connect
          {/* <MessageCircleCode color="#111" size={22} />{" "} */}
          <Icon
            name="message-circle-code"
            className="text-white dark:text-black"
            size={22}
          />
        </Link>
      </div>
    </div>
  );
};

export default Navbar;

Let's create the types file src/components/Navbar/Navbar.types.ts

interface Route {
  name: string;
  path: string;
  submenu?: Route[];
}

export interface NavbarProps {
  siteTitle: string;
  routes: Route[];
}

Alright let's create a siteConfig.ts file inside src/siteConfig.ts to add all our data in one place, so that our component's can use the json data from this file.

export const config = {
    siteTitle: "Sujay Kundu",
    siteDescription: "Full Stack Web Developer",
    siteKeywords: "portfolio, website, nextjs",
    siteUrl: "https://next-portfolio.vercel.app",
    siteLanguage: "en-US",
    authorName: "Sujay Kundu",
    authorEmail: "sujaykundu@gmail.com",
    authorBio: "I'm a full stack web developer and a wanderlust, based in India. If you are looking for a developer, I'm available for hire.",
    routes: [
    {
      name: "Home",
      path: "/",
    },
    {
      name: "About",
      path: "/about",
    },
    {
      name: "Blog",
      path: "/blog",
    },
    {
      name: "Portfolio",
      path: "/portfolio",
      submenu: [
        {
          name: "Web Development",
          path: "/portfolio/web-development",
        },
        {
          name: "Mobile Apps",
          path: "/portfolio/mobile-apps",
        },
        {
          name: "Design Projects",
          path: "/portfolio/design",
        },
      ],
    },
    {
      name: "Gallery",
      path: "/gallery",
      submenu: [
        {
          name: "Photography",
          path: "/gallery/photography",
        },
        {
          name: "Digital Art",
          path: "/gallery/digital-art",
        },
      ],
    },
  ]
}

As you can see, I have added a routes obj with all the routes. We will be using this routes object to render the Navbar menu.

Alright!, Our Navbar component is ready, let's now hook it in to our layout :

Adding Navbar to Layout :

Inside src/app/layout.tsx we will be importing the Navbar component:

import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

// site config
import { config } from "../siteConfig";

// components
import { Navbar } from "../components/Navbar/Navbar";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: config.siteTitle,
  description: config.siteDescription,
  keywords: config.siteKeywords,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-50 dark:bg-gray-900`}
      >
        <header className="bg-gray-50 dark:bg-gray-900">
          <Navbar siteTitle={config.siteTitle} routes={config.routes} />
        </header>
        <div className="pt-20">{children}</div>
      </body>
    </html>
  );
}

Our footer component will display the site title and the year. Let's create the footer component in src/components/Footer/Footer.tsx

"use client";
import { FooterProps } from "./Footer.types";

export const Footer = ({ siteTitle }: FooterProps) => {
  return (
    <div className="bg-base-100 text-base-content">
      <div className="container mx-auto flex flex-col items-center justify-center p-4">
        <p className="text-sm">
          {siteTitle} {new Date().getFullYear()}
        </p>
      </div>
    </div>
  );
};

export default Footer;

Let's create our Footer interface src/components/Footer/Footer.types.ts

export interface FooterProps {
    siteTitle: string
}

Great ! let's now hook up this in to our Layout file as well, src/app/layout.tsx

import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

// site config
import { config } from "../siteConfig";

// components
import { Navbar } from "../components/Navbar/Navbar";
import { Footer } from "../components/Footer/Footer";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: config.siteTitle,
  description: config.siteDescription,
  keywords: config.siteKeywords,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-50 dark:bg-gray-900`}
      >
        <header className="bg-gray-50 dark:bg-gray-900">
          <Navbar siteTitle={config.siteTitle} routes={config.routes} />
        </header>
        <div className="pt-20">{children}</div>
        <footer>
          <Footer siteTitle={config.siteTitle} />
        </footer>
      </body>
    </html>
  );
}

Building the Dark mode switcher Component:

Well, we want to hook the dark mode switcher component in to our Navbar component, we will be using Tailwind css light and dark utility to switch our modes.

Creating the dark mode switcher component, src/components/DarkModeSwitcher/DarkModeSwitcher.tsx

"use client";
import { useState, useEffect } from "react";
import Icon from "../Icon";

export const DarkModeSwitcher = () => {
  const [isDarkMode, setIsDarkMode] = useState(false);

  useEffect(() => {
    const isDarkMode = localStorage.getItem("darkMode") === "true";
    setIsDarkMode(isDarkMode);
  }, []);

  const toggleDarkMode = () => {
    const newDarkMode = !isDarkMode;
    setIsDarkMode(newDarkMode);
    localStorage.setItem("darkMode", newDarkMode.toString());
    document.documentElement.classList.toggle("dark");
  };

  const LightIcon = () => <Icon name="sun" size={22} />;

  const DarkIcon = () => <Icon name="moon" size={22} />;

  return (
    <button
      onClick={toggleDarkMode}
      className="btn btn-ghost dark:btn-primary btn-circle"
    >
      {isDarkMode ? <LightIcon /> : <DarkIcon />}
    </button>
  );
};

export default DarkModeSwitcher;

Great ! lets hook this in our Navbar component.

"use client";
import Link from "next/link";
import { NavbarProps } from "./Navbar.types";
import Icon from "../Icon";

// import darkmodeswitcher
import DarkModeSwitcher from "../DarkModeSwitcher/DarkModeSwitcher";

export const Navbar = ({
  siteTitle = "Next Portfolio",
  routes,
}: NavbarProps) => {
  return (
    <div className="navbar bg-gray-50/80 dark:bg-gray-900/80 border-b border-gray-200 dark:border-gray-800 fixed w-full z-50 backdrop-blur-md">
      <div className="navbar-start container mx-auto">
        <div className="dropdown">
          <div
            tabIndex={0}
            role="button"
            className="btn btn-ghost text-black dark:text-white lg:hidden"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="h-5 w-5"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="M4 6h16M4 12h8m-8 6h16"
              />
            </svg>
          </div>
          <ul
            tabIndex={0}
            className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow dark:bg-gray-900 border border-gray-200 dark:border-gray-800"
          >
            {routes.map((route, index) => (
              <li key={index}>
                <Link href={route.path} className="text-black dark:text-white">
                  {route.name}
                </Link>
              </li>
            ))}
          </ul>
        </div>
        <Link
          className="btn btn-ghost text-xl text-black dark:text-white"
          href={"/"}
        >
          <img
            src="/assets/images/sujay.png"
            alt="Sujay"
            className="w-10 h-10"
          />

          {siteTitle}
        </Link>
      </div>
      <div className="navbar-center hidden lg:flex">
        <ul className="menu menu-horizontal px-1">
          {routes.map((route, index) => (
            <li key={index}>
              <Link
                href={route.path}
                className="text-lg text-black dark:text-white"
              >
                {route.name}
              </Link>
            </li>
          ))}
        </ul>
      </div>
      <div className="navbar-end">
        <Link className="btn dark:btn-neutral" href="/contact">
          {" "}
          Let&apos;s connect
          {/* <MessageCircleCode color="#111" size={22} />{" "} */}
          <Icon
            name="message-circle-code"
            className="text-white dark:text-black"
            size={22}
          />
        </Link>
        <!-- Dark Mode Switcher -->
        <div className="flex items-center">
          <DarkModeSwitcher />
        </div>
      </div>
    </div>
  );
};

export default Navbar;

Creating the pages

We need to build the about, blog, gallery, portfolio, contact pages

About page

src/app/about/page.tsx

export default function About() {
  return (
    <div>
      <h1>About</h1>
    </div>
  );
}

Blog

We already created the blog pages.

src/app/gallery/page.tsx

export default function Gallery() {
  return (
    <div>
      <h1>Gallery</h1>
    </div>
  );
}

Portfolio

src/app/portfolio/page.tsx

export default function Portfolio() {
  return (
    <div>
      <h1>Portfolio</h1>
    </div>
  );
}

Contact

src/app/contact/page.tsx

export default function Contact() {
  return (
    <div>
      <h1>Contact</h1>
    </div>
  );
}

So now whenever we click on Navbar menu links this pages should work fine. Ofcourse we will modify this pages later. So stay tuned for the next part,

In the next part (part-3), we will be creating our Hero and Recent Posts Component

Stay tuned for more.

0
Subscribe to my newsletter

Read articles from xplor4r (Maintainer) directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

xplor4r (Maintainer)
xplor4r (Maintainer)

Open Source Developer's Community Maintainer