Build a Portfolio Website Using Next.js 14 and Tailwind

Build a Portfolio Website Using Next.js 14 and Tailwind

A portfolio is an essential tool for every developer. It provides a dedicated space to showcase your projects, allowing you to demonstrate your skills and expertise. Through your portfolio, you can highlight the programming languages and libraries you are passionate about, giving potential employers or clients a clear understanding of your capabilities and interests. A well-crafted portfolio not only displays your technical abilities but also reflects your creativity, problem-solving skills, and dedication to continuous learning. It serves as a personal brand statement, setting you apart in a competitive job market and helping you build a professional identity.

Open your terminal and navigate to the directory where you want to create your new Next.js app. Run the this command to create a new Next.js application using the create-next-app package:

npx create-next-app@latest portfolio

Answer the questions as follows:

Ok to proceed? (y) y
✔ Would you like to use TypeScript? …  Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No 

To start the development server and see your new Next.js app in action, run:

cd portfolio
npm run dev

Open the portfolio/app/page.tsx file, clear everything and return an empty div as follows:

import Image from "next/image";

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

Open the app/globals.css file and remove all the styling except the lines for importing Tailwind styles:

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

Open the app/layout.tsx file and modify the body tag to center its children horizontally. On large screens, add a horizontal padding of 24 pixels. Ensure the text uses a sans-serif font and set the body's minimum height to be at least 100% of the viewport height ensuring it covers the full screen by adding the following classes:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="mx-auto lg:px-24 font-sans min-h-screen">{children}</body>
    </html>
  );
}

Create a components folder, inside of which create the main header component:

cd components
touch MainHeader.tsx 

Open the MainHeader.tsx file and add a component with a parent div with two headers, the first h1 header contains your name and the second h2 header contains your profession:

export const MainHeader: React.FC = () => {
  return (
    <div>
      <h1>
        ahmed.bouchefra
      </h1>
      <h2>
        software engineer and technical writer
      </h2>
    </div>
  );
};

Style the h1 element to be bold, blue, and monospaced with a font size of 2.25rem, while on small screens, the font size will be 3rem. Add className="font-bold text-blue-400 font-mono text-4xl sm:text-5xl". Style the h2 element with a top margin of 0.75rem, left padding of 2.5rem, black text color, medium weight monospaced font, and a font size of 1.25rem. In the h2 add className="mt-3 pl-10 text-black font-medium font-mono text-xl"

Open the app/page.tsx file and import the MainHeader component:

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

Next, include it inside the parent div of the home component as follows:

export default function Home() {
  return (
    <div>
      <MainHeader />
    </div>
  );
}

Save and go to http://localhost:3000/. <!-- HERE --> Next, create a Navigation.tsx file inside the components folder. Add a variable named cssClass, that contains the styles for styling the text to be small, bold, monospaced, uppercase, and blue:

const cssClass = "text-sm font-bold font-mono uppercase text-blue-500"

Next, add and export a React functional component named Navigation rendering a hidden navigation menu on small screens and a visible menu on large screens. The menu includes an unordered list of links pointing to different sections of the page and positioned 4rem from the left and top, each styled with the cssClass, and constrained to fit its content in width.

export const Navigation: React.FC = () => {
  return (
    <nav className="hidden lg:block">
      <ul className="ml-16 mt-16 w-max">
        <li>
          <a className={cssClass} href="/#about">  
              about
          </a>
        </li>
        <li>
          <a className={cssClass} href="/#experience">  
          experience
          </a>
        </li>
        <li>
          <a className={cssClass} href="/#projects">  
          projects
          </a>
        </li>
        <li>
          <a className={cssClass} href="/#blog">  
          blog
          </a>
        </li>

      </ul>
    </nav>
  );
};

Each list item contains a link that uses the cssClass for styling and points to different sections that we will create later in this tutorial.

Next, in the app/page.tsx file import the Navigation component:

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

Next, include it inside the parent div of the home component as follows:

export default function Home() {
  return (
    <div>
      <MainHeader />
      <Navigation />
    </div>
  );
}

Save and go to http://localhost:3000/.

Next, create a Social.tsx file inside the components folder with the following imports to bring in specific elements and icons from the React library and the Font Awesome icon library:

import { JSXElementConstructor, ReactElement } from  "react";
import { FaMediumM, FaGithub, FaLinkedinIn } from  "react-icons/fa";
import { FaXTwitter } from  "react-icons/fa6";

In the same file, add an array of social links, each element has an id, URL and icon:

const socialLinks: {
  id: string;
  url: string;
  icon: ReactElement<any, string | JSXElementConstructor<any>>;
}[] = [
  {
    id: "Twitter",
    url: "https://x.com/ahmedbouchefra",
    icon: <FaXTwitter size={25} color="#6A6A6A" />,
  },
  {
    id: "GitHub",
    url: "http://www.github.com/techiediaries",
    icon: <FaGithub size={25} color="#6A6A6A" />,
  },
  {
    id: "LinkedIn",
    url: "https://www.linkedin.com/in/mr-ahmed",
    icon: <FaLinkedinIn size={25} color="#6A6A6A" />,
  },
  {
    id: "Medium",
    url: "https://codewithmrnerd.medium.com/",
    icon: <FaMediumM size={25} color="#6A6A6A" />,
  },
];

Next, add and export a functional component that renders a list of social media links styled with a top margin of 2rem, horizontally aligned using flexbox, and centered on large screens. Each link within the list has a right margin of 0.625rem and is styled with a smaller font size. Also, each link has an icon wrapped in an anchor tag and includes screen reader-only text for accessibility purposes:

export const Social: React.FC = () => {
  return (
    <ul className="mt-8 flex lg:justify-center">
      {socialLinks.map((link) => (
        <li key={link.id} className="mr-5 text-xs">
          <a href={link.url}>
            <span className="sr-only">{link.id}</span>
            {link.icon}
          </a>
        </li>
      ))}
    </ul>
  );
};

Next, install the react-icons library:

npm i react-icons

Next, in the app/page.tsx file import the Social component:

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

Next, include it inside the parent div of the home component as follows:

export default function Home() {
  return (
    <div>
      <MainHeader />
      <Navigation />
      <Social  />
    </div>
  );
}

Next, wrap everything inside the header tag as follows:

export default function Home() {
  return (
    <div>
      <header>
        <MainHeader />
        <Navigation />
        <Social />
      </header>
    </div>
  );
}

Style the header tag to display as a flex container with a column layout on large screens, distributing space evenly between its children, occupying half of its container's width, and applying a vertical padding of 6rem:

className="lg:flex lg:flex-col lg:justify-between lg:w-1/2 lg:py-24"

On large screens (1024px and up), the element uses a flexbox layout to arrange its children in a column with space distributed between them along the vertical axis and occupies half the width of its container.

Save and go to http://localhost:3000/.

Adding the main contents

The About Section

Now, let's add the main contents. Inside the components folder, create an AboutSection.tsx file which defines and exports a React functional component named AboutSection, rendering a section with an id of "about" containing a heading labeled "About" with a bold and uppercased font:

export const AboutSection: React.FC = () => {
  return (
    <section id="about">
      <div>
        <h2 className="font-bold uppercase">About</h2>
      </div>
    </section>
  );
};

Add a second div containing some text about the developer:

      <div>
        <p>
          I am an enthusiastic software developer and technical writer with a
          passion for creating innovative solutions and sharing knowledge. With
          a background in computer science and a keen eye for detail, I blend
          creativity and precision to develop software that is not only
          functional but also user-friendly.
        </p>
        <p>
          My journey in the tech world started at a young age, fueled by a
          curiosity for how things work and a desire to make a difference
          through technology. As a software developer, I specialize in crafting
          robust applications using a variety of programming languages and
          frameworks. My expertise ranges from front-end development with HTML,
          CSS, and JavaScript to back-end development with Node.js, Python, and
          databases like SQL and MongoDB.
        </p>
        <p>
          I thrive on solving complex problems and enjoy the challenge of
          optimizing code for better performance and scalability. In addition to
          my development work, I am a dedicated technical writer. I believe that
          clear and concise documentation is essential for the success of any
          project. I have a knack for breaking down complex technical concepts
          into easily understandable content, making it accessible to a wider
          audience. My writing covers a range of topics, from detailed API
          documentation and user manuals to informative blog posts and
          tutorials. When I'm not coding or writing, I enjoy exploring the
          latest advancements in technology, contributing to open-source
          projects, and participating in tech communities.
        </p>
        <p>
          I am committed to continuous learning and growth, always looking for
          new ways to enhance my skills and share my knowledge with others.
          Whether I'm developing cutting-edge software or crafting insightful
          technical content, my goal is to make technology more approachable and
          impactful for everyone.
        </p>
      </div>

