Revamping My Developer Portfolio: From Outdated React to a Sleek Next.js Experience

The 2nd iteration of my developer portfolio documenting my journey, challenges faced, lessons learned, and growth as a software engineer, built with Next.js, Framer Motion, and Sanity CMS.

Jun, 2024Completedmagedfaiz.xyzSource
Carousel image (1)
Carousel image (2)

Introduction

Picture this: a developer, fueled by caffeine and ambition, decides to revamp his portfolio. Sounds like the beginning of a tech joke, right? Well, grab your popcorn (or your preferred debugging snack), because you're about to witness the thrilling saga of how I transformed my digital calling card from a static snoozefest to a dynamic masterpiece. And yes, I'm fully aware of the irony of reading about the creation of v2 on v2 itself. It's like Inception, but with more CSS and fewer Leonardo DiCaprios.

The Legacy of v1: A Cautionary Tale

Once upon a time, in a galaxy not so far away (aka my university days), I crafted what I thought was the Mona Lisa of portfolios. Spoiler alert: it wasn't. Built with the now-infamous create-react-app, v1 was a single-page React app that screamed, "I just learned React, and I must show it to the world!"

If you're in the mood for some good old-fashioned cringe, you're welcome to check it out here. Yes, it's still online, and no, I’m not proud of it. But hey, we all need a reminder of how bad things once were to appreciate the present, right? It's like those moments right before you sleep, when your brain decides to remind you of something so cringeworthy from your past that you wish the earth would swallow you whole. You’ll cringe, but you’ll also see how far you’ve come.

The create-react-app Conundrum

Oh, create-react-app, how you betrayed me. What once seemed like a magic wand for spawning React projects turned out to be more like a monkey's paw. Here's what I got for my wish:

The Epiphany: When "Good Enough" Becomes "Good Grief"

As time went on, my skills grew faster than my portfolio could keep up. Looking at v1 became like stumbling upon old Facebook (or should I say Meta) photos—you know the ones, with the questionable hairstyles and even more questionable fashion choices. It was a cringeworthy reminder of past design decisions that seemed brilliant at the time.

And yet, I’ve left it there, like a digital fossil, as a reminder of my humble beginnings. Every time I visit it, I remember: progress isn't about perfection, it's about moving forward.

Enter Emmanuel Gatwech better known as Eman, my brilliantly annoying friend and fellow software engineer. His constant nagging about documentation was like a persistent pop-up ad for personal growth—annoying, but impossible to ignore forever.

The Challenge: From Nagging to Bragging

Emmanuel's incessant preaching about the virtues of blogging and documenting personal experiences planted a seed in my mind. What if my portfolio wasn't just a showcase, but a chronicle of my developer journey? A place where each project came with its own epic saga of triumphs and face-palms?

This idea came with an unexpected bonus: a built-in BS detector for my projects. If a project wasn't blog-worthy, did it deserve a spot in my portfolio? Suddenly, I had a quality threshold that would make any perfectionist proud.

The Great Escape: Breaking the Rules (Sort Of)

Now, here's where I make a confession that would make my past self gasp: v2 of my portfolio doesn't quite meet the criteria I set for including projects. It's like I'm the developer version of those "Do as I say, not as I do" parents. But hey, if we can't bend the rules for ourselves, what's the point of making them, right?😌

The Tech Stack Glow-Up: Same Tools, New Tricks

For v2, I decided to stick with technologies I knew well but push them to their limits. It's like using a familiar recipe but adding exotic spices:

Designing for Impact: The UX/UI Overhaul

Designing a portfolio is like trying to fit your entire personality into a single outfit – it's not easy, and there's always the risk of looking like you tried too hard. After scouring the internet for inspiration and trying out designs that felt more forced than a sitcom laugh track, I stumbled upon Praha's Framer template. It was love at first sight (with a few tweaks, of course).

