Improve the register page

This commit is contained in:
Urtzi Alfaro
2025-08-11 08:15:13 +02:00
parent 652a850d0f
commit 7c237c0acc
6 changed files with 523 additions and 101 deletions

View File

@@ -12,4 +12,12 @@ VITE_ENABLE_WEBSOCKETS=false
VITE_ENABLE_OFFLINE=false VITE_ENABLE_OFFLINE=false
VITE_ENABLE_OPTIMISTIC_UPDATES=true VITE_ENABLE_OPTIMISTIC_UPDATES=true
VITE_ENABLE_DEDUPLICATION=true VITE_ENABLE_DEDUPLICATION=true
VITE_ENABLE_METRICS=false VITE_ENABLE_METRICS=false
# Stripe Configuration (Spanish Market)
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_example_key_for_development
VITE_STRIPE_WEBHOOK_SECRET=whsec_example_webhook_secret_for_development
# Development Flags
VITE_BYPASS_PAYMENT=true
VITE_DEV_MODE=true

23
frontend/.env.production Normal file
View File

@@ -0,0 +1,23 @@
# API Configuration
VITE_API_URL=https://api.pania.es/api/v1
VITE_API_TIMEOUT=30000
VITE_API_RETRIES=3
VITE_API_RETRY_DELAY=1000
VITE_API_LOGGING=false
VITE_API_CACHING=true
VITE_API_CACHE_TIMEOUT=300000
# Feature Flags
VITE_ENABLE_WEBSOCKETS=true
VITE_ENABLE_OFFLINE=true
VITE_ENABLE_OPTIMISTIC_UPDATES=true
VITE_ENABLE_DEDUPLICATION=true
VITE_ENABLE_METRICS=true
# Stripe Configuration (Spanish Market)
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_your_production_stripe_key
VITE_STRIPE_WEBHOOK_SECRET=whsec_your_production_webhook_secret
# Development Flags (DISABLED IN PRODUCTION)
VITE_BYPASS_PAYMENT=false
VITE_DEV_MODE=false

View File

@@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.3.1", "@hookform/resolvers": "^3.3.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0", "date-fns-tz": "^2.0.0",
@@ -1213,6 +1215,29 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stripe/react-stripe-js": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.9.0.tgz",
"integrity": "sha512-pN1Re7zUc3m61FFQROok685g3zsBQRzCmZDmTzO8iPU6zhLvu2JnC0LrG0FCzSp6kgGa8AQSzq4rpFSgyhkjKg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": ">=1.44.1 <8.0.0",
"react": ">=16.8.0 <20.0.0",
"react-dom": ">=16.8.0 <20.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.8.0.tgz",
"integrity": "sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@tailwindcss/forms": { "node_modules/@tailwindcss/forms": {
"version": "0.5.10", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@@ -1242,20 +1267,6 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
} }
}, },
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -1605,13 +1616,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.1.0", "version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.8.0" "undici-types": "~7.10.0"
} }
}, },
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
@@ -2559,9 +2570,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.25.1", "version": "4.25.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2579,8 +2590,8 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001726", "caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.173", "electron-to-chromium": "^1.5.199",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3" "update-browserslist-db": "^1.1.3"
}, },
@@ -2672,9 +2683,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001731", "version": "1.0.30001734",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3252,9 +3263,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.194", "version": "1.5.199",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz",
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -5775,7 +5786,7 @@
"postcss": "^8.2.14" "postcss": "^8.2.14"
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
@@ -5789,6 +5800,20 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": { "node_modules/postcss-value-parser": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@@ -6869,6 +6894,20 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -7033,9 +7072,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.8.0", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -7575,9 +7614,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.8.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {

View File

@@ -14,46 +14,48 @@
"lint:fix": "eslint . --ext ts,tsx --fix" "lint:fix": "eslint . --ext ts,tsx --fix"
}, },
"dependencies": { "dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"@reduxjs/toolkit": "^1.9.5",
"react-redux": "^8.1.2",
"i18next": "^23.4.4",
"react-i18next": "^13.1.2",
"i18next-browser-languagedetector": "^7.1.0",
"react-hook-form": "^7.45.4",
"@hookform/resolvers": "^3.3.1", "@hookform/resolvers": "^3.3.1",
"zod": "^3.22.2", "@reduxjs/toolkit": "^1.9.5",
"recharts": "^2.8.0", "@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0", "date-fns-tz": "^2.0.0",
"react-hot-toast": "^2.4.1", "i18next": "^23.4.4",
"i18next-browser-languagedetector": "^7.1.0",
"lucide-react": "^0.263.1", "lucide-react": "^0.263.1",
"clsx": "^2.0.0", "react": "^18.2.0",
"tailwind-merge": "^1.14.0" "react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-hot-toast": "^2.4.1",
"react-i18next": "^13.1.2",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"recharts": "^2.8.0",
"tailwind-merge": "^1.14.0",
"zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.4",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",
"@vitest/ui": "^0.34.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"@tailwindcss/forms": "^0.5.4",
"@tailwindcss/typography": "^0.5.9",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.5", "vite": "^4.4.5",
"vitest": "^0.34.1", "vitest": "^0.34.1"
"@vitest/ui": "^0.34.1",
"@testing-library/react": "^13.4.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/user-event": "^14.4.3"
}, },
"keywords": [ "keywords": [
"bakery", "bakery",
@@ -70,4 +72,4 @@
}, },
"author": "PanIA Team", "author": "PanIA Team",
"license": "MIT" "license": "MIT"
} }

