feat: Landing page pública + Registro com seleção de plano

- Nova Landing Page institucional em /
- Seções: Hero, Features, Pricing, FAQ, CTA, Footer
- Pricing integrado com API de planos
- Register.jsx agora suporta seleção de plano
- Parâmetro ?plan=slug na URL do registro
- Traduções EN, ES, PT-BR para landing
- PayPal configurado no servidor (sandbox)

Versão: 1.54.0
This commit is contained in:
marcoitaloesp-ai 2025-12-17 19:44:12 +00:00 committed by GitHub
parent c99bca9404
commit 984855e36c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1233 additions and 87 deletions

View File

@ -5,6 +5,44 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/). Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.54.0] - 2025-12-17
### Added
- 🏠 **Landing Page Pública WebMoney** - Nova página inicial institucional
- **Navbar** com links para seções e botões login/registro
- **Hero Section** com animação de preview do dashboard
- **Features Section** com 6 recursos principais:
- Múltiplas Contas, Categorias Inteligentes, Importação Bancária
- Relatórios Detalhados, Metas e Orçamentos, Multi-moeda
- **Pricing Section** integrada com API de planos
- Exibe planos Free, Pro Mensual, Pro Anual
- Destaque para plano mais popular
- Mostra período de trial e desconto anual
- **FAQ Section** com accordion interativo
- **CTA Section** com chamada para registro
- **Footer** completo com links úteis
- 📝 **Registro com Seleção de Plano**
- Cards de planos no formulário de registro
- Suporte a parâmetro `?plan=slug` na URL
- Redirecionamento para PayPal após registro (planos pagos)
- Texto dinâmico do botão baseado no plano selecionado
### Changed
- 🔄 **Rota inicial alterada** - "/" agora mostra Landing Page em vez de redirecionar para login
- 🌍 **Traduções** adicionadas para Landing em EN, ES e PT-BR
- Namespace `landing.*` com todas as seções
### Technical Details
- Novo componente: `frontend/src/pages/Landing.jsx`
- Novo CSS: `frontend/src/pages/Landing.css`
- Atualizado: `frontend/src/App.jsx` (rota "/" e import Register)
- Atualizado: `frontend/src/pages/Register.jsx` (seleção de plano)
- Arquivos de tradução atualizados: `en.json`, `es.json`, `pt-BR.json`
- PayPal configurado no servidor (sandbox mode)
---
## [1.53.0] - 2025-12-17 ## [1.53.0] - 2025-12-17
### Fixed ### Fixed

View File

@ -1 +1 @@
1.53.0 1.54.0

View File

@ -6,6 +6,7 @@ import ProtectedRoute from './components/ProtectedRoute';
import Layout from './components/Layout'; import Layout from './components/Layout';
import CookieConsent from './components/CookieConsent'; import CookieConsent from './components/CookieConsent';
import Login from './pages/Login'; import Login from './pages/Login';
import Landing from './pages/Landing';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import Accounts from './pages/Accounts'; import Accounts from './pages/Accounts';
import CostCenters from './pages/CostCenters'; import CostCenters from './pages/CostCenters';
@ -27,6 +28,7 @@ import Pricing from './pages/Pricing';
import Billing from './pages/Billing'; import Billing from './pages/Billing';
import Users from './pages/Users'; import Users from './pages/Users';
import SiteSettings from './pages/SiteSettings'; import SiteSettings from './pages/SiteSettings';
import Register from './pages/Register';
function App() { function App() {
return ( return (
@ -35,6 +37,7 @@ function App() {
<ToastProvider> <ToastProvider>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
@ -243,7 +246,7 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route path="/" element={<Navigate to="/dashboard" />} /> <Route path="/" element={<Landing />} />
</Routes> </Routes>
<CookieConsent /> <CookieConsent />
</ToastProvider> </ToastProvider>

View File

@ -2123,5 +2123,93 @@
"cancelNote2": "Your data will not be deleted", "cancelNote2": "Your data will not be deleted",
"cancelNote3": "You can reactivate your subscription at any time", "cancelNote3": "You can reactivate your subscription at any time",
"confirmCancel": "Yes, Cancel" "confirmCancel": "Yes, Cancel"
},
"landing": {
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"login": "Login",
"register": "Start Now"
},
"hero": {
"title": "Take Control of Your",
"titleHighlight": "Finances",
"subtitle": "Intelligent financial management for individuals and businesses. Track income, expenses, and achieve your financial goals.",
"cta": "Start Free",
"ctaSecondary": "View Plans",
"previewBalance": "Total Balance",
"previewIncome": "Income",
"previewExpense": "Expenses"
},
"features": {
"title": "Everything You Need",
"subtitle": "Powerful tools to manage your money",
"item1": {
"title": "Multiple Accounts",
"description": "Manage bank accounts, cards, and cash in one place"
},
"item2": {
"title": "Smart Categories",
"description": "Automatic categorization with keywords and subcategories"
},
"item3": {
"title": "Bank Import",
"description": "Import statements from Excel, CSV, OFX, and PDF"
},
"item4": {
"title": "Detailed Reports",
"description": "Charts and analysis to understand your spending"
},
"item5": {
"title": "Goals & Budgets",
"description": "Define goals and control monthly spending"
},
"item6": {
"title": "Multi-currency",
"description": "Manage finances in different currencies"
}
},
"pricing": {
"title": "Simple Plans, Fair Prices",
"subtitle": "Choose the plan that fits your needs",
"monthly": "Monthly",
"annual": "Annual",
"popular": "Most Popular",
"perMonth": "/month",
"perYear": "/year",
"startFree": "Start Free",
"subscribe": "Subscribe Now",
"billedAnnually": "Billed annually"
},
"faq": {
"title": "Frequently Asked Questions",
"q1": "Is my data secure?",
"a1": "Yes! We use bank-level encryption (SSL/TLS) and your data is stored on secure servers with regular backups. We never share your information with third parties.",
"q2": "Can I cancel anytime?",
"a2": "Yes, you can cancel your subscription at any time without fees. You'll keep access until the end of the period you already paid.",
"q3": "Which banks are compatible?",
"a3": "You can import statements from any bank that exports to Excel, CSV, OFX, or PDF. We have predefined mappings for major banks.",
"q4": "How does the free trial work?",
"a4": "You get full access to all features during the trial period. No credit card required to start. At the end, choose the plan that fits your needs."
},
"cta": {
"title": "Ready to Transform Your Finances?",
"subtitle": "Join thousands of users who have already taken control of their money.",
"button": "Create Free Account"
},
"footer": {
"description": "Smart Financial Management for individuals and businesses.",
"product": "Product",
"company": "Company",
"legal": "Legal",
"features": "Features",
"pricing": "Pricing",
"about": "About Us",
"contact": "Contact",
"privacy": "Privacy Policy",
"terms": "Terms of Use",
"rights": "All rights reserved."
}
} }
} }

