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) => (
| {h} |
))}
{data.content.rows.map((row, i) => (
{row.map((cell, j) => (
| {cell} |
))}
))}
)}
{data.type === 'list' && data.content.items && (
{data.content.items.map((item, i) => (
-
{(i + 1).toString().padStart(2, '0')} //
{item}
))}
)}
{data.type === 'form' && (
)}
iiEasy Secure Modal // System v2.5
);
};
export default Modal;