View File

@@ -1,12 +1,48 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Eye, EyeOff, Loader2, Check } from 'lucide-react'; import { Eye, EyeOff, Loader2, Check, CreditCard, Shield, ArrowRight } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import { import {
useAuth, useAuth,
RegisterRequest RegisterRequest
} from '../../api'; } from '../../api';
// Development flags
const isDevelopment = import.meta.env.DEV;
const bypassPayment = import.meta.env.VITE_BYPASS_PAYMENT === 'true';
// Initialize Stripe with Spanish market configuration (only if not bypassing)
const stripePromise = !bypassPayment ? loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '', {
locale: 'es'
}) : null;
// Stripe card element options for Spanish market
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#374151',
'::placeholder': {
color: '#9CA3AF',
},
},
invalid: {
color: '#EF4444',
},
},
hidePostalCode: false, // Keep postal code for better fraud protection
};
// Subscription pricing (monthly)
const SUBSCRIPTION_PRICE_EUR = 29.99;
interface RegisterPageProps { interface RegisterPageProps {
onLogin: (user: any, token: string) => void; onLogin: (user: any, token: string) => void;
onNavigateToLogin: () => void; onNavigateToLogin: () => void;
@@ -15,27 +51,201 @@ interface RegisterPageProps {
interface RegisterForm { interface RegisterForm {
fullName: string; fullName: string;
email: string; email: string;
confirmEmail: string;
password: string; password: string;
confirmPassword: string; confirmPassword: string;
acceptTerms: boolean; acceptTerms: boolean;
paymentCompleted: boolean;
} }
const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => { const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => {
const { register, isLoading, error } = useAuth(); const { register, isLoading } = useAuth();
const [formData, setFormData] = useState<RegisterForm>({ const [formData, setFormData] = useState<RegisterForm>({
fullName: '', fullName: '',
email: '', email: '',
confirmEmail: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
acceptTerms: false acceptTerms: false,
paymentCompleted: false
}); });
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<Partial<RegisterForm>>({}); const [errors, setErrors] = useState<Partial<RegisterForm>>({});
const [passwordStrength, setPasswordStrength] = useState<{
score: number;
checks: { [key: string]: boolean };
message: string;
}>({ score: 0, checks: {}, message: '' });
const [paymentStep, setPaymentStep] = useState<'form' | 'payment' | 'processing' | 'completed'>('form');
const [paymentLoading, setPaymentLoading] = useState(false);
const validateForm = (): boolean => { // Update password strength in real-time
useEffect(() => {
if (formData.password) {
const validation = validatePassword(formData.password);
setPasswordStrength(validation);
}
}, [formData.password]);
// Payment processing component
const PaymentForm: React.FC<{ onPaymentSuccess: () => void }> = ({ onPaymentSuccess }) => {
const stripe = useStripe();
const elements = useElements();
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
toast.error('Stripe no está cargado correctamente');
return;
}
const card = elements.getElement(CardElement);
if (!card) {
toast.error('Elemento de tarjeta no encontrado');
return;
}
setPaymentLoading(true);
try {
// Create payment method
const { error } = await stripe.createPaymentMethod({
type: 'card',
card,
billing_details: {
name: formData.fullName,
email: formData.email,
},
});
if (error) {
throw new Error(error.message);
}
// Here you would typically create the subscription via your backend
// For now, we'll simulate a successful payment
toast.success('¡Pago procesado correctamente!');
setFormData(prev => ({ ...prev, paymentCompleted: true }));
setPaymentStep('completed');
onPaymentSuccess();
} catch (error) {
console.error('Payment error:', error);
toast.error(error instanceof Error ? error.message : 'Error procesando el pago');
} finally {
setPaymentLoading(false);
}
};
return (
<form onSubmit={handlePayment} className="space-y-6">
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6">
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center">
<CreditCard className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Suscripción PanIA Pro</h3>
<p className="text-sm text-gray-600">Facturación mensual</p>
</div>
<div className="ml-auto text-right">
<div className="text-2xl font-bold text-primary-600">{SUBSCRIPTION_PRICE_EUR}</div>
<div className="text-sm text-gray-500">/mes</div>
</div>
</div>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Predicciones de demanda ilimitadas
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Análisis de tendencias avanzado
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Soporte técnico prioritario
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Integración con sistemas de punto de venta
</div>
</div>
</div>
<div className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-gray-700 mb-2 block">
Información de la tarjeta
</span>
<div className="border border-gray-300 rounded-xl p-4 focus-within:ring-2 focus-within:ring-primary-500 focus-within:border-primary-500">
<CardElement options={cardElementOptions} />
</div>
</label>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<Shield className="w-4 h-4" />
<span>Pago seguro con encriptación SSL. Powered by Stripe.</span>
</div>
</div>
<button
type="submit"
disabled={!stripe || paymentLoading}
className="w-full bg-primary-500 text-white py-3 px-4 rounded-xl font-medium hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
>
{paymentLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Procesando pago...
</>
) : (
<>
<CreditCard className="w-5 h-5 mr-2" />
Pagar {SUBSCRIPTION_PRICE_EUR}/mes
</>
)}
</button>
</form>
);
};
// Password validation based on backend rules
const validatePassword = (password: string) => {
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
numbers: /\d/.test(password),
// symbols: /[!@#$%^&*(),.?":{}|<>]/.test(password) // Backend doesn't require symbols
};
const score = Object.values(checks).filter(Boolean).length;
let message = '';
if (score < 4) {
if (!checks.length) message += 'Mínimo 8 caracteres. ';
if (!checks.uppercase) message += 'Una mayúscula. ';
if (!checks.lowercase) message += 'Una minúscula. ';
if (!checks.numbers) message += 'Un número. ';
} else {
message = '¡Contraseña segura!';
}
return { score, checks, message: message.trim() };
};
const handleFormSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate form but exclude payment requirement for first step
const newErrors: Partial<RegisterForm> = {}; const newErrors: Partial<RegisterForm> = {};
if (!formData.fullName.trim()) { if (!formData.fullName.trim()) {
@@ -50,10 +260,19 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
newErrors.email = 'El email no es válido'; newErrors.email = 'El email no es válido';
} }
if (!formData.confirmEmail) {
newErrors.confirmEmail = 'Confirma tu email';
} else if (formData.email !== formData.confirmEmail) {
newErrors.confirmEmail = 'Los emails no coinciden';
}
if (!formData.password) { if (!formData.password) {
newErrors.password = 'La contraseña es obligatoria'; newErrors.password = 'La contraseña es obligatoria';
} else if (formData.password.length < 8) { } else {
newErrors.password = 'La contraseña debe tener al menos 8 caracteres'; const passwordValidation = validatePassword(formData.password);
if (passwordValidation.score < 4) {
newErrors.password = passwordValidation.message;
}
} }
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
@@ -65,13 +284,29 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
} }
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0;
if (Object.keys(newErrors).length > 0) return;
// Move to payment step, or bypass if in development mode
if (bypassPayment) {
// Development bypass: simulate payment completion
setFormData(prev => ({ ...prev, paymentCompleted: true }));
setPaymentStep('completed');
toast.success('🚀 Modo desarrollo: Pago omitido');
// Proceed directly to registration
setTimeout(() => {
handleRegistrationComplete();
}, 1500);
} else {
setPaymentStep('payment');
}
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleRegistrationComplete = async () => {
e.preventDefault(); if (!bypassPayment && !formData.paymentCompleted) {
toast.error('El pago debe completarse antes del registro');
if (!validateForm()) return; return;
}
try { try {
const registerData: RegisterRequest = { const registerData: RegisterRequest = {
@@ -97,6 +332,9 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
toast.error(error instanceof Error ? error.message : 'Error en el registro'); toast.error(error instanceof Error ? error.message : 'Error en el registro');
// Reset payment if registration fails
setFormData(prev => ({ ...prev, paymentCompleted: false }));
setPaymentStep('payment');
} }
}; };
@@ -116,19 +354,68 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
} }
}; };
const getPasswordStrength = (password: string) => {
let strength = 0;
if (password.length >= 8) strength++;
if (/[a-z]/.test(password)) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/\d/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
return strength;
};
const passwordStrength = getPasswordStrength(formData.password); // Render different content based on payment step
const strengthLabels = ['Muy débil', 'Débil', 'Regular', 'Buena', 'Excelente']; if (paymentStep === 'payment' && !bypassPayment) {
const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500']; return (
<Elements stripe={stripePromise}>
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Logo and Header */}
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<span className="text-white text-2xl font-bold">🥖</span>
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
Finalizar Registro
</h1>
<p className="text-gray-600 text-lg">
Solo un paso más para comenzar
</p>
<p className="text-gray-500 text-sm mt-2">
Suscripción segura con Stripe
</p>
</div>
{/* Payment Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<PaymentForm onPaymentSuccess={handleRegistrationComplete} />
<button
onClick={() => setPaymentStep('form')}
className="w-full mt-4 text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
Volver al formulario
</button>
</div>
</div>
</div>
</Elements>
);
}
if (paymentStep === 'completed') {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-green-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<Check className="text-white text-2xl" />
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
¡Bienvenido a PanIA!
</h1>
<p className="text-gray-600 text-lg">
Tu cuenta ha sido creada exitosamente
</p>
<p className="text-gray-500 text-sm mt-2">
Redirigiendo al panel de control...
</p>
</div>
</div>
</div>
);
}
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
@@ -151,7 +438,7 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{/* Register Form */} {/* Register Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl"> <div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleFormSubmit}>
{/* Full Name Field */} {/* Full Name Field */}
<div> <div>
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
@@ -212,6 +499,43 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
)} )}
</div> </div>
{/* Confirm Email Field */}
<div>
<label htmlFor="confirmEmail" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar correo electrónico
</label>
<input
id="confirmEmail"
name="confirmEmail"
type="email"
autoComplete="email"
required
value={formData.confirmEmail}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.confirmEmail
? 'border-red-300 bg-red-50'
: formData.confirmEmail && formData.email === formData.confirmEmail
? 'border-green-300 bg-green-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{formData.confirmEmail && formData.email === formData.confirmEmail && (
<div className="absolute inset-y-0 right-3 flex items-center mt-8">
<Check className="h-5 w-5 text-green-500" />
</div>
)}
{errors.confirmEmail && (
<p className="mt-1 text-sm text-red-600">{errors.confirmEmail}</p>
)}
</div>
{/* Password Field */} {/* Password Field */}
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
@@ -251,21 +575,30 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
</button> </button>
</div> </div>
{/* Password Strength Indicator */} {/* Enhanced Password Strength Indicator */}
{formData.password && ( {formData.password && (
<div className="mt-2"> <div className="mt-2">
<div className="flex space-x-1"> <div className="grid grid-cols-4 gap-1">
{[...Array(5)].map((_, i) => ( {Object.entries(passwordStrength.checks).map(([key, passed], index) => {
<div const labels = {
key={i} length: '8+ caracteres',
className={`h-1 flex-1 rounded ${ uppercase: 'Mayúscula',
i < passwordStrength ? strengthColors[passwordStrength - 1] : 'bg-gray-200' lowercase: 'Minúscula',
}`} numbers: 'Número'
/> };
))} return (
<div key={key} className={`text-xs p-1 rounded text-center ${
passed ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
}`}>
{passed ? '✓' : '○'} {labels[key as keyof typeof labels]}
</div>
);
})}
</div> </div>
<p className="text-xs text-gray-600 mt-1"> <p className={`text-xs mt-1 ${
Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'} passwordStrength.score === 4 ? 'text-green-600' : 'text-gray-600'
}`}>
{passwordStrength.message}
</p> </p>
</div> </div>
)} )}
@@ -370,10 +703,13 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="h-5 w-5 mr-2 animate-spin" /> <Loader2 className="h-5 w-5 mr-2 animate-spin" />
Creando cuenta... Validando...
</> </>
) : ( ) : (
'Crear cuenta gratis' <>
{bypassPayment ? 'Crear Cuenta (Dev)' : 'Continuar al Pago'}
<ArrowRight className="h-5 w-5 ml-2" />
</>
)} )}
</button> </button>
</div> </div>
@@ -395,8 +731,18 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{/* Benefits */} {/* Benefits */}
<div className="text-center"> <div className="text-center">
{bypassPayment && (
<div className="mb-4 p-2 bg-yellow-100 border border-yellow-300 rounded-lg">
<p className="text-xs text-yellow-800">
🚀 Modo Desarrollo: Pago omitido para pruebas
</p>
</div>
)}
<p className="text-xs text-gray-500 mb-4"> <p className="text-xs text-gray-500 mb-4">
Al registrarte obtienes acceso completo durante 30 días gratis {bypassPayment
? 'Desarrollo • Pruebas • Sin pago requerido'
: 'Proceso seguro • Cancela en cualquier momento • Soporte 24/7'
}
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-gray-400"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-gray-400">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
@@ -405,11 +751,11 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
</div> </div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span> <span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Soporte 24/7 Análisis de demanda
</div> </div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span> <span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Sin compromiso Reduce desperdicios
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,6 +13,10 @@ interface ImportMetaEnv {
readonly VITE_ENABLE_OPTIMISTIC_UPDATES: string readonly VITE_ENABLE_OPTIMISTIC_UPDATES: string
readonly VITE_ENABLE_DEDUPLICATION: string readonly VITE_ENABLE_DEDUPLICATION: string
readonly VITE_ENABLE_METRICS: string readonly VITE_ENABLE_METRICS: string
readonly VITE_STRIPE_PUBLISHABLE_KEY: string
readonly VITE_STRIPE_WEBHOOK_SECRET: string
readonly VITE_BYPASS_PAYMENT: string
readonly VITE_DEV_MODE: string
} }
interface ImportMeta { interface ImportMeta {