chore: CRO na oferta/galeria + remove rotas mortas do checkout antigo

Ancora de preco e galeria reduzida na oferta. Remove /checkout e
/sucesso (fluxo Woovi antigo, sem nenhum botao apontando pra eles --
compra real ja redireciona pro checkout Efi do site principal).
Corrige texto EN/ES que ainda citava Stripe (fluxo intl tambem e Efi).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcio Bevervanso 2026-07-09 15:19:05 -03:00
parent 29615f7103
commit da4f3195b7
7 changed files with 25 additions and 862 deletions

View file

@ -1,17 +1,13 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LandingPage from './pages/LandingPage';
import SuccessPage from './pages/SuccessPage';
import WhatsAppFunnel from './pages/WhatsAppFunnel';
import Checkout from './pages/Checkout';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/sucesso" element={<SuccessPage />} />
<Route path="/funil" element={<WhatsAppFunnel />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</BrowserRouter>
);

View file

@ -4,27 +4,15 @@ import { useT } from '../i18n';
export default function Gallery() {
const t = useT();
const BASE = "https://s3.seureview.com.br/festamagica/6a591904-cf2a-4a51-8581-1891373eea29";
const images = [
{ src: "/images/galeria-real/topo-de-bolo.webp", title: "Topo de Bolo 3D", desc: "Destaque na mesa do bolo" },
{ src: "/images/galeria-real/painel-de-fundo.webp", title: "Painel de Fundo", desc: "Transforma o cenário da festa" },
{ src: `${BASE}/008417cf-b15a-4c4e-ac95-c75b69ec3525/convite-premium.webp`, title: "Convite Premium", desc: "Para impressão ou envio digital" },
{ src: "/images/galeria-real/caixa-de-pipoca.webp", title: "Caixa de Pipoca", desc: "A alegria garantida no lanche" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/bandeirolas-com-nome.webp`, title: "Bandeirolas com Nome", desc: "Varal decorativo personalizado" },
{ src: "/images/galeria-real/convite-digital.webp", title: "Convite Digital", desc: "Para enviar no WhatsApp" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/caixa-milk.webp`, title: "Caixa Milk", desc: "Lembrancinha charmosa" },
{ src: "/images/galeria-real/saia-de-cupcake.webp", title: "Saia de Cupcake", desc: "Mais estilo para os docinhos" },
{ src: `${BASE}/15547b19-daf7-46a0-922b-e6515a320534/centro-de-mesa.webp`, title: "Centro de Mesa", desc: "Arranjo decorativo exclusivo" },
{ src: "/images/galeria-real/tag-de-agradecimento.webp", title: "Tag de Agradecimento", desc: "Pronto para as lembrancinhas" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/topper-de-cupcake-personagem.webp`, title: "Topper de Cupcake", desc: "O personagem em cada docinho" },
{ src: "/images/galeria-real/faixa-de-parede.webp", title: "Faixa de Parede", desc: "Decoração com a cara do tema" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/sacolinha-surpresa.webp`, title: "Sacolinha Surpresa", desc: "Lembrancinha que toda criança ama" },
{ src: "/images/galeria-real/chapeu-de-festa.webp", title: "Chapéu de Festa", desc: "Divertido, personalizado e único" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/porta-bis-duplo.webp`, title: "Porta Bis Duplo", desc: "Lembrancinha deliciosa e bonita" },
{ src: "/images/galeria-real/adesivos-redondos.webp", title: "Adesivos Redondos", desc: "Ideais para potinhos e lembrancinhas" },
{ src: `${BASE}/16ad6afb-4c6f-4b90-8769-cebff489bcb8/caixa-cone.webp`, title: "Caixa Cone", desc: "Diferente e super divertida" },
{ src: `${BASE}/15547b19-daf7-46a0-922b-e6515a320534/display-de-mesa.webp`, title: "Display de Mesa", desc: "Identificação elegante para a mesa" },
];
return (
@ -66,6 +54,10 @@ export default function Gallery() {
))}
</div>
<p className="text-center text-indigo-900/60 font-semibold mt-10 max-w-xl mx-auto">
{t.gallery.moreModels}
</p>
<InlineCTA
text={t.gallery.ctaText}
subtext={t.gallery.ctaSub}

View file

@ -104,7 +104,9 @@ export default function Offer() {
</div>
<div className="w-full md:w-[360px] rounded-3xl p-6 md:p-8 text-center flex flex-col justify-center relative bg-gradient-to-b from-indigo-50 to-white border border-indigo-100 shadow-inner">
<div className="mb-4 text-pink-600 font-bold text-sm tracking-[0.2em] uppercase">{t.offer.packLabel}</div>
<div className="mb-3 text-pink-600 font-bold text-sm tracking-[0.2em] uppercase">{t.offer.packLabel}</div>
<p className="text-xs text-indigo-400 font-medium mb-2">{t.offer.anchor}</p>
<div className="flex items-start justify-center mb-1">
<span className="text-2xl font-bold text-indigo-950 mt-2 mr-1">{t.offer.priceCurrency}</span>

View file

@ -30,7 +30,7 @@ const PT = {
title1: "Transforme a foto do seu filho",
title2: "em uma",
titleHighlight: "Festa Mágica!",
subtitle: "Pare de gastar horas procurando decoração. Envie uma foto e nossa IA cria, com o rostinho do seu filho no estilo de personagens de cinema, os itens que você escolher de um <strong>catálogo com 21 modelos exclusivos de Kit Digital</strong>! Prontinho para imprimir.",
subtitle: "Pare de gastar horas procurando decoração. Envie uma foto e nossa IA cria, com o rostinho do seu filho no estilo de personagens de cinema, os itens que você escolher de um <strong>catálogo com 33 modelos exclusivos de Kit Digital</strong>! Prontinho para imprimir.",
statKits: "kits criados",
statArts: "artes geradas",
statRating: "avaliação",
@ -70,23 +70,14 @@ const PT = {
items: [
{ title: "Topo de Bolo 3D", desc: "Destaque na mesa do bolo" },
{ title: "Painel de Fundo", desc: "Transforma o cenário da festa" },
{ title: "Convite Premium", desc: "Para impressão ou envio digital" },
{ title: "Caixa de Pipoca", desc: "A alegria garantida no lanche" },
{ title: "Bandeirolas com Nome", desc: "Varal decorativo personalizado" },
{ title: "Convite Digital", desc: "Para enviar no WhatsApp" },
{ title: "Caixa Milk", desc: "Lembrancinha charmosa" },
{ title: "Saia de Cupcake", desc: "Mais estilo para os docinhos" },
{ title: "Centro de Mesa", desc: "Arranjo decorativo exclusivo" },
{ title: "Tag de Agradecimento", desc: "Pronto para as lembrancinhas" },
{ title: "Topper de Cupcake", desc: "O personagem em cada docinho" },
{ title: "Faixa de Parede", desc: "Decoração com a cara do tema" },
{ title: "Sacolinha Surpresa", desc: "Lembrancinha que toda criança ama" },
{ title: "Chapéu de Festa", desc: "Divertido, personalizado e único" },
{ title: "Porta Bis Duplo", desc: "Lembrancinha deliciosa e bonita" },
{ title: "Adesivos Redondos", desc: "Ideais para potinhos e lembrancinhas" },
{ title: "Caixa Cone", desc: "Diferente e super divertida" },
{ title: "Display de Mesa", desc: "Identificação elegante para a mesa" },
],
moreModels: "E mais 25 outros modelos no catálogo — 33 no total, pra montar o kit do jeito que você quiser.",
},
howItWorks: {
title1: "Como a",
@ -120,16 +111,17 @@ const PT = {
offer: {
title1: "Crie a melhor festa que",
titleHighlight: "seu filho já teve.",
subtitle: "Adquira o pacote inicial e receba <strong>10 créditos para escolher e gerar seus itens favoritos</strong> do catálogo com 21 modelos exclusivos, por um valor simbólico.",
subtitle: "Adquira o pacote inicial e receba <strong>10 créditos para escolher e gerar seus itens favoritos</strong> do catálogo com 33 modelos exclusivos, por um valor simbólico.",
kitName: "O Kit Mágico",
benefits: [
"Criação do Personagem com IA Exclusivo",
"10 créditos para escolher e gerar os itens que quiser do catálogo de 21 modelos (1 item = 1 crédito)",
"10 créditos para escolher e gerar os itens que quiser do catálogo de 33 modelos (1 item = 1 crédito)",
"Arquivos em PDF já formatados, prontos para impressão",
"Acesso imediato à plataforma",
],
surprise: "Surpreenda todos os convidados!",
packLabel: "Pacote Inicial",
anchor: "Na gráfica, papelaria personalizada assim custa R$150 ou mais.",
priceCurrency: "R$",
priceInteger: "9",
priceDecimals: ",99",
@ -138,7 +130,7 @@ const PT = {
emailPlaceholder: "Seu melhor e-mail",
buttonIdle: "Quero Meu Kit por R$9,99",
buttonLoading: "Gerando Pedido...",
secure: "Compra 100% segura pelo Stripe.",
secure: "Compra 100% segura · Pix ou cartão · acesso na hora.",
errEmpty: "Por favor, informe seu e-mail.",
errInvalid: "Por favor, informe um e-mail válido.",
errCheckout: "Erro ao abrir o pagamento. Tente novamente.",
@ -170,7 +162,7 @@ const EN: typeof PT = {
title1: "Turn your child's photo",
title2: "into a",
titleHighlight: "Magic Party!",
subtitle: "Stop spending hours hunting for decorations. Upload a photo and our AI creates, with your child's face in a movie-character style, the items you pick from a <strong>catalog of 21 exclusive Digital Kit models</strong>! Ready to print.",
subtitle: "Stop spending hours hunting for decorations. Upload a photo and our AI creates, with your child's face in a movie-character style, the items you pick from a <strong>catalog of 33 exclusive Digital Kit models</strong>! Ready to print.",
statKits: "kits created",
statArts: "artworks generated",
statRating: "rating",
@ -210,23 +202,14 @@ const EN: typeof PT = {
items: [
{ title: "3D Cake Topper", desc: "The highlight of the cake table" },
{ title: "Backdrop Panel", desc: "Transforms the party scene" },
{ title: "Premium Invitation", desc: "For printing or digital sharing" },
{ title: "Popcorn Box", desc: "Guaranteed fun at snack time" },
{ title: "Name Bunting", desc: "Personalized decorative garland" },
{ title: "Digital Invitation", desc: "To send on WhatsApp" },
{ title: "Milk Box", desc: "A charming party favor" },
{ title: "Cupcake Wrapper", desc: "More style for the treats" },
{ title: "Table Centerpiece", desc: "An exclusive decorative arrangement" },
{ title: "Thank You Tag", desc: "Ready for the party favors" },
{ title: "Cupcake Topper", desc: "The character on every treat" },
{ title: "Wall Banner", desc: "Decoration that matches the theme" },
{ title: "Favor Bag", desc: "A favor every kid loves" },
{ title: "Party Hat", desc: "Fun, personalized and unique" },
{ title: "Double Chocolate Holder", desc: "A delicious, pretty favor" },
{ title: "Round Stickers", desc: "Perfect for jars and favors" },
{ title: "Cone Box", desc: "Different and super fun" },
{ title: "Table Display", desc: "Elegant labeling for the table" },
],
moreModels: "Plus 25 more models in the catalog — 33 in total, to build the kit your way.",
},
howItWorks: {
title1: "How the",
@ -260,16 +243,17 @@ const EN: typeof PT = {
offer: {
title1: "Create the best party",
titleHighlight: "your child has ever had.",
subtitle: "Get the starter pack and receive <strong>10 credits to choose and generate your favorite items</strong> from the catalog of 21 exclusive models, for a symbolic price.",
subtitle: "Get the starter pack and receive <strong>10 credits to choose and generate your favorite items</strong> from the catalog of 33 exclusive models, for a symbolic price.",
kitName: "The Magic Kit",
benefits: [
"Exclusive AI Character Creation",
"10 credits to choose and generate any items from the 21-model catalog (1 item = 1 credit)",
"10 credits to choose and generate any items from the 33-model catalog (1 item = 1 credit)",
"Print-ready, pre-formatted PDF files",
"Instant access to the platform",
],
surprise: "Wow all your guests!",
packLabel: "Starter Pack",
anchor: "At a print shop, custom stationery like this costs $40 or more.",
priceCurrency: "$",
priceInteger: "6",
priceDecimals: ".99",
@ -278,7 +262,7 @@ const EN: typeof PT = {
emailPlaceholder: "Your best email",
buttonIdle: "Get My Kit for $6.99",
buttonLoading: "Processing...",
secure: "100% secure checkout via Stripe.",
secure: "100% secure checkout · Pix or card · instant access.",
errEmpty: "Please enter your email.",
errInvalid: "Please enter a valid email.",
errCheckout: "Error opening checkout. Please try again.",
@ -310,7 +294,7 @@ const ES: typeof PT = {
title1: "Convierte la foto de tu hijo",
title2: "en una",
titleHighlight: "¡Fiesta Mágica!",
subtitle: "Deja de perder horas buscando decoración. Sube una foto y nuestra IA crea, con la carita de tu hijo en estilo de personajes de cine, los artículos que elijas de un <strong>catálogo con 21 modelos exclusivos de Kit Digital</strong>! Listos para imprimir.",
subtitle: "Deja de perder horas buscando decoración. Sube una foto y nuestra IA crea, con la carita de tu hijo en estilo de personajes de cine, los artículos que elijas de un <strong>catálogo con 33 modelos exclusivos de Kit Digital</strong>! Listos para imprimir.",
statKits: "kits creados",
statArts: "artes generadas",
statRating: "valoración",
@ -350,23 +334,14 @@ const ES: typeof PT = {
items: [
{ title: "Toper de Pastel 3D", desc: "El centro de la mesa del pastel" },
{ title: "Panel de Fondo", desc: "Transforma el escenario de la fiesta" },
{ title: "Invitación Premium", desc: "Para imprimir o enviar digital" },
{ title: "Caja de Palomitas", desc: "Diversión garantizada en la merienda" },
{ title: "Banderines con Nombre", desc: "Guirnalda decorativa personalizada" },
{ title: "Invitación Digital", desc: "Para enviar por WhatsApp" },
{ title: "Caja Milk", desc: "Un recuerdo encantador" },
{ title: "Capacillo de Cupcake", desc: "Más estilo para los dulces" },
{ title: "Centro de Mesa", desc: "Arreglo decorativo exclusivo" },
{ title: "Etiqueta de Agradecimiento", desc: "Lista para los recuerdos" },
{ title: "Toper de Cupcake", desc: "El personaje en cada dulce" },
{ title: "Pancarta de Pared", desc: "Decoración con la cara del tema" },
{ title: "Bolsita Sorpresa", desc: "Recuerdo que todo niño ama" },
{ title: "Gorro de Fiesta", desc: "Divertido, personalizado y único" },
{ title: "Porta Chocolate Doble", desc: "Recuerdo delicioso y bonito" },
{ title: "Stickers Redondos", desc: "Ideales para frascos y recuerdos" },
{ title: "Caja Cono", desc: "Diferente y súper divertida" },
{ title: "Display de Mesa", desc: "Identificación elegante para la mesa" },
],
moreModels: "Y 25 modelos más en el catálogo — 33 en total, para armar el kit a tu manera.",
},
howItWorks: {
title1: "Cómo Sucede la",
@ -400,16 +375,17 @@ const ES: typeof PT = {
offer: {
title1: "Crea la mejor fiesta que",
titleHighlight: "tu hijo haya tenido.",
subtitle: "Adquiere el paquete inicial y recibe <strong>10 créditos para elegir y generar tus artículos favoritos</strong> del catálogo con 21 modelos exclusivos, por un valor simbólico.",
subtitle: "Adquiere el paquete inicial y recibe <strong>10 créditos para elegir y generar tus artículos favoritos</strong> del catálogo con 33 modelos exclusivos, por un valor simbólico.",
kitName: "El Kit Mágico",
benefits: [
"Creación del Personaje con IA Exclusivo",
"10 créditos para elegir y generar los artículos que quieras del catálogo de 21 modelos (1 artículo = 1 crédito)",
"10 créditos para elegir y generar los artículos que quieras del catálogo de 33 modelos (1 artículo = 1 crédito)",
"Archivos PDF ya formateados, listos para imprimir",
"Acceso inmediato a la plataforma",
],
surprise: "¡Sorprende a todos los invitados!",
packLabel: "Paquete Inicial",
anchor: "En una imprenta, papelería personalizada así cuesta $40 o más.",
priceCurrency: "$",
priceInteger: "6",
priceDecimals: ".99",
@ -418,7 +394,7 @@ const ES: typeof PT = {
emailPlaceholder: "Tu mejor correo electrónico",
buttonIdle: "Quiero Mi Kit por $6.99",
buttonLoading: "Procesando...",
secure: "Compra 100% segura con Stripe.",
secure: "Compra 100% segura · Pix o tarjeta · acceso instantáneo.",
errEmpty: "Por favor, ingresa tu correo electrónico.",
errInvalid: "Por favor, ingresa un correo válido.",
errCheckout: "Error al abrir el pago. Inténtalo de nuevo.",

View file

@ -1,625 +0,0 @@
import { useState, useEffect, useRef, type FormEvent } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldCheck, Lock, QrCode, CreditCard, Copy, Check,
Loader2, Gift, Sparkles, ArrowRight, Star, Wand2, Zap,
} from 'lucide-react';
import { cn } from '../lib/utils';
import { createPixCharge, pollPixStatus, type PixCharge } from '../services/wooviService';
// ─── Planos (espelha src/utils/pricing.ts do app) ──────────────────────────
const PLANS = [
{
id: 'starter',
name: 'Starter',
credits: 10,
price: 999,
display: 'R$9,99',
perImg: 'R$1,00/img',
priceId: 'price_1TT5lpLjQo8Mvvq1HdDD7paN',
planName: 'Festa Magica IA - Kit Inicial',
},
{
id: 'popular',
name: 'Kit Festa',
credits: 30,
price: 1999,
display: 'R$19,99',
perImg: 'R$0,67/img',
priceId: 'price_1TT5kTLjQo8Mvvq1Tus1RS5D',
planName: 'Festa Magica IA - Kit Festa',
badge: 'Mais vendido',
},
{
id: 'pro',
name: 'Festa VIP',
credits: 80,
price: 4999,
display: 'R$49,99',
perImg: 'R$0,62/img',
priceId: 'price_1THThELjQo8Mvvq1uXkgh5jB',
planName: 'Festa Magica IA - Festa VIP',
},
{
id: 'professional',
name: 'Profissional',
credits: 200,
price: 9999,
display: 'R$99,99',
perImg: 'R$0,50/img',
priceId: 'price_1TT5i9LjQo8Mvvq12yhPqe1G',
planName: 'Festa Magica IA - Profissional',
},
] as const;
type Plan = (typeof PLANS)[number];
type Method = 'pix' | 'card';
const STRIPE_URL = 'https://n8n.seureview.com.br/webhook/festa-magica-stripe';
function getCookie(name: string) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : '';
}
export default function Checkout() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const source = searchParams.get('source') || 'landing';
const userId = searchParams.get('userId') || '';
const locked = searchParams.get('lock') === '1' || ['landing', 'funil', 'campanha'].includes(searchParams.get('source') || '');
const [email, setEmail] = useState(searchParams.get('email') || '');
const [method, setMethod] = useState<Method>('pix');
const [error, setError] = useState('');
// Plano inicial vindo da URL (?plan=popular) ou padrão = starter
const initialPlan = PLANS.find(p => p.id === searchParams.get('plan')) ?? PLANS[0];
const [plan, setPlan] = useState<Plan>(initialPlan);
// ── Pix ──────────────────────────────────────────────────────────────────
const [pixLoading, setPixLoading] = useState(false);
const [pix, setPix] = useState<PixCharge | null>(null);
const [copied, setCopied] = useState(false);
const [confirming, setConfirming] = useState(false);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
// ── Cartão ────────────────────────────────────────────────────────────────
const [cardLoading, setCardLoading] = useState(false);
// ── Skeleton de entrada ───────────────────────────────────────────────────
const [ready, setReady] = useState(false);
useEffect(() => {
const t = setTimeout(() => setReady(true), 1200);
return () => clearTimeout(t);
}, []);
useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []);
// Resetar Pix ao trocar de plano
useEffect(() => {
if (pollRef.current) clearInterval(pollRef.current);
setPix(null);
setConfirming(false);
setError('');
}, [plan.id]);
const validEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const trackEvent = () => {
if (typeof window !== 'undefined' && 'fbq' in window) {
(window as any).fbq('track', 'InitiateCheckout');
}
};
// ── Pix ──────────────────────────────────────────────────────────────────
const handlePix = async (e?: FormEvent) => {
e?.preventDefault();
if (!validEmail) { setError('Informe um e-mail válido para continuar.'); return; }
setError('');
setPixLoading(true);
trackEvent();
try {
const charge = await createPixCharge({
userEmail: email,
source,
userId,
value: plan.price,
credits: plan.credits,
planName: plan.planName,
priceId: plan.priceId,
fbp: getCookie('_fbp'),
fbc: getCookie('_fbc'),
});
setPix(charge);
startPolling(charge.correlationID);
} catch (err: any) {
setError(err.message || 'Não foi possível gerar o Pix. Tente novamente.');
} finally {
setPixLoading(false);
}
};
const startPolling = (correlationID: string) => {
setConfirming(true);
if (pollRef.current) clearInterval(pollRef.current);
pollRef.current = setInterval(async () => {
try {
const status = await pollPixStatus(correlationID);
if (status === 'COMPLETED') {
clearInterval(pollRef.current!);
navigate('/sucesso?status=approved', { replace: true });
} else if (status === 'EXPIRED') {
clearInterval(pollRef.current!);
setConfirming(false);
setPix(null);
setError('O Pix expirou. Gere um novo código para pagar.');
}
} catch { /* silencia; tenta no próximo tick */ }
}, 4000);
};
const handleCopy = async () => {
if (!pix) return;
try {
await navigator.clipboard.writeText(pix.brCode);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch { setError('Não consegui copiar. Copie o código manualmente.'); }
};
// ── Cartão ────────────────────────────────────────────────────────────────
const handleCard = async (e?: FormEvent) => {
e?.preventDefault();
if (!validEmail) { setError('Informe um e-mail válido para continuar.'); return; }
setError('');
setCardLoading(true);
trackEvent();
try {
const res = await fetch(STRIPE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source,
userEmail: email,
userId,
priceId: plan.priceId,
credits: plan.credits,
planName: plan.planName,
fbp: getCookie('_fbp'),
fbc: getCookie('_fbc'),
}),
});
const data = await res.json();
if (data.success && data.url) {
window.location.href = data.url;
} else {
setCardLoading(false);
setError('Ocorreu um erro ao abrir o pagamento. Tente novamente.');
}
} catch {
setCardLoading(false);
setError('Erro de conexão. Verifique sua internet e tente novamente.');
}
};
const features = [
'Personagem 3D exclusivo criado com IA',
`${plan.credits} créditos para itens do catálogo`,
'Arquivos em PDF prontos para impressão',
'Acesso imediato à plataforma',
];
return (
<div className="min-h-screen bg-[#f7f7f9] font-sans text-gray-900">
{/* ── Topo ─────────────────────────────────────────────────────── */}
<header className="bg-white border-b border-gray-100">
<div className="max-w-5xl mx-auto px-5 sm:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-pink-500 to-violet-500 flex items-center justify-center shadow-sm shadow-pink-500/30">
<Wand2 className="w-4 h-4 text-white" strokeWidth={2.5} />
</div>
<span className="font-display font-bold text-lg tracking-tight text-gray-900">
Festa Mágica<span className="text-pink-500">IA</span>
</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-emerald-700 font-semibold bg-emerald-50 border border-emerald-100 px-3 py-1.5 rounded-full">
<Lock className="w-3.5 h-3.5" strokeWidth={2.5} />
Ambiente seguro
</div>
</div>
</header>
<div className="max-w-5xl mx-auto px-5 sm:px-8 py-8 sm:py-12">
{/* ── Skeleton ─────────────────────────────────────────────────────── */}
<AnimatePresence>
{!ready && (
<motion.div
key="skeleton"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="grid lg:grid-cols-[1fr_380px] gap-6"
>
{/* Esquerda */}
<div className="space-y-5">
{!locked && (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 space-y-4">
<div className="h-3 w-24 bg-gray-100 rounded-full animate-pulse" />
<div className="grid grid-cols-2 gap-3">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" style={{ animationDelay: `${i * 80}ms` }} />
))}
</div>
</div>
)}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 space-y-4">
<div className="h-3 w-32 bg-gray-100 rounded-full animate-pulse" />
{[...Array(4)].map((_, i) => (
<div key={i} className="h-3 bg-gray-100 rounded-full animate-pulse" style={{ width: `${75 + i * 5}%`, animationDelay: `${i * 60}ms` }} />
))}
<div className="pt-4 border-t border-gray-100 flex justify-between">
<div className="h-3 w-28 bg-gray-100 rounded-full animate-pulse" />
<div className="h-6 w-20 bg-gray-100 rounded-full animate-pulse" />
</div>
</div>
</div>
{/* Direita */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 space-y-5">
<div className="h-4 w-24 bg-gray-100 rounded-full animate-pulse" />
<div className="h-11 bg-gray-100 rounded-xl animate-pulse" />
<div className="grid grid-cols-2 gap-2">
<div className="h-16 bg-gray-100 rounded-xl animate-pulse" />
<div className="h-16 bg-gray-100 rounded-xl animate-pulse" style={{ animationDelay: '80ms' }} />
</div>
<div className="h-12 bg-gray-100 rounded-xl animate-pulse" />
<div className="pt-4 border-t border-gray-100 grid grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-8 bg-gray-100 rounded-lg animate-pulse" style={{ animationDelay: `${i * 50}ms` }} />
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* ── Conteúdo real ────────────────────────────────────────────────── */}
<AnimatePresence>
{ready && (
<motion.div
key="content"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<div className="grid lg:grid-cols-[1fr_380px] gap-6 items-start">
{/* ── Coluna esquerda: seletor de plano + resumo ───────────── */}
<div className="space-y-5">
{/* Seletor de plano — oculto no modo locked (funil/campanha) */}
{!locked && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6"
>
<p className="text-xs font-bold uppercase tracking-widest text-pink-500 mb-4">
Escolha o plano
</p>
<div className="grid grid-cols-2 gap-3">
{PLANS.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setPlan(p as Plan)}
className={cn(
'relative text-left rounded-xl border-2 p-4 transition-all duration-200',
plan.id === p.id
? 'border-pink-500 bg-pink-50/60 shadow-sm shadow-pink-500/10'
: 'border-gray-100 bg-white hover:border-pink-200 hover:bg-pink-50/30',
)}
>
{'badge' in p && p.badge && (
<span className="absolute -top-2.5 left-3 bg-gradient-to-r from-pink-500 to-violet-500 text-white text-[9px] font-black uppercase tracking-wider px-2 py-0.5 rounded-full">
{p.badge}
</span>
)}
<p className={cn(
'text-sm font-bold mb-1',
plan.id === p.id ? 'text-pink-700' : 'text-gray-700',
)}>
{p.name}
</p>
<p className={cn(
'font-display font-black text-2xl leading-none mb-1',
plan.id === p.id ? 'text-pink-600' : 'text-gray-900',
)}>
{p.display}
</p>
<p className="text-[11px] font-semibold text-gray-400">
{p.credits} imagens · {p.perImg}
</p>
{plan.id === p.id && (
<span className="absolute top-3 right-3 w-4 h-4 rounded-full bg-pink-500 flex items-center justify-center">
<Check className="w-2.5 h-2.5 text-white" strokeWidth={3} />
</span>
)}
</button>
))}
</div>
</motion.div>
)}
{/* Resumo do pedido */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6"
>
<div className="flex items-center gap-2 mb-5">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-pink-500 to-violet-500 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-xs font-bold uppercase tracking-widest text-gray-400">Resumo</p>
<p className="font-display font-bold text-gray-900 leading-tight">{plan.name}</p>
</div>
</div>
<ul className="space-y-3 mb-6">
{features.map((f, i) => (
<li key={i} className="flex items-start gap-3">
<span className="w-5 h-5 rounded-full bg-pink-100 flex items-center justify-center shrink-0 mt-0.5">
<Check className="w-3 h-3 text-pink-600" strokeWidth={3} />
</span>
<span className="text-sm font-medium text-gray-700 leading-snug">{f}</span>
</li>
))}
</ul>
{/* Primeira imagem grátis — só pro starter */}
{plan.id === 'starter' && (
<div className="rounded-xl bg-emerald-50 border border-emerald-100 p-3.5 flex items-start gap-2.5 mb-5">
<Gift className="w-4 h-4 text-emerald-600 shrink-0 mt-0.5" />
<p className="text-xs font-semibold text-emerald-800 leading-snug">
Sua <strong>primeira imagem por nossa conta</strong> não consome nenhum crédito.
</p>
</div>
)}
{/* Valor final */}
<div className="pt-5 border-t border-gray-100 flex items-baseline justify-between">
<span className="text-sm font-semibold text-gray-500">Você paga hoje</span>
<span className="font-display font-black text-3xl text-gray-900">{plan.display}</span>
</div>
<p className="text-xs text-gray-400 text-right mt-1">Pagamento único · sem assinatura</p>
{/* Prova social */}
<div className="flex items-center gap-2 mt-5 pt-5 border-t border-gray-100">
<div className="flex -space-x-1">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-3.5 h-3.5 text-amber-400 fill-amber-400" />
))}
</div>
<span className="text-xs text-gray-500 font-semibold">+10.000 mães fizeram a festa</span>
</div>
</motion.div>
</div>
{/* ── Coluna direita: pagamento ─────────────────────────────── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 lg:sticky lg:top-6"
>
<h2 className="font-display font-bold text-lg text-gray-900 mb-5">Pagamento</h2>
{/* E-mail */}
<div className="mb-5">
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1.5">
E-mail
</label>
<input
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); if (error) setError(''); }}
placeholder="seu@email.com"
disabled={!!pix}
className="w-full h-11 px-4 rounded-xl border-2 border-gray-100 focus:border-pink-400 outline-none text-sm font-medium text-gray-900 transition-colors placeholder:text-gray-300 disabled:bg-gray-50 disabled:text-gray-400"
/>
<p className="text-[11px] text-gray-400 mt-1.5">
É aqui que enviamos o acesso ao painel.
</p>
</div>
{/* Método */}
<div className="grid grid-cols-2 gap-2 mb-5">
<button
type="button"
onClick={() => { setMethod('pix'); setError(''); }}
className={cn(
'flex flex-col items-center gap-1 py-3 rounded-xl border-2 text-sm font-bold transition-all',
method === 'pix'
? 'border-pink-500 bg-pink-50/60 text-pink-700'
: 'border-gray-100 text-gray-400 hover:border-gray-200',
)}
>
<QrCode className="w-4 h-4" />
Pix
<span className={cn(
'text-[9px] font-black uppercase tracking-wide px-2 py-0.5 rounded-full',
method === 'pix' ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-400',
)}>
Aprovação imediata
</span>
</button>
<button
type="button"
onClick={() => { setMethod('card'); setError(''); }}
className={cn(
'flex flex-col items-center gap-1 py-3 rounded-xl border-2 text-sm font-bold transition-all',
method === 'card'
? 'border-pink-500 bg-pink-50/60 text-pink-700'
: 'border-gray-100 text-gray-400 hover:border-gray-200',
)}
>
<CreditCard className="w-4 h-4" />
Cartão
<span className="text-[9px] font-black uppercase tracking-wide px-2 py-0.5 rounded-full bg-gray-100 text-gray-400">
Crédito
</span>
</button>
</div>
{/* Erro */}
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4 text-xs text-red-600 bg-red-50 border border-red-100 rounded-xl px-3.5 py-2.5 font-semibold"
>
{error}
</motion.div>
)}
</AnimatePresence>
{/* ── Pix ── */}
<AnimatePresence mode="wait">
{method === 'pix' && (
<motion.div
key="pix"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{!pix ? (
<form onSubmit={handlePix}>
<button
type="submit"
disabled={pixLoading}
className="w-full h-13 py-3.5 rounded-xl bg-gradient-to-r from-pink-500 to-violet-500 text-white font-bold text-base flex items-center justify-center gap-2 shadow-md shadow-pink-500/20 hover:shadow-lg hover:shadow-pink-500/30 hover:scale-[1.01] transition-all disabled:opacity-60 disabled:hover:scale-100 disabled:hover:shadow-md"
>
{pixLoading ? (
<><Loader2 className="w-4 h-4 animate-spin" /> Gerando Pix...</>
) : (
<><QrCode className="w-4 h-4" /> Gerar Pix · {plan.display}</>
)}
</button>
<p className="text-[11px] text-gray-400 mt-2.5 text-center flex items-center justify-center gap-1">
<Zap className="w-3 h-3 text-emerald-500" />
QR Code gerado na hora · pague pelo app do banco
</p>
</form>
) : (
<div className="text-center">
<div className="inline-block p-3 bg-white rounded-2xl border-2 border-gray-100 shadow-sm mb-4">
<img src={pix.qrCodeImage} alt="QR Code Pix" className="w-44 h-44 mx-auto" />
</div>
<p className="text-xs font-bold text-gray-600 mb-2">
Escaneie o QR Code ou use o Pix Copia e Cola:
</p>
<div className="flex items-center gap-2 mb-4">
<input
readOnly
value={pix.brCode}
className="flex-1 text-[10px] bg-gray-50 border border-gray-100 rounded-xl px-3 py-2.5 text-gray-400 truncate"
/>
<button
type="button"
onClick={handleCopy}
className="shrink-0 h-9 px-3 rounded-xl bg-gray-900 text-white text-xs font-bold flex items-center gap-1.5 hover:bg-gray-700 transition-colors"
>
{copied
? <><Check className="w-3.5 h-3.5" /> Copiado</>
: <><Copy className="w-3.5 h-3.5" /> Copiar</>}
</button>
</div>
{confirming && (
<div className="flex items-center justify-center gap-2 text-pink-600 font-semibold text-xs mb-2">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
Aguardando confirmação do pagamento...
</div>
)}
<p className="text-[11px] text-gray-400">
Assim que o Pix cair, liberamos seu acesso. Pode deixar esta tela aberta.
</p>
</div>
)}
</motion.div>
)}
{/* ── Cartão ── */}
{method === 'card' && (
<motion.div
key="card"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<form onSubmit={handleCard}>
<button
type="submit"
disabled={cardLoading}
className="w-full py-3.5 rounded-xl bg-gradient-to-r from-pink-500 to-violet-500 text-white font-bold text-base flex items-center justify-center gap-2 shadow-md shadow-pink-500/20 hover:shadow-lg hover:shadow-pink-500/30 hover:scale-[1.01] transition-all disabled:opacity-60 disabled:hover:scale-100 disabled:hover:shadow-md"
>
{cardLoading ? (
<><Loader2 className="w-4 h-4 animate-spin" /> Abrindo checkout...</>
) : (
<>Pagar {plan.display} com cartão <ArrowRight className="w-4 h-4" /></>
)}
</button>
<p className="text-[11px] text-gray-400 mt-2.5 text-center flex items-center justify-center gap-1">
<Lock className="w-3 h-3" />
Você finaliza no ambiente seguro do Stripe
</p>
</form>
</motion.div>
)}
</AnimatePresence>
{/* ── Selos de confiança ── */}
<div className="mt-6 pt-5 border-t border-gray-100">
<div className="flex items-center justify-center gap-1.5 text-xs font-bold text-emerald-700 mb-4">
<ShieldCheck className="w-4 h-4" /> Pagamento 100% seguro e criptografado
</div>
<div className="grid grid-cols-2 gap-2">
{[
{ icon: QrCode, label: 'Pix · Banco Central' },
{ icon: CreditCard, label: 'Cartão via Stripe' },
{ icon: Lock, label: 'SSL 256-bit' },
{ icon: ShieldCheck, label: 'Garantia de 7 dias' },
].map(({ icon: Icon, label }) => (
<div key={label} className="flex items-center gap-2 bg-gray-50 rounded-lg px-3 py-2">
<Icon className="w-3.5 h-3.5 text-pink-400 shrink-0" />
<span className="text-[11px] font-semibold text-gray-500">{label}</span>
</div>
))}
</div>
</div>
</motion.div>
</div>
{/* Rodapé */}
<p className="mt-8 text-center text-xs text-gray-400 font-medium">
Ao prosseguir você concorda com os Termos de Uso e Política de Privacidade.
Garantia de reembolso integral em até 7 dias, sem perguntas.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}

View file

@ -1,80 +0,0 @@
import { useEffect, useState } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { CheckCircle, Mail, LayoutDashboard, Sparkles } from "lucide-react";
import Footer from "../components/Footer";
export default function SuccessPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [isValid, setIsValid] = useState(false);
useEffect(() => {
// O Stripe passa 'session_id' automaticamente quando redireciona,
// Ou podemos checar um parâmetro manual como '?status=approved'
const sessionId = searchParams.get("session_id");
const status = searchParams.get("status");
if (sessionId || status === "approved") {
setIsValid(true);
} else {
// Se não tiver nenhum parâmetro válido, joga de volta pra home
navigate("/", { replace: true });
}
}, [searchParams, navigate]);
if (!isValid) {
return null; // Retorna nulo enquanto valida para não piscar a tela
}
return (
<div className="min-h-screen bg-pink-50 text-indigo-900 selection:bg-pink-300 font-sans flex flex-col">
<main className="flex-1 py-12 px-4 md:px-6 flex items-center justify-center">
<div className="max-w-5xl mx-auto w-full">
{/* Success Banner */}
<section className="text-center bg-white rounded-3xl p-8 md:p-12 shadow-xl border border-pink-100">
<CheckCircle className="w-20 h-20 text-emerald-500 mx-auto mb-6" />
<h1 className="text-3xl md:text-5xl font-display font-bold text-indigo-950 mb-4">
Pagamento Confirmado!
</h1>
<p className="text-lg text-indigo-600 md:text-xl max-w-2xl mx-auto mb-12">
Tudo certo com o seu pedido! Preparamos tudo para você começar a criar a festa dos sonhos em instantes.
</p>
<div className="grid md:grid-cols-3 gap-6 text-left max-w-4xl mx-auto">
<div className="bg-pink-50/50 rounded-2xl p-6 border border-pink-100 relative hover:shadow-md transition-shadow">
<div className="bg-white w-12 h-12 rounded-full flex items-center justify-center shadow-sm text-pink-500 mb-4">
<Mail className="w-6 h-6" />
</div>
<h3 className="font-bold text-indigo-950 text-lg mb-2">1. Fique de olho no e-mail</h3>
<p className="text-indigo-700/80 text-sm leading-relaxed">
Em até 5 minutos, você receberá um e-mail de confirmação com a nota do seu pedido e todos os detalhes.
</p>
</div>
<div className="bg-pink-50/50 rounded-2xl p-6 border border-pink-100 relative hover:shadow-md transition-shadow">
<div className="bg-white w-12 h-12 rounded-full flex items-center justify-center shadow-sm text-violet-500 mb-4">
<LayoutDashboard className="w-6 h-6" />
</div>
<h3 className="font-bold text-indigo-950 text-lg mb-2">2. Acesse o Painel</h3>
<p className="text-indigo-700/80 text-sm leading-relaxed">
No mesmo e-mail, enviaremos um <strong>link de acesso exclusivo</strong> direto para o nosso Painel de Criação Mágica. Não se preocupe em criar conta!
</p>
</div>
<div className="bg-pink-50/50 rounded-2xl p-6 border border-pink-100 relative hover:shadow-md transition-shadow">
<div className="bg-white w-12 h-12 rounded-full flex items-center justify-center shadow-sm text-amber-500 mb-4">
<Sparkles className="w-6 h-6" />
</div>
<h3 className="font-bold text-indigo-950 text-lg mb-2">3. Crie a Magia</h3>
<p className="text-indigo-700/80 text-sm leading-relaxed">
Dentro do painel, siga as instruções rápidas, faça o upload da foto e deixe a inteligência artificial transformar tudo em um kit inesquecível!
</p>
</div>
</div>
</section>
</div>
</main>
<Footer />
</div>
);
}

View file

@ -1,98 +0,0 @@
// ============================================================
// Festa Mágica IA — Woovi (Pix) Service
// ============================================================
// Fluxo Pix transparente:
// Front → n8n (cria cobrança Woovi) → QR/copia-e-cola na tela
// Front faz polling do status via n8n até COMPLETED
// Webhook Woovi → n8n credita / cria usuário / magic link / e-mail
//
// IMPORTANTE: o App ID da Woovi NUNCA passa pelo browser.
// Tanto a criação da cobrança quanto a consulta de status são
// feitas server-side pelo n8n. O front só fala com o n8n.
// ============================================================
const N8N_BASE = 'https://n8n.seureview.com.br/webhook';
const PIX_CREATE_URL = `${N8N_BASE}/festa-magica-pix`;
const PIX_STATUS_URL = `${N8N_BASE}/festa-magica-pix-status`;
export interface CreatePixParams {
userEmail: string;
source?: string; // 'landing' | 'funil' | 'default'
userId?: string;
value?: number; // em centavos BRL
credits?: number;
planName?: string;
priceId?: string;
fbp?: string;
fbc?: string;
}
export interface PixCharge {
correlationID: string;
brCode: string; // código copia-e-cola
qrCodeImage: string; // URL da imagem do QR (PNG)
value: number; // em centavos
expiresDate?: string;
}
export type PixStatus = 'ACTIVE' | 'COMPLETED' | 'EXPIRED';
/**
* Cria uma cobrança Pix na Woovi (via n8n) e retorna os dados
* para exibir o QR Code e o copia-e-cola na própria tela.
*/
export async function createPixCharge(params: CreatePixParams): Promise<PixCharge> {
const response = await fetch(PIX_CREATE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: params.source || 'landing',
userEmail: params.userEmail,
userId: params.userId || '',
value: params.value,
credits: params.credits,
planName: params.planName,
priceId: params.priceId,
fbp: params.fbp || '',
fbc: params.fbc || '',
}),
});
if (!response.ok) {
throw new Error(`Erro ao gerar Pix: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.brCode) {
throw new Error(data.error || 'Não foi possível gerar o Pix.');
}
return {
correlationID: data.correlationID,
brCode: data.brCode,
qrCodeImage: data.qrCodeImage,
value: data.value,
expiresDate: data.expiresDate,
};
}
/**
* Consulta o status de uma cobrança Pix (via n8n).
* Usado no polling enquanto o usuário paga.
*/
export async function pollPixStatus(correlationID: string): Promise<PixStatus> {
const response = await fetch(PIX_STATUS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ correlationID }),
});
if (!response.ok) {
throw new Error(`Erro ao consultar status do Pix: ${response.status}`);
}
const data = await response.json();
return (data.status as PixStatus) || 'ACTIVE';
}