Blocks

Links 12

Link Pages
Text Align Center1 ColumnImage/Video CenterModalButtonsSlider
Links 12

Code

relume-links12.tsx
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import { Button, type ButtonProps } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
  type CarouselApi,
} from "@/components/ui/carousel";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
  FacebookLogo,
  InstagramLogo,
  LinkedinLogo,
  LocationOn,
  Mail,
  XLogo,
  YoutubeLogo,
} from "relume-icons";

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

type AuthorDetailsProps = {
  avatar: ImageProps;
  fullName: string;
  position: string;
  location: string;
};

enum CategoryLinkType {
  Button = "button",
  Card = "card",
}

type CategoryLinkProps = {
  image?: ImageProps;
  description?: string;
  type: CategoryLinkType;
  button: ButtonProps & {
    url: string;
    price?: string;
  };
};

type CategoryProps = {
  heading?: string;
  links: CategoryLinkProps[];
};

type SocialLinkProps = {
  href: string;
  icon: React.ReactNode;
};

type NewsletterDialog = {
  heading: string;
  description: string;
  inputPlaceholder?: string;
  submitButton: ButtonProps;
  termsAndConditions: string;
};

type NewsletterCard = {
  icon: React.ReactNode;
  title: string;
  description: string;
};

type Props = {
  author: AuthorDetailsProps;
  categories: CategoryProps[];
  newsLetterCard: NewsletterCard;
  newsLetterDialog: NewsletterDialog;
  socialLinks: SocialLinkProps[];
};

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