View File

@ -2125,5 +2125,93 @@
"cancelNote2": "Tus datos no se eliminarán", "cancelNote2": "Tus datos no se eliminarán",
"cancelNote3": "Puedes reactivar tu suscripción en cualquier momento", "cancelNote3": "Puedes reactivar tu suscripción en cualquier momento",
"confirmCancel": "Sí, Cancelar" "confirmCancel": "Sí, Cancelar"
},
"landing": {
"nav": {
"features": "Funcionalidades",
"pricing": "Precios",
"faq": "FAQ",
"login": "Iniciar Sesión",
"register": "Empezar Ahora"
},
"hero": {
"title": "Toma el Control de tus",
"titleHighlight": "Finanzas",
"subtitle": "Gestión financiera inteligente para personas y empresas. Controla ingresos, gastos y alcanza tus metas financieras.",
"cta": "Comenzar Gratis",
"ctaSecondary": "Ver Planes",
"previewBalance": "Saldo Total",
"previewIncome": "Ingresos",
"previewExpense": "Gastos"
},
"features": {
"title": "Todo lo que Necesitas",
"subtitle": "Herramientas potentes para gestionar tu dinero",
"item1": {
"title": "Múltiples Cuentas",
"description": "Gestiona cuentas bancarias, tarjetas y efectivo en un solo lugar"
},
"item2": {
"title": "Categorías Inteligentes",
"description": "Categorización automática con palabras clave y subcategorías"
},
"item3": {
"title": "Importación Bancaria",
"description": "Importa extractos de Excel, CSV, OFX y PDF"
},
"item4": {
"title": "Reportes Detallados",
"description": "Gráficos y análisis para entender tus gastos"
},
"item5": {
"title": "Metas y Presupuestos",
"description": "Define objetivos y controla gastos mensuales"
},
"item6": {
"title": "Multi-moneda",
"description": "Gestiona finanzas en diferentes monedas"
}
},
"pricing": {
"title": "Planes Simples, Precios Justos",
"subtitle": "Elige el plan que se adapte a tus necesidades",
"monthly": "Mensual",
"annual": "Anual",
"popular": "Más Popular",
"perMonth": "/mes",
"perYear": "/año",
"startFree": "Comenzar Gratis",
"subscribe": "Suscribirse Ahora",
"billedAnnually": "Facturado anualmente"
},
"faq": {
"title": "Preguntas Frecuentes",
"q1": "¿Mis datos están seguros?",
"a1": "¡Sí! Utilizamos cifrado de nivel bancario (SSL/TLS) y tus datos se almacenan en servidores seguros con copias de seguridad regulares. Nunca compartimos tu información con terceros.",
"q2": "¿Puedo cancelar cuando quiera?",
"a2": "Sí, puedes cancelar tu suscripción en cualquier momento sin cargos. Mantendrás el acceso hasta el final del período que ya pagaste.",
"q3": "¿Qué bancos son compatibles?",
"a3": "Puedes importar extractos de cualquier banco que exporte a Excel, CSV, OFX o PDF. Tenemos mapeos predefinidos para los principales bancos.",
"q4": "¿Cómo funciona la prueba gratuita?",
"a4": "Tienes acceso completo a todas las funcionalidades durante el período de prueba. No se requiere tarjeta de crédito para empezar. Al final, elige el plan que se adapte a tus necesidades."
},
"cta": {
"title": "¿Listo para Transformar tus Finanzas?",
"subtitle": "Únete a miles de usuarios que ya tomaron el control de su dinero.",
"button": "Crear Cuenta Gratis"
},
"footer": {
"description": "Gestión Financiera Inteligente para personas y empresas.",
"product": "Producto",
"company": "Empresa",
"legal": "Legal",
"features": "Funcionalidades",
"pricing": "Precios",
"about": "Sobre Nosotros",
"contact": "Contacto",
"privacy": "Política de Privacidad",
"terms": "Términos de Uso",
"rights": "Todos los derechos reservados."
}
} }
} }

