Files
aggios.app/front-end-dash.aggios.app/app/cadastro/page.tsx
2025-12-17 13:36:23 -03:00

1423 lines
84 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Input, Checkbox, Button, Select, SearchableSelect } from "@/components/ui";
import DynamicBranding from "@/components/cadastro/DynamicBranding";
import DashboardPreview from "@/components/cadastro/DashboardPreview";
import { saveAuth } from '@/lib/auth';
import { API_ENDPOINTS, apiRequest } from '@/lib/api';
import dynamic from 'next/dynamic';
import {
UserIcon,
EnvelopeIcon,
LockClosedIcon,
KeyIcon,
BuildingOfficeIcon,
GlobeAltIcon,
DocumentTextIcon,
BuildingOffice2Icon,
MapPinIcon,
LinkIcon,
BriefcaseIcon,
UserGroupIcon,
MapIcon,
HomeIcon,
PhoneIcon,
CheckCircleIcon,
XCircleIcon,
ArrowPathIcon,
PlusIcon,
XMarkIcon,
ArrowLeftIcon,
ArrowRightIcon,
CheckIcon,
EyeIcon,
PencilIcon,
PhotoIcon,
ArrowUpTrayIcon,
PaintBrushIcon,
InformationCircleIcon
} from '@heroicons/react/24/outline';
const ThemeToggle = dynamic(() => import('@/components/ThemeToggle'), { ssr: false });
interface ContactField {
id: number;
whatsapp: string;
}
export default function CadastroPage() {
const router = useRouter();
const [currentStep, setCurrentStep] = useState(1);
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
const [formData, setFormData] = useState<Record<string, any>>({});
const [contacts, setContacts] = useState<ContactField[]>([{ id: 1, whatsapp: "" }]);
const [password, setPassword] = useState("");
const [passwordStrength, setPasswordStrength] = useState(0);
const [cnpjData, setCnpjData] = useState({ razaoSocial: "", endereco: "" });
const [cepData, setCepData] = useState({ state: "", city: "", neighborhood: "", street: "" });
const [loadingCnpj, setLoadingCnpj] = useState(false);
const [loadingCep, setLoadingCep] = useState(false);
const [subdomain, setSubdomain] = useState("");
const [domainAvailable, setDomainAvailable] = useState<boolean | null>(null);
const [checkingDomain, setCheckingDomain] = useState(false);
const [primaryColor, setPrimaryColor] = useState("#FF3A05");
const [secondaryColor, setSecondaryColor] = useState("#FF0080");
const [logoUrl, setLogoUrl] = useState<string>("");
const [showPreviewMobile, setShowPreviewMobile] = useState(false);
const [domainCheckTimeout, setDomainCheckTimeout] = useState<NodeJS.Timeout | null>(null);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [showWelcomeAnimation, setShowWelcomeAnimation] = useState(false);
// Carregar dados do localStorage ao montar
useEffect(() => {
const saved = localStorage.getItem('cadastroFormData');
if (saved) {
try {
const data = JSON.parse(saved);
setCurrentStep(data.currentStep || 1);
setCompletedSteps(data.completedSteps || []);
setFormData(data.formData || {});
setContacts(data.contacts || [{ id: 1, whatsapp: "" }]);
setPassword(data.password || "");
setPasswordStrength(data.passwordStrength || 0);
setCnpjData(data.cnpjData || { razaoSocial: "", endereco: "" });
setCepData(data.cepData || { state: "", city: "", neighborhood: "", street: "" });
setSubdomain(data.subdomain || "");
setDomainAvailable(data.domainAvailable ?? null);
setPrimaryColor(data.primaryColor || "#FF3A05");
setSecondaryColor(data.secondaryColor || "#FF0080");
setLogoUrl(data.logoUrl || "");
} catch (error) {
console.error('Erro ao carregar dados:', error);
}
}
}, []);
// Salvar no localStorage sempre que houver mudanças
useEffect(() => {
const dataToSave = {
currentStep,
completedSteps,
formData,
contacts,
password,
passwordStrength,
cnpjData,
cepData,
subdomain,
domainAvailable,
primaryColor,
secondaryColor,
logoUrl
};
localStorage.setItem('cadastroFormData', JSON.stringify(dataToSave));
}, [currentStep, completedSteps, formData, contacts, password, passwordStrength, cnpjData, cepData, subdomain, domainAvailable, primaryColor, secondaryColor, logoUrl]);
// Função para atualizar formData
const updateFormData = (name: string, value: any) => {
setFormData(prev => ({ ...prev, [name]: value }));
};
const steps = [
{
number: 1,
title: "Dados Pessoais",
heading: "Seus Dados Pessoais",
description: "Informe seus dados para criar sua conta de administrador."
},
{
number: 2,
title: "Empresa",
heading: "Dados da Empresa",
description: "Cadastre as informações básicas da sua empresa."
},
{
number: 3,
title: "Localização e Contato",
heading: "Endereço e Contato",
description: "Informe a localização da sua empresa e os contatos para comunicação."
},
{
number: 4,
title: "Personalização",
heading: "Personalize seu Painel",
description: "Configure as cores e identidade visual da sua empresa."
},
];
const currentStepData = steps.find(s => s.number === currentStep);
const validateCurrentStep = () => {
setFieldErrors({});
const errors: Record<string, string> = {};
if (currentStep === 1) {
if (!formData.fullName || formData.fullName.trim().length < 3) {
errors.fullName = 'Nome completo deve ter no mínimo 3 caracteres';
}
if (!formData.email || !formData.email.includes('@')) {
errors.email = 'Email inválido';
}
if (password.length < 8) {
errors.password = 'Senha deve ter no mínimo 8 caracteres';
} else if (passwordStrength < 2) {
errors.password = 'Senha muito fraca';
}
if (password !== formData.confirmPassword) {
errors.confirmPassword = 'Senhas não coincidem';
}
if (!formData.terms) {
errors.terms = 'Aceite os Termos de Uso';
}
}
if (currentStep === 2) {
if (!formData.companyName || formData.companyName.trim().length < 3) {
errors.companyName = 'Mínimo 3 caracteres';
}
const cnpjNumbers = formData.cnpj?.replace(/\D/g, '') || '';
if (cnpjNumbers.length !== 14) {
errors.cnpj = 'CNPJ incompleto';
}
if (!formData.description || formData.description.trim().length < 10) {
errors.description = 'Mínimo 10 caracteres';
}
if (!formData.industry) {
errors.industry = 'Selecione o segmento';
}
if (!formData.teamSize) {
errors.teamSize = 'Selecione o tamanho';
}
if (!subdomain || subdomain.trim().length < 3) {
errors.subdomain = 'Mínimo 3 caracteres';
} else if (!/^[a-z0-9-]+$/.test(subdomain)) {
errors.subdomain = 'Apenas letras, números e hífens';
} else if (domainAvailable === false) {
errors.subdomain = 'Subdomínio já está em uso';
} else if (domainAvailable === null && subdomain.length >= 3) {
errors.subdomain = 'Aguarde verificação';
}
}
if (currentStep === 3) {
const cepNumbers = formData.cep?.replace(/\D/g, '') || '';
if (cepNumbers.length !== 8) {
errors.cep = 'CEP incompleto';
}
if (!formData.number || formData.number.trim().length < 1) {
errors.number = 'Obrigatório';
}
for (let i = 0; i < contacts.length; i++) {
if (!contacts[i].whatsapp || contacts[i].whatsapp.replace(/\D/g, '').length < 10) {
errors[`whatsapp-${contacts[i].id}`] = 'WhatsApp incompleto';
}
}
}
setFieldErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSubmitRegistration = async () => {
try {
const payload = {
// Step 1 - Dados Pessoais
email: formData.email,
password: password,
fullName: formData.fullName,
newsletter: formData.newsletter || false,
// Step 2 - Empresa
companyName: formData.companyName,
cnpj: formData.cnpj,
razaoSocial: cnpjData.razaoSocial,
description: formData.description,
website: formData.website,
industry: formData.industry,
teamSize: formData.teamSize,
subdomain: subdomain,
// Step 3 - Localização e Contato
cep: formData.cep,
state: cepData.state,
city: cepData.city,
neighborhood: cepData.neighborhood,
street: cepData.street,
number: formData.number,
complement: formData.complement,
contacts: contacts,
// Step 4 - Personalização
primaryColor: primaryColor,
secondaryColor: secondaryColor,
logoUrl: logoUrl,
};
console.log('📤 Enviando cadastro completo:', payload);
const data = await apiRequest(API_ENDPOINTS.register, {
method: 'POST',
body: JSON.stringify(payload),
});
console.log('📥 Resposta data:', data);
// Salvar autenticação
if (data.token) {
saveAuth(data.token, {
id: data.id,
email: data.email,
name: data.name,
role: data.role,
tenantId: data.tenantId,
company: data.company,
subdomain: data.subdomain
});
}
// Sucesso - limpar localStorage do form
localStorage.removeItem('cadastroFormData');
console.log('✓ Conta criada com sucesso! Redirecionando...');
// Mostrar animação de boas-vindas
setShowWelcomeAnimation(true);
// Aguardar 4 segundos e redirecionar para o painel da agência
setTimeout(() => {
// Construir URL do tenant baseado no subdomínio
const tenantUrl = `http://${data.subdomain || subdomain}.localhost:3000`;
console.log('Redirecionando para:', tenantUrl);
window.location.href = tenantUrl;
}, 4000);
} catch (error: any) {
console.error('❌ Erro no cadastro:', error);
let errorMessage = 'Não conseguimos criar sua conta. Por favor, tente novamente.';
// Mensagens humanizadas baseadas no erro
if (error.message) {
const msg = error.message.toLowerCase();
if (msg.includes('subdomain') || msg.includes('domínio') || msg.includes('domain')) {
errorMessage = `O subdomínio "${subdomain}" já está sendo usado. Por favor, escolha outro nome para sua empresa.`;
} else if (msg.includes('email')) {
errorMessage = 'Este email já está cadastrado. Você já tem uma conta? Tente fazer login.';
} else if (msg.includes('cnpj')) {
errorMessage = 'Este CNPJ já está cadastrado no sistema.';
} else if (msg.includes('network') || msg.includes('fetch')) {
errorMessage = 'Problemas de conexão. Verifique sua internet e tente novamente.';
} else {
errorMessage = error.message;
}
}
console.error('Erro:', errorMessage);
alert(errorMessage);
}
};
const canNavigateToStep = (targetStep: number) => {
// Pode navegar para trás sempre
if (targetStep < currentStep) {
return true;
}
// Pode navegar para a etapa atual
if (targetStep === currentStep) {
return true;
}
// Só pode navegar para frente se a etapa anterior estiver completa
if (targetStep === currentStep + 1 && completedSteps.includes(currentStep)) {
return true;
}
// Pode navegar para qualquer etapa já completada
if (completedSteps.includes(targetStep - 1)) {
return true;
}
return false;
};
const handleNext = (e?: React.FormEvent) => {
if (e) {
e.preventDefault();
}
if (!validateCurrentStep()) {
alert('Por favor, preencha todos os campos obrigatórios antes de continuar.');
return;
}
if (currentStep < 4) {
setCompletedSteps([...completedSteps, currentStep]);
setCurrentStep(currentStep + 1);
} else {
// Última etapa - enviar dados para o backend
handleSubmitRegistration();
}
};
const addContact = () => {
const newId = contacts.length > 0 ? Math.max(...contacts.map(c => c.id)) + 1 : 1;
setContacts([...contacts, { id: newId, whatsapp: "" }]);
};
const removeContact = (id: number) => {
if (contacts.length > 1) {
setContacts(contacts.filter(c => c.id !== id));
}
};
const formatPhone = (value: string) => {
const numbers = value.replace(/\D/g, "");
if (numbers.length <= 10) {
return numbers.replace(/(\d{2})(\d{4})(\d{0,4})/, "($1) $2-$3").replace(/-$/, "");
}
return numbers.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3").replace(/-$/, "");
};
const calculatePasswordStrength = (pwd: string): number => {
let strength = 0;
if (pwd.length >= 8) strength++;
if (pwd.length >= 12) strength++;
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++;
if (/\d/.test(pwd)) strength++;
if (/[^a-zA-Z0-9]/.test(pwd)) strength++;
return strength;
};
const checkDomainAvailability = async (domain: string) => {
if (!domain || domain.length < 3) {
setDomainAvailable(null);
return;
}
if (!/^[a-z0-9-]+$/.test(domain)) {
setDomainAvailable(null);
console.error('O subdomínio deve conter apenas letras minúsculas, números e hífens.');
return;
}
setCheckingDomain(true);
setDomainAvailable(null);
try {
// Verificar disponibilidade via API - usar porta 8085 do backend
const response = await fetch(`http://localhost:8085/api/tenant/check?subdomain=${domain}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
// Se retornar 200, o tenant existe (indisponível)
// Se retornar 404, o tenant não existe (disponível)
const isAvailable = response.status === 404;
setDomainAvailable(isAvailable);
if (isAvailable) {
console.log(`${domain}.aggios.app está disponível!`);
} else {
console.log(`${domain}.aggios.app já está em uso. Tente outro.`);
}
} catch (error) {
console.error('Erro ao verificar domínio:', error);
setDomainAvailable(null);
} finally {
setCheckingDomain(false);
}
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value;
setPassword(newPassword);
setPasswordStrength(calculatePasswordStrength(newPassword));
};
const getPasswordStrengthLabel = () => {
if (password.length === 0) return "";
if (passwordStrength <= 1) return "Muito fraca";
if (passwordStrength === 2) return "Fraca";
if (passwordStrength === 3) return "Média";
if (passwordStrength === 4) return "Forte";
return "Muito forte";
};
const getPasswordStrengthColor = () => {
if (passwordStrength <= 1) return "#EF4444";
if (passwordStrength === 2) return "#F59E0B";
if (passwordStrength === 3) return "#3B82F6";
if (passwordStrength === 4) return "#10B981";
return "#059669";
};
const fetchCnpjData = async (cnpj: string) => {
const numbers = cnpj.replace(/\D/g, "");
if (numbers.length !== 14) return;
setLoadingCnpj(true);
try {
const response = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${numbers}`);
if (response.ok) {
const data = await response.json();
setCnpjData({
razaoSocial: data.razao_social || "",
endereco: `${data.logradouro}, ${data.numero} - ${data.bairro}, ${data.municipio}/${data.uf}`
});
}
} catch (error) {
console.error("Erro ao buscar CNPJ:", error);
} finally {
setLoadingCnpj(false);
}
};
const fetchCepData = async (cep: string) => {
const numbers = cep.replace(/\D/g, "");
if (numbers.length !== 8) return;
setLoadingCep(true);
try {
const response = await fetch(`https://viacep.com.br/ws/${numbers}/json/`);
if (response.ok) {
const data = await response.json();
if (!data.erro) {
setCepData({
state: data.uf || "",
city: data.localidade || "",
neighborhood: data.bairro || "",
street: data.logradouro || ""
});
}
}
} catch (error) {
console.error("Erro ao buscar CEP:", error);
} finally {
setLoadingCep(false);
}
};
const formatCnpj = (value: string) => {
const numbers = value.replace(/\D/g, "");
return numbers.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5").substring(0, 18);
};
const formatCep = (value: string) => {
const numbers = value.replace(/\D/g, "");
return numbers.replace(/(\d{5})(\d{3})/, "$1-$2").substring(0, 9);
};
return (
<>
{/* Modal de Boas-vindas com Animação */}
{showWelcomeAnimation && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="relative max-w-lg w-full mx-4 text-center">
{/* Animação de círculos expandindo */}
<div className="relative mx-auto w-32 h-32 mb-8">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#FF3A05] to-[#FF0080] animate-ping opacity-20"></div>
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#FF3A05] to-[#FF0080] opacity-40 animate-pulse"></div>
<div className="absolute inset-0 flex items-center justify-center">
<CheckCircleIcon className="w-20 h-20 text-white animate-bounce" />
</div>
</div>
{/* Texto animado */}
<div className="space-y-4 animate-fade-in">
<h2 className="text-4xl font-bold text-white mb-2">
Bem-vindo! 🎉
</h2>
<p className="text-xl text-gray-200">
Estamos criando seu painel personalizado...
</p>
<p className="text-base text-gray-300 mt-4">
Em breve você terá a melhor experiência de gestão!
</p>
{/* Barra de progresso animada */}
<div className="mt-8 w-full bg-gray-700 rounded-full h-2 overflow-hidden">
<div className="h-full bg-gradient-to-r from-[#FF3A05] to-[#FF0080] rounded-full animate-progress"></div>
</div>
</div>
</div>
</div>
)}
<div className="flex min-h-screen">
{/* Lado Esquerdo - Formulário */}
<div className="w-full lg:w-[50%] h-screen flex flex-col">
{/* Título e texto */}
<div className="px-6 sm:px-12 py-6 bg-white dark:bg-gray-800 border-b border-[#E5E5E5] dark:border-gray-700">
<div className="max-w-2xl mx-auto flex items-center gap-6">
{/* Theme Toggle */}
<div className="ml-auto">
<ThemeToggle />
</div>
</div>
<div className="max-w-2xl mx-auto flex items-center gap-6 mt-4">
{/* Progresso Circular */}
<div className="relative flex items-center justify-center w-16 h-16 shrink-0">
<svg className="w-16 h-16 transform -rotate-90">
<circle
cx="32"
cy="32"
r="28"
stroke="#E5E5E5"
strokeWidth="4"
fill="none"
className="dark:stroke-gray-600"
/>
<circle
cx="32"
cy="32"
r="28"
stroke="url(#gradient)"
strokeWidth="4"
fill="none"
strokeDasharray={`${2 * Math.PI * 28}`}
strokeDashoffset={`${2 * Math.PI * 28 * (1 - (currentStep / 4))}`}
strokeLinecap="round"
className="transition-all duration-300"
/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#FF3A05" />
<stop offset="100%" stopColor="#FF0080" />
</linearGradient>
</defs>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-sm font-bold text-[#000000] dark:text-white">{Math.round((currentStep / 4) * 100)}%</span>
</div>
</div>
{/* Título e Descrição */}
<div className="flex-1">
<h1 className="text-[28px] font-bold text-[#000000] dark:text-white mb-1">{currentStepData?.heading}</h1>
<p className="text-[14px] text-[#7D7D7D] dark:text-gray-400">
{currentStepData?.description}
</p>
</div>
</div>
</div>
{/* Formulário */}
<div className="flex-1 overflow-y-auto bg-[#FDFDFC] dark:bg-gray-900 px-6 sm:px-12 py-6">
<div className="max-w-2xl mx-auto">
<form onSubmit={(e) => { e.preventDefault(); handleNext(e); }} className="space-y-6">
{currentStep === 1 && (
<div className="space-y-5">
<Input
name="fullName"
label="Nome Completo"
placeholder="Seu nome completo"
leftIcon={<UserIcon />}
value={formData.fullName || ''}
onChange={(e) => updateFormData('fullName', e.target.value)}
error={fieldErrors.fullName}
required
/>
<Input
name="email"
label="Email"
type="email"
placeholder="seu@email.com"
leftIcon={<EnvelopeIcon />}
helperText="Será usado para seu login"
value={formData.email || ''}
onChange={(e) => updateFormData('email', e.target.value)}
required
/>
{/* Separador de seção */}
<div className="pt-4 border-t border-[#E5E5E5]">
<h3 className="text-sm font-semibold text-[#000000] dark:text-white mb-4">Crie sua senha de acesso</h3>
<div className="space-y-4">
<div>
<Input
name="password"
label="Senha"
type="password"
placeholder="Mínimo 8 caracteres"
leftIcon={<LockClosedIcon />}
helperText="Use maiúsculas, minúsculas, números e símbolos"
value={password}
onChange={handlePasswordChange}
required
/>
{password.length > 0 && (
<div className="mt-2">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-[#7D7D7D]">Força da senha:</span>
<span className="text-xs font-semibold" style={{ color: getPasswordStrengthColor() }}>
{getPasswordStrengthLabel()}
</span>
</div>
<div className="h-1.5 w-full bg-[#E5E5E5] rounded-full overflow-hidden">
<div
className="h-full transition-all duration-300 rounded-full"
style={{
width: `${(passwordStrength / 5) * 100}%`,
backgroundColor: getPasswordStrengthColor()
}}
/>
</div>
</div>
)}
</div>
<Input
name="confirmPassword"
label="Confirmar Senha"
type="password"
placeholder="Repita a senha"
leftIcon={<KeyIcon />}
value={formData.confirmPassword || ''}
onChange={(e) => updateFormData('confirmPassword', e.target.value)}
error={fieldErrors.confirmPassword}
required
/>
</div>
</div>
<Checkbox
name="terms"
checked={formData.terms || false}
onChange={(e) => updateFormData('terms', e.target.checked)}
label={
<span>
Concordo com os{" "}
<Link href="/termos" className="bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent hover:underline cursor-pointer font-medium">
Termos de Uso
</Link>
</span>
}
/>
<Checkbox
name="newsletter"
label="Desejo receber newsletters e novidades"
checked={formData.newsletter || false}
onChange={(e) => updateFormData('newsletter', e.target.checked)}
/>
{/* Link para login */}
<p className="text-center mt-6 text-[14px] text-[#7D7D7D]">
possui uma conta?{" "}
<Link href="/login" className="bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent font-medium hover:underline cursor-pointer">
Fazer login
</Link>
</p>
</div>
)} {currentStep === 2 && (
<div className="space-y-5">
<Input
name="companyName"
label="Nome da Empresa"
placeholder="Ex: IdeaPages, DevStudio"
leftIcon={<BuildingOfficeIcon />}
value={formData.companyName || ''}
onChange={(e) => {
const name = e.target.value;
updateFormData('companyName', name);
// Auto-generate subdomain
const slug = name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
setSubdomain(slug);
setDomainAvailable(null);
// Limpar timeout anterior
if (domainCheckTimeout) {
clearTimeout(domainCheckTimeout);
}
// Verificar automaticamente após 800ms se tiver 3+ caracteres
if (slug.length >= 3) {
const timeout = setTimeout(() => {
checkDomainAvailability(slug);
}, 800);
setDomainCheckTimeout(timeout);
}
}}
required
/>
<div className="relative">
<div className="relative">
<label className="block text-[13px] font-semibold text-[#000000] dark:text-white mb-2">
Subdomínio (URL do Painel)<span className="text-[#FF3A05] ml-1">*</span>
</label>
<div className="relative">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-zinc-400">
<GlobeAltIcon className="w-5 h-5" />
</div>
<input
type="text"
name="subdomain"
placeholder="minhaempresa"
className="w-full pl-11 pr-11 py-3 text-[14px] font-normal border rounded-md bg-white dark:bg-gray-700 dark:text-white placeholder:text-[#7D7D7D] dark:placeholder:text-gray-400 border-[#E5E5E5] dark:border-gray-600 focus:border-[#FF3A05] outline-none"
value={subdomain}
onChange={(e) => {
const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
setSubdomain(value);
setDomainAvailable(null);
// Limpar timeout anterior
if (domainCheckTimeout) {
clearTimeout(domainCheckTimeout);
}
// Verificar automaticamente após 800ms
if (value.length >= 3) {
const timeout = setTimeout(() => {
checkDomainAvailability(value);
}, 800);
setDomainCheckTimeout(timeout);
}
}}
/>
<div className="absolute right-3.5 top-1/2 -translate-y-1/2">
{checkingDomain && <ArrowPathIcon className="w-5 h-5 animate-spin text-brand-500" />}
{domainAvailable === true && <CheckCircleIcon className="w-5 h-5 text-green-500" />}
{domainAvailable === false && <XCircleIcon className="w-5 h-5 text-red-500" />}
</div>
</div>
<div className="flex items-center justify-between mt-2">
<p className="text-[13px] text-zinc-500 dark:text-gray-400">
Seu painel: <strong className="text-zinc-900 dark:text-white">{subdomain || '...'}</strong>.aggios.app
</p>
{checkingDomain && (
<span className="flex items-center gap-1.5 text-[11px] font-semibold text-brand-600 bg-brand-50 px-2.5 py-0.5 rounded-full border border-brand-100">
<ArrowPathIcon className="w-3 h-3 animate-spin" /> VERIFICANDO
</span>
)}
{domainAvailable === true && (
<span className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-700 bg-emerald-50 px-2.5 py-0.5 rounded-full border border-emerald-200">
<CheckCircleIcon className="w-3 h-3" /> DISPONÍVEL
</span>
)}
{domainAvailable === false && (
<span className="flex items-center gap-1.5 text-[11px] font-semibold text-red-700 bg-red-50 px-2.5 py-0.5 rounded-full border border-red-200">
<XCircleIcon className="w-3 h-3" /> INDISPONÍVEL
</span>
)}
</div>
</div>
</div>
<Input
name="cnpj"
label="CNPJ"
placeholder="00.000.000/0000-00"
leftIcon={<DocumentTextIcon />}
helperText="Preencheremos automaticamente razão social e endereço"
maxLength={18}
value={formData.cnpj || ''}
onChange={(e) => {
const formatted = formatCnpj(e.target.value);
updateFormData('cnpj', formatted);
if (formatted.replace(/\D/g, "").length === 14) {
fetchCnpjData(formatted);
}
}}
required
/>
<Input
name="razaoSocial"
label="Razão Social"
placeholder="Será preenchido automaticamente"
leftIcon={<BuildingOffice2Icon />}
value={cnpjData.razaoSocial}
disabled
/>
<Input
name="cnpjAddress"
label="Endereço (CNPJ)"
placeholder="Será preenchido automaticamente"
leftIcon={<MapPinIcon />}
value={cnpjData.endereco}
disabled
/>
<div>
<label className="block text-[13px] font-semibold text-[#000000] dark:text-white mb-2">
Descrição Breve<span className="text-[#FF3A05] ml-1">*</span>
</label>
<textarea
name="description"
placeholder="Apresente sua empresa em poucas palavras (máx 300 caracteres)"
className="w-full px-3.5 py-3 text-[14px] font-normal border rounded-md bg-white dark:bg-gray-700 dark:text-white placeholder:text-[#7D7D7D] dark:placeholder:text-gray-400 border-[#E5E5E5] dark:border-gray-600 focus:border-[#FF3A05] outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none resize-none"
rows={4}
maxLength={300}
value={formData.description || ''}
onChange={(e) => updateFormData('description', e.target.value)}
required
/>
</div>
<Input
name="website"
label="Website/Portfolio (opcional)"
placeholder="https://suaagencia.com.br"
leftIcon={<LinkIcon />}
value={formData.website || ''}
onChange={(e) => updateFormData('website', e.target.value)}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<SearchableSelect
name="industry"
label="Segmento/Indústria"
leftIcon={<BriefcaseIcon />}
placeholder="Selecione o segmento"
value={formData.industry || ''}
onChange={(value) => updateFormData('industry', value)}
options={[
{ value: "agencia-digital", label: "Agência Digital" },
{ value: "agencia-publicidade", label: "Agência de Publicidade" },
{ value: "agencia-marketing", label: "Agência de Marketing" },
{ value: "desenvolvimento-software", label: "Desenvolvimento de Software" },
{ value: "desenvolvimento-web", label: "Desenvolvimento Web" },
{ value: "desenvolvimento-mobile", label: "Desenvolvimento Mobile" },
{ value: "saas", label: "SaaS (Software as a Service)" },
{ value: "consultoria-ti", label: "Consultoria em TI" },
{ value: "consultoria-negocios", label: "Consultoria de Negócios" },
{ value: "marketing-digital", label: "Marketing Digital" },
{ value: "design-grafico", label: "Design Gráfico" },
{ value: "design-ui-ux", label: "Design UI/UX" },
{ value: "tecnologia", label: "Tecnologia" },
{ value: "ecommerce", label: "E-commerce" },
{ value: "educacao", label: "Educação" },
{ value: "educacao-online", label: "Educação Online" },
{ value: "saude", label: "Saúde" },
{ value: "financas", label: "Finanças" },
{ value: "fintech", label: "Fintech" },
{ value: "imobiliario", label: "Imobiliário" },
{ value: "varejo", label: "Varejo" },
{ value: "logistica", label: "Logística" },
{ value: "turismo", label: "Turismo" },
{ value: "alimentacao", label: "Alimentação" },
{ value: "industria", label: "Indústria" },
{ value: "servicos", label: "Serviços" },
{ value: "outros", label: "Outros" }
]}
required
/>
<SearchableSelect
name="teamSize"
label="Tamanho da Equipe"
leftIcon={<UserGroupIcon />}
placeholder="Selecione o tamanho"
value={formData.teamSize || ''}
onChange={(value) => updateFormData('teamSize', value)}
options={[
{ value: "1-10", label: "1-10 pessoas" },
{ value: "11-50", label: "11-50 pessoas" },
{ value: "51-100", label: "51-100 pessoas" },
{ value: "101-250", label: "101-250 pessoas" },
{ value: "251-500", label: "251-500 pessoas" },
{ value: "500+", label: "Mais de 500 pessoas" }
]}
required
/>
</div>
</div>
)}
{currentStep === 3 && (
<div className="space-y-4">
<Input
name="cep"
label="CEP"
placeholder="00000-000"
leftIcon={<MapPinIcon />}
maxLength={9}
value={formData.cep || ''}
onChange={(e) => {
const formatted = formatCep(e.target.value);
updateFormData('cep', formatted);
const numbers = formatted.replace(/\D/g, "");
// Se campo vazio, limpar dados
if (numbers.length === 0) {
setCepData({ state: "", city: "", neighborhood: "", street: "" });
}
// Se CEP completo, buscar dados
else if (numbers.length === 8) {
fetchCepData(formatted);
}
}}
required
/>
<div className="grid grid-cols-2 gap-4">
<Input
name="state"
label="Estado"
placeholder="UF"
leftIcon={<MapIcon />}
value={cepData.state}
disabled
required
/>
<Input
name="city"
label="Cidade"
placeholder="Nome da cidade"
leftIcon={<HomeIcon />}
value={cepData.city}
disabled
required
/>
</div>
<Input
name="neighborhood"
label="Bairro"
placeholder="Centro"
value={cepData.neighborhood}
disabled
required
/>
<Input
name="street"
label="Rua/Avenida"
placeholder="Rua das Flores"
value={cepData.street}
disabled
required
/>
<div className="grid grid-cols-2 gap-4">
<Input
name="number"
label="Número"
placeholder="123"
value={formData.number || ''}
onChange={(e) => updateFormData('number', e.target.value)}
required
/>
<Input
name="complement"
label="Complemento (opcional)"
placeholder="Apto, Sala..."
value={formData.complement || ''}
onChange={(e) => updateFormData('complement', e.target.value)}
/>
</div>
{/* Contatos da Empresa */}
<div className="pt-4 border-t border-[#E5E5E5] dark:border-gray-700">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-[#000000] dark:text-white">Contatos da Empresa</h3>
</div>
{contacts.map((contact, index) => (
<div key={contact.id} className="space-y-4 p-4 border border-[#E5E5E5] dark:border-gray-600 rounded-md bg-white dark:bg-gray-800">
{contacts.length > 1 && (
<div className="flex items-center justify-end -mt-2 -mr-2">
<button
type="button"
onClick={() => removeContact(contact.id)}
className="text-[#7D7D7D] dark:text-gray-400 hover:text-[#FF3A05] transition-colors"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
)}
<Input
name={`whatsapp-${contact.id}`}
label="WhatsApp"
placeholder="(00) 00000-0000"
leftIcon={<PhoneIcon />}
maxLength={15}
value={contact.whatsapp}
onChange={(e) => {
const formatted = formatPhone(e.target.value);
setContacts(contacts.map(c =>
c.id === contact.id
? { ...c, whatsapp: formatted }
: c
));
}}
required
/>
</div>
))}
<Button
type="button"
variant="outline"
onClick={addContact}
leftIcon={<PlusIcon />}
className="w-full"
>
Adicionar mais contato
</Button>
</div>
</div>
</div>
)}
{currentStep === 4 && (
<div className="space-y-6">
{/* Botão Toggle Preview (Mobile Only) */}
<div className="lg:hidden">
<button
type="button"
onClick={() => setShowPreviewMobile(!showPreviewMobile)}
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg border-2 border-[#FF3A05] text-[#FF3A05] font-medium hover:bg-[#FF3A05]/5 transition-colors"
>
<i className={`${showPreviewMobile ? 'ri-edit-line' : 'ri-eye-line'} text-xl`} />
{showPreviewMobile ? 'Voltar ao Formulário' : 'Ver Preview do Painel'}
</button>
</div>
{/* Preview Mobile */}
{showPreviewMobile && (
<div className="lg:hidden">
<DashboardPreview
companyName={formData.companyName || 'Sua Empresa'}
subdomain={subdomain}
primaryColor={primaryColor}
secondaryColor={secondaryColor}
logoUrl={logoUrl}
/>
</div>
)}
{/* Formulário (oculto quando preview ativo no mobile) */}
<div className={showPreviewMobile ? 'hidden lg:block space-y-4' : 'block space-y-4'}>
{/* Upload de Logo */}
<div>
<label className="block text-sm font-medium text-[#000000] dark:text-white mb-3">
Logo da Empresa <span className="text-[#7D7D7D] dark:text-gray-400">(opcional)</span>
</label>
<div className="flex items-center gap-6">
{/* Preview do Logo */}
<div className="w-20 h-20 rounded-lg border-2 border-dashed border-[#E5E5E5] dark:border-gray-600 flex items-center justify-center overflow-hidden bg-[#F5F5F5] dark:bg-gray-700">
{logoUrl ? (
<img src={logoUrl} alt="Logo preview" className="w-full h-full object-cover" />
) : (
<i className="ri-image-line text-3xl text-[#7D7D7D] dark:text-gray-400" />
)}
</div>
{/* Input de Upload */}
<div className="flex-1">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setLogoUrl(reader.result as string);
};
reader.readAsDataURL(file);
}
}}
className="hidden"
id="logo-upload"
/>
<label
htmlFor="logo-upload"
className="inline-flex items-center gap-2 px-4 py-2 border border-[#E5E5E5] dark:border-gray-600 rounded-md text-sm font-medium text-[#000000] dark:text-white hover:bg-[#F5F5F5] dark:hover:bg-gray-700 transition-colors cursor-pointer"
>
<i className="ri-upload-2-line" />
Escolher arquivo
</label>
{logoUrl && (
<button
type="button"
onClick={() => setLogoUrl('')}
className="ml-2 text-sm bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent hover:underline font-medium"
>
Remover
</button>
)}
<p className="text-xs text-[#7D7D7D] dark:text-gray-400 mt-2">
PNG, JPG ou SVG. Tamanho recomendado: 200x200px
</p>
</div>
</div>
</div>
{/* Cores do Painel */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Cor Primária */}
<div>
<label className="block text-sm font-medium text-[#000000] dark:text-white mb-3">
Cor Primária <span className="text-[#FF3A05]">*</span>
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i className="ri-palette-line text-[#7D7D7D] dark:text-gray-400 text-[18px]" />
</div>
<input
type="text"
value={primaryColor}
onChange={(e) => {
const value = e.target.value;
if (/^#[0-9A-Fa-f]{0,6}$/.test(value)) {
setPrimaryColor(value);
}
}}
placeholder="#FF3A05"
className="w-full pl-10 pr-4 py-2 text-sm border border-[#E5E5E5] dark:border-gray-600 bg-white dark:bg-gray-700 dark:text-white rounded-md focus:border-[#FF3A05] transition-colors font-mono"
/>
</div>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
className="w-14 h-10 border-2 border-[#E5E5E5] dark:border-gray-600 rounded-md cursor-pointer"
/>
</div>
<p className="text-xs text-[#7D7D7D] dark:text-gray-400 mt-1 flex items-center gap-1">
<i className="ri-information-line" />
Usada em menus, botões e destaques
</p>
</div>
{/* Cor Secundária */}
<div>
<label className="block text-sm font-medium text-[#000000] dark:text-white mb-3">
Cor Secundária <span className="text-[#7D7D7D] dark:text-gray-400">(opcional)</span>
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i className="ri-brush-line text-[#7D7D7D] dark:text-gray-400 text-[18px]" />
</div>
<input
type="text"
value={secondaryColor}
onChange={(e) => {
const value = e.target.value;
if (/^#[0-9A-Fa-f]{0,6}$/.test(value)) {
setSecondaryColor(value);
}
}}
placeholder="#FF0080"
className="w-full pl-10 pr-4 py-2 text-sm border border-[#E5E5E5] dark:border-gray-600 bg-white dark:bg-gray-700 dark:text-white rounded-md focus:border-[#FF3A05] transition-colors font-mono"
/>
</div>
<input
type="color"
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
className="w-14 h-10 border-2 border-[#E5E5E5] dark:border-gray-600 rounded-md cursor-pointer"
/>
</div>
<p className="text-xs text-[#7D7D7D] dark:text-gray-400 mt-1 flex items-center gap-1">
<i className="ri-information-line" />
Usada em cards e elementos secundários
</p>
</div>
</div>
{/* Paletas Sugeridas */}
<div>
<h4 className="text-sm font-semibold text-[#000000] dark:text-white mb-4">Paletas Sugeridas</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ name: 'Fogo', primary: '#FF3A05', secondary: '#FF0080' },
{ name: 'Oceano', primary: '#0EA5E9', secondary: '#3B82F6' },
{ name: 'Natureza', primary: '#10B981', secondary: '#059669' },
{ name: 'Elegante', primary: '#8B5CF6', secondary: '#A78BFA' },
{ name: 'Solar', primary: '#F59E0B', secondary: '#FBBF24' },
{ name: 'Noturno', primary: '#1E293B', secondary: '#475569' },
{ name: 'Rosa', primary: '#EC4899', secondary: '#F472B6' },
{ name: 'Ciano', primary: '#06B6D4', secondary: '#22D3EE' },
].map((palette) => (
<button
key={palette.name}
type="button"
onClick={() => {
setPrimaryColor(palette.primary);
setSecondaryColor(palette.secondary);
}}
className="flex items-center gap-2 p-2 rounded-md border border-[#E5E5E5] dark:border-gray-600 hover:border-[#FF3A05] transition-colors group cursor-pointer bg-white dark:bg-gray-800"
>
<div className="flex gap-1">
<div
className="w-6 h-6 rounded"
style={{ backgroundColor: palette.primary }}
/>
<div
className="w-6 h-6 rounded"
style={{ backgroundColor: palette.secondary }}
/>
</div>
<span className="text-xs font-medium text-[#7D7D7D] dark:text-gray-400 group-hover:text-[#000000] dark:group-hover:text-white">
{palette.name}
</span>
</button>
))}
</div>
</div>
{/* Informações */}
<div className="p-6 bg-[#F0F9FF] dark:bg-gray-800 border border-[#BAE6FD] dark:border-gray-600 rounded-md">
<div className="flex gap-4">
<i className="ri-information-line text-[#0EA5E9] dark:text-blue-400 text-xl mt-0.5" />
<div>
<h4 className="text-sm font-semibold text-[#000000] dark:text-white mb-1">
Você pode alterar depois
</h4>
<p className="text-xs text-[#7D7D7D] dark:text-gray-400">
As cores do seu painel podem ser ajustadas a qualquer momento nas configurações.
Experimente diferentes combinações até encontrar a ideal!
</p>
</div>
</div>
</div>
</div>
</div>
)}
</form>
</div>
</div>
{/* Rodapé - botão voltar à esquerda, etapas e botão ação à direita */}
<div className="border-t border-[#E5E5E5] dark:border-gray-700 bg-white dark:bg-gray-800 px-4 sm:px-12 py-4">
{/* Desktop: Linha única com tudo */}
<div className="hidden md:flex items-center justify-between">
{/* Botão voltar à esquerda */}
<div>
{currentStep > 1 && (
<Button variant="outline" onClick={() => setCurrentStep(currentStep - 1)} leftIcon={<ArrowLeftIcon />}>
Voltar
</Button>
)}
</div>
{/* Etapas centralizadas e enumeradas */}
<div className="flex items-center justify-center gap-3">
{steps.map((step, index) => (
<div key={step.number} className="flex items-center gap-3">
<button
type="button"
onClick={() => {
if (canNavigateToStep(step.number)) {
setCurrentStep(step.number);
} else {
alert('Complete a etapa atual antes de avançar.');
}
}}
disabled={!canNavigateToStep(step.number)}
className={`flex flex-col items-center gap-1.5 group transition-all ${canNavigateToStep(step.number)
? 'cursor-pointer hover:scale-105'
: 'cursor-not-allowed opacity-50'
}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold transition-all ${completedSteps.includes(step.number)
? "bg-[#10B981] text-white"
: currentStep === step.number
? "text-white"
: "bg-[#E5E5E5] text-[#7D7D7D] group-hover:bg-[#D5D5D5]"
}`}
style={currentStep === step.number ? { background: 'linear-gradient(90deg, #FF3A05, #FF0080)' } : undefined}
>
{step.number}
</div>
<span className={`text-xs transition-colors ${currentStep === step.number
? "text-[#000000] dark:text-white font-semibold"
: "text-[#7D7D7D] dark:text-gray-400 group-hover:text-[#000000] dark:group-hover:text-white"
}`}>
{step.title}
</span>
</button>
{index < steps.length - 1 && (
<div className="w-12 h-0.5 bg-[#E5E5E5] mb-5" />
)}
</div>
))}
</div>
{/* Botão de ação */}
<Button
variant="primary"
type="button"
onClick={handleNext}
rightIcon={currentStep === 4 ? <CheckIcon /> : <ArrowRightIcon />}
>
{currentStep === 4 ? "Finalizar" : "Continuar"}
</Button>
</div>
{/* Mobile: Layout empilhado */}
<div className="flex md:hidden flex-col gap-4">
{/* Etapas simplificadas - apenas bolinhas */}
<div className="flex items-center justify-center gap-2">
{steps.map((step) => (
<button
key={step.number}
type="button"
onClick={() => {
if (canNavigateToStep(step.number)) {
setCurrentStep(step.number);
} else {
alert('Complete a etapa atual antes de avançar.');
}
}}
disabled={!canNavigateToStep(step.number)}
className={`h-2 rounded-full transition-all ${!canNavigateToStep(step.number)
? 'opacity-50 cursor-not-allowed'
: completedSteps.includes(step.number)
? "w-2 bg-[#10B981]"
: currentStep === step.number
? "w-8"
: "w-2 bg-[#E5E5E5] hover:bg-[#D5D5D5]"
}`}
style={currentStep === step.number ? { background: 'linear-gradient(90deg, #FF3A05, #FF0080)' } : undefined}
aria-label={`Ir para ${step.title}`}
/>
))}
</div>
{/* Botões */}
<div className="flex gap-2">
{currentStep > 1 && (
<Button
variant="outline"
onClick={() => setCurrentStep(currentStep - 1)}
leftIcon={<ArrowLeftIcon />}
className="flex-1"
>
Voltar
</Button>
)}
<Button
variant="primary"
type="button"
onClick={handleNext}
rightIcon={currentStep === 4 ? <CheckIcon /> : <ArrowRightIcon />}
className="flex-1"
>
{currentStep === 4 ? "Finalizar" : "Continuar"}
</Button>
</div>
</div>
</div>
</div>
{/* Lado Direito - Branding Dinâmico */}
<div className="hidden lg:flex lg:w-[50%] relative overflow-hidden" style={{ background: 'linear-gradient(90deg, #FF3A05, #FF0080)' }}>
<DynamicBranding
currentStep={currentStep}
companyName={formData.companyName}
subdomain={subdomain}
primaryColor={primaryColor}
secondaryColor={secondaryColor}
logoUrl={logoUrl}
/>
</div>
</div>
</>
);
}