The goal was to create a UI that would make recruiters say "Wow!" and a UX that would make them say "That was easy!" I focused on stripping down the unnecessary fluff, prioritizing what recruiters and potential clients would want to see. The result? A sleek, dark-mode design with a single accent color that screamed "I'm professional, but I also know how to party (with code)."

Animation Station: Making Things Move (and Sometimes Disappear)

In v1, I was proud of my custom hook using the Intersection Observer API for scroll animations. Figuring out how those smooth animations worked online felt like unlocking a secret. Without relying on external tools, I built everything using the native browser API. The process was more complex than standard CSS animations, but managing each element manually with JavaScript taught me a lot about control and precision.

But v2? Oh, it's a whole new ballgame. Enter Framer Motion, the superhero of the animation world. Now, I’ve moved from manually crafting animations to effortlessly making them come to life. It's like going from flip-book animation to Disney-level smoothness overnight.

File:AnimateInView.tsx

'use client';
import { motion } from 'framer-motion';
import { BaseAnimationWrapperProps } from '@/lib/types';

const AnimateInView = ({
  children,
  threshold = 0.4,
  delay = 0.4,
  className = '',
  tag = 'div',
  initial = { opacity: 0, y: 15 },
  whileInView = { opacity: 1, y: 0 },
  once = true,
  duration = 0.5,
  ...rest
}: BaseAnimationWrapperProps) => {
  const MotionComponent = motion[tag];

  return (
    <MotionComponent
      initial={initial as any}
      whileInView={whileInView}
      transition={{
        delay: delay,
        duration,
      }}
      viewport={{ amount: threshold, once: once }}
      className={` ${className}`}
      {...rest}
    >
      {children}
    </MotionComponent>
  );
};

export default AnimateInView;

And let's not forget the pièce de résistance—the box reveal animation:

File:BoxesReveal.tsx

'use client';
import { BaseAnimationWrapperProps } from '@/lib/types';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';

const BoxesReveal = ({
  children,
  className,
  threshold = 0.4,
  once = true,
}: BaseAnimationWrapperProps) => {
  const [boxes, setBoxes] = useState<Array<number>>([]);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    // Create an array of 100 numbers (for 100 boxes)
    setBoxes(Array.from({ length: 100 }, (_, i) => i));
    // Set the image to visible after the grid is rendered
    setIsVisible(true);
  }, []);

  return (
    <div className={`relative h-full ${className}`}>
      <div className='absolute inset-0 grid grid-cols-10 grid-rows-10 pointer-events-none'>
        {boxes.map((box) => (
          <motion.div
            key={box}
            initial={{ opacity: 1 }}
            whileInView={{ opacity: 0 }}
            transition={{ delay: Math.random() * 2, duration: 1 }}
            viewport={{ amount: threshold, once }}
            className='bg-background z-10'
          />
        ))}
      </div>
      <div
        className={`h-full w-full ${isVisible ? 'opacity-100' : 'opacity-0'}`}
      >
        {children}
      </div>
    </div>
  );
};

export default BoxesReveal;

This little number turns image loading from a boring wait into a mesmerizing reveal. It's like those fancy scratch-off lottery tickets, but instead of disappointment, you get eye candy every time.

Now, buckle up, because we're about to dive into the treacherous waters of page transitions. Remember when I said Framer Motion was the superhero of the animation world? Well, even superheroes have their kryptonite, and for Framer Motion, it was Next.js's App Router.

The Great Page Transition Debacle

Picture this: you're cruising along, building your portfolio, feeling like the Tony Stark of web development. Then suddenly, you hit a wall. Implementing smooth page transitions with Framer Motion in Next.js's App Router turned out to be about as straightforward as teaching a cat to fetch while blindfolded. There's an open issue that's been haunting developers for nearly two years, turning what should be a simple animation into a Herculean task.

