583 lines
21 KiB
Text
583 lines
21 KiB
Text
|
|
---
|
|||
|
|
import SectionHeader from "@components/base/SectionHeader.astro";
|
|||
|
|
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
|
|||
|
|
|
|||
|
|
const contactIcons = {
|
|||
|
|
phone:
|
|||
|
|
"M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z",
|
|||
|
|
location:
|
|||
|
|
"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
|
|||
|
|
email:
|
|||
|
|
"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const PRACTICE_AREAS = [
|
|||
|
|
{ value: "civil", label: "Гражданское право" },
|
|||
|
|
{ value: "admin", label: "Административное право" },
|
|||
|
|
{ value: "family", label: "Семейное право" },
|
|||
|
|
{ value: "arbitration", label: "Арбитражные дела" },
|
|||
|
|
{ value: "realestate", label: "Недвижимость" },
|
|||
|
|
{ value: "svo", label: "СВО" },
|
|||
|
|
] as const;
|
|||
|
|
|
|||
|
|
interface ContactInfo {
|
|||
|
|
icon: keyof typeof contactIcons;
|
|||
|
|
label: string;
|
|||
|
|
type: "phone" | "address" | "email";
|
|||
|
|
href?: string;
|
|||
|
|
value: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contactInfo: ContactInfo[] = [
|
|||
|
|
{
|
|||
|
|
icon: "phone",
|
|||
|
|
label: "Телефон",
|
|||
|
|
type: "phone",
|
|||
|
|
href: CONTACT_CONSTANTS.phoneHref,
|
|||
|
|
value: CONTACT_CONSTANTS.phone,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "location",
|
|||
|
|
label: "Адрес",
|
|||
|
|
type: "address",
|
|||
|
|
value: CONTACT_CONSTANTS.address,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "email",
|
|||
|
|
label: "Email",
|
|||
|
|
type: "email",
|
|||
|
|
href: `mailto:${CONTACT_CONSTANTS.email}`,
|
|||
|
|
value: CONTACT_CONSTANTS.email,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
<section class="relative py-24 bg-gray-50" id="contact">
|
|||
|
|
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
|
|||
|
|
<SectionHeader
|
|||
|
|
label="Свяжитесь с нами"
|
|||
|
|
title="Получите бесплатную консультацию"
|
|||
|
|
description="Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для первичного правового анализа"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
|
|||
|
|
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
|
|||
|
|
<div class="flex flex-col lg:flex-row">
|
|||
|
|
<!-- Левая колонка -->
|
|||
|
|
<div
|
|||
|
|
class="w-full lg:w-2/5 bg-gradient-to-br from-[var(--color-navy)] to-[#1a1f3d] p-10 lg:p-12 text-white relative overflow-hidden"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"
|
|||
|
|
>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
class="absolute bottom-0 left-0 w-48 h-48 bg-[var(--color-blue-primary)] opacity-20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"
|
|||
|
|
>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="relative z-10">
|
|||
|
|
<span
|
|||
|
|
class="block text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-4"
|
|||
|
|
>Свяжитесь с нами</span
|
|||
|
|
>
|
|||
|
|
<p class="text-gray-400 leading-relaxed mb-12">
|
|||
|
|
Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для
|
|||
|
|
первичного правового анализа.
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<div class="space-y-8">
|
|||
|
|
{
|
|||
|
|
contactInfo.map((item) => (
|
|||
|
|
<div class="flex items-start gap-4 group">
|
|||
|
|
<div class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:bg-[var(--color-gold)] transition-colors">
|
|||
|
|
<svg
|
|||
|
|
class="w-5 h-5 text-[var(--color-gold)] group-hover:text-white transition-colors"
|
|||
|
|
fill="none"
|
|||
|
|
stroke="currentColor"
|
|||
|
|
viewBox="0 0 24 24"
|
|||
|
|
>
|
|||
|
|
<path
|
|||
|
|
stroke-linecap="round"
|
|||
|
|
stroke-linejoin="round"
|
|||
|
|
stroke-width="1.5"
|
|||
|
|
d={contactIcons[item.icon]}
|
|||
|
|
/>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span class="block text-xs text-gray-500 uppercase tracking-wider mb-1">
|
|||
|
|
{item.label}
|
|||
|
|
</span>
|
|||
|
|
{item.href && item.type !== "phone" ? (
|
|||
|
|
<a
|
|||
|
|
href={item.href}
|
|||
|
|
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors"
|
|||
|
|
>
|
|||
|
|
{item.value}
|
|||
|
|
</a>
|
|||
|
|
) : item.type === "phone" ? (
|
|||
|
|
<button
|
|||
|
|
data-consultation-modal
|
|||
|
|
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors cursor-pointer text-left"
|
|||
|
|
>
|
|||
|
|
{item.value}
|
|||
|
|
</button>
|
|||
|
|
) : (
|
|||
|
|
<span class="text-lg font-bold">{item.value}</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Правая колонка (Форма) -->
|
|||
|
|
<div class="w-full lg:w-3/5 p-10 lg:p-12 bg-white">
|
|||
|
|
<form class="space-y-8" id="consultation-form" novalidate>
|
|||
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|||
|
|
<!-- Имя -->
|
|||
|
|
<div class="relative group">
|
|||
|
|
<label
|
|||
|
|
for="name"
|
|||
|
|
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
|
|||
|
|
>
|
|||
|
|
Ваше имя <span class="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
id="name"
|
|||
|
|
name="name"
|
|||
|
|
required
|
|||
|
|
minlength="2"
|
|||
|
|
maxlength="50"
|
|||
|
|
pattern="[А-Яа-яЁёA-Za-z\s\-]+"
|
|||
|
|
placeholder="Иван Иванов"
|
|||
|
|
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
|
|||
|
|
/>
|
|||
|
|
<span class="error-message hidden text-red-500 text-xs mt-1">
|
|||
|
|
Введите корректное имя (минимум 2 символа)
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Телефон -->
|
|||
|
|
<div class="relative group">
|
|||
|
|
<label
|
|||
|
|
for="phone"
|
|||
|
|
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
|
|||
|
|
>
|
|||
|
|
Телефон <span class="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="tel"
|
|||
|
|
id="phone"
|
|||
|
|
name="phone"
|
|||
|
|
required
|
|||
|
|
pattern="\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}"
|
|||
|
|
placeholder="+7 (___) ___-__-__"
|
|||
|
|
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
|
|||
|
|
/>
|
|||
|
|
<span class="error-message hidden text-red-500 text-xs mt-1">
|
|||
|
|
Введите полный номер телефона
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Сфера вопроса -->
|
|||
|
|
<div class="relative">
|
|||
|
|
<label
|
|||
|
|
for="practice"
|
|||
|
|
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
|
|||
|
|
>
|
|||
|
|
Сфера вопроса
|
|||
|
|
</label>
|
|||
|
|
<div class="relative">
|
|||
|
|
<select
|
|||
|
|
id="practice"
|
|||
|
|
name="practice"
|
|||
|
|
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 appearance-none cursor-pointer transition-all"
|
|||
|
|
>
|
|||
|
|
{
|
|||
|
|
PRACTICE_AREAS.map((area) => (
|
|||
|
|
<option value={area.value}>{area.label}</option>
|
|||
|
|
))
|
|||
|
|
}
|
|||
|
|
</select>
|
|||
|
|
<div
|
|||
|
|
class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400"
|
|||
|
|
>
|
|||
|
|
<svg
|
|||
|
|
class="w-5 h-5"
|
|||
|
|
fill="none"
|
|||
|
|
stroke="currentColor"
|
|||
|
|
viewBox="0 0 24 24"
|
|||
|
|
>
|
|||
|
|
<path
|
|||
|
|
stroke-linecap="round"
|
|||
|
|
stroke-linejoin="round"
|
|||
|
|
stroke-width="2"
|
|||
|
|
d="M19 9l-7 7-7-7"></path>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Сообщение -->
|
|||
|
|
<div class="relative">
|
|||
|
|
<label
|
|||
|
|
for="message"
|
|||
|
|
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
|
|||
|
|
>
|
|||
|
|
Ваше сообщение <span class="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
id="message"
|
|||
|
|
name="message"
|
|||
|
|
required
|
|||
|
|
minlength="10"
|
|||
|
|
maxlength="1000"
|
|||
|
|
rows="4"
|
|||
|
|
placeholder="Опишите ситуацию..."
|
|||
|
|
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 resize-none transition-all"
|
|||
|
|
></textarea>
|
|||
|
|
<div class="flex justify-between mt-1">
|
|||
|
|
<span class="error-message hidden text-red-500 text-xs">
|
|||
|
|
Опишите ситуацию подробнее (минимум 10 символов)
|
|||
|
|
</span>
|
|||
|
|
<span class="char-count text-xs text-gray-400 ml-auto">
|
|||
|
|
0 / 1000
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
id="submit-btn"
|
|||
|
|
disabled
|
|||
|
|
class="w-full py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white font-bold rounded-xl shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 hover:-translate-y-0.5 hover:cursor-pointer transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:transform-none"
|
|||
|
|
>
|
|||
|
|
<span class="btn-text">Отправить запрос</span>
|
|||
|
|
<span class="btn-loading hidden">Отправка...</span>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<p class="text-center text-xs text-gray-400">
|
|||
|
|
Нажимая кнопку, вы соглашаетесь с{" "}
|
|||
|
|
<a
|
|||
|
|
href="/policy"
|
|||
|
|
class="text-[var(--color-gold)] hover:underline"
|
|||
|
|
>
|
|||
|
|
политикой конфиденциальности
|
|||
|
|
</a>
|
|||
|
|
</p>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// Типы для валидации
|
|||
|
|
type ValidationRule = {
|
|||
|
|
pattern?: RegExp;
|
|||
|
|
minLength?: number;
|
|||
|
|
maxLength?: number;
|
|||
|
|
required?: boolean;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const validationRules: Record<string, ValidationRule> = {
|
|||
|
|
name: {
|
|||
|
|
required: true,
|
|||
|
|
minLength: 2,
|
|||
|
|
maxLength: 50,
|
|||
|
|
pattern: /^[А-Яа-яЁёA-Za-z\s\-]+$/,
|
|||
|
|
},
|
|||
|
|
phone: {
|
|||
|
|
required: true,
|
|||
|
|
pattern: /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/,
|
|||
|
|
},
|
|||
|
|
message: {
|
|||
|
|
required: true,
|
|||
|
|
minLength: 10,
|
|||
|
|
maxLength: 1000,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Элементы формы
|
|||
|
|
const form = document.getElementById("consultation-form") as HTMLFormElement;
|
|||
|
|
const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
|
|||
|
|
const btnText = submitBtn.querySelector(".btn-text") as HTMLSpanElement;
|
|||
|
|
const btnLoading = submitBtn.querySelector(".btn-loading") as HTMLSpanElement;
|
|||
|
|
|
|||
|
|
// Отслеживаем, было ли поле в фокусе (для показа ошибок только после взаимодействия)
|
|||
|
|
const touchedFields = new Set<string>();
|
|||
|
|
|
|||
|
|
// Валидация поля
|
|||
|
|
function validateField(
|
|||
|
|
field: HTMLInputElement | HTMLTextAreaElement,
|
|||
|
|
showError: boolean = false,
|
|||
|
|
): boolean {
|
|||
|
|
const name = field.name;
|
|||
|
|
const rules = validationRules[name];
|
|||
|
|
const errorEl = field.parentElement?.querySelector(
|
|||
|
|
".error-message",
|
|||
|
|
) as HTMLElement;
|
|||
|
|
|
|||
|
|
if (!rules) return true;
|
|||
|
|
|
|||
|
|
let isValid = true;
|
|||
|
|
let errorMsg = "";
|
|||
|
|
|
|||
|
|
// Проверка обязательности
|
|||
|
|
if (rules.required && !field.value.trim()) {
|
|||
|
|
isValid = false;
|
|||
|
|
errorMsg = "Обязательное поле";
|
|||
|
|
}
|
|||
|
|
// Проверка минимальной длины (только если поле не пустое)
|
|||
|
|
else if (
|
|||
|
|
rules.minLength &&
|
|||
|
|
field.value.length > 0 &&
|
|||
|
|
field.value.length < rules.minLength
|
|||
|
|
) {
|
|||
|
|
isValid = false;
|
|||
|
|
errorMsg = `Минимум ${rules.minLength} символов`;
|
|||
|
|
}
|
|||
|
|
// Проверка максимальной длины
|
|||
|
|
else if (rules.maxLength && field.value.length > rules.maxLength) {
|
|||
|
|
isValid = false;
|
|||
|
|
errorMsg = `Максимум ${rules.maxLength} символов`;
|
|||
|
|
}
|
|||
|
|
// Проверка паттерна (только если поле не пустое)
|
|||
|
|
else if (
|
|||
|
|
rules.pattern &&
|
|||
|
|
field.value.length > 0 &&
|
|||
|
|
!rules.pattern.test(field.value)
|
|||
|
|
) {
|
|||
|
|
isValid = false;
|
|||
|
|
errorMsg = "Некорректный формат";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Отображение ошибки только если поле было в фокусе или принудительный показ
|
|||
|
|
if (errorEl && (showError || touchedFields.has(name))) {
|
|||
|
|
if (!isValid && (field.value.length > 0 || showError)) {
|
|||
|
|
errorEl.textContent = errorMsg;
|
|||
|
|
errorEl.classList.remove("hidden");
|
|||
|
|
field.classList.add(
|
|||
|
|
"border-red-500",
|
|||
|
|
"focus:border-red-500",
|
|||
|
|
"focus:ring-red-500/20",
|
|||
|
|
);
|
|||
|
|
field.classList.remove(
|
|||
|
|
"focus:border-[var(--color-gold)]",
|
|||
|
|
"focus:ring-[var(--color-gold)]/20",
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
errorEl.classList.add("hidden");
|
|||
|
|
field.classList.remove(
|
|||
|
|
"border-red-500",
|
|||
|
|
"focus:border-red-500",
|
|||
|
|
"focus:ring-red-500/20",
|
|||
|
|
);
|
|||
|
|
field.classList.add(
|
|||
|
|
"focus:border-[var(--color-gold)]",
|
|||
|
|
"focus:ring-[var(--color-gold)]/20",
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return isValid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверка всей формы (без показа ошибок, только для активации кнопки)
|
|||
|
|
function checkFormValidity(): boolean {
|
|||
|
|
const fields = form.querySelectorAll<
|
|||
|
|
HTMLInputElement | HTMLTextAreaElement
|
|||
|
|
>("input[required], textarea[required]");
|
|||
|
|
let isValid = true;
|
|||
|
|
|
|||
|
|
fields.forEach((field) => {
|
|||
|
|
if (!validateField(field, false)) isValid = false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
submitBtn.disabled = !isValid;
|
|||
|
|
return isValid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Показать все ошибки (при попытке отправки)
|
|||
|
|
function showAllErrors(): boolean {
|
|||
|
|
const fields = form.querySelectorAll<
|
|||
|
|
HTMLInputElement | HTMLTextAreaElement
|
|||
|
|
>("input[required], textarea[required]");
|
|||
|
|
let isValid = true;
|
|||
|
|
|
|||
|
|
fields.forEach((field) => {
|
|||
|
|
touchedFields.add(field.name);
|
|||
|
|
if (!validateField(field, true)) isValid = false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return isValid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Маска телефона с ограничением ввода
|
|||
|
|
const phoneInput = document.getElementById("phone") as HTMLInputElement;
|
|||
|
|
|
|||
|
|
phoneInput?.addEventListener("keypress", (e) => {
|
|||
|
|
// Разрешаем только цифры и управляющие клавиши
|
|||
|
|
if (
|
|||
|
|
!/\d/.test(e.key) &&
|
|||
|
|
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
|
|||
|
|
) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
phoneInput?.addEventListener("input", (e) => {
|
|||
|
|
const target = e.target as HTMLInputElement;
|
|||
|
|
let value = target.value.replace(/\D/g, "");
|
|||
|
|
|
|||
|
|
// Ограничиваем длину
|
|||
|
|
if (value.length > 11) value = value.slice(0, 11);
|
|||
|
|
|
|||
|
|
// Убираем 7 или 8 в начале
|
|||
|
|
if (value.startsWith("7")) value = value.slice(1);
|
|||
|
|
if (value.startsWith("8")) value = value.slice(1);
|
|||
|
|
|
|||
|
|
// Форматируем
|
|||
|
|
let formatted = "+7";
|
|||
|
|
if (value.length > 0) formatted += " (" + value.slice(0, 3);
|
|||
|
|
if (value.length > 3) formatted += ") " + value.slice(3, 6);
|
|||
|
|
if (value.length > 6) formatted += "-" + value.slice(6, 8);
|
|||
|
|
if (value.length > 8) formatted += "-" + value.slice(8, 10);
|
|||
|
|
|
|||
|
|
target.value = formatted;
|
|||
|
|
validateField(target);
|
|||
|
|
checkFormValidity();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Ограничение ввода для имени (только буквы, пробелы, дефис)
|
|||
|
|
const nameInput = document.getElementById("name") as HTMLInputElement;
|
|||
|
|
|
|||
|
|
nameInput?.addEventListener("keypress", (e) => {
|
|||
|
|
if (
|
|||
|
|
!/[А-Яа-яЁёA-Za-z\s\-]/.test(e.key) &&
|
|||
|
|
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
|
|||
|
|
) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
nameInput?.addEventListener("input", () => {
|
|||
|
|
validateField(nameInput);
|
|||
|
|
checkFormValidity();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Счетчик символов для сообщения
|
|||
|
|
const messageInput = document.getElementById(
|
|||
|
|
"message",
|
|||
|
|
) as HTMLTextAreaElement;
|
|||
|
|
const charCount = messageInput?.parentElement?.querySelector(
|
|||
|
|
".char-count",
|
|||
|
|
) as HTMLElement;
|
|||
|
|
|
|||
|
|
messageInput?.addEventListener("input", () => {
|
|||
|
|
const length = messageInput.value.length;
|
|||
|
|
if (charCount) {
|
|||
|
|
charCount.textContent = `${length} / 1000`;
|
|||
|
|
charCount.classList.toggle("text-red-500", length > 1000);
|
|||
|
|
}
|
|||
|
|
validateField(messageInput);
|
|||
|
|
checkFormValidity();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Отмечаем поле как "тронутое" при фокусе
|
|||
|
|
form.querySelectorAll("input, textarea").forEach((field) => {
|
|||
|
|
field.addEventListener("focus", () => {
|
|||
|
|
touchedFields.add((field as HTMLInputElement).name);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Валидация при потере фокуса (показываем ошибки только если поле было заполнено неверно)
|
|||
|
|
form.querySelectorAll("input, textarea").forEach((field) => {
|
|||
|
|
field.addEventListener("blur", () => {
|
|||
|
|
const input = field as HTMLInputElement;
|
|||
|
|
touchedFields.add(input.name);
|
|||
|
|
// Показываем ошибку только если поле не пустое и невалидно, или если пытались отправить
|
|||
|
|
if (input.value.length > 0) {
|
|||
|
|
validateField(input, true);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Отправка формы
|
|||
|
|
form?.addEventListener("submit", async (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
|
|||
|
|
// Показываем все ошибки при попытке отправки
|
|||
|
|
if (!showAllErrors()) return;
|
|||
|
|
|
|||
|
|
// Блокировка кнопки
|
|||
|
|
submitBtn.disabled = true;
|
|||
|
|
btnText.classList.add("hidden");
|
|||
|
|
btnLoading.classList.remove("hidden");
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const formData = new FormData(form);
|
|||
|
|
const data = Object.fromEntries(formData);
|
|||
|
|
|
|||
|
|
// Отправка на API
|
|||
|
|
const response = await fetch("/api/contact", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify(data),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(result.error || "Ошибка при отправке");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Успех
|
|||
|
|
if (typeof window.showToast === "function") {
|
|||
|
|
window.showToast(
|
|||
|
|
"Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.",
|
|||
|
|
"success",
|
|||
|
|
5000,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
form.reset();
|
|||
|
|
touchedFields.clear();
|
|||
|
|
checkFormValidity(); // Сброс состояния кнопки
|
|||
|
|
|
|||
|
|
// Сброс счётчика символов
|
|||
|
|
if (charCount) {
|
|||
|
|
charCount.textContent = "0 / 1000";
|
|||
|
|
charCount.classList.remove("text-red-500");
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("[ContactForm] Ошибка:", error);
|
|||
|
|
const errorMessage =
|
|||
|
|
error instanceof Error
|
|||
|
|
? error.message
|
|||
|
|
: "Ошибка при отправке. Попробуйте позже.";
|
|||
|
|
|
|||
|
|
if (typeof window.showToast === "function") {
|
|||
|
|
window.showToast(errorMessage, "error", 5000);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
submitBtn.disabled = false;
|
|||
|
|
btnText.classList.remove("hidden");
|
|||
|
|
btnLoading.classList.add("hidden");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Начальная проверка (без показа ошибок)
|
|||
|
|
checkFormValidity();
|
|||
|
|
</script>
|