Adiciona rota /funil com conteudo adaptado para Kit Festa R$19,99
- Rota /funil adicionada ao App.tsx - Preco e creditos atualizados: R$19,99 / 30 creditos - Checkout aponta para plan=popular Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6877cf9911
commit
4942bfd585
2 changed files with 465 additions and 0 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import LandingPage from './pages/LandingPage';
|
||||
import SuccessPage from './pages/SuccessPage';
|
||||
import WhatsAppFunnel from './pages/WhatsAppFunnel';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
|
@ -8,6 +9,7 @@ export default function App() {
|
|||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/sucesso" element={<SuccessPage />} />
|
||||
<Route path="/funil" element={<WhatsAppFunnel />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
|
|
|||
463
src/pages/WhatsAppFunnel.tsx
Normal file
463
src/pages/WhatsAppFunnel.tsx
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
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" },
|
||||
{ text: "Já conheço, quero comprar agora", nextId: "step4" }
|
||||
]
|
||||
},
|
||||
{
|
||||
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: "Olha essa transformação real que a gente fez aqui — essa é a foto original do Miguel, filho da nossa fundadora:", delay: 1500, type: "text" },
|
||||
{ text: "/images/antes-depois/miguel-real.webp", delay: 1500, type: "image" },
|
||||
{ text: "E esse foi o personagem 3D que a nossa IA criou com o rostinho dele, pronto pra virar topo de bolo, convite, adesivo... tudo no tema! 🤩", delay: 1800, type: "text" },
|
||||
{ text: "/images/antes-depois/miguel-avatar.webp", delay: 1800, type: "image" },
|
||||
{ text: "Mágico, né? E ela já gera todos os arquivos em PDF prontinhos pra imprimir.", 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 o Kit Festa completo por apenas R$ 19,99 (pagamento único).", delay: 1500, type: "text" },
|
||||
{ text: "Você já entra com 30 créditos pra gerar os itens que quiser — e a primeira imagem é por nossa conta, não consome nenhum crédito seu! 🎁", delay: 1800, type: "text" },
|
||||
{ text: "Quer garantir o seu agora?", 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;
|
||||
}
|
||||
|
||||
const FUNNEL_STORAGE_KEY = 'fm_whatsapp_funnel_state';
|
||||
|
||||
function loadSavedFunnelState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(FUNNEL_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed.messages) && parsed.messages.length > 0) return parsed;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const saved = loadSavedFunnelState();
|
||||
if (saved) {
|
||||
setMessages(saved.messages);
|
||||
setCurrentOptions(saved.currentOptions || []);
|
||||
setCurrentRequireEmail(saved.currentRequireEmail || false);
|
||||
scrollToBottom();
|
||||
} else {
|
||||
playNode('start');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Salva o progresso da conversa para retomar de onde parou caso a página recarregue
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return;
|
||||
try {
|
||||
localStorage.setItem(FUNNEL_STORAGE_KEY, JSON.stringify({
|
||||
messages,
|
||||
currentOptions,
|
||||
currentRequireEmail,
|
||||
}));
|
||||
} catch (e) {}
|
||||
}, [messages, currentOptions, currentRequireEmail]);
|
||||
|
||||
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: 'Perfeito! Abrindo o checkout seguro pra você 🎉',
|
||||
}
|
||||
]);
|
||||
|
||||
setCurrentRequireEmail(false);
|
||||
setIsLoadingCheckout(true);
|
||||
scrollToBottom();
|
||||
|
||||
if (typeof window !== 'undefined' && 'fbq' in window) {
|
||||
(window as any).fbq('track', 'InitiateCheckout');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = `https://festamagicaia.com.br/checkout?plan=popular&source=funil-30&email=${encodeURIComponent(emailInput)}`;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue