Blocks

Layout 348

standard
Text Align Left2 ColumnsImage/Video LeftButtonsImageMultiple Images
Layout 348

Code

relume-layout348.tsx
"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import clsx from "clsx";
import { Button, type ButtonProps } from "@/components/ui/button";
import { ChevronRight } from "relume-icons";

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

type ContentProps = {
  tagline: string;
  heading: string;
  description: string;
  buttons: ButtonProps[];
  image: ImageProps;
};

type Props = {
  contents: ContentProps[];
  images: ImageProps[];
};

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

export const Layout348 = (props: Layout348Props) => {
  const { contents, images } = {
    ...Layout348Defaults,
    ...props,
  };

  const [activeSection, setActiveSection] = useState(0);
  const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
  const overlayIndices = useMemo(() => new Set([0, 2]), []);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const visible = entries
          .filter((e) => e.isIntersecting)
          .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
        if (visible) {
          const idx = Number((visible.target as HTMLElement).dataset.index);
          if (!Number.isNaN(idx)) setActiveSection((prev) => (prev === idx ? prev : idx));
        }
      },
      { threshold: [0.5, 0.75, 1] },
    );
    sectionRefs.current.forEach((el: HTMLDivElement | null) => el && observer.observe(el));
    return () => observer.disconnect();
  }, []);

  return (
    <section className="relative isolate">
      <div className="pointer-events-none absolute inset-0 -z-10">
        <div
          className={clsx(
            "h-full bg-scheme-foreground transition-opacity duration-300 md:sticky md:top-0 md:h-screen",
            overlayIndices.has(activeSection) ? "opacity-100" : "opacity-0",
          )}
        />
      </div>
      <div className="px-[5%]">
        <div className="container">
          <div className="relative grid gap-x-12 py-16 sm:gap-y-12 md:grid-cols-2 md:py-0 lg:gap-x-20">
            <div className="sticky top-0 hidden h-screen md:flex md:flex-col md:items-center md:justify-center">
              {images.map((image, index) => (
                <img
                  key={index}
                  src={image.src}
                  className={clsx("absolute w-full rounded-image", {
                    "opacity-100": activeSection === index,
                    "opacity-0": activeSection !== index,
                  })}
                  alt={image.alt}
                />
              ))}
            </div>

            <div className="grid grid-cols-1 gap-12 md:block">
              {contents.map((content, index) => (
                <div
                  key={index}
                  ref={(el) => {
                    sectionRefs.current[index] = el;
                  }}
                  data-index={index}
                >
                  <div className="flex flex-col items-start justify-center md:h-screen">
                    <p className="mb-3 font-semibold md:mb-4">{content.tagline}</p>
                    <h2 className="mb-5 text-h2 font-bold md:mb-6">{content.heading}</h2>
                    <p className="text-medium">{content.description}</p>
                    <div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8">
                      {content.buttons.map((button, index) => (
                        <Button key={index} {...button}>
                          {button.title}
                        </Button>
                      ))}
                    </div>
                    <div className="mt-10 block w-full md:hidden">
                      <img
                        src={content.image.src}
                        className="w-full rounded-image"
                        alt={content.image.alt}
                      />
                    </div>
                    {null}
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export const Layout348Defaults: Props = {
  contents: [
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
        alt: "Relume placeholder image 1",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
        alt: "Relume placeholder image 2",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
        alt: "Relume placeholder image 3",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
        alt: "Relume placeholder image 4",
      },
    },
  ],
  images: [
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
      alt: "Relume placeholder image 1",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
      alt: "Relume placeholder image 2",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
      alt: "Relume placeholder image 3",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
      alt: "Relume placeholder image 4",
    },
  ],
};

npm i @radix-ui/react-dialog @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