Blocks

Onboarding 3

Onboarding
1 ColumnText Align CenterButtonsFormProgress BarRadio Buttons
Onboarding 3

Code

relume-onboarding3.tsx
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import clsx from "clsx";
import { Button, type ButtonProps } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Check, Mail } from "relume-icons";
import { zodResolver } from "@hookform/resolvers/zod";

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

type Props = {
  logo: ImageProps;
  navText: string;
  navButton: ButtonProps;
  footerText: string;
};

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

const formSchema = z.object({
  name: z.string().min(1, {
    message: "This field is required",
  }),
  email: z
    .string()
    .min(1, { message: "This field is required" })
    .email({ message: "Please enter a valid email address" }),
  companyName: z.string().min(1, { message: "This field is required" }),
  employees: z.string().min(1, { message: "This field is required" }),
  jobTitle: z.string().min(1, { message: "This field is required" }),
  employmentType: z.string().min(1, { message: "This field is required" }),
  aboutUs: z.string().min(1, { message: "This field is required" }),
});

type FormValues = z.infer<typeof formSchema>;

interface StepProps {
  form: ReturnType<typeof useForm<FormValues>>;
}

export const Onboarding3 = (props: Onboarding3Props) => {
  const { logo, navText, navButton, footerText } = {
    ...Onboarding3Defaults,
    ...props,
  };
  const [step, setStep] = useState(0);
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      companyName: "",
      employees: "",
      jobTitle: "",
      employmentType: "",
      aboutUs: "",
    },
    mode: "onChange",
  });
  const onSubmit = (values: FormValues) => {
    console.log(values);
    // Handle form submission here
  };
  const steps = [
    { component: StepOne, fields: ["name", "email"] },
    { component: StepTwo, fields: ["companyName", "employees"] },
    { component: StepThree, fields: ["jobTitle", "employmentType"] },
    { component: StepFour, fields: ["aboutUs"] },
  ] as const;
  const Step = steps[step].component;
  const currentFields = steps[step].fields;
  const totalSteps = steps.length;
  const isLastStep = step === steps.length - 1;
  const isFirstStep = step === 0;
  const handleNext = async () => {
    const output = await form.trigger(currentFields);
    if (output) {
      setStep((prev) => prev + 1);
    }
  };

  const handlePrev = () => {
    if (!isFirstStep) {
      setStep((prev) => prev - 1);
    }
  };

  return (
    <div className="flex min-h-dvh flex-col">
      <nav className="px-[5%]">
        <div className="flex min-h-16 items-center justify-between md:min-h-18">
          <a href={logo.url}>
            <img src={logo.src} alt={logo.alt} />
          </a>
          <div className="flex items-center gap-x-1">
            <span className="hidden md:inline-block">{navText}</span>
            <Button className="underline" {...navButton}>
              {navButton.title}
            </Button>
          </div>
        </div>
      </nav>
      <section className="flex grow flex-col justify-center px-[5%] pt-11 pb-5">
        <div className="mx-auto w-full max-w-md">
          <Form {...form}>
            <form
              onSubmit={(e) => {
                e.preventDefault();
                if (!isLastStep) {
                  handleNext();
                } else {
                  form.handleSubmit(onSubmit)();
                }
              }}
            >
              <ProgressBar step={step} totalSteps={totalSteps} />
              <Step form={form} />
              <StepAction
                isFirstStep={isFirstStep}
                isLastStep={isLastStep}
                handlePrev={handlePrev}
              />
            </form>
          </Form>
        </div>
      </section>
      <footer className="px-[5%]">
        <div className="flex min-h-16 items-center justify-center md:min-h-18">
          <p className="text-small">{footerText}</p>
        </div>
      </footer>
    </div>
  );
};