But fear not, dear reader, for every bug is just an opportunity for a developer to flex their problem-solving muscles! After diving deep into the GitHub issue trenches, I emerged with a solution. The key? A "freeze fix" that stops the unmount to allow the exit animation to work normally before updating the page. Sounds simple, right? Well, as any developer knows, solving one problem often leads to two more (it's like the hydra of the coding world).

File:ClientSideTransition.tsx

'use client';
import React, { PropsWithChildren, useContext, useRef } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { usePathname, useSearchParams } from 'next/navigation';
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { useIsClient } from '@/lib/context/IsClientContext';

// Client-side component to use useSearchParams() for optimal AnimatePresence keying
// Ensures transitions occur on both pathname and search param changes but requires
// to be wrapped in a Suspense component to prevent build errors
const ClientSideTransition = ({ children }: PropsWithChildren<{}>) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const isClient = useIsClient();

  // Create a key that includes both pathname and search params
  const pageKey = `${pathname}?${searchParams.toString()}`;

  // Avoid "Detected multiple renderers concurrently rendering the same context provider" error
  // by only rendering the animation wrapper on the client side
  if (!isClient) return <>{children}</>;

  return (
    <AnimatePresence mode='wait'>
      <motion.main
        key={pageKey}
        initial={{ opacity: 0, y: 50 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -50 }}
        transition={{ duration: 0.5 }}
      >
        <FrozenRouter>{children}</FrozenRouter>
      </motion.main>
    </AnimatePresence>
  );
};

export default ClientSideTransition;

// This component "freezes" the router context to prevent unwanted re-renders
// during page transitions. It's necessary because Framer Motion's AnimatePresence
// can cause multiple re-renders, which can lead to routing issues in Next.js 13+ App Router.
const FrozenRouter = (props: PropsWithChildren<{}>) => {
  const context = useContext(LayoutRouterContext);
  const frozen = useRef(context).current;

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {props.children}
    </LayoutRouterContext.Provider>
  );
};

First, my project filter suddenly decided to take an unscheduled vacation. Turns out, the animation key was only considering the pathname, completely ignoring the search params. The fix? Create a key that combines both the pathname and search params. Voila! The project filter was back in business.

And just when I thought I was out of the woods, Next.js threw another curveball. Apparently, using the useSearchParams hook is like handling an infinity stone - it needs to be wrapped carefully. Enter the Suspense component, swooping in like a safety net for our volatile hook.

File:PageTransition.tsx

import { PropsWithChildren, Suspense } from 'react';
import ClientSideTransition from './ClientSideTransition';

const PageTransition = ({ children }: PropsWithChildren<{}>) => {
  return (
    <Suspense fallback={<>{children}</>}>
      <ClientSideTransition>{children}</ClientSideTransition>
    </Suspense>
  );
};

export default PageTransition;

In the end, was it worth it? Absolutely. The portfolio now transitions between pages with the smoothness of a well-oiled machine. While I had to make some sacrifices along the way, the result is a more cohesive and polished user experience. After all, in the world of web development, sometimes less is more.

As we move on to the next challenge, remember: every bug is just an opportunity to level up your coding skills. Now, let's see what other surprises this portfolio has in store for us...

Rich Text and Code Parsing: Because Plain Text is So 1999

One of the crown jewels of v2 is the rich text and code parsing system. It's what allows you to read this very blog post without wanting to gouge your eyes out. Thanks to Sanity's flexible content structure and a custom-built parser, I can now write posts that are a joy to read and a breeze to maintain.

File:RichTextParser.tsx

import React, { memo } from 'react';
import Image from 'next/image';
import {
  PortableText,
  PortableTextReactComponents,
  PortableTextTypeComponentProps,
} from '@portabletext/react';
import { Code, RichText } from '@/lib/types';
import CodeParser from './CodeParser';

type props = {
  content: RichText;
};

