// V23 — Etapas. Terminal mostra processo do projeto como wizard de etapas.
// Mail / envelope icon.
const MailIcon = ({ size = 18, color = 'currentColor' }) => (
);
// WhatsApp icon (official-ish glyph).
const WhatsAppIcon = ({ size = 16, color = 'currentColor' }) => (
);
// Icon-only email + WhatsApp buttons, side by side.
const CTAButtons = ({ showWhatsApp }) => (
);
// Internal CTA block — types the headline on view, then shows bouncing arrow + button.
const CTABlock = ({ showWhatsApp }) => {
const ref = React.useRef(null);
const [started, setStarted] = React.useState(false);
const [typed, setTyped] = React.useState('');
const TEXT = 'Pronto pra dar o\nprimeiro passo?';
React.useEffect(() => {
if (!ref.current) return;
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { setStarted(true); obs.disconnect(); }
}, { threshold: 0.4 });
obs.observe(ref.current);
return () => obs.disconnect();
}, []);
React.useEffect(() => {
if (!started || typed.length >= TEXT.length) return;
const t = setTimeout(() => setTyped(TEXT.slice(0, typed.length + 1)), 60);
return () => clearTimeout(t);
}, [started, typed]);
const done = typed.length >= TEXT.length;
const parts = typed.split('\n');
// Show the icons only after "vamos conversar" has had time to appear.
const [showButtons, setShowButtons] = React.useState(false);
React.useEffect(() => {
if (!done) return;
const t = setTimeout(() => setShowButtons(true), 500);
return () => clearTimeout(t);
}, [done]);
return (
{parts.map((p, i) => (
{i > 0 &&
}
{p}
))}
{/* Bouncing scroll-style indicator */}
vamos conversar
↓
);
};
window.CTABlock = CTABlock;
const V23Etapas = ({
eyebrow = '◇ Software house brasileira',
headline,
sub = 'Sites, apps, sistemas, automações e IA pra empresas que precisam de algo no ar, não no PowerPoint.',
logo,
footerLogo,
navLinks,
navCta,
showCases = true,
showServices = true,
showSecondaryCTA = true,
showWhatsApp = false,
showGrid = true,
typingCTA = false,
autoSnapToCTA = false,
fullScreenCTA = false,
showHeroScrollHint = false,
width = 1440,
minHeight = 2400,
} = {}) => {
const heroRef = React.useRef(null);
const ctaRef = React.useRef(null);
const [showHint, setShowHint] = React.useState(false);
// Show "role para continuar" hint 1s after hero enters view.
React.useEffect(() => {
if (!showHeroScrollHint) return;
const hero = heroRef.current;
if (!hero) return;
let timer = null;
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !timer) {
timer = setTimeout(() => setShowHint(true), 1000);
}
}, { threshold: 0.5 });
obs.observe(hero);
return () => { obs.disconnect(); if (timer) clearTimeout(timer); };
}, [showHeroScrollHint]);
const scrollToCTA = (e) => {
if (e) e.preventDefault();
ctaRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
// Auto-snap to CTA: when the hero has been in view for 1s,
// any further scroll of 20px+ smooth-scrolls to the contact section.
React.useEffect(() => {
if (!autoSnapToCTA) return;
const hero = heroRef.current;
if (!hero) return;
let timer = null;
let armed = false;
let triggered = false;
let baseline = 0;
const onScroll = () => {
if (!armed || triggered) return;
if (window.scrollY - baseline >= 20) {
triggered = true;
ctaRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !timer && !triggered) {
timer = setTimeout(() => {
armed = true;
baseline = window.scrollY;
window.addEventListener('scroll', onScroll, { passive: true });
}, 1000);
}
}, { threshold: 0.5 });
obs.observe(hero);
return () => {
obs.disconnect();
if (timer) clearTimeout(timer);
window.removeEventListener('scroll', onScroll);
};
}, [autoSnapToCTA]);
const defaultHeadline = (
<>
Sem promessa
de revolução.
Só software que funciona.
>
);
const steps = [
{ n: 1, t: 'Conversa inicial', d: 'Conta seu desafio sem precisar de termo técnico', status: 'done' },
{ n: 2, t: 'Proposta', d: 'Escopo, prazo e investimento em até 24h', status: 'done' },
{ n: 3, t: 'Design', d: 'Você aprova antes da gente começar a construir', status: 'active' },
{ n: 4, t: 'Construção', d: 'Acompanhamento semanal, sem caixa preta', status: 'todo' },
{ n: 5, t: 'Entrega', d: 'No ar, com suporte e ajustes inclusos', status: 'todo' },
];
return (
{/* Hero — split */}
{eyebrow}
{headline || defaultHeadline}
{sub}
{/* Terminal — etapas */}
{/* progress bar */}
Progresso
2 de 5 etapas
{steps.map((s, i) => (
))}
{/* steps */}
{steps.map((s, i) => (
{s.status === 'done' ? '✓' : s.n}
{s.t}
{s.status === 'active' && (
EM ANDAMENTO
)}
{s.d}
))}
{/* Scroll hint — appears 1s after hero enters view */}
{showHeroScrollHint && (
role para continuar
↓
)}
{/* Services */}
{showServices && (
{SERVICES_SHORT.map((s, i) => (
{s}
))}
)}
{/* Cases */}
{showCases && (
Projetos no ar.
{CASES.map((c, i) => (
))}
)}
{/* CTA — full-screen wraps contact + footer so they fit in one viewport */}
{fullScreenCTA ? (
{typingCTA ? (
) : (
<>
Pronto pra dar o
primeiro passo?
>
)}
) : (
<>
{typingCTA ? (
) : (
<>
Pronto pra dar o
primeiro passo?
>
)}
>
)}
);
};
window.V23Etapas = V23Etapas;