import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Send, Check, Loader2, AlertCircle } from 'lucide-react'; import { ModalData } from '../types'; interface ModalProps { data: ModalData; onClose: () => void; } // --- КОНФИГУРАЦИЯ ДЛЯ ПРОДАКШЕНА --- // В реальном проекте используйте переменные окружения (process.env.REACT_APP_...) const UNISENDER_CONFIG = { // Если true, запрос идет на ваш бэкенд (рекомендуется для PROD). // Если false, запрос идет напрямую в Unisender (работает только если отключен CORS, для DEV). USE_PROXY: true, // URL вашего бэкенда, который перенаправит запрос в Unisender PROXY_URL: '/api/unisender-proxy', }; const formatText = (text: string, isLight: boolean) => { const lines = text.split(/\r?\n/); const elements: React.ReactNode[] = []; let currentList: React.ReactNode[] = []; const processInline = (str: string) => { const parts = str.split(/(\*\*.*?\*\*)/g); return parts.map((part, i) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; } return part; }); }; lines.forEach((line, i) => { const trimmed = line.trim(); if (!trimmed) { if (currentList.length > 0) { elements.push(); currentList = []; } return; } if (trimmed.startsWith('* ')) { currentList.push(
  • {processInline(trimmed.slice(2))}
  • ); } else { if (currentList.length > 0) { elements.push(); currentList = []; } elements.push(

    {processInline(trimmed)}

    ); } }); if (currentList.length > 0) { elements.push(); } return elements; }; const Modal: React.FC = ({ data, onClose }) => { const isLight = data.theme === 'light'; // Form State const [formState, setFormState] = useState({ name: '', phone: '', email: '', honeypot: '' }); const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); // Phone Mask Handler const handlePhoneChange = (e: React.ChangeEvent) => { let input = e.target.value; // Оставляем только цифры let numbers = input.replace(/\D/g, ''); // Блокируем удаление префикса +7, если пользователь пытается стереть все if (!numbers) { setFormState(prev => ({ ...prev, phone: '' })); return; } // Если ввод начинается с 7, 8 или 9 - считаем это российским номером if (['7', '8', '9'].includes(numbers[0])) { // Нормализация: если первая цифра 9, добавляем 7 в начало. Если 8 - меняем на 7. if (numbers[0] === '9') numbers = '7' + numbers; if (numbers[0] === '8') numbers = '7' + numbers.slice(1); // Обрезаем до 11 символов (7 + 10 цифр) numbers = numbers.slice(0, 11); // Формирование маски +7 (XXX) XXX-XX-XX let formatted = '+7'; if (numbers.length > 1) formatted += ' (' + numbers.slice(1, 4); if (numbers.length >= 5) formatted += ') ' + numbers.slice(4, 7); if (numbers.length >= 8) formatted += '-' + numbers.slice(7, 9); if (numbers.length >= 10) formatted += '-' + numbers.slice(9, 11); setFormState(prev => ({ ...prev, phone: formatted })); } else { // Для международных номеров просто добавляем + setFormState(prev => ({ ...prev, phone: '+' + numbers })); } }; // Validation const validateForm = () => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Проверка телефона: должен содержать минимум 10 цифр. // Российский номер в маске +7 (XXX) XXX-XX-XX содержит 11 цифр. const phoneDigits = formState.phone.replace(/\D/g, ''); const isRussianLike = formState.phone.startsWith('+7'); if (!formState.name.trim()) return "Введите имя"; if (!emailRegex.test(formState.email)) return "Некорректный Email"; if (!formState.phone) return "Введите телефон"; if (isRussianLike && phoneDigits.length !== 11) return "Введите полный номер телефона"; if (phoneDigits.length < 10) return "Слишком короткий номер телефона"; return null; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorMessage(null); // 1. Anti-spam check (Honeypot) if (formState.honeypot) { // Бот заполнил скрытое поле, имитируем успех, но не отправляем console.warn("Spam detected"); setIsSubmitted(true); return; } // 2. Validation const validationError = validateForm(); if (validationError) { setErrorMessage(validationError); return; } setIsSubmitting(true); try { const formData = new URLSearchParams(); // --- Логика формирования запроса для Unisender --- // Если используем прокси, API ключ лучше добавлять на бэкенде, но здесь показан полный payload formData.append('format', 'json'); formData.append('api_key', UNISENDER_CONFIG.API_KEY); // Настройка полей formData.append('field_names[0]', 'email'); formData.append('field_names[1]', 'phone'); formData.append('field_names[2]', 'Name'); formData.append('field_names[3]', 'email_list_ids'); // Значения полей formData.append('data[0][0]', formState.email); formData.append('data[0][1]', formState.phone); formData.append('data[0][2]', formState.name); formData.append('data[0][3]', UNISENDER_CONFIG.LIST_ID); // Опции // formData.append('double_optin', '3'); // Раскомментировать, если нужно письмо-подтверждение // NOTE: Параметр 'overwrite' удален, так как он вызывает ошибку invalid_arg в методе importContacts const targetUrl = UNISENDER_CONFIG.USE_PROXY ? UNISENDER_CONFIG.PROXY_URL : UNISENDER_CONFIG.DIRECT_URL; console.log(`Sending to: ${targetUrl} (Proxy Mode: ${UNISENDER_CONFIG.USE_PROXY})`); const response = await fetch(targetUrl, { method: 'POST', body: formData, headers: { 'Content-Type': 'application/x-www-form-urlencoded', } }); if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } const result = await response.json(); // Проверяем ошибки самого API Unisender if (result.error) { throw new Error(`Unisender Error: ${result.error} (code: ${result.code})`); } setIsSubmitted(true); } catch (error: any) { console.error('Submission error:', error); // Удобное сообщение для разработчика/пользователя if (error.message && error.message.includes('Failed to fetch')) { setErrorMessage('Ошибка CORS или Сети. Для продакшена требуется серверный прокси, т.к. браузер блокирует прямые запросы к Unisender.'); } else { setErrorMessage(error.message || 'Произошла ошибка при отправке данных.'); } // --- DEV ONLY: Фейковый успех для демонстрации интерфейса, если ключи не настроены --- if (UNISENDER_CONFIG.API_KEY === 'YOUR_PRODUCTION_API_KEY') { console.warn("DEV MODE: Simulating success because API Key is generic."); setTimeout(() => { setErrorMessage(null); setIsSubmitted(true); }, 1500); } } finally { setIsSubmitting(false); } }; return (

    {data.title}

    {data.type === 'article' && data.content.text && (
    {formatText(data.content.text, isLight)}
    )} {data.type === 'table' && data.content.headers && data.content.rows && (
    {data.content.headers.map((h, i) => ( ))} {data.content.rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
    {h}
    {cell}
    )} {data.type === 'list' && data.content.items && (
      {data.content.items.map((item, i) => (
    • {(i + 1).toString().padStart(2, '0')} // {item}
    • ))}
    )} {data.type === 'form' && (
    {!isSubmitted ? (

    {data.content.text || 'Заполните форму, и мы свяжемся с вами для обсуждения деталей внедрения.'}

    {/* Honeypot Field (Invisible to humans) */} setFormState({...formState, honeypot: e.target.value})} style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
    setFormState({...formState, name: e.target.value})} className={`w-full p-4 bg-transparent border rounded-sm outline-none focus:border-[#ff4b4b] transition-colors disabled:opacity-50 ${isLight ? 'border-black/20 text-black' : 'border-white/20 text-white'}`} placeholder="Иванов Иван Иванович" />
    setFormState({...formState, email: e.target.value})} className={`w-full p-4 bg-transparent border rounded-sm outline-none focus:border-[#ff4b4b] transition-colors disabled:opacity-50 ${isLight ? 'border-black/20 text-black' : 'border-white/20 text-white'}`} placeholder="corp@gov.rb.ru" />
    {errorMessage && ( {errorMessage} )}

    Нажимая кнопку, вы подтверждаете согласие с Политикой обработки данных и даете разрешение на коммуникацию.

    ) : (

    Заявка принята

    Успешно.
    Наши специалисты свяжутся с вами в ближайшее время.

    )}
    )}
    iiEasy Secure Modal // System v2.5
    ); }; export default Modal;