Building a portfolio website using Nextjs - Part 2 - (building the components for site layout)
In this part, we will build the components required for our 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'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>
);
}
Building the Footer Component
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'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.
Gallery
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.