const RichTextParser = memo(({ content }: props) => {
  let codeBlockCounter = 0;

  const myPortableTextComponents: PortableTextReactComponents = {
    types: {
      image: ({ value }) => (
        <Image
          loading='lazy'
          src={value.imageUrl}
          alt=''
          className='w-4/5 mx-auto'
        />
      ),
      callToAction: ({ value, isInline }) =>
        isInline ? (
          <a
            href={value.url}
            className='underline underline-offset-4 text-primary'
          >
            {value.text}
          </a>
        ) : (
          <div className='callToAction'>{value.text}</div>
        ),
      code: ({ value }: PortableTextTypeComponentProps<Code>) => {
        const id = `code-${++codeBlockCounter}`;
        return (
          <CodeParser
            id={id}
            code={value.code}
            language={value.language}
            filename={value.filename}
          />
        );
      },
    },
    marks: {
      em: ({ children }) => <em className=''>{children}</em>,
      link: ({ children, value }) => {
        const target = value.href.startsWith('http') ? '_blank' : undefined;
        const rel = target === '_blank' ? 'noreferrer noopener' : undefined;

        return (
          <a
            href={value.href}
            target={target}
            rel={rel}
            className='underline underline-offset-4 text-primary'
          >
            {children}
          </a>
        );
      },

      // Add any other custom marks you want to handle
    },
    block: {
      h1: ({ children }) => (
        <h1
          id={children?.toString().toLowerCase().replace(/\s+/g, '-')}
          className='text-4xl px-2 mt-10 mb-8'
        >
          {children}
        </h1>
      ),
      h2: ({ children }) => (
        <h2
          id={children?.toString().toLowerCase().replace(/\s+/g, '-')}
          className='text-3xl px-2 mt-8 mb-6'
        >
          {children}
        </h2>
      ),
      h3: ({ children }) => (
        <h3
          id={children?.toString().toLowerCase().replace(/\s+/g, '-')}
          className='text-2xl px-2 mt-6 mb-4'
        >
          {children}
        </h3>
      ),
      h4: ({ children }) => (
        <h4 className='text-xl px-2 mt-5 mb-3 '>{children}</h4>
      ),
      h5: ({ children }) => (
        <h5 className='text-lg px-2 mt-4 mb-2 '>{children}</h5>
      ),
      h6: ({ children }) => (
        <h6 className='text-base px-2 mt-3 mb-1 '>{children}</h6>
      ),
      normal: ({ children }) => (
        <p className='text-base mb-2 p-2 opacity-80'>{children}</p>
      ),
      blockquote: ({ children }) => (
        <blockquote className='text-base italic border-l-4 pl-2 mb-2 opacity-80'>
          {children}
        </blockquote>
      ),
    },
    list: {
      bullet: ({ children }) => (
        <ul className='list-disc pl-10 pr-2 space-y-2 opacity-80 mb-4'>
          {children}
        </ul>
      ),
      number: ({ children }) => (
        <ol className='list-decimal pl-10 pr-2 space-y-2'>{children}</ol>
      ),
      // Add any other custom list types you want to handle
    },
    listItem: {
      bullet: ({ children }) => <li className=''>{children}</li>,
      // Add any other custom list item types you want to handle
    },
    hardBreak: () => <br />,
    unknownMark: () => null,
    unknownType: () => null,
    unknownBlockStyle: () => null,
    unknownList: () => null,
    unknownListItem: () => null,
  };

  return (
    <PortableText
      value={content}
      components={myPortableTextComponents}
      onMissingComponent={false}
    />
  );
});

RichTextParser.displayName = 'Rich Text Parser';

export default RichTextParser;

File:CodeParser.tsx

import { codeToHtml } from 'shiki';
import { extractFilename } from '@/lib/utilities';
import { Code } from '@/lib/types';
import { HiOutlineArrowTopRightOnSquare } from 'react-icons/hi2';

