Blocks

Portfolio 18

background
1 ColumnText Align CenterButtonsTagsMultiple ImagesBackground Image
Portfolio 18

Code

relume-portfolio18.tsx
import { useRef } from "react";
import { motion, useInView } from "motion/react";
import { Badge } from "@/components/ui/badge";
import { Button, type ButtonProps } from "@/components/ui/button";

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

type Tag = {
  label: string;
  url: string;
};

type Project = {
  heading: string;
  image: ImageProps;
  tags: Tag[];
  button: ButtonProps;
};

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

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

export const Portfolio18 = (props: Portfolio18Props) => {
  const { projects } = {
    ...Portfolio18Defaults,
    ...props,
  };

  const refs = projects.map(() => useRef(null));

  const isInView = refs.map((ref) =>
    useInView(ref, {
      amount: 0.33,
      once: false,
    }),
  );

  const isLeavingView = useInView(refs[refs.length - 1], {
    amount: 0,
    once: false,
  });

  return (
    <section className="relative">
      <div>
        {projects.map((project, index) => {
          const isLastItem = index === projects.length - 1;
          const shouldBeVisible =
            isInView[index] && (!isInView[index + 1] || (isLastItem && isLeavingView));

          return (
            <div
              key={index}
              ref={refs[index]}
              className="flex h-screen items-center justify-center"
            >
              <div className="absolute inset-0">
                <motion.div
                  className="sticky top-0 h-screen w-full overflow-hidden"
                  animate={{
                    opacity: shouldBeVisible ? 1 : 0,
                  }}
                  transition={{ duration: 0 }}
                >
                  <div className="absolute inset-0 z-10 bg-neutral-darkest/50" />
                  <img
                    src={project.image.src}
                    alt={project.image.alt}
                    className="absolute inset-0 size-full object-cover"
                  />
                </motion.div>
              </div>
              <div className="relative z-20 px-[5%]">
                <div className="mx-auto w-full max-w-xxl">
                  <div className="py-20">
                    <div className="relative z-[2] w-full max-w-lg">
                      <div className="text-center">
                        <h2 className="mb-5 text-h2 font-bold text-white md:mb-6">
                          {project.heading}
                        </h2>
                        <div className="flex flex-wrap justify-center gap-2">
                          {project.tags.map((tag, index) => (
                            <Badge key={index} variant="alternate">
                              <a href={tag.url}>{tag.label}</a>
                            </Badge>
                          ))}
                        </div>
                        <Button {...project.button} className="mt-6 h-auto text-regular md:mt-8">
                          {project.button.title}
                        </Button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </section>
  );
};

export const Portfolio18Defaults: Props = {
  projects: [
    {
      heading: "Project name",
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
        alt: "Relume placeholder image 1",
      },
      tags: [
        {
          label: "Tag one",
          url: "#",
        },
        {
          label: "Tag two",
          url: "#",
        },
        {
          label: "Tag three",
          url: "#",
        },
      ],
      button: {
        variant: "secondary-alt",
        title: "View Project",
      },
    },
    {
      heading: "Project name",
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
        alt: "Relume placeholder image 2",
      },
      tags: [
        {
          label: "Tag one",
          url: "#",
        },
        {
          label: "Tag two",
          url: "#",
        },
        {
          label: "Tag three",
          url: "#",
        },
      ],
      button: {
        variant: "secondary-alt",
        title: "View Project",
      },
    },
    {
      heading: "Project name",
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
        alt: "Relume placeholder image 3",
      },
      tags: [
        {
          label: "Tag one",
          url: "#",
        },
        {
          label: "Tag two",
          url: "#",
        },
        {
          label: "Tag three",
          url: "#",
        },
      ],
      button: {
        variant: "secondary-alt",
        title: "View Project",
      },
    },
  ],
};

export default Portfolio18;

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