View File

@ -2143,5 +2143,93 @@
"cancelNote2": "Seus dados não serão excluídos", "cancelNote2": "Seus dados não serão excluídos",
"cancelNote3": "Você pode reativar sua assinatura a qualquer momento", "cancelNote3": "Você pode reativar sua assinatura a qualquer momento",
"confirmCancel": "Sim, Cancelar" "confirmCancel": "Sim, Cancelar"
},
"landing": {
"nav": {
"features": "Recursos",
"pricing": "Preços",
"faq": "FAQ",
"login": "Entrar",
"register": "Começar Agora"
},
"hero": {
"title": "Assuma o Controle das suas",
"titleHighlight": "Finanças",
"subtitle": "Gestão financeira inteligente para pessoas e empresas. Acompanhe receitas, despesas e alcance seus objetivos financeiros.",
"cta": "Começar Grátis",
"ctaSecondary": "Ver Planos",
"previewBalance": "Saldo Total",
"previewIncome": "Receitas",
"previewExpense": "Despesas"
},
"features": {
"title": "Tudo que Você Precisa",
"subtitle": "Ferramentas poderosas para gerenciar seu dinheiro",
"item1": {
"title": "Múltiplas Contas",
"description": "Gerencie contas bancárias, cartões e dinheiro em um só lugar"
},
"item2": {
"title": "Categorias Inteligentes",
"description": "Categorização automática com palavras-chave e subcategorias"
},
"item3": {
"title": "Importação Bancária",
"description": "Importe extratos de Excel, CSV, OFX e PDF"
},
"item4": {
"title": "Relatórios Detalhados",
"description": "Gráficos e análises para entender seus gastos"
},
"item5": {
"title": "Metas e Orçamentos",
"description": "Defina objetivos e controle gastos mensais"
},
"item6": {
"title": "Multi-moeda",
"description": "Gerencie finanças em diferentes moedas"
}
},
"pricing": {
"title": "Planos Simples, Preços Justos",
"subtitle": "Escolha o plano que atende às suas necessidades",
"monthly": "Mensal",
"annual": "Anual",
"popular": "Mais Popular",
"perMonth": "/mês",
"perYear": "/ano",
"startFree": "Começar Grátis",
"subscribe": "Assinar Agora",
"billedAnnually": "Cobrado anualmente"
},
"faq": {
"title": "Perguntas Frequentes",
"q1": "Meus dados estão seguros?",
"a1": "Sim! Usamos criptografia de nível bancário (SSL/TLS) e seus dados são armazenados em servidores seguros com backups regulares. Nunca compartilhamos suas informações com terceiros.",
"q2": "Posso cancelar quando quiser?",
"a2": "Sim, você pode cancelar sua assinatura a qualquer momento sem taxas. Você manterá o acesso até o final do período que já pagou.",
"q3": "Quais bancos são compatíveis?",
"a3": "Você pode importar extratos de qualquer banco que exporte para Excel, CSV, OFX ou PDF. Temos mapeamentos predefinidos para os principais bancos.",
"q4": "Como funciona o período de teste?",
"a4": "Você tem acesso completo a todos os recursos durante o período de teste. Não é necessário cartão de crédito para começar. No final, escolha o plano que atende às suas necessidades."
},
"cta": {
"title": "Pronto para Transformar suas Finanças?",
"subtitle": "Junte-se a milhares de usuários que já assumiram o controle do seu dinheiro.",
"button": "Criar Conta Grátis"
},
"footer": {
"description": "Gestão Financeira Inteligente para pessoas e empresas.",
"product": "Produto",
"company": "Empresa",
"legal": "Legal",
"features": "Recursos",
"pricing": "Preços",
"about": "Sobre Nós",
"contact": "Contato",
"privacy": "Política de Privacidade",
"terms": "Termos de Uso",
"rights": "Todos os direitos reservados."
}
} }
} }

View File