const CodeParser = async ({ id, language, code, filename }: Code) => {
  const { name, link } = extractFilename(filename);
  const html = await codeToHtml(code, {
    lang: language,
    theme: 'material-theme-ocean',
  });

  return (
    <div id={id}>
      <h3 className='bg-foreground flex w-full -mb-[7px] p-1 rounded-t-lg border-b border-border'>
        File:
        <a
          href={link!}
          target='_blank'
          rel='noreferrer noopener'
          className={`flex items-center gap-1 text-primary pl-2 ${
            link ? 'hover:underline underline-offset-4' : ''
          }`}
        >
          <span className='opacity-100'>{name}</span>
          {link && <HiOutlineArrowTopRightOnSquare />}
        </a>
      </h3>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </div>
  );
};

export default CodeParser;

The result? A blogging system that makes both the reader and the developer in me proud. It ensures every post not only reads well but also functions seamlessly, like having both creative freedom and technical control in perfect balance.

SEO Optimization: Being Found in the Vast Internet Ocean

Because what's the point of having a beautiful website if it's easier to find Waldo than your portfolio online? I implemented a dynamic sitemap generation system to ensure that every nook and cranny of my site is discoverable by search engines.

File:route.ts

import { NextResponse } from 'next/server';
import { BASEURL, ROUTES } from '@/lib/Constants';
import { fetchSanityData } from '@/lib/sanity/client';
import { getProjectsForSEO } from '@/lib/sanity/queries';
import { Project } from '@/lib/types';

export const dynamic = 'force-dynamic';
export const revalidate = 0;

function generateSitemapXml(urls: { url: string; lastModified: string }[]) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  ${urls
    .map(
      (url) => `
    <url>
      <loc>${url.url}</loc>
      <lastmod>${url.lastModified}</lastmod>
    </url>
  `
    )
    .join('')}
</urlset>`;
}

export async function GET() {
  const allProjects: Project[] = await fetchSanityData(getProjectsForSEO);

  const posts = allProjects.map(({ slug, date }) => ({
    url: `${BASEURL}/projects/${slug.current}`,
    lastModified: date,
  }));

  const routes = ROUTES.map(({ href }) => ({
    url: `${BASEURL}${href}`,
    lastModified: new Date().toISOString(),
  }));

  const sitemapContent = generateSitemapXml([...routes, ...posts]);

  return new NextResponse(sitemapContent, {
    headers: {
      'Content-Type': 'application/xml',
    },
  });
}

This isn't just about being found; it's about making a grand entrance. Now, when Google's crawlers come knocking, my site rolls out the red carpet, complete with a detailed map of all the cool stuff they should check out.

Lessons Learned: The Portfolio Philosopher

Building v2 wasn’t about learning new tools—I was already familiar with the tech stack. The real challenge lay in designing a UI/UX that matched the vision I had for my portfolio. It was about taking what I already knew and pushing it further to create something I could truly be proud of. While v2 is a massive improvement over v1, the process wasn’t about a steep learning curve, but about refining my skills and building a portfolio that showcased how far I’ve come. Still, there were some valuable takeaways:

These challenges remind us that even with familiar tools, web development is full of surprises. It's not just about knowing your tech stack—it's about being ready to problem-solve and adapt when things don't go as planned.

The Road Ahead: This Journey's Far From Over

While v2 is out in the wild, the adventure is far from over. Future plans include:

In conclusion, creating v2 of my portfolio has been a journey of self-discovery, technical growth, and more than a few facepalm moments. It's a living document of my evolution as a developer, a testament to the power of persistent friends, and proof that you can teach an old dog new JavaScript frameworks.

So, fellow code wranglers, what's your portfolio journey been like? Have you faced similar challenges? Let's swap stories and keep pushing the boundaries of what a developer portfolio can be. After all, in the world of web development, the only constant is change—and the occasional bout of imposter syndrome.

Now, if you'll excuse me, I have a date with my code editor. These bugs aren't going to create themselves!

Share Project