export const Links12 = (props: Links12Props) => {
  const { author, categories, newsLetterCard, newsLetterDialog, socialLinks } = {
    ...Links12Defaults,
    ...props,
  };
  const [emailInput, setEmailInput] = useState<string>("");
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log({
      emailInput,
    });
  };
  return (
    <section className="px-[5%] py-16 md:py-24 lg:py-28">
      <div className="container max-w-md">
        <div className="mb-10 flex flex-col items-center text-center md:mb-14 lg:mb-16">
          <div className="mx-auto mb-5 size-24 min-h-24 min-w-24 overflow-hidden rounded-full md:mb-6">
            <img
              src={author.avatar.src}
              alt={author.avatar.alt}
              className="size-full object-cover"
            />
          </div>
          <h2 className="mb-2 text-h5 font-bold">{author.fullName}</h2>
          <p>{author.position}</p>
          <div className="mt-3 flex items-center justify-center gap-2 md:mt-4">
            <LocationOn className="size-5 text-scheme-text" />
            <p>{author.location}</p>
          </div>
        </div>
        <div className="space-y-8">
          {categories.map((category, index) => (
            <Category key={index} {...category} />
          ))}
          <Dialog>
            <DialogTrigger asChild>
              <Card className="flex items-center gap-4 p-4">
                <span>{newsLetterCard.icon}</span>
                <span className="flex grow flex-col items-start">
                  <span className="text-medium font-semibold">{newsLetterCard.title}</span>
                  <span className="text-small">{newsLetterCard.description} </span>
                </span>
              </Card>
            </DialogTrigger>
            <DialogContent
              closeIconPosition="inside"
              overlayClassName="bg-neutral-darkest/25"
              className="flex size-full flex-col justify-center bg-white px-[5%] pt-12 pb-28 md:size-auto md:w-[90%] md:px-12 md:py-12 lg:max-w-sm"
            >
              <div className="mb-8 text-center md:mb-12">
                <h2 className="mb-3 text-h3 font-bold md:mb-4">{newsLetterDialog.heading}</h2>
                <p>{newsLetterDialog.description}</p>
              </div>
              <form className="mb-4 flex flex-col gap-y-4" onSubmit={handleSubmit}>
                <Input
                  id="email"
                  type="email"
                  placeholder={newsLetterDialog.inputPlaceholder}
                  value={emailInput}
                  onChange={(e) => setEmailInput(e.target.value)}
                />
                <Button {...newsLetterDialog.submitButton}>
                  {newsLetterDialog.submitButton.title}
                </Button>
              </form>
              <div dangerouslySetInnerHTML={{ __html: newsLetterDialog.termsAndConditions }} />
            </DialogContent>
          </Dialog>
          <div className="flex flex-wrap justify-center gap-3">
            {socialLinks.map((link, index) => (
              <a key={index} href={link.href}>
                {link.icon}
              </a>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
};

const Category = ({ heading, links }: CategoryProps) => {
  const [api, setApi] = useState<CarouselApi>();
  const [current, setCurrent] = useState(0);

  useEffect(() => {
    if (!api) return;
    setCurrent(api.selectedScrollSnap() + 1);
    api.on("select", () => {
      setCurrent(api.selectedScrollSnap() + 1);
    });
  }, [api]);

  const cardLinks = links.filter((link) => link.type === CategoryLinkType.Card);
  const buttonLinks = links.filter((link) => link.type === CategoryLinkType.Button);
  const regularButtons = cardLinks.length > 0 ? buttonLinks.slice(0, -1) : buttonLinks;
  const storeButton = cardLinks.length > 0 ? buttonLinks[buttonLinks.length - 1] : null;

  return (
    <div className="flex flex-col gap-4 text-center">
      {heading && <h3 className="text-h6 font-bold">{heading}</h3>}
      <div className="relative grid grid-cols-1 gap-4">
        {regularButtons.map((link, index) => (
          <CategoryButton key={index} link={link} />
        ))}
        {cardLinks.length > 0 && (
          <Card className="px-4 py-6 md:px-6">
            <Carousel
              setApi={setApi}
              opts={{
                loop: true,
                align: "start",
              }}
              className="-mx-4 mb-[26px] overflow-hidden px-2 md:-mx-9 md:px-3 lg:-mx-6"
            >
              <div className="relative">
                <CarouselContent className="ml-0">
                  {cardLinks.map((slide, index) => (
                    <CarouselItem key={index} className="basis-[46.5%] px-2 md:basis-1/3 md:px-3">
                      <CategoryCard link={slide} />
                    </CarouselItem>
                  ))}
                </CarouselContent>
                <CarouselPrevious className="ml-1 hidden md:-mt-[4.5%] md:ml-4 md:flex md:size-12 lg:ml-1" />
                <CarouselNext className="mr-1 hidden md:-mt-[4.5%] md:mr-4 md:flex md:size-12 lg:mr-1" />
              </div>
              <div className="mt-[30px] flex items-center justify-center">
                {cardLinks.map((_, index) => (
                  <button
                    key={index}
                    onClick={() => api?.scrollTo(index)}
                    className={clsx("relative mx-[3px] inline-block size-2 rounded-full", {
                      "bg-scheme-text": current === index + 1,
                      "bg-scheme-text/20": current !== index + 1,
                    })}
                  />
                ))}
              </div>
            </Carousel>
            {storeButton && <CategoryButton link={storeButton} />}
          </Card>
        )}
      </div>
    </div>
  );
};

const CategoryCard = ({ link }: { link: CategoryLinkProps }) => {
  return (
    <Button className="flex-col gap-0 p-0" {...link.button} asChild>
      <a href={link.button.url}>
        <span className="block">
          <img
            src={link.image?.src}
            alt={link.image?.alt}
            className="aspect-square size-full object-cover"
          />
        </span>
        <span className="mt-2 flex flex-col gap-y-1 text-center whitespace-normal">
          <span>{link.button.title}</span>
          <span className="text-medium font-semibold">{link.button.price}</span>
        </span>
      </a>
    </Button>
  );
};

const CategoryButton = ({ link }: { link: CategoryLinkProps }) => {
  return (
    <Button {...link.button} className="col-span-2 w-full sm:col-span-3" asChild>
      <a href={link.button.url}>{link.button.title}</a>
    </Button>
  );
};

export const Links12Defaults: Props = {
  author: {
    avatar: {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-small.svg",
      alt: "Relume placeholder avatar",
    },
    fullName: "Name Surname",
    position: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
    location: "Location",
  },
  categories: [
    {
      links: [
        {
          type: CategoryLinkType.Button,
          button: {
            url: "#",
            title: "Link one",
            variant: "secondary",
          },
        },
      ],
    },
    {
      heading: "Category name",
      links: [
        {
          type: CategoryLinkType.Button,
          button: {
            url: "#",
            title: "Link two",
            variant: "secondary",
          },
        },
        {
          type: CategoryLinkType.Button,
          button: {
            url: "#",
            title: "Link three",
            variant: "secondary",
          },
        },
        {
          type: CategoryLinkType.Button,
          button: {
            url: "#",
            title: "Link four",
            variant: "secondary",
          },
        },
      ],
    },
    {
      heading: "Category name",
      links: [
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 1",
          },
        },
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 2",
          },
        },
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 3",
          },
        },
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 4",
          },
        },
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 5",
          },
        },
        {
          type: CategoryLinkType.Card,
          button: {
            url: "#",
            title: "Product name",
            price: "$55",
            variant: "link",
          },
          image: {
            src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
            alt: "Relume placeholder image 6",
          },
        },
        {
          type: CategoryLinkType.Button,
          button: {
            url: "#",
            title: "Go to the store",
            variant: "secondary",
          },
        },
      ],
    },
  ],
  newsLetterCard: {
    icon: <Mail className="size-8 text-scheme-text" />,
    title: "Join our newsletter",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
  },
  newsLetterDialog: {
    heading: "Join our newsletter",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
    inputPlaceholder: "Enter your email",
    submitButton: {
      title: "Subscribe",
    },
    termsAndConditions: `
		<p class='text-tiny'>
			By subscribing you agree to with our  
			<a href='#' class='underline'>Privacy Policy</a> and provide consent to receive updates from our company.
		</p>
  `,
  },
  socialLinks: [
    { href: "#", icon: <FacebookLogo className="size-8 text-scheme-text" /> },
    { href: "#", icon: <InstagramLogo className="size-8 text-scheme-text" /> },
    { href: "#", icon: <XLogo className="size-8 p-0.5 text-scheme-text" /> },
    { href: "#", icon: <LinkedinLogo className="size-8 text-scheme-text" /> },
    { href: "#", icon: <YoutubeLogo className="size-8 text-scheme-text" /> },
  ],
};

npm i @radix-ui/react-accordion @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-tooltip class-variance-authority@0.7.1 clsx@^2.1.1 embla-carousel-react@^8.5.2 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2