1423 lines
84 KiB
TypeScript
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]">
|
|
Já 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>
|
|
</>
|
|
);
|
|
}
|
|
|