@ -0,0 +1,349 @@
/* Landing Page Styles */
/* Variables */
:root {
--landing-primary: #3b82f6;
--landing-primary-dark: #1e40af;
--landing-secondary: #10b981;
--landing-dark: #0f172a;
--landing-dark-lighter: #1e293b;
--landing-text: #f1f5f9;
--landing-text-muted: #94a3b8;
--landing-border: #334155;
}
.landing-page {
background: var(--landing-dark);
color: var(--landing-text);
min-height: 100vh;
}
/* Navbar */
.landing-navbar {
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--landing-border);
padding: 1rem 0;
transition: all 0.3s ease;
}
.landing-navbar.scrolled {
padding: 0.5rem 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.landing-navbar .nav-link {
color: var(--landing-text-muted) !important;
font-weight: 500;
padding: 0.5rem 1rem !important;
transition: color 0.3s ease;
background: none;
border: none;
}
.landing-navbar .nav-link:hover {
color: var(--landing-text) !important;
}
/* Hero Section */
.hero-section {
position: relative;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 20% 20%, rgba(59, 130, 246, 0.15), transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(16, 185, 129, 0.1), transparent 50%);
}
.hero-section .container {
position: relative;
z-index: 1;
}
/* Dashboard Preview */
.hero-image {
perspective: 1000px;
}
.dashboard-preview {
background: var(--landing-dark-lighter);
border-radius: 16px;
border: 1px solid var(--landing-border);
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
transform: rotateY(-5deg) rotateX(5deg);
transition: transform 0.5s ease;
}
.dashboard-preview:hover {
transform: rotateY(0) rotateX(0);
}
.preview-header {
background: var(--landing-dark);
padding: 12px 16px;
border-bottom: 1px solid var(--landing-border);
}
.preview-dots {
display: flex;
gap: 8px;
}
.preview-dots .dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.preview-dots .dot.red { background: #ef4444; }
.preview-dots .dot.yellow { background: #f59e0b; }
.preview-dots .dot.green { background: #22c55e; }
.preview-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.preview-card {
background: var(--landing-dark);
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
font-size: 1.25rem;
font-weight: 600;
}
.preview-card i {
font-size: 2rem;
}
.preview-card.balance i { color: var(--landing-primary); }
.preview-card.income i { color: #22c55e; }
.preview-card.expense i { color: #ef4444; }
.preview-card.income span { color: #22c55e; }
.preview-card.expense span { color: #ef4444; }
/* Features Section */
.features-section {
background: var(--landing-dark-lighter);
}
.feature-card {
background: var(--landing-dark);
border-radius: 16px;
padding: 32px;
border: 1px solid var(--landing-border);
transition: transform 0.3s ease, border-color 0.3s ease;
}
.feature-card:hover {
transform: translateY(-8px);
border-color: var(--landing-primary);
}
.feature-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--landing-primary), var(--landing-secondary));
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.feature-icon i {
font-size: 28px;
color: white;
}
.feature-card h4 {
margin-bottom: 12px;
color: var(--landing-text);
}
/* Pricing Section */
.pricing-section {
background: var(--landing-dark);
}
.pricing-card {
background: var(--landing-dark-lighter);
border-radius: 16px;
padding: 32px;
border: 1px solid var(--landing-border);
transition: transform 0.3s ease, border-color 0.3s ease;
display: flex;
flex-direction: column;
}
.pricing-card:hover {
transform: translateY(-8px);
}
.pricing-card.featured {
border-color: var(--landing-primary);
box-shadow: 0 0 40px rgba(59, 130, 246, 0.2);
position: relative;
}
.featured-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--landing-primary), var(--landing-primary-dark));
color: white;
padding: 6px 20px;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.pricing-header {
text-align: center;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--landing-border);
}
.pricing-header h3 {
font-size: 1.5rem;
margin-bottom: 16px;
}
.price {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
}
.price .currency {
font-size: 1.5rem;
font-weight: 600;
color: var(--landing-text-muted);
}
.price .amount {
font-size: 3rem;
font-weight: 700;
color: var(--landing-text);
}
.price .period {
font-size: 1rem;
color: var(--landing-text-muted);
}
.billing-note {
font-size: 0.875rem;
color: var(--landing-text-muted);
margin-top: 8px;
}
.pricing-features {
list-style: none;
padding: 0;
margin: 0 0 24px 0;
flex-grow: 1;
}
.pricing-features li {
padding: 12px 0;
border-bottom: 1px solid var(--landing-border);
font-size: 0.95rem;
}
.pricing-features li:last-child {
border-bottom: none;
}
.pricing-footer {
margin-top: auto;
}
/* FAQ Section */
.faq-section {
background: var(--landing-dark-lighter);
}
.faq-section .accordion-item {
background: var(--landing-dark);
border: 1px solid var(--landing-border);
margin-bottom: 12px;
border-radius: 12px !important;
overflow: hidden;
}
.faq-section .accordion-button {
background: var(--landing-dark);
color: var(--landing-text);
font-weight: 500;
padding: 20px 24px;
}
.faq-section .accordion-button:not(.collapsed) {
background: var(--landing-dark);
color: var(--landing-primary);
}
.faq-section .accordion-button::after {
filter: invert(1);
}
.faq-section .accordion-button:focus {
box-shadow: none;
border-color: var(--landing-primary);
}
.faq-section .accordion-body {
background: var(--landing-dark);
color: var(--landing-text-muted);
padding: 0 24px 20px 24px;
}
/* CTA Section */
.cta-section {
background: linear-gradient(135deg, var(--landing-primary), var(--landing-primary-dark));
}
/* Footer */
.landing-footer {
background: var(--landing-dark);
border-top: 1px solid var(--landing-border);
}
.landing-footer a {
text-decoration: none;
transition: color 0.3s ease;
}
.landing-footer a:hover {
color: var(--landing-primary) !important;
}
/* Responsive */
@media (max-width: 768px) {
.hero-section .row {
min-height: auto;
padding-top: 100px;
}
.price .amount {
font-size: 2.5rem;
}
.feature-card {
padding: 24px;
}
}

View File

@ -0,0 +1,385 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import api from '../services/api';
import logo from '../assets/logo-white.png';
import './Landing.css';
export default function Landing() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { user, isAuthenticated } = useAuth();
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(true);
// Se já está autenticado, vai para dashboard
useEffect(() => {
if (isAuthenticated && user) {
navigate('/dashboard');
}
}, [isAuthenticated, user, navigate]);
useEffect(() => {
loadPlans();
}, []);
const loadPlans = async () => {
try {
const response = await api.get('/plans');
if (response.data.success) {
setPlans(response.data.data.plans);
}
} catch (error) {
console.error('Error loading plans:', error);
} finally {
setLoading(false);
}
};
const scrollToSection = (id) => {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
};
const features = [
{
icon: 'bi-wallet2',
title: t('landing.features.accounts.title'),
description: t('landing.features.accounts.description'),
},
{
icon: 'bi-graph-up-arrow',
title: t('landing.features.analytics.title'),
description: t('landing.features.analytics.description'),
},
{
icon: 'bi-tags',
title: t('landing.features.categories.title'),
description: t('landing.features.categories.description'),
},
{
icon: 'bi-cloud-upload',
title: t('landing.features.import.title'),
description: t('landing.features.import.description'),
},
{
icon: 'bi-arrow-repeat',
title: t('landing.features.recurring.title'),
description: t('landing.features.recurring.description'),
},
{
icon: 'bi-shield-check',
title: t('landing.features.security.title'),
description: t('landing.features.security.description'),
},
];
const faqs = [
{
question: t('landing.faq.q1'),
answer: t('landing.faq.a1'),
},
{
question: t('landing.faq.q2'),
answer: t('landing.faq.a2'),
},
{
question: t('landing.faq.q3'),
answer: t('landing.faq.a3'),
},
{
question: t('landing.faq.q4'),
answer: t('landing.faq.a4'),
},
];
return (
<div className="landing-page">
{/* Navigation */}
<nav className="navbar navbar-expand-lg navbar-dark fixed-top landing-navbar">
<div className="container">
<Link to="/" className="navbar-brand d-flex align-items-center">
<img src={logo} alt="WebMoney" height="40" className="me-2" />
<span className="fw-bold">WebMoney</span>
</Link>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav mx-auto">
<li className="nav-item">
<button className="nav-link btn btn-link" onClick={() => scrollToSection('features')}>
{t('landing.nav.features')}
</button>
</li>
<li className="nav-item">
<button className="nav-link btn btn-link" onClick={() => scrollToSection('pricing')}>
{t('landing.nav.pricing')}
</button>
</li>
<li className="nav-item">
<button className="nav-link btn btn-link" onClick={() => scrollToSection('faq')}>
{t('landing.nav.faq')}
</button>
</li>
</ul>
<div className="d-flex align-items-center gap-3">
<Link to="/login" className="btn btn-outline-light">
{t('landing.nav.login')}
</Link>
<Link to="/register" className="btn btn-primary">
{t('landing.nav.register')}
</Link>
</div>
</div>
</div>
</nav>
{/* Hero Section */}
<section className="hero-section">
<div className="hero-bg"></div>
<div className="container">
<div className="row align-items-center min-vh-100 py-5">
<div className="col-lg-6">
<h1 className="display-3 fw-bold text-white mb-4">
{t('landing.hero.title')}
</h1>
<p className="lead text-white-50 mb-4">
{t('landing.hero.subtitle')}
</p>
<div className="d-flex gap-3 flex-wrap">
<Link to="/register" className="btn btn-primary btn-lg">
<i className="bi bi-rocket-takeoff me-2"></i>
{t('landing.hero.cta')}
</Link>
<button
className="btn btn-outline-light btn-lg"
onClick={() => scrollToSection('features')}
>
{t('landing.hero.learnMore')}
</button>
</div>
{/* Trust badges */}
<div className="mt-5 d-flex align-items-center gap-4 text-white-50">
<div className="d-flex align-items-center">
<i className="bi bi-shield-check fs-4 me-2 text-success"></i>
<span>{t('landing.hero.secure')}</span>
</div>
<div className="d-flex align-items-center">
<i className="bi bi-credit-card fs-4 me-2 text-primary"></i>
<span>PayPal</span>
</div>
</div>
</div>
<div className="col-lg-6 d-none d-lg-block">
<div className="hero-image">
<div className="dashboard-preview">
<div className="preview-header">
<div className="preview-dots">
<span className="dot red"></span>
<span className="dot yellow"></span>
<span className="dot green"></span>
</div>
</div>
<div className="preview-content">
<div className="preview-card balance">
<i className="bi bi-wallet2"></i>
<span>12,450.00</span>
</div>
<div className="preview-card income">
<i className="bi bi-arrow-down-circle"></i>
<span>+3,200</span>
</div>
<div className="preview-card expense">
<i className="bi bi-arrow-up-circle"></i>
<span>-1,850</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="features-section py-5">
<div className="container py-5">
<div className="text-center mb-5">
<h2 className="display-5 fw-bold">{t('landing.features.title')}</h2>
<p className="lead text-muted">{t('landing.features.subtitle')}</p>
</div>
<div className="row g-4">
{features.map((feature, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="feature-card h-100">
<div className="feature-icon">
<i className={`bi ${feature.icon}`}></i>
</div>
<h4>{feature.title}</h4>
<p className="text-muted">{feature.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Pricing Section */}
<section id="pricing" className="pricing-section py-5">
<div className="container py-5">
<div className="text-center mb-5">
<h2 className="display-5 fw-bold">{t('landing.pricing.title')}</h2>
<p className="lead text-muted">{t('landing.pricing.subtitle')}</p>
</div>
{loading ? (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<div className="row justify-content-center g-4">
{plans.map((plan) => (
<div key={plan.id} className="col-lg-4 col-md-6">
<div className={`pricing-card h-100 ${plan.is_featured ? 'featured' : ''}`}>
{plan.is_featured && (
<div className="featured-badge">
<i className="bi bi-star-fill me-1"></i>
{t('landing.pricing.popular')}
</div>
)}
<div className="pricing-header">
<h3>{plan.name}</h3>
<div className="price">
{plan.is_free ? (
<span className="amount">{t('landing.pricing.free')}</span>
) : (
<>
<span className="currency"></span>
<span className="amount">{plan.monthly_price?.toFixed(2) || plan.price}</span>
<span className="period">/{t('landing.pricing.month')}</span>
</>
)}
</div>
{plan.billing_period === 'annual' && !plan.is_free && (
<p className="billing-note">
{t('landing.pricing.billedAnnually', { price: plan.price })}
</p>
)}
</div>
<ul className="pricing-features">
{(plan.features || []).map((feature, idx) => (
<li key={idx}>
<i className="bi bi-check-circle-fill text-success me-2"></i>
{feature}
</li>
))}
</ul>
<div className="pricing-footer">
<Link
to={`/register?plan=${plan.slug}`}
className={`btn w-100 ${plan.is_featured ? 'btn-primary' : 'btn-outline-primary'}`}
>
{plan.is_free ? t('landing.pricing.startFree') : t('landing.pricing.subscribe')}
</Link>
</div>
</div>
</div>
))}
</div>
)}
</div>
</section>
{/* FAQ Section */}
<section id="faq" className="faq-section py-5">
<div className="container py-5">
<div className="text-center mb-5">
<h2 className="display-5 fw-bold">{t('landing.faq.title')}</h2>
</div>
<div className="row justify-content-center">
<div className="col-lg-8">
<div className="accordion" id="faqAccordion">
{faqs.map((faq, index) => (
<div key={index} className="accordion-item">
<h2 className="accordion-header">
<button
className={`accordion-button ${index !== 0 ? 'collapsed' : ''}`}
type="button"
data-bs-toggle="collapse"
data-bs-target={`#faq${index}`}
>
{faq.question}
</button>
</h2>
<div
id={`faq${index}`}
className={`accordion-collapse collapse ${index === 0 ? 'show' : ''}`}
data-bs-parent="#faqAccordion"
>
<div className="accordion-body">
{faq.answer}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="cta-section py-5">
<div className="container py-5 text-center">
<h2 className="display-5 fw-bold text-white mb-4">
{t('landing.cta.title')}
</h2>
<p className="lead text-white-50 mb-4">
{t('landing.cta.subtitle')}
</p>
<Link to="/register" className="btn btn-light btn-lg">
<i className="bi bi-person-plus me-2"></i>
{t('landing.cta.button')}
</Link>
</div>
</section>
{/* Footer */}
<footer className="landing-footer py-4">
<div className="container">
<div className="row align-items-center">
<div className="col-md-6">
<div className="d-flex align-items-center">
<img src={logo} alt="WebMoney" height="30" className="me-2" />
<span className="text-muted">© 2025 WebMoney. {t('landing.footer.rights')}</span>
</div>
</div>
<div className="col-md-6 text-md-end mt-3 mt-md-0">
<a href="#" className="text-muted me-3">{t('landing.footer.privacy')}</a>
<a href="#" className="text-muted me-3">{t('landing.footer.terms')}</a>
<a href="mailto:support@webmoney.app" className="text-muted">{t('landing.footer.contact')}</a>
</div>
</div>
</div>
</footer>
</div>
);
}

View File

@ -1,12 +1,18 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import api from '../services/api';
import logo from '../assets/logo-white.png'; import logo from '../assets/logo-white.png';
const Register = () => { const Register = () => {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { register } = useAuth(); const { register } = useAuth();
const [plans, setPlans] = useState([]);
const [selectedPlan, setSelectedPlan] = useState(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
@ -15,6 +21,31 @@ const Register = () => {
}); });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPlans, setLoadingPlans] = useState(true);
// Carregar planos
useEffect(() => {
const fetchPlans = async () => {
try {
const response = await api.get('/plans');
setPlans(response.data.data || response.data);
// Verificar se há plano na URL
const planSlug = searchParams.get('plan');
if (planSlug && response.data.data) {
const plan = response.data.data.find(p => p.slug === planSlug);
if (plan) {
setSelectedPlan(plan);
}
}
} catch (error) {
console.error('Error loading plans:', error);
} finally {
setLoadingPlans(false);
}
};
fetchPlans();
}, [searchParams]);
const handleChange = (e) => { const handleChange = (e) => {
setFormData({ setFormData({
@ -33,8 +64,26 @@ const Register = () => {
setErrors({}); setErrors({});
try { try {
const response = await register(formData); const response = await register({
...formData,
plan_id: selectedPlan?.id,
});
if (response.success) { if (response.success) {
// Se for plano pago, redirecionar para pagamento
if (selectedPlan && selectedPlan.price > 0) {
try {
const subscriptionResponse = await api.post('/subscription/subscribe', {
plan_id: selectedPlan.id,
});
if (subscriptionResponse.data.approve_url) {
window.location.href = subscriptionResponse.data.approve_url;
return;
}
} catch (subError) {
console.error('Subscription error:', subError);
// Continuar para o dashboard mesmo sem subscrição
}
}
navigate('/dashboard'); navigate('/dashboard');
} }
} catch (error) { } catch (error) {
@ -53,15 +102,57 @@ const Register = () => {
return ( return (
<div className="container"> <div className="container">
<div className="row justify-content-center align-items-center min-vh-100"> <div className="row justify-content-center align-items-center min-vh-100">
<div className="col-md-6"> <div className="col-lg-8">
<div className="card shadow-lg border-0"> <div className="card shadow-lg border-0">
<div className="card-body p-5"> <div className="card-body p-5">
<div className="text-center mb-4"> <div className="text-center mb-4">
<img src={logo} alt="WebMoney" className="mb-3" style={{ height: '80px', width: 'auto' }} /> <Link to="/">
<img src={logo} alt="WebMoney" className="mb-3" style={{ height: '80px', width: 'auto' }} />
</Link>
<h2 className="fw-bold text-primary">WebMoney</h2> <h2 className="fw-bold text-primary">WebMoney</h2>
<p className="text-muted">Crie sua conta</p> <p className="text-muted">{t('auth.createAccount', 'Crea tu cuenta')}</p>
</div> </div>
{/* Plan Selection */}
{!loadingPlans && plans.length > 0 && (
<div className="mb-4">
<label className="form-label fw-semibold">
<i className="bi bi-box me-2"></i>
{t('register.selectPlan', 'Selecciona un plan')}
</label>
<div className="row g-3">
{plans.map((plan) => (
<div key={plan.id} className="col-md-4">
<div
className={`card h-100 cursor-pointer ${selectedPlan?.id === plan.id ? 'border-primary bg-primary bg-opacity-10' : ''}`}
style={{ cursor: 'pointer' }}
onClick={() => setSelectedPlan(plan)}
>
<div className="card-body text-center p-3">
<h6 className="card-title mb-1">{plan.name}</h6>
<p className="h5 mb-1 text-primary">
{plan.price > 0 ? `${plan.price}` : t('pricing.free', 'Gratis')}
{plan.price > 0 && <small className="text-muted fs-6">/{plan.billing_period === 'yearly' ? t('pricing.year', 'año') : t('pricing.month', 'mes')}</small>}
</p>
{plan.trial_days > 0 && (
<small className="text-success">
<i className="bi bi-gift me-1"></i>
{plan.trial_days} {t('common.days', 'días')} {t('pricing.trial', 'de prueba')}
</small>
)}
{selectedPlan?.id === plan.id && (
<div className="mt-2">
<i className="bi bi-check-circle-fill text-primary"></i>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{errors.general && ( {errors.general && (
<div className="alert alert-danger" role="alert"> <div className="alert alert-danger" role="alert">
<i className="bi bi-exclamation-circle me-2"></i> <i className="bi bi-exclamation-circle me-2"></i>
@ -70,106 +161,122 @@ const Register = () => {
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mb-3"> <div className="row">
<label htmlFor="name" className="form-label"> <div className="col-md-6 mb-3">
Nombre completo <label htmlFor="name" className="form-label">
</label> {t('profile.name', 'Nombre completo')}
<input </label>
type="text" <input
className={`form-control ${errors.name ? 'is-invalid' : ''}`} type="text"
id="name" className={`form-control ${errors.name ? 'is-invalid' : ''}`}
name="name" id="name"
value={formData.name} name="name"
onChange={handleChange} value={formData.name}
placeholder="Tu nombre" onChange={handleChange}
required placeholder={t('profile.namePlaceholder', 'Tu nombre')}
/> required
{errors.name && ( />
<div className="invalid-feedback">{errors.name}</div> {errors.name && (
)} <div className="invalid-feedback">{errors.name}</div>
)}
</div>
<div className="col-md-6 mb-3">
<label htmlFor="email" className="form-label">
{t('auth.email', 'Email')}
</label>
<input
type="email"
className={`form-control ${errors.email ? 'is-invalid' : ''}`}
id="email"
name="email"
autoComplete="email"
value={formData.email}
onChange={handleChange}
placeholder="tu@email.com"
required
/>
{errors.email && (
<div className="invalid-feedback">{errors.email}</div>
)}
</div>
</div> </div>
<div className="mb-3"> <div className="row">
<label htmlFor="email" className="form-label"> <div className="col-md-6 mb-3">
Email <label htmlFor="password" className="form-label">
</label> {t('auth.password', 'Contraseña')}
<input </label>
type="email" <input
className={`form-control ${errors.email ? 'is-invalid' : ''}`} type="password"
id="email" className={`form-control ${errors.password ? 'is-invalid' : ''}`}
name="email" id="password"
autoComplete="email" name="password"
value={formData.email} autoComplete="new-password"
onChange={handleChange} value={formData.password}
placeholder="tu@email.com" onChange={handleChange}
required placeholder={t('profile.passwordHint', 'Mínimo 8 caracteres')}
/> required
{errors.email && ( />
<div className="invalid-feedback">{errors.email}</div> {errors.password && (
)} <div className="invalid-feedback">{errors.password}</div>
</div> )}
</div>
<div className="mb-3"> <div className="col-md-6 mb-3">
<label htmlFor="password" className="form-label"> <label htmlFor="password_confirmation" className="form-label">
Contraseña {t('profile.confirmPassword', 'Confirmar contraseña')}
</label> </label>
<input <input
type="password" type="password"
className={`form-control ${errors.password ? 'is-invalid' : ''}`} className={`form-control ${errors.password_confirmation ? 'is-invalid' : ''}`}
id="password" id="password_confirmation"
name="password" name="password_confirmation"
autoComplete="new-password" autoComplete="new-password"
value={formData.password} value={formData.password_confirmation}
onChange={handleChange} onChange={handleChange}
placeholder="Mínimo 8 caracteres" placeholder={t('register.repeatPassword', 'Repite tu contraseña')}
required required
/> />
{errors.password && ( {errors.password_confirmation && (
<div className="invalid-feedback">{errors.password}</div> <div className="invalid-feedback">{errors.password_confirmation}</div>
)} )}
</div> </div>
<div className="mb-3">
<label htmlFor="password_confirmation" className="form-label">
Confirmar contraseña
</label>
<input
type="password"
className={`form-control ${errors.password_confirmation ? 'is-invalid' : ''}`}
id="password_confirmation"
name="password_confirmation"
autoComplete="new-password"
value={formData.password_confirmation}
onChange={handleChange}
placeholder="Repite tu contraseña"
required
/>
{errors.password_confirmation && (
<div className="invalid-feedback">{errors.password_confirmation}</div>
)}
</div> </div>
<button <button
type="submit" type="submit"
className="btn btn-primary w-100 py-2" className="btn btn-primary w-100 py-2 mt-3"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (
<> <>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> <span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Registrando... {t('common.processing', 'Procesando...')}
</>
) : selectedPlan && selectedPlan.price > 0 ? (
<>
<i className="bi bi-credit-card me-2"></i>
{t('register.continueToPayment', 'Continuar al pago')}
</> </>
) : ( ) : (
'Crear Cuenta' t('register.createAccount', 'Crear Cuenta')
)} )}
</button> </button>
{selectedPlan && selectedPlan.price > 0 && (
<p className="text-center text-muted mt-2 small">
<i className="bi bi-shield-check me-1"></i>
{t('pricing.paypalSecure', 'Pago seguro con PayPal')}
</p>
)}
</form> </form>
<div className="text-center mt-4"> <div className="text-center mt-4">
<p className="mb-0"> <p className="mb-0">
¿Ya tienes cuenta?{' '} {t('register.alreadyHaveAccount', '¿Ya tienes cuenta?')}{' '}
<Link to="/login" className="text-decoration-none fw-semibold"> <Link to="/login" className="text-decoration-none fw-semibold">
Inicia sesión aquí {t('register.loginHere', 'Inicia sesión aquí')}
</Link> </Link>
</p> </p>
</div> </div>