// 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 => ( ))}
)}
ou continue com
Novo na Ezza?
v2.6.1 · Termos · Privacidade · Status
); } 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
7
negócios movidos
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
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.

{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}
  • ))}
{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 */}
ezzaCRM
Online · 500+ times ativos agora
{/* ESQUERDA — DEMO */} {/* divisor diagonal */}
ou
{/* DIREITA — LOGIN */}
{/* ── Barra "Acesse meu CRM" ── */}
Já é cliente?
ezzacrm.com.br/ { setSlug(e.target.value); setSlugErro(''); }} autoCapitalize="none" autoCorrect="off" spellCheck="false" />
{slugErro &&
{slugErro}
}
); } 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.
); } Object.assign(window, { LoginScreen, SplashScreen, EntryGate, DemoBanner });