Improve the register page
This commit is contained in:
@@ -13,3 +13,11 @@ 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
23
frontend/.env.production
Normal 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
|
||||||
111
frontend/package-lock.json
generated
111
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
4
frontend/vite-env.d.ts
vendored
4
frontend/vite-env.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user