Open-sourcing my personal portfolio

Introduction

I've always wanted to have a personal portfolio website. A little corner of the internet where I could showcase my work, share my hobbies, and just say I have a personal website. But for the longest time, I wasn't sure what would go on it. I dabbled in various hobbies and projects but needed more time to be ready to combine them into a cohesive presentation. But now here we are!

Open-Source Software and Photography

As you probably already know, I am a software engineer, developer, r&d engineer, or whatever you like to call it → I write code. One of the most beautiful things about being a developer is the open-source software and its community, especially in the crypto/web3 space. Supporting each other (primarily) and building in the open is a different kind of passion. I started contributing to open-source software when we began building Masca at Blockchain Lab:UM. With Masca being a fantastic project, the idea of having a website to showcase projects I'm working on came up.

Additionally, I picked up photography as an amateur a couple of years ago. I have always wanted to shoot beautiful portraits of my girlfriend, family, and friends, and bokeh is one of the core effects I consider to make up a great portrait photo. The reason for my picking up photography was the poor performance of the bokeh effect on the iPhone's portrait mode - you can see the blur being digitally applied instead of it being there because of the actual lens settings. With more and more photos piling up, I wanted to show some of them to more people, and this is how the idea of having a Gallery on my personal website came up.

The more photos I took and the more open-source projects I contributed to, the more I wanted a place to display them. Building a website now made more and more sense because I had something to put on there.

The Tech Stack

We mostly used Next.js with React and Supabase for frontend projects I worked on, so deciding which tech stack to use was a no-brainer for me. I used NextUI with TailwindCSS for styling because of their simplicity and whatnot. Oh, and I made the logo myself, I hope you dig it! For hosting, I use Vercel because of their amazing free tier.

Bunny's den logo
Bunny's den logo

Optimizing photos' loading times was the most excellent "feature" I implemented. Next.js heavily pushes server-side rendering, so I wanted to get and cache as much data as I could on the server. I get the URLs of the first 10 photos on the server side and then serve them to the client, which then fetches the following 10 pictures by using infinite scroll with TanStack Query (kudos to the developer for abstracting those principles and everything else it supports to such an extent - I recommend you check it out). Check out below how I build the useInfiniteQuery to handle this.

const useImages = (initialData: { src: string; placeholder: string }[]) => {
  return useInfiniteQuery({
    queryKey: ['images'],
    queryFn: async ({ pageParam }) => {
      const response = await fetch('/api/images', {
        headers: {
          'Content-Type': 'application/json',
        },
        method: 'POST',
        body: JSON.stringify({
          limit: 5,
          offset: pageParam,
        }),
        next: { revalidate: 60 * 60 * 24 * 7 }, // cache for 7 days
      });
      if (response.status !== 200) {
        return [];
      }
      return (await response.json()).urls;
    },
    staleTime: Number.POSITIVE_INFINITY,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => {
      if (lastPage.length === 0) {
        return null;
      }
      return pages.flat().length + 1;
    },
    initialData: {
      pages: [initialData],
      pageParams: [0],
    },
    placeholderData: keepPreviousData,
  });
};

And then, in the GalleryGrid component, I make sure to load new photos each time the user reaches the bottom of the page like shown in the code below.

export default function GalleryGrid({
  data,
}: { data: { src: string; placeholder: string }[] }) {
  const images = useImages(data);
  const observer = useRef<IntersectionObserver | null>(null);
  const sentinelRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (observer.current) observer.current.disconnect();

    observer.current = new IntersectionObserver(
      (entries) => {
        if (
          entries[0].isIntersecting &&
          !images.isFetching &&
          images.hasNextPage
        ) {
          images.fetchNextPage();
        }
      },
      { root: null, rootMargin: '0px', threshold: 1.0 }
    );

    if (sentinelRef.current) {
      observer.current.observe(sentinelRef.current);
    }

    return () => {
      if (observer.current) observer.current.disconnect();
    };
  }, [images]);

  return (
    <div className="flex flex-col justify-center">
      <Masonry
        items={images.data.pages.flat()}
        config={{
          columns: [1, 2, 3, 4],
          gap: [12, 12, 6, 3],
          media: [640, 768, 1024, 1280],
        }}
        render={(item, idx) => (
          <Image
            key={idx}
            src={item.src}
            alt={`Gallery image at: ${item}`}
            style={{
              width: '100%',
              height: 'auto',
            }}
            width={512}
            placeholder="blur"
            blurDataURL={item.placeholder}
            height={512}
            sizes="100vw"
            className="rounded-xl object-fit shadow-lg"
          />
        )}
      />
      <div ref={sentinelRef} />
      {images.isFetching && <Spinner size="md" className="my-12" />}
    </div>
  );
}

I first set the initialData to what I fetch already on the server side and then useInfiniteQuery to handle "pagination".

Challenges and Learnings

Even though I prefer working more on the backend, infrastructure, and DevOps, I had a blast creating my personal website. One challenge I faced was using the Plaiceholder package to fetch placeholder images for my photos. Still, it doesn't make sense to use it for so many pictures because it sends a request for every single one, and because of it, Vercel times out in production. Even though those placeholders are cached for the next week, the loading time is more significant and can exceed 10 seconds, leading to errors. To solve this, I just used a blank grey placeholder instead of generating unique ones using Plaiceholder.

"Almost" perfect Lighthouse score

I also scored an almost perfect score on Lighthouse. The only issue is accessibility because the Hamburger nav button does not have aria labels set. Besides that, everything is perfect! You can view the report here.

Lighthouse score
Lighthouse score

Conclusion

So, there you have it! That's the story of how I decided to create my personal website. It's been a fun journey, and I'm excited to continue improving it. The website is open source and available on GitHub. Check it out, and let me know what you think!

Inspiration


Written with ❤️, from me to you.

Twitter | Farcaster | Lens

Subscribe to pseudobun
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.