festa-magica-ia/src/pages/WhatsAppFunnel.tsx
2026-05-16 22:55:15 +00:00

471 lines
16 KiB
TypeScript

import { useState, useEffect, useRef, type FormEvent } from 'react';
import { Loader2, Send } from 'lucide-react';
type MessageType = 'text' | 'audio' | 'image' | 'video' | 'embed';
interface MessageConfig {
text: string;
delay?: number;
type?: MessageType;
}
interface OptionConfig {
text: string;
nextId?: string;
actionLink?: string;
}
interface FunnelNode {
id: string;
messages: MessageConfig[];
options?: OptionConfig[];
requireEmail?: boolean;
}
interface RenderedMessage {
id: string;
sender: 'bot' | 'user';
type: MessageType;
content: string;
}
const funnelData: FunnelNode[] = [
{
id: "start",
messages: [
{ text: "Oi! 👋 Tudo bem? Sou do atendimento da Festa Mágica IA.", delay: 1000, type: "text" },
{ text: "Posso te mostrar rapidinho como criar a festa do seu filho gastando muito menos?", delay: 1500, type: "text" }
],
options: [
{ text: "Pode!", nextId: "step2" },
{ text: "Como assim?", nextId: "step2" }
]
},
{
id: "step2",
messages: [
{ text: "Mandar fazer personalizados é caro e demora, né?", delay: 1800, type: "text" },
{ text: "Nossa Inteligência Artificial permite que você mesmo faça tudo na hora, do celular.", delay: 2000, type: "text" },
{ text: "Assiste esse vídeo aqui embaixo que mostra a ferramenta funcionando e como é fácil criar as imagens e os kits👇", delay: 1500, type: "text" },
{ text: "https://s3.seureview.com.br/festamagica/0510(2).mp4", delay: 2000, type: "video" }
],
options: [
{ text: "Que legal!", nextId: "step3" }
]
},
{
id: "step3",
messages: [
{ text: "Legal né? A IA cria um personagem 3D super realista tipo Disney/Pixar com o rostinho dele(a)!", delay: 1800, type: "text" },
{ text: "Ela já gera todos os arquivos em PDF prontinhos. Olha esses exemplos:", delay: 1500, type: "text" },
{ text: "https://s3.seureview.com.br/festamagica/6a591904-cf2a-4a51-8581-1891373eea29/805441e0-925e-4674-b7b3-ba640431eeb1/adesivos-redondos.webp", delay: 1500, type: "image" },
{ text: "https://s3.seureview.com.br/festamagica/6a591904-cf2a-4a51-8581-1891373eea29/805441e0-925e-4674-b7b3-ba640431eeb1/convite-digital.webp", delay: 1500, type: "image" },
{ text: "Dá para fazer topo de bolo, painel, convite, adesivo... tudo no tema.", delay: 1800, type: "text" }
],
options: [
{ text: "Amei! Quanto custa?", nextId: "step4" }
]
},
{
id: "step4",
messages: [
{ text: "Bem menos que você imagina! 🥰", delay: 1200, type: "text" },
{ text: "Liberamos um Pacote Inicial promocional por apenas R$ 9,99 (pagamento único).", delay: 1500, type: "text" },
{ text: "Você já entra com 10 créditos para usar como quiser na nossa plataforma.", delay: 1500, type: "text" },
{ text: "Quer garantir agora antes que o lote acabe?", delay: 1200, type: "text" }
],
options: [
{ text: "Eu quero!", nextId: "checkout_step" }
]
},
{
id: "checkout_step",
messages: [
{ text: "Ótimo! Pra gente gerar o seu acesso seguro, qual é o seu melhor e-mail?", delay: 1000, type: "text" }
],
requireEmail: true
}
];
let sharedAudioCtx: AudioContext | null = null;
function getAudioContext() {
if (!sharedAudioCtx) {
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
if (AudioContextClass) {
sharedAudioCtx = new AudioContextClass();
}
}
return sharedAudioCtx;
}
function getCookie(name: string) {
const match = document.cookie.match(
new RegExp('(^| )' + name + '=([^;]+)')
);
return match ? match[2] : '';
}
function playMessageSound() {
try {
const ctx = getAudioContext();
if (!ctx) return;
if (ctx.state === 'suspended') ctx.resume();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(800, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.1);
} catch (e) {}
}
function playTypingSound() {
try {
const ctx = getAudioContext();
if (!ctx) return;
if (ctx.state === 'suspended') ctx.resume();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(200, ctx.currentTime);
gain.gain.setValueAtTime(0.01, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.05);
} catch (e) {}
}
export default function WhatsAppFunnel() {
const [messages, setMessages] = useState<RenderedMessage[]>([]);
const [isTyping, setIsTyping] = useState(false);
const [currentOptions, setCurrentOptions] = useState<OptionConfig[]>([]);
const [currentRequireEmail, setCurrentRequireEmail] = useState(false);
const [emailInput, setEmailInput] = useState('');
const [isLoadingCheckout, setIsLoadingCheckout] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
setTimeout(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 100);
};
const playNode = async (nodeId: string) => {
setCurrentOptions([]);
setCurrentRequireEmail(false);
const node = funnelData.find((n) => n.id === nodeId);
if (!node) return;
for (let i = 0; i < node.messages.length; i++) {
const msg = node.messages[i];
const delayBefore = msg.delay
? msg.delay
: ((msg.type && msg.type !== 'text') ? 1500 : Math.min(400 + (msg.text.length * 30), 3000));
setIsTyping(true);
scrollToBottom();
const typingInterval = setInterval(() => {
if (Math.random() > 0.3) playTypingSound();
}, 150);
await new Promise((r) => setTimeout(r, delayBefore));
clearInterval(typingInterval);
setIsTyping(false);
playMessageSound();
setMessages((prev) => [
...prev,
{
id: Math.random().toString(),
sender: 'bot',
type: msg.type || 'text',
content: msg.text,
},
]);
scrollToBottom();
await new Promise((r) => setTimeout(r, 400));
}
if (node.options && node.options.length > 0) {
setCurrentOptions(node.options);
scrollToBottom();
}
if (node.requireEmail) {
setCurrentRequireEmail(true);
scrollToBottom();
}
};
const initialized = useRef(false);
useEffect(() => {
if (!initialized.current) {
initialized.current = true;
getAudioContext();
playNode('start');
}
}, []);
const handleOptionClick = (opt: OptionConfig) => {
const ctx = getAudioContext();
if (ctx && ctx.state === 'suspended') {
ctx.resume();
}
setMessages((prev) => [
...prev,
{
id: Math.random().toString(),
sender: 'user',
type: 'text',
content: opt.text,
},
]);
setCurrentOptions([]);
scrollToBottom();
if (opt.actionLink) {
setTimeout(() => {
window.location.href = opt.actionLink!;
}, 800);
} else if (opt.nextId) {
setTimeout(() => {
playNode(opt.nextId!);
}, 600);
}
};
const handleEmailSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!emailInput) return;
// Add user email message and a bot "loading" message
setMessages((prev) => [
...prev,
{
id: Math.random().toString(),
sender: 'user',
type: 'text',
content: emailInput,
},
{
id: 'loading-checkout',
sender: 'bot',
type: 'text',
content: 'Processando seu e-mail e gerando o link de pagamento ⏳...',
}
]);
setCurrentRequireEmail(false);
setIsLoadingCheckout(true);
scrollToBottom();
// Fire Meta Pixel event
if (typeof window !== 'undefined' && 'fbq' in window) {
(window as any).fbq('track', 'InitiateCheckout');
}
const fbp = getCookie('_fbp');
const fbc = getCookie('_fbc');
console.log('FBP:', fbp);
console.log('FBC:', fbc);
try {
const res = await fetch('https://n8n.seureview.com.br/webhook/festa-magica-stripe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'landing', // Usando 'landing' para aproveitar a mesma configuração do n8n
userEmail: emailInput,
fbp,
fbc
}),
});
const data = await res.json();
if (data.success && data.url) {
// Redireciona para o checkout do stripe
window.location.href = data.url;
} else {
setIsLoadingCheckout(false);
setCurrentRequireEmail(true);
// Remove loading message
setMessages((prev) => prev.filter(m => m.id !== 'loading-checkout'));
alert('Ocorreu um erro ao gerar o checkout. Tente novamente.');
}
} catch (err) {
setIsLoadingCheckout(false);
setCurrentRequireEmail(true);
// Remove loading message
setMessages((prev) => prev.filter(m => m.id !== 'loading-checkout'));
alert('Erro de conexão. Verifique sua internet e tente novamente.');
}
};
const renderMessageContent = (m: RenderedMessage) => {
if (m.type === 'image') {
return (
<img
src={m.content}
className="max-w-full sm:max-w-[240px] rounded-lg object-cover mt-1"
alt="Media"
/>
);
} else if (m.type === 'video') {
return (
<video
controls
playsInline
className="max-w-full sm:max-w-[240px] rounded-lg mt-1"
src={m.content}
/>
);
} else if (m.type === 'audio') {
return (
<audio
controls
className="w-[200px] h-[40px] rounded-full mt-1"
src={m.content}
/>
);
} else if (m.type === 'embed') {
return (
<div
dangerouslySetInnerHTML={{ __html: m.content }}
className="w-[260px] max-w-full sm:max-w-[300px] rounded-lg overflow-hidden mt-1 [&_iframe]:w-full [&_iframe]:aspect-video [&_iframe]:rounded-lg"
/>
);
}
// Simple text handling including line breaks if necessary
return <span className="whitespace-pre-wrap word-break">{m.content}</span>;
};
return (
<div className="h-[100dvh] bg-black w-full flex justify-center font-sans overflow-hidden">
<div className="w-full max-w-[450px] bg-[#e5ddd5] flex flex-col relative shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden h-full">
{/* Header */}
<div className="bg-[#075e54] text-white p-2.5 px-4 flex items-center gap-3 sticky top-0 z-10 shadow-md">
<div className="relative">
<img
src="https://s3.seureview.com.br/festamagica/6a591904-cf2a-4a51-8581-1891373eea29/805441e0-925e-4674-b7b3-ba640431eeb1/convite-digital.webp"
className="w-10 h-10 rounded-full object-cover bg-[#075e54] border border-white/20"
alt="Avatar"
/>
{/* Online indicator */}
<div className="absolute bottom-0 right-0 w-3 h-3 bg-[#25d366] border-2 border-[#075e54] rounded-full"></div>
</div>
<div className="flex flex-col flex-1 leading-tight">
<span className="font-semibold text-[16px] truncate">Atendimento Festa Mágica</span>
<span className="text-[12px] font-normal opacity-90 h-4">
{isTyping ? 'digitando...' : 'online'}
</span>
</div>
</div>
{/* Chat Area */}
<div
className="flex-1 p-4 flex flex-col gap-2.5 overflow-y-auto pb-8 scroll-smooth"
style={{
backgroundImage: "url('https://web.whatsapp.com/img/bg-chat-tile-light_04fcacde539c58cca6745483d4858c52.png')",
opacity: 1,
backgroundSize: 'contain',
backgroundRepeat: 'repeat',
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}}
>
<style>{`
.flex-1::-webkit-scrollbar {
display: none;
}
`}</style>
{messages.map((m) => (
<div
key={m.id}
className={`max-w-[85%] sm:max-w-[75%] p-2 px-3 rounded-lg text-[15px] leading-snug shadow-sm transform transition-all animate-in fade-in slide-in-from-bottom-2
${
m.sender === 'bot'
? 'bg-white text-[#111111] self-start rounded-tl-sm'
: 'bg-[#dcf8c6] text-[#111111] self-end rounded-tr-sm'
}`}
>
{renderMessageContent(m)}
{/* Fake timestamp */}
<div className="flex justify-end mt-1 opacity-60">
<span className="text-[10px]">{new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
{m.sender === 'user' && (
<svg viewBox="0 0 16 15" width="16" height="15" className="ml-1 text-[#4fc3f7]">
<path fill="currentColor" d="M15.01 3.316l-.478-.372a.365.365 0 0 0-.51.063L8.666 9.879a.32.32 0 0 1-.484.033l-.358-.325a.319.319 0 0 0-.484.032l-.378.483a.418.418 0 0 0 .036.541l1.32 1.266c.143.14.361.125.484-.033l6.272-8.048a.366.366 0 0 0-.064-.512zm-4.1 0l-.478-.372a.365.365 0 0 0-.51.063L4.566 9.879a.32.32 0 0 1-.484.033L1.891 7.769a.366.366 0 0 0-.515.006l-.423.433a.364.364 0 0 0 .006.514l3.258 3.185c.143.14.361.125.484-.033l6.272-8.048a.365.365 0 0 0-.063-.51z"></path>
</svg>
)}
</div>
</div>
))}
{isTyping && (
<div className="bg-white text-gray-500 text-[14px] self-start rounded-lg rounded-tl-sm p-2.5 px-4 shadow-sm italic animate-pulse">
digitando...
</div>
)}
{currentOptions.length > 0 && (
<div className="flex flex-col gap-2 mt-3 self-end w-full max-w-[300px]">
{currentOptions.map((opt, i) => (
<button
key={i}
onClick={() => handleOptionClick(opt)}
className="w-full bg-[#128c7e] text-white font-bold py-3.5 px-4 rounded-full shadow hover:bg-[#075e54] transition-colors text-[15px]"
>
{opt.text}
</button>
))}
</div>
)}
<div ref={chatEndRef} className="h-4" />
</div>
{/* Email Input Area */}
{currentRequireEmail && (
<div className="bg-[#f0f0f0] p-2 sm:p-3 pb-8 sm:pb-3 w-full border-t border-gray-300">
<form onSubmit={handleEmailSubmit} className="flex items-center gap-2">
<input
type="email"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
placeholder="Digite seu melhor e-mail..."
className="flex-1 bg-white rounded-full px-4 py-3 outline-none text-[15px] shadow-sm border border-gray-200 focus:border-[#128c7e]"
required
disabled={isLoadingCheckout}
/>
<button
type="submit"
disabled={isLoadingCheckout}
className="bg-[#00a884] hover:bg-[#128c7e] text-white w-12 h-12 rounded-full flex items-center justify-center shrink-0 shadow-sm transition-colors disabled:opacity-70"
>
{isLoadingCheckout ? <Loader2 className="w-6 h-6 animate-spin" /> : <Send className="w-5 h-5 ml-1" />}
</button>
</form>
</div>
)}
</div>
</div>
);
}