Blocks

Links 5

Link Pages
Text Align Center1 ColumnImage/Video CenterModalButtons
Links 5

Code

relume-links5.tsx
import React, { useState } from "react";
import { Button, type ButtonProps } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
  FacebookLogo,
  InstagramLogo,
  LinkedinLogo,
  LocationOn,
  Mail,
  PlayCircle,
  ProgressActivity,
  XLogo,
  YoutubeLogo,
} from "relume-icons";
import clsx from "clsx";

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

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

type CategoryProps = {
  heading?: string;
  links: (ButtonProps & {
    url: string;
  })[];
};

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 = {
  video: string;
  image: ImageProps;
  author: AuthorDetailsProps;
  categories: CategoryProps[];
  newsLetterCard: NewsletterCard;
  newsLetterDialog: NewsletterDialog;
  socialLinks: SocialLinkProps[];
};

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

export const Links5 = (props: Links5Props) => {
  const { video, author, image, categories, newsLetterCard, newsLetterDialog, socialLinks } = {
    ...Links5Defaults,
    ...props,
  };
  const [emailInput, setEmailInput] = useState<string>("");
  const [isIframeLoaded, setIsIframeLoaded] = useState(false);
  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">
          <div className="flex flex-wrap justify-center gap-3">
            {socialLinks.map((link, index) => (
              <a key={index} href={link.href}>
                {link.icon}
              </a>
            ))}
          </div>
          <Dialog>
            <DialogTrigger asChild>
              <button className="relative flex size-full items-center justify-center">
                <img
                  src={image.src}
                  alt={image.alt}
                  className="aspect-[3/2] size-full object-cover"
                />
                <span className="absolute inset-0 z-10 bg-neutral-darkest/50" />
                <PlayCircle className="absolute z-20 size-20 text-white" />
              </button>
            </DialogTrigger>
            <DialogContent>
              {!isIframeLoaded && (
                <ProgressActivity className="mx-auto size-16 animate-spin text-white" />
              )}
              <iframe
                className={clsx("z-0 mx-auto aspect-video size-full md:w-[738px] lg:w-[940px]", {
                  visible: isIframeLoaded,
                  hidden: !isIframeLoaded,
                })}
                src={video}
                allow="autoplay; encrypted-media; picture-in-picture"
                allowFullScreen
                onLoad={() => setIsIframeLoaded(true)}
              />
            </DialogContent>
          </Dialog>
          {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>
      </div>
    </section>
  );
};

const Category = (category: CategoryProps) => {
  return (
    <div className="flex flex-col gap-4 text-center">
      {category.heading && <h3 className="text-h6 font-bold">{category.heading}</h3>}
      {category.links.map((link, index) => (
        <Button key={index} {...link} asChild>
          <a href={link.url}>{link.title}</a>
        </Button>
      ))}
    </div>
  );
};

export const Links5Defaults: Props = {
  video: "https://www.youtube.com/embed/8DKLYsikxTs?si=Ch9W0KrDWWUiCMMW",
  image: {
    src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-video-thumbnail.svg",
    alt: "Relume placeholder image",
  },
  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: [
        {
          url: "#",
          title: "Link one",
          variant: "secondary",
        },
      ],
    },
    {
      heading: "Category name",
      links: [
        {
          url: "#",
          title: "Link two",
          variant: "secondary",
        },
        {
          url: "#",
          title: "Link three",
          variant: "secondary",
        },
        {
          url: "#",
          title: "Link four",
          variant: "secondary",
        },
      ],
    },
    {
      heading: "Category name",
      links: [
        {
          url: "#",
          title: "Link five",
          variant: "secondary",
        },
        {
          url: "#",
          title: "Link six",
          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-checkbox @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-radio-group @radix-ui/react-select @radix-ui/react-slot @radix-ui/react-switch class-variance-authority@0.7.1 clsx@^2.1.1 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2