Blocks

Content 28

standard
Text Align Left2 ColumnsRich TextTable of ContentsBlog Post Body SectionCase Study Body Section
Content 28

Code

relume-content28.tsx
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";

type Aside = {
  title?: string;
  links: Array<{
    title: string;
    href: string;
  }>;
};

type Props = {
  aside: Aside;
  children: React.ReactNode;
};

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

export const Content28 = (props: Content28Props) => {
  const { aside, children } = {
    ...Content28Defaults,
    ...props,
  };

  const [currentHash, setCurrentHash] = useState<string>(aside.links[0]?.href || "");

  useEffect(() => {
    if (typeof window !== "undefined") {
      setCurrentHash(window.location.hash || aside.links[0]?.href || "");

      const handleHashChange = () => {
        setCurrentHash(window.location.hash);
      };
      window.addEventListener("hashchange", handleHashChange);
      return () => {
        window.removeEventListener("hashchange", handleHashChange);
      };
    }
  }, [aside.links]);

  const isActiveLink = (href: string) => {
    if (!href) return false;
    const hrefWithoutHash = href.replace(/^#/, "");
    const currentHashWithoutHash = currentHash.replace(/^#/, "");
    return hrefWithoutHash === currentHashWithoutHash;
  };

  return (
    <section className="px-[5%] py-16 md:py-24 lg:py-28">
      <div className="container">
        <div className="grid grid-cols-1 gap-y-8 lg:grid-cols-[1fr_20rem] lg:gap-x-16">
          <div className="w-full max-w-lg">
            <div className="prose-figcaption:border-l-border prose-base prose-headings:font-bold prose-h2:mt-6 prose-h2:mb-4 prose-h2:text-[2.25rem] prose-h2:md:text-[2.75rem] prose-h2:lg:text-[3rem] prose-h3:my-6 prose-h3:text-[2rem] prose-h3:md:text-[2.25rem] prose-h3:lg:text-[2.5rem] prose-h4:mt-6 prose-h4:mb-5 prose-h4:text-[1.5rem] prose-h4:md:text-[1.75rem] prose-h4:md:leading-[1.3] prose-h4:lg:text-[2rem] prose-h4:lg:leading-[1.3] prose-h5:mt-5 prose-h5:mb-4 prose-h5:text-[1.25rem] prose-h5:leading-[1.4] prose-h5:md:text-[1.5rem] prose-h6:mt-5 prose-h6:mb-4 prose-h6:text-[1.125rem] prose-h6:leading-[1.4] md:prose-h6:text-[1.25rem] prose-p:m-0 prose-p:mb-4 prose-p:leading-[1.5] prose-blockquote:my-6 prose-blockquote:border-l-[.1875rem] prose-blockquote:border-l-scheme-border prose-blockquote:px-5 prose-blockquote:py-3 prose-blockquote:text-[1.25rem] prose-blockquote:italic prose-figure:my-10 prose-figure:md:my-12 prose-figcaption:mt-2 prose-figcaption:border-l prose-figcaption:pl-2 prose-figcaption:text-[0.875rem] prose-figcaption:opacity-80 prose-strong:font-bold">
              {children}
            </div>
          </div>
          <div className="relative order-first w-full justify-self-end lg:sticky lg:top-24 lg:order-none lg:ml-48 lg:flex lg:flex-col">
            <Accordion type="single" defaultValue="aside-menu" className="lg:block" collapsible>
              <AccordionItem value="aside-menu" className="border-none">
                <AccordionTrigger className="flex cursor-pointer items-center justify-between gap-3 border border-scheme-border px-4 py-3 lg:pointer-events-none lg:cursor-auto lg:border-none lg:p-0 [&_svg]:size-4 [&_svg]:lg:hidden">
                  <p className="text-h5 leading-[1.4] font-semibold">{aside.title}</p>
                </AccordionTrigger>
                <AccordionContent>
                  <div className="mt-3 md:mt-4">
                    {aside.links.map((link, index) => (
                      <a
                        href={link.href}
                        key={index}
                        className={clsx("block px-4 py-3 text-medium", {
                          "border border-scheme-border bg-scheme-foreground font-semibold":
                            isActiveLink(link.href),
                        })}
                        style={{
                          marginLeft: index === 0 ? 0 : `${Math.min(index * 16, 80)}px`,
                        }}
                      >
                        {link.title}
                      </a>
                    ))}
                  </div>
                </AccordionContent>
              </AccordionItem>
            </Accordion>
          </div>
        </div>
      </div>
    </section>
  );
};

export const Content28Defaults: Props = {
  aside: {
    title: "Table of contents",
    links: [
      {
        title: "Heading 2",
        href: "#heading-2",
      },
      {
        title: "Heading 3",
        href: "#heading-3",
      },
      {
        title: "Heading 4",
        href: "#heading-4",
      },
      {
        title: "Heading 5",
        href: "#heading-5",
      },
      {
        title: "Heading 6",
        href: "#heading-6",
      },
    ],
  },
  children: (
    <React.Fragment>
      <h2 id="heading-2">Heading 2</h2>
      <p>
        <strong>
          Dolor enim eu tortor urna sed duis nulla. Aliquam vestibulum, nulla odio nisl vitae. In
          aliquet pellentesque aenean hac vestibulum turpis mi bibendum diam. Tempor integer aliquam
          in vitae malesuada fringilla.
        </strong>
      </p>
      <p>
        Mi tincidunt elit, id quisque ligula ac diam, amet. Vel etiam suspendisse morbi eleifend
        faucibus eget vestibulum felis. Dictum quis montes, sit sit. Tellus aliquam enim urna,
        etiam. Mauris posuere vulputate arcu amet, vitae nisi, tellus tincidunt. At feugiat sapien
        varius id.
      </p>
      <h3 id="heading-3">Heading 3</h3>
      <p>
        Eget quis mi enim, leo lacinia pharetra, semper. Eget in volutpat mollis at volutpat lectus
        velit, sed auctor. Porttitor fames arcu quis fusce augue enim. Quis at habitant diam at.
        Suscipit tristique risus, at donec. In turpis vel et quam imperdiet. Ipsum molestie aliquet
        sodales id est ac volutpat.
      </p>
      <p>
        Tristique odio senectus nam posuere ornare leo metus, ultricies. Blandit duis ultricies
        vulputate morbi feugiat cras placerat elit. Aliquam tellus lorem sed ac. Montes, sed mattis
        pellentesque suscipit accumsan. Cursus viverra aenean magna risus elementum faucibus
        molestie pellentesque. Arcu ultricies sed mauris vestibulum.
      </p>
      <h4 id="heading-4">Heading 4</h4>
      <p>
        Morbi sed imperdiet in ipsum, adipiscing elit dui lectus. Tellus id scelerisque est
        ultricies ultricies. Duis est sit sed leo nisl, blandit elit sagittis. Quisque tristique
        consequat quam sed. Nisl at scelerisque amet nulla purus habitasse.
      </p>
      <figure>
        <img
          assetName="asset-1"
          src="https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg"
          alt="Relume placeholder image"
        />
        <figcaption>Image caption goes here</figcaption>
      </figure>
      <h5 id="heading-5">Heading 5</h5>
      <p>
        Morbi sed imperdiet in ipsum, adipiscing elit dui lectus. Tellus id scelerisque est
        ultricies ultricies. Duis est sit sed leo nisl, blandit elit sagittis. Quisque tristique
        consequat quam sed. Nisl at scelerisque amet nulla purus habitasse.
      </p>
      <blockquote>
        "Ipsum sit mattis nulla quam nulla. Gravida id gravida ac enim mauris id. Non pellentesque
        congue eget consectetur turpis. Sapien, dictum molestie sem tempor. Diam elit, orci,
        tincidunt aenean tempus."
      </blockquote>
      <h6 id="heading-5">Heading 6</h6>
      <p>
        Nunc sed faucibus bibendum feugiat sed interdum. Ipsum egestas condimentum mi massa. In
        tincidunt pharetra consectetur sed duis facilisis metus. Etiam egestas in nec sed et. Quis
        lobortis at sit dictum eget nibh tortor commodo cursus.
      </p>
    </React.Fragment>
  ),
};

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