Blocks

Portfolio 21

off_grid
1 ColumnText Align CenterTagsMultiple ImagesOverlapping ImagesImage
Portfolio 21

Code

relume-portfolio21.tsx
import { useRef } from "react";
import { motion, useScroll, useTransform, useSpring } from "motion/react";
import clsx from "clsx";
import { Badge } from "@/components/ui/badge";

type ImageProps = {
  src: string;
  alt?: string;
};

type Project = {
  heading: string;
  imageCenter: ImageProps;
  imageLeft: ImageProps;
  imageRight: ImageProps;
  tags: string[];
};

type Props = {
  projects: Project[];
};

export type Portfolio21Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;

export const Portfolio21 = (props: Portfolio21Props) => {
  const { projects } = {
    ...Portfolio21Defaults,
    ...props,
  };

  const transformRef = useRef(null);
  const { scrollYProgress } = useScroll({
    target: transformRef,
    offset: ["start start", "end end"],
  });

  const smoothProgress = useSpring(scrollYProgress, {
    stiffness: 200,
    damping: 25,
    restDelta: 0.001,
    mass: 0.5,
  });

  const computedStyles = projects.map((_, index) => {
    const blockOffset = projects.length ? index / projects.length : 0;
    return {
      headerStyles: {
        opacity: useTransform(
          smoothProgress,
          [0.0045 + blockOffset, 0.022 + blockOffset, 0.06 + blockOffset, 0.082 + blockOffset],
          [0, 1, 1, 0],
        ),
        scale: useTransform(
          smoothProgress,
          [0 + blockOffset, 0.022 + blockOffset, 0.06 + blockOffset, 0.082 + blockOffset],
          [0.9, 1, 1, 0.9],
        ),
      },
      imageLeftStyles: {
        y: useTransform(
          smoothProgress,
          [0.17 + blockOffset, 0.25 + blockOffset],
          ["0vh", "-200vh"],
        ),
      },
      imageRightStyles: {
        y: useTransform(
          smoothProgress,
          [0.22 + blockOffset, 0.26 + blockOffset],
          ["0vh", "-200vh"],
        ),
      },
    };
  });

  return (
    <section className="relative px-[5%]">
      <div ref={transformRef} className="container">
        <div className="py-12 md:py-16 lg:py-20">
          {projects.map((project, index) => {
            return (
              <div
                key={index}
                className={clsx("relative flex h-[500vh] flex-col", {
                  "mt-[-40vh]": index > 0,
                })}
              >
                <motion.div
                  className="sticky top-0 flex h-[150vh] justify-center"
                  style={computedStyles[index].headerStyles}
                >
                  <div className="absolute top-[50vh] -z-10 flex w-full max-w-lg flex-col items-center text-center">
                    <h3 className="text-h2 font-bold">{project.heading}</h3>
                    <div className="mt-4 flex flex-wrap justify-center gap-2">
                      {project.tags.map((tag, index) => (
                        <Badge key={index}>{tag}</Badge>
                      ))}
                    </div>
                  </div>
                </motion.div>
                <div className="sticky top-0 z-10 flex h-screen items-center justify-center overflow-hidden">
                  <a href="#" className="inline-block max-w-full">
                    <div className="flex h-screen w-full max-w-lg items-center justify-center overflow-hidden">
                      <img
                        src={project.imageCenter.src}
                        alt={project.imageCenter.alt}
                        className="w-full rounded-image"
                      />
                    </div>
                  </a>
                  <div className="absolute inset-y-0 right-auto left-0 z-20 hidden h-screen w-full max-w-xxs lg:flex">
                    <motion.div
                      className="relative w-full pt-[100vh]"
                      style={computedStyles[index].imageLeftStyles}
                    >
                      <img
                        src={project.imageLeft.src}
                        alt={project.imageLeft.alt}
                        className="w-full rounded-image"
                      />
                    </motion.div>
                  </div>
                  <div className="absolute inset-y-0 right-0 left-auto z-20 hidden h-screen w-full max-w-xxs lg:flex">
                    <motion.div
                      className="relative w-full pt-[100vh]"
                      style={computedStyles[index].imageRightStyles}
                    >
                      <img
                        src={project.imageRight.src}
                        alt={project.imageRight.alt}
                        className="w-full rounded-image"
                      />
                    </motion.div>
                  </div>
                </div>
                <div className="absolute inset-0 -z-10 mt-[100vh]" />
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
};

export const Portfolio21Defaults: Props = {
  projects: [
    {
      heading: "Project name",
      tags: ["Tag one", "Tag two", "Tag three"],
      imageCenter: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image center",
      },
      imageLeft: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image left",
      },
      imageRight: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image right",
      },
    },
    {
      heading: "Project name",
      tags: ["Tag one", "Tag two", "Tag three"],
      imageCenter: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image center",
      },
      imageLeft: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image left",
      },
      imageRight: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image right",
      },
    },
    {
      heading: "Project name",
      tags: ["Tag one", "Tag two", "Tag three"],
      imageCenter: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image center",
      },
      imageLeft: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image left",
      },
      imageRight: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
        alt: "Relume placeholder image right",
      },
    },
  ],
};

npm i @radix-ui/react-slot @radix-ui/react-tabs class-variance-authority@0.7.1 clsx@^2.1.1 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2