472 lines
16 KiB
TypeScript
472 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>
|
||
|
|
);
|
||
|
|
}
|