In the app/page.tsx file and below the header tag, add a main tag that contains the about section component, after importing it, with a top padding of 6rem and on large screens, the width of the element should be half of its container:

<main className="pt-24 lg:w-1/2">
   <AboutSection  />
</main>

Save and go to http://localhost:3000/.

Now we want to make the header and navigation aligned with space evenly distributed vertically on large screens with a padding of 6 pixels and a gap of 4 pixels using flexbox. To the top div add the following classes: className="p-5 lg:flex lg:justify-between lg:gap-4"

Save and go to http://localhost:3000/.

The Experience Section

Next, let's add the next section for displaying experiences. Inside the components folder, add an ExpSection.tsx file with the following array of experiences:

const experiences = [
  {
    id: "Tech Innovators Inc.",
    title: "Junior Software Engineer",
    url: "https://techinnovators.com/",
    duration: "Dec 2019 - Dec 2020",
    about:
      "Worked on various front-end and back-end projects, improving the functionality and performance of web applications, and collaborating with senior developers to implement new features.",
  },
  {
    id: "Creative Coders",
    title: "Front-End Developer",
    url: "https://creativecoders.com/",
    duration: "Jan 2021 - June 2022",
    about:
      "Focused on creating responsive and user-friendly web interfaces using modern JavaScript frameworks, CSS, and HTML, while ensuring cross-browser compatibility and accessibility.",
  },
  {
    id: "NextGen Solutions",
    title: "Software Engineer",
    url: "https://nextgensolutions.com/",
    duration: "July 2022 - Present",
    about:
      "Developing and maintaining scalable software solutions, leading a team of developers, and utilizing cloud technologies to enhance application performance and reliability.",
  }
];

Next, add and export a functional component that renders a section with id "experience", and a top margin of 16 pixels. The section contains two divs with the first one styled using a bold font, uppercased and containing a header labeled "Experience" :

export const ExpSection: React.FC = () => {
  return (
    <section id="experience" className="mt-16">
      <div className="font-bold uppercase">
        <h2>Experience</h2>
      </div>
      <div>  
      </div>
    </section>
  );
};

Inside the second div, iterate over the experiences array using the map method and display them using an ordered list. Each element of the list has a bottom margin of 12 pixels and contains a div that contains a header of the job duration with z-index set to 10, a bottom margin of 2 pixels, a top margin of 1 pixel, and the text styled with a font size equivalent to 12 pixels, bold weight, uppercase letters, and a monospaced font family:

        <ol>
          {experiences.map((job) => (
            <li key={job.id} className="mb-12">
              <div className="hover:bg-blue-500/20 px-1 py-5 rounded">
                <header className="z-10 mb-2 mt-1 text-xs font-semibold uppercase font-mono">
                  {job.duration}
                </header>
              </div>
            </li>
          ))}
        </ol>

Below the header, add the following div that contains an h3 header with a bold font which display the job title and ID, and a paragraph which displays the job description:

                <div>
                  <h3 className="font-bold">
                    {job.title} ⚡️ {job.id}
                  </h3>

                  <p>{job.about}</p>
                </div>

Next, inside the app/page.tsx, import the component:

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

Then add it inside the main tag:

export default function Home() {
  return (
    <div className="lg:flex lg:justify-between lg:gap-4">
      <header className="lg:flex lg:flex-col lg:justify-between lg:w-1/2 lg:py-24">
        <MainHeader />
        <Navigation />
        <Social />
      </header>
      <main className="pt-24 lg:w-1/2">
        <AboutSection />
        <ExpSection />
      </main>
    </div>
  );
}

The Project Section

Next, let's add the next section for displaying projects. Inside the components folder, add a ProjectSection.tsx file with the following code to define and export a client component that renders a section with two divs. The first div is styled with a bold font, uppercase and bottom margin of 8 pixels and contains an h2 header labeled "Projects":

