205 lines
6.9 KiB
TypeScript
205 lines
6.9 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { MAIL_REQUEST_FROM } from "@/mailFrom";
|
|
import { mailApi } from "@/landing/lib/api";
|
|
import { productOptionsFromT } from "@/landing/lib/productLabels";
|
|
import type { Product } from "@/landing/types";
|
|
import { Button } from "@/landing/ui/Button";
|
|
import { CheckboxesGroup } from "@/landing/ui/CheckboxesGroup";
|
|
import { ruPhoneDigits } from "@/landing/lib/phoneRu";
|
|
import { INTL_PHONE_MAX_DIGITS } from "@/landing/lib/phoneIntl";
|
|
import { PhoneInputIntl } from "@/landing/ui/PhoneInputIntl";
|
|
import { PhoneInputRu } from "@/landing/ui/PhoneInputRu";
|
|
import { useRefererStore } from "@/landing/stores/useRefererStore";
|
|
import {
|
|
Controller,
|
|
FormProvider,
|
|
useForm,
|
|
type SubmitHandler,
|
|
} from "react-hook-form";
|
|
import type { LeadFormValues } from "./types";
|
|
|
|
export type { LeadFormValues } from "./types";
|
|
|
|
export function LeadForm({
|
|
defaultProducts,
|
|
idPrefix = "",
|
|
onSuccess,
|
|
phonePlaceholder,
|
|
formClassName = "lg:space-y-[1.944vw] md:max-lg:space-y-7 space-y-3",
|
|
}: {
|
|
defaultProducts: Product[];
|
|
/** Префикс для id полей (например `modal-`), чтобы избежать дублей в DOM */
|
|
idPrefix?: string;
|
|
onSuccess: (id: string) => void;
|
|
phonePlaceholder?: string;
|
|
formClassName?: string;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
const isRuLocale = i18n.language.startsWith("ru");
|
|
const { referer } = useRefererStore();
|
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
const projectOptions = useMemo(() => productOptionsFromT(t), [t]);
|
|
|
|
const form = useForm<LeadFormValues>({
|
|
defaultValues: {
|
|
fullname: "",
|
|
email: "",
|
|
phone: "",
|
|
products: defaultProducts,
|
|
},
|
|
});
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors, isSubmitting },
|
|
control,
|
|
} = form;
|
|
const nameId = idPrefix ? `${idPrefix}name` : "name";
|
|
const emailId = idPrefix ? `${idPrefix}email` : "email";
|
|
const phoneId = idPrefix ? `${idPrefix}tel` : "tel";
|
|
|
|
const onSubmit: SubmitHandler<LeadFormValues> = async (data) => {
|
|
setSubmitError(null);
|
|
try {
|
|
const { id } = await mailApi
|
|
.post("mail", {
|
|
json: { ...data, referer, from: MAIL_REQUEST_FROM },
|
|
})
|
|
.json<{ id: string }>();
|
|
onSuccess(id);
|
|
} catch {
|
|
setSubmitError(t("leadForm.submitError"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form
|
|
className={formClassName}
|
|
onSubmit={handleSubmit(onSubmit)}
|
|
noValidate
|
|
>
|
|
{submitError ? (
|
|
<p className="text-sm text-red-400" role="alert">
|
|
{submitError}
|
|
</p>
|
|
) : null}
|
|
<div className="lg:space-y-[1.111vw] space-y-4">
|
|
<p className="font-medium heading2">{t("leadForm.needTitle")}</p>
|
|
<CheckboxesGroup<LeadFormValues>
|
|
name="products"
|
|
options={projectOptions}
|
|
/>
|
|
</div>
|
|
<input
|
|
id={nameId}
|
|
autoComplete="none"
|
|
type="text"
|
|
required
|
|
placeholder={t("leadForm.namePlaceholder")}
|
|
{...register("fullname")}
|
|
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:btnl btnl placeholder:font-medium placeholder:select-none"
|
|
/>
|
|
<input
|
|
autoComplete="none"
|
|
required
|
|
id={emailId}
|
|
type="email"
|
|
placeholder={t("leadForm.emailPlaceholder")}
|
|
{...register("email")}
|
|
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none"
|
|
/>
|
|
<div className="flex gap-x-3 py-2 border-[#3D425C] relative">
|
|
<Controller
|
|
name="phone"
|
|
control={control}
|
|
rules={{
|
|
validate: (value: string) => {
|
|
if (!value?.trim()) return t("leadForm.phoneRequired");
|
|
if (isRuLocale) {
|
|
return (
|
|
ruPhoneDigits(value).length === 11 ||
|
|
t("leadForm.phoneInvalid")
|
|
);
|
|
}
|
|
const digits = value.replace(/\D/g, "");
|
|
return (
|
|
(digits.length >= 8 &&
|
|
digits.length <= INTL_PHONE_MAX_DIGITS) ||
|
|
t("leadForm.phoneInvalid")
|
|
);
|
|
},
|
|
}}
|
|
render={({ field }) =>
|
|
isRuLocale ? (
|
|
<PhoneInputRu
|
|
id={phoneId}
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
onBlur={field.onBlur}
|
|
inputRef={field.ref}
|
|
placeholder={
|
|
phonePlaceholder ?? t("leadForm.phonePlaceholder")
|
|
}
|
|
/>
|
|
) : (
|
|
<PhoneInputIntl
|
|
id={phoneId}
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
onBlur={field.onBlur}
|
|
inputRef={field.ref}
|
|
placeholder={
|
|
phonePlaceholder ?? t("leadForm.phonePlaceholder")
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
/>
|
|
<div className="bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2" />
|
|
</div>
|
|
{errors.phone?.message ? (
|
|
<p className="text-sm text-red-400 -mt-1" role="alert">
|
|
{String(errors.phone.message)}
|
|
</p>
|
|
) : null}
|
|
<div className="md:flex items-stretch lg:gap-[0.833vw] gap-3 max-md:translate-y-2">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl disabled:opacity-60"
|
|
>
|
|
{t("leadForm.submit")}
|
|
</Button>
|
|
<div className="text2 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1">
|
|
<span className="text-[#7A7A7A]">
|
|
{t("leadForm.consentBefore")}
|
|
</span>{" "}
|
|
<a
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href={t("legalLinks.privacyConsent")}
|
|
className="underline"
|
|
>
|
|
{t("leadForm.consentLinkData")}
|
|
</a>{" "}
|
|
<span className="text-[#7A7A7A]">
|
|
{t("leadForm.consentMiddle")}{" "}
|
|
</span>
|
|
<a
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href={t("legalLinks.policy")}
|
|
className="underline"
|
|
>
|
|
{t("leadForm.consentLinkPolicy")}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</FormProvider>
|
|
);
|
|
}
|