const ProgressBar = ({ step, totalSteps }: { step: number; totalSteps: number }) => {
  return (
    <div className="relative mb-8 flex justify-between before:absolute before:top-1/2 before:left-0 before:-z-10 before:h-px before:w-full before:-translate-y-1/2 before:bg-scheme-border">
      {Array.from({ length: totalSteps }, (_, index) => (
        <div
          key={index}
          className={clsx(
            "flex size-8 items-center justify-center rounded-full border border-scheme-border",
            step >= index ? "bg-neutral-darkest text-white" : "bg-white text-scheme-text",
          )}
        >
          {step > index ? <Check className="size-6 text-scheme-text" /> : index + 1}
        </div>
      ))}
    </div>
  );
};

const StepAction = ({
  isFirstStep,
  isLastStep,
  handlePrev,
}: {
  isFirstStep: boolean;
  isLastStep: boolean;
  handlePrev: () => void;
}) => (
  <div className="mt-6 flex flex-wrap justify-end gap-4">
    <Button
      type="button"
      variant="secondary"
      onClick={() => {
        if (!isFirstStep) {
          handlePrev();
        }
      }}
    >
      {isFirstStep ? "Cancel" : "Back"}
    </Button>
    <Button type="submit">{isLastStep ? "Get started" : "Next"}</Button>
  </div>
);

const StepOne: React.FC<StepProps> = ({ form }) => (
  <React.Fragment>
    <div className="mb-6 text-center md:mb-8">
      <h2 className="text-h4 font-bold">Let’s start with your name & email</h2>
      <p className="mt-3 md:mt-4">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.
      </p>
    </div>
    <div className="flex flex-col gap-y-6">
      <FormField
        control={form.control}
        name="name"
        render={({ field, fieldState }) => (
          <FormItem>
            <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
              Enter your name
            </FormLabel>
            <FormControl>
              <Input className={clsx({ "border-red-500": fieldState.invalid })} {...field} />
            </FormControl>
            <FormMessage className="text-regular text-red-500" />
          </FormItem>
        )}
      />
      <FormField
        control={form.control}
        name="email"
        render={({ field, fieldState }) => (
          <FormItem>
            <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
              Enter your email*
            </FormLabel>
            <FormControl>
              <Input
                placeholder="hello@relume.io"
                iconPosition="left"
                icon={<Mail className="size-6 text-scheme-text" />}
                className={clsx({ "border-red-500": fieldState.invalid })}
                {...field}
              />
            </FormControl>
            <FormMessage className="text-regular text-red-500" />
          </FormItem>
        )}
      />
    </div>
  </React.Fragment>
);

