refactor: extract contact form

This commit is contained in:
Zotov Ivan Yuryevich
2026-04-21 21:51:27 +03:00
parent 08f1e352a6
commit c9a199da91
3 changed files with 289 additions and 218 deletions

View File

@@ -0,0 +1,252 @@
import { Calendar } from 'lucide-react'
export type ContactFormField =
| 'name'
| 'company'
| 'email'
| 'phone'
| 'purpose'
| 'timeline'
| 'taskComment'
| 'meetingSlots'
| 'message'
export interface ContactFormValues {
name?: string
company?: string
email?: string
phone?: string
purpose?: string
timeline?: string
taskComment?: string
meetingSlot1?: string
meetingSlot2?: string
message?: string
}
export interface ContactFormProps {
visibleFields?: readonly ContactFormField[]
prefillValues?: Partial<ContactFormValues>
submitLabel?: string
className?: string
idPrefix?: string
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void
}
const DEFAULT_VISIBLE_FIELDS: readonly ContactFormField[] = ['name', 'company', 'email', 'phone', 'message']
function withPrefix(prefix: string, fieldName: string): string {
return `${prefix}-${fieldName}`
}
export function ContactForm({
visibleFields = DEFAULT_VISIBLE_FIELDS,
prefillValues,
submitLabel = 'Отправить заявку',
className = 'space-y-5',
idPrefix = 'contact-form',
onSubmit,
}: ContactFormProps) {
const fields = new Set(visibleFields)
const showNameCompanyRow = fields.has('name') || fields.has('company')
const showEmailPhoneRow = fields.has('email') || fields.has('phone')
const showPurposeTimelineRow = fields.has('purpose') || fields.has('timeline')
return (
<form className={className} onSubmit={onSubmit}>
{showNameCompanyRow && (
<div className="grid md:grid-cols-2 gap-5">
{fields.has('name') && (
<div>
<input
type="text"
id={withPrefix(idPrefix, 'name')}
name="name"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Ваше имя"
defaultValue={prefillValues?.name}
required
/>
</div>
)}
{fields.has('company') && (
<div>
<input
type="text"
id={withPrefix(idPrefix, 'company')}
name="company"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Компания"
defaultValue={prefillValues?.company}
/>
</div>
)}
</div>
)}
{showEmailPhoneRow && (
<div className="grid md:grid-cols-2 gap-5">
{fields.has('email') && (
<div>
<input
type="email"
id={withPrefix(idPrefix, 'email')}
name="email"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Email"
defaultValue={prefillValues?.email}
required
/>
</div>
)}
{fields.has('phone') && (
<div>
<input
type="tel"
id={withPrefix(idPrefix, 'phone')}
name="phone"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Телефон"
defaultValue={prefillValues?.phone}
required
/>
</div>
)}
</div>
)}
{showPurposeTimelineRow && (
<div className="grid md:grid-cols-2 gap-5">
{fields.has('purpose') && (
<div>
<label htmlFor={withPrefix(idPrefix, 'purpose')} className="block text-sm text-gray-700 mb-2">
Какую задачу решаете?
</label>
<select
id={withPrefix(idPrefix, 'purpose')}
name="purpose"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
defaultValue={prefillValues?.purpose ?? 'emergency'}
required
>
<option value="">Выберите цель покупки</option>
<option value="emergency">🔥 Всё горит, нужна помощь!</option>
<option value="modernization">Модернизация инфраструктуры</option>
<option value="software-replacement">Замена импортного ПО на российское</option>
<option value="capacity-expansion">Расширение мощностей</option>
<option value="new-infrastructure">Создание новой инфраструктуры</option>
<option value="security-improvement">Повышение безопасности</option>
<option value="automation">Автоматизация процессов</option>
<option value="cloud-migration">Миграция в облако</option>
<option value="disaster-recovery">Резервирование и восстановление</option>
<option value="compliance">Соответствие требованиям регуляторов</option>
<option value="other">Другое</option>
</select>
</div>
)}
{fields.has('timeline') && (
<div>
<label htmlFor={withPrefix(idPrefix, 'timeline')} className="block text-sm text-gray-700 mb-2">
Сроки реализации
</label>
<select
id={withPrefix(idPrefix, 'timeline')}
name="timeline"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
defaultValue={prefillValues?.timeline ?? 'yesterday'}
required
>
<option value="">Выберите срок</option>
<option value="yesterday">Еще вчера</option>
<option value="urgent">Срочно (до 1 месяца)</option>
<option value="1-3-months">1-3 месяца</option>
<option value="3-6-months">3-6 месяцев</option>
<option value="6-12-months">6-12 месяцев</option>
<option value="12-months-plus">Более 12 месяцев</option>
<option value="planning">На стадии планирования</option>
</select>
</div>
)}
</div>
)}
{fields.has('taskComment') && (
<div>
<label htmlFor={withPrefix(idPrefix, 'taskComment')} className="block text-sm text-gray-700 mb-2">
Комментарий к задаче
</label>
<textarea
id={withPrefix(idPrefix, 'taskComment')}
name="taskComment"
rows={4}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all resize-none"
placeholder="Дополнительная информация, требования, бюджет..."
defaultValue={prefillValues?.taskComment}
/>
</div>
)}
{fields.has('message') && (
<div>
<textarea
id={withPrefix(idPrefix, 'message')}
name="message"
rows={4}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all resize-none"
placeholder="Расскажите о вашем проекте"
defaultValue={prefillValues?.message}
/>
</div>
)}
{fields.has('meetingSlots') && (
<div>
<label className=" text-sm text-gray-700 mb-2 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Предпочитаемое время для встречи в ВКС (выберите 2 варианта)
</label>
<div className="grid md:grid-cols-2 gap-4">
<div>
<label htmlFor={withPrefix(idPrefix, 'meetingSlot1')} className="block text-xs text-gray-600 mb-1">
Вариант 1
</label>
<input
type="datetime-local"
id={withPrefix(idPrefix, 'meetingSlot1')}
name="meetingSlot1"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
defaultValue={prefillValues?.meetingSlot1}
/>
</div>
<div>
<label htmlFor={withPrefix(idPrefix, 'meetingSlot2')} className="block text-xs text-gray-600 mb-1">
Вариант 2
</label>
<input
type="datetime-local"
id={withPrefix(idPrefix, 'meetingSlot2')}
name="meetingSlot2"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
defaultValue={prefillValues?.meetingSlot2}
/>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">Мы отправим вам приглашение в ВКС на один из выбранных слотов</p>
</div>
)}
<div className="flex flex-col sm:flex-row items-center gap-4">
<button
type="submit"
className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
>
{submitLabel}
</button>
<p className="text-sm text-gray-500">Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности</p>
</div>
</form>
)
}

