UNDR

Jorge Delgadillo

#website link UNDR

Crafting an Aesthetic Music Portfolio with Next.js and Framer Motion

Building a compelling online presence is crucial for any artist, and for musicians, a visually engaging portfolio is paramount. In this blog post, I'll walk you through my experience developing an aesthetic music portfolio website using Next.js, TypeScript, SCSS, and the powerful animation library, Framer Motion.

Why These Technologies?

For this project, I chose Next.js for its server-side rendering capabilities and performance optimizations. I opted for the pages router over the app router due to the complexity of implementing smooth page transitions. TypeScript brought type safety and improved code maintainability. SCSS allowed for clean and organized styling, and Framer Motion provided the tools for creating fluid and engaging animations.

To manage the portfolio's content, I utilized a Google Sheets API as a makeshift database. This allowed me to easily store and retrieve data such as image links, iframe links, data names, credits, and titles.

The Preloader Experience

One of the most enjoyable aspects of this project was developing a preloader animation. A well-crafted preloader can significantly enhance the user experience by providing visual feedback during the initial loading phase.

Here's the code snippet for the preloader component:

import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import styles from "./Preloader.module.scss";

export default function Preloader() {
  const [loadingPercentage, setLoadingPercentage] = useState(0);
  const [isLoaded, setIsLoaded] = useState(false);

  // Simulate loading progress
  useEffect(() => {
    const interval = setInterval(() => {
      setLoadingPercentage((prev) => {
        if (prev < 100) {
          return prev + 1;
        } else {
          clearInterval(interval);
          setIsLoaded(true);
          return prev;
        }
      });
    }, 12); // Adjust speed of count

    return () => clearInterval(interval); // Cleanup the interval
  }, []);

  return (
    <AnimatePresence>
      {!isLoaded && (
        <motion.div
          className={`${styles.preloader} fixed inset-0 flex items-end w-screen bg-[#0A0A0A] z-50`}
          initial={{ y: 0 }} // Start at its original position
          animate={{ y: 0 }} // Keep in place while loading
          exit={{ y: "100%" }} // Slide background out after delay
          transition={{ delay: 0.6, duration: 0.3, ease: "easeInOut" }} // Delay background slide
        >
          {/* Loading Percentage */}
          <motion.div
            className="text-white text-4xl  flex justify-between items-end w-full px-3 "
            initial={{ y: 0 }}
            animate={{ y: 0 }}
            exit={{ y: "100%" }} // Slide loading percentage out first
            transition={{ delay: 0.5, duration: 0.5, ease: "easeInOut" }} // Shorter duration for loading percentage
          >
            <div className="text-[15rem] leading-none tracking-tight font-black text-stone-800">
              {loadingPercentage}%
            </div>
            <motion.div
              initial={{ y: 0 }}
              animate={{ y: 0 }}
              exit={{ y: "100%" }}
              transition={{ delay: 0.5, duration: 0.5, ease: "easeInOut" }}
              className=" text-[10rem] pr-4 leading-none font-black scale-x-105 tracking-tight "
            >
              UNDR
            </motion.div>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

And here is the SCSS file for the preloader.

.preloader {
  background-color: #0a0a0a; // Dark background with opacity
  color: white;
  font-family: "Inter", sans-serif;
  z-index: 9999; // Ensure it stays on top
  //   transition: opacity 0.5s ease-in-out;
}

Explanation:

Key Takeaways

This project was a great learning experience, and I'm excited to continue exploring the possibilities of Next.js and Framer Motion.

Text Scramble Animation

Another great animation to experiment with was the text-scramble effect on the about page. This adds a dynamic and engaging element to the text, revealing it piece by piece with a randomized character effect.

Here's the code snippet:

"use client";
import { type JSX, useEffect, useState } from "react";
import { motion, MotionProps } from "framer-motion";

type TextScrambleProps = {
  children: string;
  duration?: number;
  speed?: number;
  characterSet?: string;
  as?: React.ElementType;
  className?: string;
  contentClassName?: string;
  trigger?: boolean;
  onScrambleComplete?: () => void;
} & MotionProps;

const defaultChars =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";

export function TextScramble({
  children,
  duration = 0.8,
  speed = 0.04,
  characterSet = defaultChars,
  className,
  contentClassName,
  as: Component = "p",
  trigger = true,
  onScrambleComplete,
  ...props
}: TextScrambleProps) {
  const MotionComponent = motion.create(
    Component as keyof JSX.IntrinsicElements
  );
  const [displayText, setDisplayText] = useState(children);
  const [isAnimating, setIsAnimating] = useState(false);
  const text = children;

  const scramble = async () => {
    if (isAnimating) return;
    setIsAnimating(true);

    const steps = duration / speed;
    let step = 0;

    const interval = setInterval(() => {
      let scrambled = "";
      const progress = step / steps;

      for (let i = 0; i < text.length; i++) {
        if (text[i] === " ") {
          scrambled += " ";
          continue;
        }

        if (progress * text.length > i) {
          scrambled += text[i];
        } else {
          scrambled +=
            characterSet[Math.floor(Math.random() * characterSet.length)];
        }
      }

      setDisplayText(scrambled);
      step++;

      if (step > steps) {
        clearInterval(interval);
        setDisplayText(text);
        setIsAnimating(false);
        onScrambleComplete?.();
      }
    }, speed * 1000);
  };

  useEffect(() => {
    if (!trigger) return;

    scramble();
  }, [trigger]);

  return (
    <MotionComponent className={className} {...props}>
      <span className={contentClassName}>{displayText}</span>
    </MotionComponent>
  );
}

Integrating Google Sheets as a Dynamic Data Source in Next.js API Routes

In this section, we'll dissect a Next.js API route that leverages Google Sheets as a dynamic data source. This is a powerful technique for managing content without needing a full-fledged database, especially for projects with frequently updated data.

Understanding the Code

The provided code creates a Next.js API route that fetches data from a Google Sheet and returns it as JSON. Let's break down the key components:

1. CORS Configuration

import Cors from "cors";

const cors = Cors({
  methods: ["GET"],
  origin:
    process.env.NODE_ENV === "production"
      ? process.env.NEXT_PUBLIC_SITE_URL
      : "*",
});

2. Middleware Helper

const runMiddleware = (
  req: NextApiRequest,
  res: NextApiResponse,
  fn: (
    req: NextApiRequest,
    res: NextApiResponse,
    callback: (result: unknown) => void
  ) => void
) => {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: unknown) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
};

3. Environment Variables and Configuration

const GOOGLE_SPREADSHEET_ID = process.env.GOOGLE_SPREADSHEET_ID;
const API_KEY = process.env.GOOGLE_API_KEY;
const RANGE = "Sheet1!B4:F";

4. Data Fetching and Caching

// Cache the data for 5 minutes
let cachedData: ProjectData[] | null = null;
let lastFetchTime: number = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds

4. Data Fetching and Caching

// Cache the data for 5 minutes
let cachedData: ProjectData[] | null = null;
let lastFetchTime: number = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds

6. Data Formatting

const formattedData = data.values.map((row: string[]) => {
  const [
    artist = "",
    projectName = "",
    credits = "",
    artworkUrl = "",
    iframe = "",
  ] = row;
  return {
    artist,
    projectName,
    credits,
    artworkUrl,
    iframe,
  };
});

How to Use This Code

  1. Set up Google Sheets API:
    • Enable the Google Sheets API in the Google Cloud Console.
    • Create an API key.
    • Create a Google Sheet and note its ID.
  2. Configure Environment Variables:
    • Create a .env.local file in your Next.js project.
    • Add GOOGLE_SPREADSHEET_ID and GOOGLE_API_KEY variables.
  3. Create API Route:
    • Create a file pages/api/sheets.ts and paste the code.
  4. Fetch Data:
    • Use fetch or axios in your Next.js components to call the API route.

This pattern makes it easy to update content by simply modifying the spreadsheet.


Thoughts

This project was a fantastic opportunity to deepen my understanding of Next.js, Framer Motion, and API integration. I learned a lot about creating seamless animations, managing data with Google Sheets, and optimizing API routes for performance. The process of debugging and refining the animations and API calls was challenging but incredibly rewarding.

I'm excited to apply these learnings to future projects and continue exploring the possibilities of web development.