const StepTwo: React.FC<StepProps> = ({ form }) => {
  const employeesData = [
    {
      label: "Just me",
      value: "1",
    },
    {
      label: "2-10",
      value: "2-10",
    },
    {
      label: "11-50",
      value: "11-50",
    },
    {
      label: "51-100",
      value: "51-100",
    },
    {
      label: "101-500",
      value: "101-500",
    },
    {
      label: "501+",
      value: "501+",
    },
  ];
  return (
    <React.Fragment>
      <div className="mb-6 text-center md:mb-8">
        <h2 className="text-h4 font-bold">Let's confirm your company info</h2>
        <p className="mt-3 md:mt-4">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.
        </p>
      </div>
      <div className="flex flex-col gap-y-6">
        <FormField
          control={form.control}
          name="companyName"
          render={({ field, fieldState }) => (
            <FormItem>
              <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
                What is your company name?
              </FormLabel>
              <FormControl>
                <Input className={clsx({ "border-red-500": fieldState.invalid })} {...field} />
              </FormControl>
              <FormMessage className="text-regular text-red-500" />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="employees"
          render={({ field, fieldState }) => (
            <FormItem>
              <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
                How many people are you working with?
              </FormLabel>
              <FormControl>
                <div className="flex flex-wrap gap-2">
                  {employeesData.map((item, index) => {
                    return (
                      <Button
                        key={index}
                        type="button"
                        className="px-4 py-2"
                        variant={field.value === item.value ? "default" : "secondary"}
                        onClick={() => field.onChange(item.value)}
                      >
                        {item.label}
                      </Button>
                    );
                  })}
                </div>
              </FormControl>
              <FormMessage className="text-regular text-red-500" />
            </FormItem>
          )}
        />
      </div>
    </React.Fragment>
  );
};

const StepThree: React.FC<StepProps> = ({ form }) => {
  const employmentTypeData = [
    {
      label: "Part-time",
      value: "part-time",
    },
    {
      label: "Full-time",
      value: "full-time",
    },
    {
      label: "Contract",
      value: "contract",
    },
  ];
  return (
    <React.Fragment>
      <div className="mb-6 text-center md:mb-8">
        <h2 className="text-h4 font-bold">Nice, let's get some basic info about you</h2>
        <p className="mt-3 md:mt-4">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.
        </p>
      </div>
      <div className="flex flex-col gap-y-6">
        <FormField
          control={form.control}
          name="jobTitle"
          render={({ field, fieldState }) => (
            <FormItem>
              <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
                Current job title
              </FormLabel>
              <FormControl>
                <Input className={clsx({ "border-red-500": fieldState.invalid })} {...field} />
              </FormControl>
              <FormMessage className="text-regular text-red-500" />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="employmentType"
          render={({ field, fieldState }) => (
            <FormItem>
              <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
                Employment type
              </FormLabel>
              <FormControl>
                <div className="flex flex-wrap gap-2">
                  {employmentTypeData.map((item, index) => {
                    return (
                      <Button
                        key={index}
                        type="button"
                        className="px-4 py-2"
                        variant={field.value === item.value ? "default" : "secondary"}
                        onClick={() => field.onChange(item.value)}
                      >
                        {item.label}
                      </Button>
                    );
                  })}
                </div>
              </FormControl>
              <FormMessage className="text-regular text-red-500" />
            </FormItem>
          )}
        />
      </div>
    </React.Fragment>
  );
};

const StepFour: React.FC<StepProps> = ({ form }) => {
  return (
    <React.Fragment>
      <div className="mb-6 text-center md:mb-8">
        <h2 className="text-h4 font-bold">Finally, how did you hear about us?</h2>
        <p className="mt-3 md:mt-4">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.
        </p>
      </div>
      <div className="flex flex-col gap-y-6">
        <FormField
          control={form.control}
          name="aboutUs"
          render={({ field, fieldState }) => (
            <FormItem>
              <FormLabel className={clsx({ "text-red-500": fieldState.invalid })}>
                How did you hear about us?
              </FormLabel>
              <FormControl>
                <Select onValueChange={field.onChange} defaultValue={field.value}>
                  <SelectTrigger className={clsx({ "border-red-500": fieldState.invalid })}>
                    <SelectValue placeholder="Select one..." />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="first">First choice</SelectItem>
                    <SelectItem value="second">Second choice</SelectItem>
                    <SelectItem value="third">Third choice</SelectItem>
                  </SelectContent>
                </Select>
              </FormControl>
              <FormMessage className="text-regular text-red-500" />
            </FormItem>
          )}
        />
      </div>
    </React.Fragment>
  );
};

export const Onboarding3Defaults: Props = {
  logo: {
    url: "#",
    src: "https://d22po4pjz3o32e.cloudfront.net/logo-image.svg",
    alt: "Logo image",
  },
  navText: "Already have an account?",
  navButton: {
    title: "Log In",
    variant: "link",
    size: "link",
  },
  footerText: "© 2024 Relume",
};

npm i @hookform/resolvers@^3.10.0 @radix-ui/react-label @radix-ui/react-popover @radix-ui/react-select @radix-ui/react-slot class-variance-authority@0.7.1 clsx@^2.1.1 luxon@^3.5.0 react-day-picker@9.5.0 react-hook-form@^7.62.0 relume-icons@1.3.0 tailwind-merge@^2.2.2 zod@^3.24.1