View File

@@ -1,17 +1,37 @@
import { X, Calendar } from 'lucide-react'
import { useEffect, useState } from 'react'
import { X } from 'lucide-react'
import { useEffect } from 'react'
import { createPortal } from 'react-dom'
import { SiteContactItems } from './SiteContactItems'
import { ContactForm, type ContactFormField, type ContactFormValues } from './ContactForm'
import type { SiteConfigContact } from '../lib/cms/contact/types'
interface ContactModalProps {
isOpen: boolean
onClose: () => void
contacts: readonly SiteConfigContact[]
formVisibleFields?: readonly ContactFormField[]
formPrefillValues?: Partial<ContactFormValues>
}
export function ContactModal({ isOpen, onClose, contacts }: ContactModalProps) {
const DEFAULT_MODAL_VISIBLE_FIELDS: readonly ContactFormField[] = [
'name',
'company',
'email',
'phone',
'purpose',
'timeline',
'taskComment',
'meetingSlots',
]
export function ContactModal({
isOpen,
onClose,
contacts,
formVisibleFields = DEFAULT_MODAL_VISIBLE_FIELDS,
formPrefillValues,
}: ContactModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
@@ -50,157 +70,11 @@ export function ContactModal({ isOpen, onClose, contacts }: ContactModalProps) {
{/* Форма */}
<div className="p-8">
<form className="space-y-5">
<div className="grid md:grid-cols-2 gap-5">
<div>
<input
type="text"
id="modal-name"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Ваше имя"
required
/>
</div>
<div>
<input
type="text"
id="modal-company"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Компания"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-5">
<div>
<input
type="email"
id="modal-email"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Email"
required
/>
</div>
<div>
<input
type="tel"
id="modal-phone"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Телефон"
required
/>
</div>
</div>
{/* Для чего нужна покупка и Сроки */}
<div className="grid md:grid-cols-2 gap-5">
<div>
<label htmlFor="modal-purpose" className="block text-sm text-gray-700 mb-2">
Какую задачу решаете?
</label>
<select
id="modal-purpose"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
required
>
<option value="">Выберите цель покупки</option>
<option value="emergency" selected>
🔥 Всё горит, нужна помощь!
</option>
<option value="modernization">Модернизация инфраструктуры</option>
<option value="software-replacement">Замена импортного ПО на российское</option>
<option value="capacity-expansion">Расширение мощностей</option>
<option value="new-infrastructure">Создание новой инфраструктуры</option>
<option value="security-improvement">Повышение безопасности</option>
<option value="automation">Автоматизация процессов</option>
<option value="cloud-migration">Миграция в облако</option>
<option value="disaster-recovery">Резервирование и восстановление</option>
<option value="compliance">Соответствие требованиям регуляторов</option>
<option value="other">Другое</option>
</select>
</div>
<div>
<label htmlFor="modal-timeline" className="block text-sm text-gray-700 mb-2">
Сроки реализации
</label>
<select
id="modal-timeline"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
required
>
<option value="">Выберите срок</option>
<option value="yesterday" selected>
Еще вчера
</option>
<option value="urgent">Срочно (до 1 месяца)</option>
<option value="1-3-months">1-3 месяца</option>
<option value="3-6-months">3-6 месяцев</option>
<option value="6-12-months">6-12 месяцев</option>
<option value="12-months-plus">Более 12 месяцев</option>
<option value="planning">На стадии планирования</option>
</select>
</div>
</div>
{/* Комментарий к задаче */}
<div>
<label htmlFor="modal-task" className="block text-sm text-gray-700 mb-2">
Комментарий к задаче
</label>
<textarea
id="modal-task"
rows={4}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all resize-none"
placeholder="Дополнительная информация, требования, бюджет..."
></textarea>
</div>
{/* Слоты для встречи в ВКС */}
<div>
<label className=" text-sm text-gray-700 mb-2 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Предпочитаемое время для встречи в ВКС (выберите 2 варианта)
</label>
<div className="grid md:grid-cols-2 gap-4">
<div>
<label htmlFor="slot1" className="block text-xs text-gray-600 mb-1">
Вариант 1
</label>
<input
type="datetime-local"
id="slot1"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
/>
</div>
<div>
<label htmlFor="slot2" className="block text-xs text-gray-600 mb-1">
Вариант 2
</label>
<input
type="datetime-local"
id="slot2"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
/>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Мы отправим вам приглашение в ВКС на один из выбранных слотов
</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4">
<button
type="submit"
className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
>
Отправить заявку
</button>
<p className="text-sm text-gray-500">Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности</p>
</div>
</form>
<ContactForm
idPrefix="contact-modal-form"
visibleFields={formVisibleFields}
prefillValues={formPrefillValues}
/>
<div className="mt-8 pt-8 border-t border-gray-100">
<SiteContactItems contacts={contacts} variant="inline" />

View File

@@ -1,11 +1,14 @@
import { SiteContactItems } from '../components/SiteContactItems'
import { ContactForm, type ContactFormField, type ContactFormValues } from '../components/ContactForm'
import type { SiteConfigContact } from '../lib/cms/contact/types'
export interface ContactProps {
contacts: readonly SiteConfigContact[]
formVisibleFields?: readonly ContactFormField[]
formPrefillValues?: Partial<ContactFormValues>
}
export function Contact({ contacts }: ContactProps) {
export function Contact({ contacts, formVisibleFields, formPrefillValues }: ContactProps) {
return (
<section className="py-4 bg-gray-50">
<div className="max-w-225 mx-auto px-6">
@@ -17,69 +20,11 @@ export function Contact({ contacts }: ContactProps) {
{/* Форма */}
<div className="bg-white rounded-2xl p-8 shadow-sm border border-gray-100">
<form className="space-y-5">
<div className="grid md:grid-cols-2 gap-5">
<div>
<input
type="text"
id="name"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Ваше имя"
required
/>
</div>
<div>
<input
type="text"
id="company"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Компания"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-5">
<div>
<input
type="email"
id="email"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Email"
required
/>
</div>
<div>
<input
type="tel"
id="phone"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="Телефон"
required
/>
</div>
</div>
<div>
<textarea
id="message"
rows={4}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all resize-none"
placeholder="Расскажите о вашем проекте"
></textarea>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4">
<button
type="submit"
className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
>
Отправить заявку
</button>
<p className="text-sm text-gray-500">Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности</p>
</div>
</form>
<ContactForm
idPrefix="contact-section-form"
visibleFields={formVisibleFields}
prefillValues={formPrefillValues}
/>
</div>
<SiteContactItems