Files
workparking/components/lead-form.tsx
deonisii 97b996edc8
All checks were successful
Auto Deploy / deploy (push) Successful in 17s
Добавить форму заявок и серверную интеграцию лидов с EspoCRM
2026-04-19 01:44:53 +03:00

315 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
type FieldErrors = {
name?: string;
phone?: string;
email?: string;
};
type LeadFormProps = {
id?: string;
title?: string;
description?: string;
compact?: boolean;
};
function getPhoneDigits(input: string) {
return input.replace(/\D/g, "").slice(0, 11);
}
function formatRussianPhoneInput(input: string) {
const digits = getPhoneDigits(input);
if (!digits) {
return "";
}
let normalized = digits;
if (normalized[0] === "8") {
normalized = `7${normalized.slice(1)}`;
}
if (normalized[0] !== "7") {
normalized = `7${normalized.slice(0, 10)}`;
}
normalized = normalized.slice(0, 11);
const country = "+7";
const part1 = normalized.slice(1, 4);
const part2 = normalized.slice(4, 7);
const part3 = normalized.slice(7, 9);
const part4 = normalized.slice(9, 11);
let result = country;
if (part1) {
result += ` (${part1}`;
}
if (part1.length === 3) {
result += ")";
}
if (part2) {
result += ` ${part2}`;
}
if (part3) {
result += `-${part3}`;
}
if (part4) {
result += `-${part4}`;
}
return result;
}
function normalizePhone(input: string) {
const digits = getPhoneDigits(input);
if (digits.length === 11 && (digits.startsWith("7") || digits.startsWith("8"))) {
return `7${digits.slice(1)}`;
}
if (digits.length === 10) {
return `7${digits}`;
}
return null;
}
function isValidEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export default function LeadForm({
id,
title,
description,
compact = false,
}: LeadFormProps) {
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [resultMessage, setResultMessage] = useState("");
const [submitError, setSubmitError] = useState(false);
const [errors, setErrors] = useState<FieldErrors>({});
function validateFields() {
const nextErrors: FieldErrors = {};
const trimmedName = name.trim();
const trimmedEmail = email.trim();
const normalizedPhone = normalizePhone(phone);
if (trimmedName.length < 2) {
nextErrors.name = "Укажите название объекта или компании";
}
if (!normalizedPhone) {
nextErrors.phone = "Введите российский номер в формате +7";
}
if (trimmedEmail && !isValidEmail(trimmedEmail)) {
nextErrors.email = "Проверьте корректность email";
}
return {
nextErrors,
trimmedName,
trimmedEmail,
normalizedPhone,
};
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setResultMessage("");
setSubmitError(false);
const { nextErrors, trimmedName, trimmedEmail, normalizedPhone } =
validateFields();
if (Object.keys(nextErrors).length > 0 || !normalizedPhone) {
setErrors(nextErrors);
return;
}
setErrors({});
setIsSubmitting(true);
try {
const response = await fetch("/api/leads", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: trimmedName,
phone: normalizedPhone,
email: trimmedEmail || null,
message: message.trim() || null,
}),
});
const data = await response.json();
if (!response.ok) {
setSubmitError(true);
setResultMessage(data.error || "Не удалось отправить заявку");
return;
}
setName("");
setPhone("");
setEmail("");
setMessage("");
setResultMessage("Заявка отправлена. Мы свяжемся с вами.");
setSubmitError(false);
} catch {
setSubmitError(true);
setResultMessage("Не удалось отправить заявку");
} finally {
setIsSubmitting(false);
}
}
const fieldClassName =
"w-full rounded-2xl border bg-black/30 px-4 py-3.5 text-white outline-none transition-colors placeholder:text-neutral-500 focus:border-emerald-500 sm:px-5 sm:py-4";
const labelClassName = "mb-2 block text-sm font-medium text-neutral-200";
const helperClassName = "mt-2 text-xs text-neutral-500";
const errorClassName = "mt-2 text-xs text-rose-300";
return (
<div id={id}>
{(title || description) && (
<div className={compact ? "mb-6" : "mb-8"}>
{title && (
<h3 className="text-2xl font-semibold tracking-[-0.03em] sm:text-3xl">
{title}
</h3>
)}
{description && (
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-neutral-400 sm:text-base">
{description}
</p>
)}
</div>
)}
<form onSubmit={handleSubmit} className="grid gap-4 sm:gap-5" noValidate>
<div>
<label htmlFor="lead-name" className={labelClassName}>
Объект или компания
</label>
<input
id="lead-name"
type="text"
placeholder="Например: ЖК Сокол, ТСЖ Север, УК Домсервис"
value={name}
onChange={(e) => {
setName(e.target.value);
if (errors.name) {
setErrors((current) => ({ ...current, name: undefined }));
}
}}
className={`${fieldClassName} ${errors.name ? "border-rose-400/70" : "border-white/10"}`}
autoComplete="organization"
required
/>
{errors.name ? (
<p className={errorClassName}>{errors.name}</p>
) : (
<p className={helperClassName}>Укажите ЖК, ТСЖ, УК или адрес объекта</p>
)}
</div>
<div>
<label htmlFor="lead-phone" className={labelClassName}>
Телефон
</label>
<input
id="lead-phone"
type="tel"
placeholder="+7 (999) 123-45-67"
value={phone}
onChange={(e) => {
setPhone(formatRussianPhoneInput(e.target.value));
if (errors.phone) {
setErrors((current) => ({ ...current, phone: undefined }));
}
}}
className={`${fieldClassName} ${errors.phone ? "border-rose-400/70" : "border-white/10"}`}
inputMode="numeric"
autoComplete="tel"
required
/>
{errors.phone ? (
<p className={errorClassName}>{errors.phone}</p>
) : (
<p className={helperClassName}>Только российские номера</p>
)}
</div>
<div>
<label htmlFor="lead-email" className={labelClassName}>
Email
</label>
<input
id="lead-email"
type="email"
placeholder="name@company.ru"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (errors.email) {
setErrors((current) => ({ ...current, email: undefined }));
}
}}
className={`${fieldClassName} ${errors.email ? "border-rose-400/70" : "border-white/10"}`}
autoComplete="email"
/>
{errors.email ? (
<p className={errorClassName}>{errors.email}</p>
) : (
<p className={helperClassName}>Необязательно, но удобно для ответа</p>
)}
</div>
<div>
<label htmlFor="lead-message" className={labelClassName}>
Комментарий
</label>
<textarea
id="lead-message"
placeholder="Коротко опишите объект и задачу"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="min-h-32 w-full rounded-2xl border border-white/10 bg-black/30 px-4 py-3.5 text-white outline-none transition-colors placeholder:text-neutral-500 focus:border-emerald-500 sm:px-5 sm:py-4"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="inline-flex items-center justify-center rounded-2xl bg-emerald-600 px-6 py-4 text-base font-semibold text-white transition-colors hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? "Отправка..." : "Оставить заявку"}
</button>
{resultMessage && (
<p className={`text-sm ${submitError ? "text-rose-300" : "text-emerald-300"}`}>
{resultMessage}
</p>
)}
</form>
</div>
);
}