// Tela de Login — split editorial com mockup vivo + form clean
// Estado: 'login' → 'splash' → 'app' (em app.jsx)
const { useState: useLoginState, useEffect: useLoginEffect } = React;
// Detecta o slug do cliente pelo subdomínio
// Ex: meldica.ezzacrm.com.br → "meldica" | localhost → "demo"
function getClienteSlug() {
// 1. Parâmetro ?slug=xxx na URL (funciona em qualquer domínio)
try {
const urlSlug = new URLSearchParams(window.location.search).get('slug');
if (urlSlug) return urlSlug.toLowerCase().trim();
} catch (e) {}
const host = window.location.hostname;
// 2. localhost: verifica slug-override do campo de acesso
if (host === 'localhost' || host === '127.0.0.1') {
try {
const override = sessionStorage.getItem('ezza-slug-override');
if (override) { sessionStorage.removeItem('ezza-slug-override'); return override; }
} catch (e) {}
return '';
}
// 3. Subdomínio próprio: meldica.ezzacrm.com.br → "meldica"
// (exceto subdomínios reservados que NÃO são clientes)
if (host.endsWith('.ezzacrm.com.br')) {
const sub = host.split('.')[0];
const reservados = ['hub', 'www', 'app', 'crm', 'painel'];
if (!reservados.includes(sub)) return sub;
}
// 4. Genérico (hub/reservado/outro domínio) → vazio: login identifica a empresa pelo e-mail
return '';
}
function LoginScreen({ onLogin }) {
const [email, setEmail] = useLoginState('');
const [password, setPassword] = useLoginState('');
const [remember, setRemember] = useLoginState(true);
const [showPwd, setShowPwd] = useLoginState(false);
const [loading, setLoading] = useLoginState(false);
const [erro, setErro] = useLoginState('');
const [cliente, setCliente] = useLoginState(null);
const clienteSlug = getClienteSlug();
const [empresas, setEmpresas] = useLoginState(null); // lista quando o e-mail existe em +1 empresa
// Carrega branding do cliente só quando há slug (subdomínio/atalho). No genérico, branding padrão Ezza.
useLoginEffect(() => {
if (!clienteSlug) return;
fetch(`/api/clientes/${clienteSlug}`)
.then(r => r.json())
.then(data => { if (data.ok) setCliente(data.cliente); })
.catch(() => {});
}, []);
// tenta logar; slugForce sobrepõe quando o usuário escolhe a empresa
const doLogin = async (slugForce) => {
setErro(''); setLoading(true);
try {
const body = { email, senha: password };
const slug = slugForce || clienteSlug;
if (slug) body.clienteSlug = slug;
const res = await fetch('/auth/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (data.needsChoice) { setEmpresas(data.empresas); setLoading(false); return; }
if (!res.ok || !data.ok) { setErro(data.error || 'E-mail ou senha incorretos.'); setLoading(false); return; }
localStorage.setItem('ezza_token', data.token);
onLogin({ name: data.usuario.nome, email: data.usuario.email, token: data.token });
} catch {
setErro('Erro de conexão. Tente novamente.');
setLoading(false);
}
};
const submit = async (e) => {
e?.preventDefault();
if (!email || !password) { setErro('Preencha e-mail e senha.'); return; }
doLogin();
};
const loginGoogle = () => {
setErro('');
if (!window.google?.accounts?.id) {
setErro('Login Google não disponível. Recarregue a página.');
return;
}
setLoading(true);
window.google.accounts.id.initialize({
client_id: window.GOOGLE_CLIENT_ID || '',
callback: async ({ credential }) => {
try {
const gBody = { idToken: credential };
if (clienteSlug) gBody.clienteSlug = clienteSlug;
const res = await fetch('/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gBody),
});
const data = await res.json();
if (!res.ok) { setErro(data.error || 'Erro no login Google.'); setLoading(false); return; }
localStorage.setItem('ezza_token', data.token);
onLogin({ name: data.usuario.nome, email: data.usuario.email, token: data.token });
} catch {
setErro('Erro ao autenticar com Google.');
setLoading(false);
}
},
});
window.google.accounts.id.prompt();
};
const nomeCliente = cliente?.nome || 'Ezza CRM';
const rememberedAvatar = null;
const rememberedName = '';
return (
{/* ─── lateral esquerda — editorial + mockup vivo ────────────── */}
{/* ─── lateral direita — form ─────────────────────────────────── */}
{cliente ? `Bem-vindo a ${nomeCliente}` : 'Bem-vindo de volta'}
{erro && (
{erro}
)}
{empresas && (
Sua conta existe em mais de uma empresa. Escolha:
{empresas.map(em => (
{ setEmpresas(null); doLogin(em.slug); }}>
{em.nome} {ICN.arrowRight}
))}
)}
Novo na Ezza?
onLogin({ name: 'Novo usuário', email, hint: 'signup' })}>Criar conta gratuita
);
}
function GoogleGlyph() {
return (
);
}
// ────────────────────────────────────────────────────────────────────
// Splash screen — após login, transição pro app
// ────────────────────────────────────────────────────────────────────
function SplashScreen({ user, demo, onDone }) {
const [stage, setStage] = useLoginState(0);
const stages = demo ? [
'Preparando dados de demonstração…',
'Carregando workspace fictício…',
'Tudo pronto. Boa exploração!',
] : [
'Conectando à conta…',
'Carregando seu workspace…',
`Bem-vinda de volta, ${user?.name?.split(' ')[0] || 'Letícia'}.`,
];
useLoginEffect(() => {
const t1 = setTimeout(() => setStage(1), 700);
const t2 = setTimeout(() => setStage(2), 1500);
const t3 = setTimeout(() => onDone(), 2400);
return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
}, []);
return (
{demo ? 'Modo demonstração' : (user?.name || 'Letícia Andrade')}
{stages.map((s, i) => (
= i ? 'splash-stage-on' : ''} ${stage === i ? 'splash-stage-cur' : ''}`}>
{stage > i ? ICN.check : stage === i ? : }
{s}
))}
);
}
// Styles ───────────────────────────────────────────────────────────────
function LoginStyles() {
return (
);
}
function SplashStyles() {
return (
);
}
// Add eye / eyeOff icons (extend ICN if not present)
if (!window.ICN.eye) {
window.ICN.eye = (
);
window.ICN.eyeOff = (
);
}
Object.assign(window, { LoginScreen, SplashScreen, EntryGate, DemoBanner, ThanksScreen, PricingScreen });
// ═════════════════════════════════════════════════════════════════
// ThanksScreen — tela de despedida para usuário pago
// ═════════════════════════════════════════════════════════════════
function ThanksScreen({ user, onDone }) {
const [progress, setProgress] = useLoginState(0);
useLoginEffect(() => {
const t = setTimeout(() => onDone(), 3200);
const t2 = setTimeout(() => setProgress(1), 100);
return () => { clearTimeout(t); clearTimeout(t2); };
}, []);
const firstName = user?.name?.split(' ')[0] || 'Letícia';
return (
👋
Até logo,
{firstName} .
Foi um prazer ter você aqui hoje.
Você fechou R$ 982k em maio — mande bem aí fora.
23
tarefas concluídas hoje
3h12min
de foco sem interrupção
Encerrando sua sessão com segurança…
);
}
// ═════════════════════════════════════════════════════════════════
// PricingScreen — tela de planos para quem está saindo da demo
// ═════════════════════════════════════════════════════════════════
function PricingScreen({ onBack, onPick }) {
const [billing, setBilling] = useLoginState('anual');
const [picked, setPicked] = useLoginState(null);
const plans = [
{ id: 'starter', name: 'Starter', eyebrow: 'Para times pequenos começando', price: { mensal: 297, anual: 247 }, color: 'lav',
features: ['Até 3 usuários','1.000 leads','Pipeline completo','WhatsApp integrado','IA de qualificação','Suporte por e-mail'], cta: 'Começar agora', hint: '14 dias de teste grátis' },
{ id: 'pro', name: 'Pro', eyebrow: 'Mais escolhido por times de vendas', price: { mensal: 597, anual: 497 }, color: 'pink', highlight: true,
features: ['Até 10 usuários','Leads ilimitados','Jornadas customizadas','Automações ilimitadas','WhatsApp + IA SDR','Relatórios avançados','Suporte prioritário'], cta: 'Assinar Pro', hint: 'Sem fidelidade' },
{ id: 'business', name: 'Enterprise', eyebrow: 'Para operações comerciais grandes', price: { mensal: 'sob consulta', anual: 'sob consulta' }, color: 'blue',
features: ['Usuários ilimitados','White-label','SSO + auditoria LGPD','API + webhooks','CSM exclusivo','SLA 99,9%','Onboarding dedicado'], cta: 'Falar com vendas', hint: 'Custom' },
];
return (
ezzaCRM
{ICN.arrowLeft} Voltar à entrada
PRONTA PARA O REAL?
Pegue um plano .
Comece a fechar.
Você acabou de explorar a Ezza com dados fictícios — agora escolha o plano que faz sentido pra sua equipe e comece a usar com dados reais hoje.
setBilling('mensal')}>Cobrança mensal
setBilling('anual')}>Anual economize 23%
{plans.map(p => (
{p.highlight &&
⭐ Mais popular
}
{p.eyebrow}
{p.name}
{typeof p.price[billing] === 'number' ? (
<>
R$
{p.price[billing]}
/workspace /mês
>
) : (
{p.price[billing]}
)}
{billing === 'anual' && typeof p.price.anual === 'number' && (
de R$ {p.price.mensal}/mês
)}
{!(billing === 'anual' && typeof p.price.anual === 'number') && (
—
)}
{p.features.map((f, i) => (
{ICN.check} {f}
))}
{ setPicked(p.id); setTimeout(() => onPick(p.id), 600); }}>
{picked === p.id ? : <>{p.cta} {ICN.arrowRight}>}
{p.hint}
))}
Posso trocar de plano depois? A qualquer momento, sem perder dados.
Os dados do demo migram? Não — você começa em um workspace limpo.
Aceitam Pix e boleto? Sim, além de cartão e fatura para Business.
);
}
// ════════════════════════════════════════════════════════════════════
function EntryGate({ onDemo, onLogin }) {
const [hover, setHover] = useLoginState(null); // 'demo' | 'login' | null
const [slug, setSlug] = useLoginState('');
const [slugErro, setSlugErro] = useLoginState('');
const acessarCRM = (e) => {
e?.preventDefault();
const s = slug.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
if (!s) { setSlugErro('Digite o nome da sua empresa.'); return; }
// Redireciona para o subdomínio do cliente
const host = window.location.hostname;
if (host === 'localhost' || host === '127.0.0.1') {
// Em localhost: simula indo pro login com aquele slug
sessionStorage.setItem('ezza-slug-override', s);
onLogin();
} else {
window.location.href = `https://${s}.ezzacrm.com.br`;
}
};
return (
setHover(null)}>
{/* topo: logo + tag */}
{/* ESQUERDA — DEMO */}
setHover('demo')}
onClick={onDemo}
>
{/* avatares flutuantes */}
{[
{ i: 10, x: '15%', y: '20%', d: 0 },
{ i: 22, x: '70%', y: '15%', d: 1.2 },
{ i: 33, x: '85%', y: '60%', d: 2.4 },
{ i: 44, x: '20%', y: '70%', d: 3.6 },
{ i: 55, x: '55%', y: '80%', d: 1.8 },
{ i: 12, x: '5%', y: '45%', d: 0.6 },
].map((a, k) => (
))}
NÃO TENHO CONTA
Quero conhecer a Ezza por dentro .
Entre na demo com dados fictícios.
Nada pra cadastrar, nada pra instalar.
Iniciar demo gratuita
{ICN.arrowRight}
~ 30 segundos
Sem cartão
12 telas · dados reais
{/* divisor diagonal */}
ou
{/* DIREITA — LOGIN */}
setHover('login')}
onClick={onLogin}
>
{/* mini cards flutuantes */}
JÁ SOU CLIENTE
Entrar na minha conta .
Acesse seu workspace com e-mail e senha,
ou pelo SSO da sua empresa.
Acessar conta
{ICN.arrowRight}
E-mail + senha
SSO Google
2FA
{/* ── Barra "Acesse meu CRM" ── */}
);
}
function GateStyles() {
return (
);
}
// ═════════════════════════════════════════════════════════════════
// DemoBanner — barra inferior fixa quando o app está em modo demo
// ═════════════════════════════════════════════════════════════════
function DemoBanner({ onExit, onSignup }) {
return (
DEMO
Você está explorando a Ezza com dados fictícios.
Nada que você fizer aqui é salvo de verdade.
Criar conta gratuita {ICN.arrowRight}
{ICN.x || '×'}
);
}
Object.assign(window, { LoginScreen, SplashScreen, EntryGate, DemoBanner });