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:
parent
c99bca9404
commit
984855e36c
38
CHANGELOG.md
38
CHANGELOG.md
@ -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/).
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -6,6 +6,7 @@ import ProtectedRoute from './components/ProtectedRoute';
|
||||
import Layout from './components/Layout';
|
||||
import CookieConsent from './components/CookieConsent';
|
||||
import Login from './pages/Login';
|
||||
import Landing from './pages/Landing';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Accounts from './pages/Accounts';
|
||||
import CostCenters from './pages/CostCenters';
|
||||
@ -27,6 +28,7 @@ import Pricing from './pages/Pricing';
|
||||
import Billing from './pages/Billing';
|
||||
import Users from './pages/Users';
|
||||
import SiteSettings from './pages/SiteSettings';
|
||||
import Register from './pages/Register';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -35,6 +37,7 @@ function App() {
|
||||
<ToastProvider>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
@ -243,7 +246,7 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/dashboard" />} />
|
||||
<Route path="/" element={<Landing />} />
|
||||
</Routes>
|
||||
<CookieConsent />
|
||||
</ToastProvider>
|
||||
|
||||
@ -2123,5 +2123,93 @@
|
||||
"cancelNote2": "Your data will not be deleted",
|
||||
"cancelNote3": "You can reactivate your subscription at any time",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2125,5 +2125,93 @@
|
||||
"cancelNote2": "Tus datos no se eliminarán",
|
||||
"cancelNote3": "Puedes reactivar tu suscripción en cualquier momento",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2143,5 +2143,93 @@
|
||||
"cancelNote2": "Seus dados não serão excluídos",
|
||||
"cancelNote3": "Você pode reativar sua assinatura a qualquer momento",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
349
frontend/src/pages/Landing.css
Normal file
349
frontend/src/pages/Landing.css
Normal 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;
|
||||
}
|
||||
}
|
||||
385
frontend/src/pages/Landing.jsx
Normal file
385
frontend/src/pages/Landing.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,18 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Footer from '../components/Footer';
|
||||
import api from '../services/api';
|
||||
import logo from '../assets/logo-white.png';
|
||||
|
||||
const Register = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { register } = useAuth();
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
@ -15,6 +21,31 @@ const Register = () => {
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
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) => {
|
||||
setFormData({
|
||||
@ -33,8 +64,26 @@ const Register = () => {
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
const response = await register(formData);
|
||||
const response = await register({
|
||||
...formData,
|
||||
plan_id: selectedPlan?.id,
|
||||
});
|
||||
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');
|
||||
}
|
||||
} catch (error) {
|
||||
@ -53,15 +102,57 @@ const Register = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<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-body p-5">
|
||||
<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>
|
||||
<p className="text-muted">Crie sua conta</p>
|
||||
<p className="text-muted">{t('auth.createAccount', 'Crea tu cuenta')}</p>
|
||||
</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 && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<i className="bi bi-exclamation-circle me-2"></i>
|
||||
@ -70,106 +161,122 @@ const Register = () => {
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="name" className="form-label">
|
||||
Nombre completo
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${errors.name ? 'is-invalid' : ''}`}
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Tu nombre"
|
||||
required
|
||||
/>
|
||||
{errors.name && (
|
||||
<div className="invalid-feedback">{errors.name}</div>
|
||||
)}
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-3">
|
||||
<label htmlFor="name" className="form-label">
|
||||
{t('profile.name', 'Nombre completo')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${errors.name ? 'is-invalid' : ''}`}
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder={t('profile.namePlaceholder', 'Tu nombre')}
|
||||
required
|
||||
/>
|
||||
{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 className="mb-3">
|
||||
<label htmlFor="email" className="form-label">
|
||||
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 className="row">
|
||||
<div className="col-md-6 mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
{t('auth.password', 'Contraseña')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${errors.password ? 'is-invalid' : ''}`}
|
||||
id="password"
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder={t('profile.passwordHint', 'Mínimo 8 caracteres')}
|
||||
required
|
||||
/>
|
||||
{errors.password && (
|
||||
<div className="invalid-feedback">{errors.password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${errors.password ? 'is-invalid' : ''}`}
|
||||
id="password"
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
required
|
||||
/>
|
||||
{errors.password && (
|
||||
<div className="invalid-feedback">{errors.password}</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 className="col-md-6 mb-3">
|
||||
<label htmlFor="password_confirmation" className="form-label">
|
||||
{t('profile.confirmPassword', '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={t('register.repeatPassword', 'Repite tu contraseña')}
|
||||
required
|
||||
/>
|
||||
{errors.password_confirmation && (
|
||||
<div className="invalid-feedback">{errors.password_confirmation}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100 py-2"
|
||||
className="btn btn-primary w-100 py-2 mt-3"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{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>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<p className="mb-0">
|
||||
¿Ya tienes cuenta?{' '}
|
||||
{t('register.alreadyHaveAccount', '¿Ya tienes cuenta?')}{' '}
|
||||
<Link to="/login" className="text-decoration-none fw-semibold">
|
||||
Inicia sesión aquí
|
||||
{t('register.loginHere', 'Inicia sesión aquí')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user