"use client"
export const ProjectSection: React.FC = () => {
  return (
    <section id="projects" className="mt-16">
      <div className="font-bold uppercase mb-8">
        <h2>Projects</h2>
      </div>
      <div></div>
    </section>
  );
};

Next, on the top of the component import useEffect and useState:

import { useEffect, useState } from  "react";

Next, create a state variable for holding projects:

  const [projects, setProjects] = useState([]);

Next, fetch the projects from your GitHub repository using the fetch method and store them in the projects variable:

  useEffect(() => {
    async function fetchProjects() {
      try {
        const res = await fetch(
          "https://api.github.com/users/techiediaries/repos"
        );
        const data = await res.json();
        setProjects(data);
      } catch (error) {
        setError(true);
      }
    }
    fetchProjects();
  }, []);

Inside the second div of the component, include an ordered list and iterate over the projects using the map method. Each list item has a margin bottom of 10 pixels and contains an anchor tag linking to the project's URL, which opens in a new tab. The content of the item is organized within a container grid with four columns and a 4-pixel gap, rounded edges, and a hover effect. This hover effect changes the background color of the element to a semi-transparent blue with 20% opacity. Within the container, there is a header, styled with an extra small font size, semi-bold weight, monospaced font, and uppercase text transformation, spanning one column, and displaying the last update date, and a div spanning three columns containing an h3 tag with the project name and URL, styled with a bold font weight. Additionally, there's another div containing the project description, styled with a monospaced font.

        <ol>
          {projects.map((project) => (
            <li key={project.id} className="mb-10">
              <a key={project.name} href={project.html_url} target="_blank">
                <div className="grid grid-cols-4 gap-4 rounded hover:bg-blue-500/20">
                  <header className="text-xs font-semibold font-mono uppercase col-span-1">
                    Last updated on {new Date(project.pushed_at).toDateString()}
                  </header>
                  <div className="col-span-3">
                    <h3 className="font-bold">
                      {project.name} {project.html_url && "🔗"}
                    </h3>
                    <div className="font-mono">{project?.description}</div>
                  </div>
                </div>
              </a>
            </li>
          ))}
        </ol>

Next, inside the app/page.tsx, import the component:

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

Then add it inside the main tag of the home component:

export default function Home() {
  return (
    <div className="p-5 lg:flex lg:justify-between lg:gap-4">
      <header className="lg:flex lg:flex-col lg:justify-between lg:w-1/2 lg:py-24">
        <MainHeader />
        <Navigation />
        <Social />
      </header>
      <main className="pt-24 lg:w-1/2">
        <AboutSection />
        <ExpSection />
        <ProjectSection />
      </main>
    </div>
  );
}

Save and go to http://localhost:3000/.

The Blog Section

Now, let's add the blog section. Inside the components folder, create a BlogSection.tsx file which defines and exports a React functional component named BlogSection, rendering a section with an id of "blog" containing a heading labeled "Blog" with a bold and uppercased font:

export const BlogSection: React.FC = () => {
  return (
    <section id="blog" className="mt-16">
      <div className="font-bold uppercase">
        <h2>Blog</h2>
      </div>
      <div>
      <p className="text-center">
          <a href="https://codewithmrnerd.medium.com/" target="_blank">
            👉 Read my posts on Medium
          </a>
        </p>
      </div>
    </section>
  );
};

Next, inside the app/page.tsx, import the component:

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

Then add it inside the main tag of the home component:

export default function Home() {
  return (
    <div className="p-5 lg:flex lg:justify-between lg:gap-4">
      <header className="lg:flex lg:flex-col lg:justify-between lg:w-1/2 lg:py-24">
        <MainHeader />
        <Navigation />
        <Social />
      </header>
      <main className="pt-24 lg:w-1/2">
        <AboutSection />
        <ExpSection />
        <ProjectSection />
        <BlogSection />
      </main>
    </div>
  );
}

Save and go to your browser.

Making the Navigation Sticky

Now, let's make the navigation menu sticky on large screens. To achieve that, in the app/page.tsx file add the lg:sticky lg:top-0 lg:max-h-screen to the header tag.

Save and go to your browser.


  • Date: