refactor: quickactions / login

This commit is contained in:
DeathKaioken 2026-01-13 13:58:28 +01:00
parent 20c39fcd4e
commit b4a7a5f840
20 changed files with 2440 additions and 1789 deletions

108
package-lock.json generated
View File

@ -45,6 +45,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.14",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.4", "eslint-config-next": "15.5.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
@ -96,6 +97,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@ -506,6 +508,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@ -529,6 +532,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -2660,9 +2664,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@ -2676,9 +2680,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2692,9 +2696,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2708,9 +2712,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2724,9 +2728,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2740,9 +2744,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2756,9 +2760,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2772,9 +2776,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2788,9 +2792,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3567,6 +3571,7 @@
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==", "integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -3633,6 +3638,7 @@
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/scope-manager": "8.44.1",
"@typescript-eslint/types": "8.44.1", "@typescript-eslint/types": "8.44.1",
@ -4156,6 +4162,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -4525,9 +4532,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.9", "version": "2.9.14",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
@ -4603,6 +4610,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.3", "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001741",
@ -4996,7 +5004,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -5420,6 +5429,7 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -5594,6 +5604,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -7691,13 +7702,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/next": { "node_modules/next": {
"version": "16.0.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "16.0.7", "@next/env": "16.1.1",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31", "postcss": "8.4.31",
"styled-jsx": "5.1.6" "styled-jsx": "5.1.6"
@ -7709,14 +7721,14 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-arm64": "16.1.1",
"@next/swc-darwin-x64": "16.0.7", "@next/swc-darwin-x64": "16.1.1",
"@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-gnu": "16.1.1",
"@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-arm64-musl": "16.1.1",
"@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-gnu": "16.1.1",
"@next/swc-linux-x64-musl": "16.0.7", "@next/swc-linux-x64-musl": "16.1.1",
"@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-arm64-msvc": "16.1.1",
"@next/swc-win32-x64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.4" "sharp": "^0.34.4"
}, },
"peerDependencies": { "peerDependencies": {
@ -8113,6 +8125,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -8831,6 +8844,7 @@
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@ -8923,6 +8937,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -8932,6 +8947,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -8958,6 +8974,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -9764,7 +9781,8 @@
"version": "4.1.13", "version": "4.1.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.3", "version": "2.2.3",
@ -9862,6 +9880,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -10038,6 +10057,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@ -46,6 +46,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.14",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.4", "eslint-config-next": "15.5.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",

View File

@ -27,26 +27,42 @@ import {
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { ChevronDownIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useAuthStore from '../../store/authStore'; import useAuthStore from '../../store/authStore';
import { Avatar } from '../avatar'; import { Avatar } from '../avatar'
// ENV-BASED FEATURE FLAGS (string envs: treat "false" as off, everything else as on)
const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false'
const DISPLAY_MEMBERSHIP = process.env.NEXT_PUBLIC_DISPLAY_MEMBERSHIP !== 'false'
const DISPLAY_ABOUT_US = process.env.NEXT_PUBLIC_DISPLAY_ABOUT_US !== 'false'
const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false'
const DISPLAY_ABONEMMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false'
const DISPLAY_POOLS = process.env.NEXT_PUBLIC_DISPLAY_POOLS !== 'false'
// Replace current shopItems / informationItems / navLinks block
// ...existing code...
// Replace current shopItems definition with detailed version (adds icon & description) // Replace current shopItems definition with detailed version (adds icon & description)
const shopItems = [ const shopItems = [
{ name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon }, { name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon },
{ name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon }, { name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon },
]; ]
// Information dropdown, controlled by env flags
const informationItems = [ const informationItems = [
{ name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' }, { name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' },
{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }, ...(DISPLAY_MEMBERSHIP
{ name: 'About us', href: '/about-us', description: 'Learn more about us' }, ? [{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }]
]; : []),
...(DISPLAY_ABOUT_US
? [{ name: 'About us', href: '/about-us', description: 'Learn more about us' }]
: []),
]
// Top-level navigation links, controlled by env flags
const navLinks = [ const navLinks = [
{ name: 'News', href: '/news' }, ...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []),
]; ]
// Toggle visibility of Shop navigation across header (desktop + mobile) // Toggle visibility of Shop navigation across header (desktop + mobile)
const showShop = false; const showShop = false
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@ -312,18 +328,24 @@ export default function Header() {
> >
Referral Management Referral Management
</button> </button>
<button
onClick={() => router.push('/personal-matrix')} {DISPLAY_MATRIX && (
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" <button
> onClick={() => router.push('/personal-matrix')}
Personal Matrix className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
</button> >
<button Personal Matrix
onClick={() => router.push('/coffee-abonnements')} </button>
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" )}
>
Coffee Abonnements {DISPLAY_ABONEMMENTS && (
</button> <button
onClick={() => router.push('/coffee-abonnements')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Coffee Abonnements
</button>
)}
</> </>
)} )}
@ -479,10 +501,7 @@ export default function Header() {
User Verify User Verify
</button> </button>
{/* Updated Management dropdown */} {/* Updated Management dropdown */}
<div <div ref={managementRef} className="relative">
ref={managementRef}
className="relative"
>
<button <button
onClick={() => setAdminMgmtOpen(o => !o)} onClick={() => setAdminMgmtOpen(o => !o)}
aria-haspopup="true" aria-haspopup="true"
@ -507,13 +526,17 @@ export default function Header() {
> >
User Management User Management
</button> </button>
<button
onClick={() => { router.push('/admin/matrix-management'); setAdminMgmtOpen(false); }} {DISPLAY_MATRIX && (
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" <button
role="menuitem" onClick={() => { router.push('/admin/matrix-management'); setAdminMgmtOpen(false); }}
> className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
Matrix Management role="menuitem"
</button> >
Matrix Management
</button>
)}
<button <button
onClick={() => { router.push('/admin/contract-management'); setAdminMgmtOpen(false); }} onClick={() => { router.push('/admin/contract-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
@ -521,27 +544,36 @@ export default function Header() {
> >
Contract Management Contract Management
</button> </button>
<button
onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }} {DISPLAY_ABONEMMENTS && (
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" <>
role="menuitem" <button
> onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }}
Coffee Management className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
</button> role="menuitem"
<button >
onClick={() => { router.push('/admin/finance-management'); setAdminMgmtOpen(false); }} Coffee Management
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" </button>
role="menuitem" <button
> onClick={() => { router.push('/admin/finance-management'); setAdminMgmtOpen(false); }}
Finance Management className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
</button> role="menuitem"
<button >
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }} Finance Management
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" </button>
role="menuitem" </>
> )}
Pool Management
</button> {DISPLAY_POOLS && (
<button
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Pool Management
</button>
)}
<button <button
onClick={() => { router.push('/admin/affiliate-management'); setAdminMgmtOpen(false); }} onClick={() => { router.push('/admin/affiliate-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
@ -549,13 +581,16 @@ export default function Header() {
> >
Affiliate Management Affiliate Management
</button> </button>
<button
onClick={() => { router.push('/admin/news-management'); setAdminMgmtOpen(false); }} {DISPLAY_NEWS && (
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]" <button
role="menuitem" onClick={() => { router.push('/admin/news-management'); setAdminMgmtOpen(false); }}
> className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
News Management role="menuitem"
</button> >
News Management
</button>
)}
</div> </div>
</div> </div>
)} )}
@ -735,18 +770,22 @@ export default function Header() {
> >
Referral Management Referral Management
</button> </button>
<button {DISPLAY_MATRIX && (
onClick={() => { router.push('/personal-matrix'); setMobileMenuOpen(false); }} <button
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" onClick={() => { router.push('/personal-matrix'); setMobileMenuOpen(false); }}
> className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
Personal Matrix >
</button> Personal Matrix
<button </button>
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }} )}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" {DISPLAY_ABONEMMENTS && (
> <button
Coffee Abonnements onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
</button> className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Coffee Abonnements
</button>
)}
</> </>
)} )}
</div> </div>

View File

@ -0,0 +1,264 @@
'use client'
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
type ReactNode
} from 'react'
import { createRoot } from 'react-dom/client'
type ToastVariant = 'success' | 'error' | 'info' | 'warning'
export interface ToastOptions {
id?: string
title?: string
message: string
variant?: ToastVariant
duration?: number // ms, default 4000
}
// add optional closing flag for exit animation
interface ToastInternal extends ToastOptions {
id: string
closing?: boolean
}
interface ToastContextValue {
showToast: (options: ToastOptions) => void
}
// increase fade duration
const TOAST_ANIMATION_MS = 400 // fade-out duration in ms
// --- global toast store so toasts survive route changes ---
let globalToasts: ToastInternal[] = []
type ToastListener = (toasts: ToastInternal[]) => void
const toastListeners = new Set<ToastListener>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
// NEW: portal state (single global React root)
let toastPortalContainer: HTMLDivElement | null = null
let toastPortalRoot: ReturnType<typeof createRoot> | null = null
let toastPortalMounted = false
function notifyToastListeners() {
for (const listener of toastListeners) {
listener(globalToasts)
}
}
function removeToastInternal(id: string) {
// clear auto-dismiss timer
const timeout = toastTimeouts.get(id)
if (timeout) {
clearTimeout(timeout)
toastTimeouts.delete(id)
}
const existing = globalToasts.find(t => t.id === id)
if (!existing) return
if (existing.closing) {
// already closing: hard-remove immediately
globalToasts = globalToasts.filter(t => t.id !== id)
notifyToastListeners()
return
}
// mark as closing to trigger fade-out
globalToasts = globalToasts.map(t =>
t.id === id ? { ...t, closing: true } : t
)
notifyToastListeners()
// remove after animation finishes
setTimeout(() => {
globalToasts = globalToasts.filter(t => t.id !== id)
notifyToastListeners()
}, TOAST_ANIMATION_MS)
}
function addToast(options: ToastOptions) {
const id = options.id ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
const toast: ToastInternal = {
id,
variant: options.variant ?? 'info',
duration: options.duration ?? 4000,
...options
}
globalToasts = [...globalToasts, toast]
notifyToastListeners()
if (toast.duration && toast.duration > 0) {
const timeout = setTimeout(() => {
removeToastInternal(id)
}, toast.duration)
toastTimeouts.set(id, timeout)
}
}
// NEW: mount a global portal once per browser session
function ensureToastPortalMounted() {
if (toastPortalMounted) return
if (typeof document === 'undefined') return
toastPortalMounted = true
toastPortalContainer = document.createElement('div')
document.body.appendChild(toastPortalContainer)
toastPortalRoot = createRoot(toastPortalContainer)
// Defer actual render to avoid triggering nested updates during React render
setTimeout(() => {
if (!toastPortalRoot) return
toastPortalRoot.render(<ToastViewport />)
}, 0)
}
// --- context & provider ---
const ToastContext = createContext<ToastContextValue | undefined>(undefined)
export function useToast(): ToastContextValue {
const ctx = useContext(ToastContext)
if (ctx) {
// Normal path: inside <ToastProvider>
return ctx
}
// Fallback path: no provider mounted, still use global store/portal
if (typeof window !== 'undefined') {
ensureToastPortalMounted()
}
return {
showToast: (options: ToastOptions) => {
addToast(options)
}
}
}
interface ToastProviderProps {
children: ReactNode
}
export function ToastProvider({ children }: ToastProviderProps) {
// ensure the global portal is mounted when any provider appears
useEffect(() => {
ensureToastPortalMounted()
}, [])
const showToast = useCallback((options: ToastOptions) => {
addToast(options)
}, [])
return (
<ToastContext.Provider value={{ showToast }}>
{children}
</ToastContext.Provider>
)
}
// NEW: global viewport rendered via portal, independent of pages/providers
function ToastViewport() {
const [toasts, setToasts] = useState<ToastInternal[]>(globalToasts)
useEffect(() => {
const listener: ToastListener = (next) => setToasts(next)
toastListeners.add(listener)
setToasts(globalToasts)
return () => {
toastListeners.delete(listener)
}
}, [])
const handleClose = useCallback((id: string) => {
removeToastInternal(id)
}, [])
if (!toasts.length) return null
return (
<div className="pointer-events-none fixed inset-x-4 bottom-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto">
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
{toasts.map(t => (
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
))}
</div>
</div>
)
}
interface ToastItemProps {
toast: ToastInternal
onClose: () => void
}
function ToastItem({ toast, onClose }: ToastItemProps) {
const { title, message, variant } = toast
// local visible state for entry animation
const [visible, setVisible] = useState(false)
useEffect(() => {
const frame = requestAnimationFrame(() => setVisible(true))
return () => cancelAnimationFrame(frame)
}, [])
const variantClasses: Record<ToastVariant, string> = {
success: 'border-emerald-400/80 bg-emerald-900/90',
error: 'border-red-400/80 bg-red-900/90',
info: 'border-sky-400/80 bg-slate-900/90',
warning: 'border-amber-400/80 bg-amber-900/90'
}
const iconBg: Record<ToastVariant, string> = {
success: 'bg-emerald-500/10 text-emerald-300',
error: 'bg-red-500/10 text-red-300',
info: 'bg-sky-500/10 text-sky-300',
warning: 'bg-amber-500/10 text-amber-300'
}
const isClosing = !!toast.closing
const motionClasses = isClosing
? 'opacity-0 translate-y-2'
: visible
? 'opacity-100 translate-y-0'
: 'opacity-0 translate-y-2'
return (
<div
className={`
pointer-events-auto flex w-full items-start gap-3 rounded-xl border-l-4
px-4 py-3 text-sm text-slate-50 shadow-xl shadow-black/40
backdrop-blur-md transform transition-all duration-400
${motionClasses}
${variantClasses[variant ?? 'info']}
`}
>
<div className={`mt-0.5 flex h-8 w-8 items-center justify-center rounded-full ${iconBg[variant ?? 'info']}`}>
{/* Simple dot indicator */}
<span className="h-2 w-2 rounded-full bg-current" />
</div>
<div className="flex-1">
{title && (
<div className="mb-0.5 text-xs font-semibold uppercase tracking-wide text-slate-200">
{title}
</div>
)}
<div className="text-[13px] leading-snug text-slate-50">{message}</div>
</div>
<button
type="button"
onClick={onClose}
className="ml-1 mt-0.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-xs text-slate-300 hover:bg-slate-800/70 hover:text-white focus:outline-none focus:ring-2 focus:ring-slate-500"
aria-label="Close notification"
>
×
</button>
</div>
)
}

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline' import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useLogin } from '../hooks/useLogin' import { useLogin } from '../hooks/useLogin'
import { useToast } from '../../components/toast/toastComponent'
export default function LoginForm() { export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
@ -13,11 +14,11 @@ export default function LoginForm() {
password: '', password: '',
rememberMe: false rememberMe: false
}) })
const [viewportWidth, setViewportWidth] = useState<number>( // FIX: use a static initial width so SSR and first client render match
typeof window !== 'undefined' ? window.innerWidth : 1200 const [viewportWidth, setViewportWidth] = useState<number>(1200)
)
const router = useRouter() const router = useRouter()
const { login, error, setError, loading } = useLogin() const { login, error, setError, loading } = useLogin()
const { showToast } = useToast()
// Responsive ball visibility // Responsive ball visibility
useEffect(() => { useEffect(() => {
@ -30,6 +31,7 @@ export default function LoginForm() {
// Track viewport width for dynamic scaling // Track viewport width for dynamic scaling
useEffect(() => { useEffect(() => {
const handleResize = () => setViewportWidth(window.innerWidth) const handleResize = () => setViewportWidth(window.innerWidth)
handleResize() // initialize on mount (runs only on client)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize)
}, []) }, [])
@ -72,11 +74,29 @@ export default function LoginForm() {
if (!validateForm()) return if (!validateForm()) return
await login({ const result = await login({
email: formData.email, email: formData.email,
password: formData.password, password: formData.password,
rememberMe: formData.rememberMe rememberMe: formData.rememberMe
}) })
if (result?.success) {
showToast({
variant: 'success',
title: 'Login successful',
message: 'You are now logged in.'
})
const redirectPath = (result as any).redirectPath || '/dashboard'
// instant redirect; toast persists via global store
router.push(redirectPath)
} else {
showToast({
variant: 'error',
title: 'Login failed',
message: result?.error || 'Login failed. Please check your credentials and try again.'
})
}
} }
// Dynamic breakpoints // Dynamic breakpoints

View File

@ -114,7 +114,7 @@ export function useLogin() {
status: progressData.status status: progressData.status
}) })
// Redirect to dashboard only if all steps completed AND status is active // Redirect decision logic (keep as-is, but do not push here)
if (allStepsCompleted && isActive) { if (allStepsCompleted && isActive) {
redirectPath = '/dashboard' redirectPath = '/dashboard'
console.log('✅ User fully onboarded, redirecting to dashboard') console.log('✅ User fully onboarded, redirecting to dashboard')
@ -128,10 +128,8 @@ export function useLogin() {
console.error('❌ Error fetching user status-progress:', statusError) console.error('❌ Error fetching user status-progress:', statusError)
} }
// Redirect based on status check // NOTE: no router.push here; caller will handle redirect after showing toast
router.push(redirectPath) return { success: true, user: data.user, redirectPath }
return { success: true, user: data.user }
} else { } else {
throw new Error(data.message || 'Login failed') throw new Error(data.message || 'Login failed')
} }

View File

@ -6,13 +6,20 @@ import LoginForm from './components/LoginForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground' import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground'
import { ToastProvider } from '../components/toast/toastComponent'
export default function LoginPage() { export default function LoginPage() {
const [showBackground, setShowBackground] = useState(false) const [showBackground, setShowBackground] = useState(false)
const [hasHydrated, setHasHydrated] = useState(false)
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
// Check if user is already logged in // Mark when the component has hydrated on the client
useEffect(() => {
setHasHydrated(true)
}, [])
// Redirect if user is already logged in
useEffect(() => { useEffect(() => {
if (user) { if (user) {
router.push('/dashboard') router.push('/dashboard')
@ -27,36 +34,40 @@ export default function LoginPage() {
return () => window.removeEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize)
}, []) }, [])
// Don't render if user is already logged in // Don't render if user is already logged in (only after hydration to avoid SSR mismatch)
if (user) { if (hasHydrated && user) {
return ( return (
<PageLayout> <ToastProvider>
<div className="min-h-screen flex items-center justify-center"> <PageLayout>
<div className="text-center"> <div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div> <div className="text-center">
<p className="text-slate-700">You are already logged in. Redirecting...</p> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">You are already logged in. Redirecting...</p>
</div>
</div> </div>
</div> </PageLayout>
</PageLayout> </ToastProvider>
) )
} }
return ( return (
<PageLayout showFooter={true}> <ToastProvider>
<div <PageLayout showFooter={true}>
className="relative w-full flex flex-col min-h-screen" <div
style={{ className="relative w-full flex flex-col min-h-screen"
backgroundImage: 'url(/images/misc/marble_bluegoldwhite_BG.jpg)', style={{
backgroundSize: 'cover', backgroundImage: 'url(/images/misc/marble_bluegoldwhite_BG.jpg)',
backgroundPosition: 'center' backgroundSize: 'cover',
}} backgroundPosition: 'center'
> }}
<div className="relative z-10 flex-1 flex items-center justify-center"> >
<div className="w-full"> <div className="relative z-10 flex-1 flex items-center justify-center">
<LoginForm /> <div className="w-full">
<LoginForm />
</div>
</div> </div>
</div> </div>
</div> </PageLayout>
</PageLayout> </ToastProvider>
) )
} }

View File

@ -204,8 +204,18 @@ export default function QuickActionDashboardPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> {/* Animated background */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div>
<main className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900">

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
interface CompanyProfileData { interface CompanyProfileData {
companyName: string companyName: string
@ -50,6 +51,7 @@ export default function CompanyAdditionalInformationPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(init) const [form, setForm] = useState(init)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -68,12 +70,24 @@ export default function CompanyAdditionalInformationPage() {
] ]
for (const k of required) { for (const k of required) {
if (!form[k].trim()) { if (!form[k].trim()) {
setError('Bitte alle Pflichtfelder ausfüllen.') const msg = 'Bitte alle Pflichtfelder ausfüllen.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
} }
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) { if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
setError('Ungültige IBAN.') const msg = 'Ungültige IBAN.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid IBAN',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -86,7 +100,13 @@ export default function CompanyAdditionalInformationPage() {
if (!validate()) return if (!validate()) return
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -122,6 +142,11 @@ export default function CompanyAdditionalInformationPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Profile saved',
message: 'Your company profile has been saved successfully.',
})
// Refresh user status to update profile completion state // Refresh user status to update profile completion state
await refreshStatus() await refreshStatus()
@ -141,7 +166,13 @@ export default function CompanyAdditionalInformationPage() {
} catch (error: any) { } catch (error: any) {
console.error('Company profile save error:', error) console.error('Company profile save error:', error)
setError(error.message || 'Speichern fehlgeschlagen.') const msg = error.message || 'Speichern fehlgeschlagen.'
setError(msg)
showToast({
variant: 'error',
title: 'Save failed',
message: msg,
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -149,243 +180,243 @@ export default function CompanyAdditionalInformationPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="company-additional-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#company-additional-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath: 'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)' }}
/>
</div>
</div> </div>
<form <main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
onSubmit={submit} <form
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10" onSubmit={submit}
> className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
<div className="px-6 py-8 sm:px-10 lg:px-16"> >
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6"> <div className="px-6 py-8 sm:px-10 lg:px-16">
Complete Company Profile <h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
</h1> Complete Company Profile
</h1>
{/* Company Details */} {/* Company Details */}
<section> <section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4"> <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Company Details Company Details
</h2> </h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3"> <div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Company Name * Company Name *
</label> </label>
<input <input
name="companyName" name="companyName"
value={form.companyName} value={form.companyName}
onChange={handleChange} onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required required
/> />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
VAT / Reg No. *
</label>
<input
name="vatNumber"
value={form.vatNumber}
onChange={handleChange}
placeholder="e.g. DE123456789"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div> </div>
<div> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
VAT / Reg No. * <hr className="my-8 border-gray-200" />
</label>
<input {/* Bank Details */}
name="vatNumber" <section>
value={form.vatNumber} <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
onChange={handleChange} Bank Details
placeholder="e.g. DE123456789" </h2>
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent" <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
required <div className="sm:col-span-2 lg:col-span-3">
/> <label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
BIC (optional)
</label>
<input
name="bic"
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div> </div>
<div className="sm:col-span-2 lg:col-span-3"> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & Number * <hr className="my-8 border-gray-200" />
</label>
<input {/* Additional Information */}
name="street" <section>
value={form.street} <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
onChange={handleChange} Additional Information
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" </h2>
required <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
/> <div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div> </div>
<div> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code * {error && (
</label> <div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
<input {error}
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div> </div>
<div> )}
<label className="block text-sm font-medium text-gray-700 mb-1"> {success && (
City * <div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
</label> Data saved. Redirecting shortly
<input
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div> </div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>
</div> </div>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
BIC (optional)
</label>
<input
name="bic"
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
interface PersonalProfileData { interface PersonalProfileData {
dob: string dob: string
@ -58,6 +59,7 @@ export default function PersonalAdditionalInformationPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(initialData) const [form, setForm] = useState(initialData)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -145,20 +147,38 @@ export default function PersonalAdditionalInformationPage() {
] ]
for (const k of requiredKeys) { for (const k of requiredKeys) {
if (!form[k].trim()) { if (!form[k].trim()) {
setError('Please fill in all required fields.') const msg = 'Please fill in all required fields.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
} }
// Date of birth validation // Date of birth validation
if (!validateDateOfBirth(form.dob)) { if (!validateDateOfBirth(form.dob)) {
setError('Invalid date of birth. You must be at least 18 years old.') const msg = 'Invalid date of birth. You must be at least 18 years old.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid date of birth',
message: msg,
})
return false return false
} }
// very loose IBAN check // very loose IBAN check
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) { if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
setError('Invalid IBAN.') const msg = 'Invalid IBAN.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid IBAN',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -171,7 +191,13 @@ export default function PersonalAdditionalInformationPage() {
if (!validate()) return if (!validate()) return
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -208,6 +234,11 @@ export default function PersonalAdditionalInformationPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Profile saved',
message: 'Your personal profile has been saved successfully.',
})
// Refresh user status to update profile completion state // Refresh user status to update profile completion state
await refreshStatus() await refreshStatus()
@ -227,7 +258,13 @@ export default function PersonalAdditionalInformationPage() {
} catch (error: any) { } catch (error: any) {
console.error('Personal profile save error:', error) console.error('Personal profile save error:', error)
setError(error.message || 'Save failed. Please try again.') const msg = error.message || 'Save failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Save failed',
message: msg,
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -235,243 +272,243 @@ export default function PersonalAdditionalInformationPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="personal-additional-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#personal-additional-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath: 'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)' }}
/>
</div>
</div> </div>
<form <main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
onSubmit={handleSubmit} <form
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10" onSubmit={handleSubmit}
> className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
<div className="px-6 py-8 sm:px-10 lg:px-16"> >
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6"> <div className="px-6 py-8 sm:px-10 lg:px-16">
Complete Your Profile <h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
</h1> Complete Your Profile
</h1>
{/* Personal Information */} {/* Personal Information */}
<section> <section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4"> <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Personal Information Personal Information
</h2> </h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Date of Birth * Date of Birth *
</label> </label>
<input <input
type="date" type="date"
name="dob" name="dob"
value={form.dob} value={form.dob}
onChange={handleChange} onChange={handleChange}
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]} min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]} max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required required
/> />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality *
</label>
<select
name="nationality"
value={form.nationality}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select nationality...</option>
{NATIONALITIES.map(nationality => (
<option key={nationality} value={nationality}>
{nationality}
</option>
))}
</select>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & House Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
placeholder="Street & House Number"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div> </div>
<div> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality * <hr className="my-8 border-gray-200" />
</label>
<select {/* Bank Details */}
name="nationality" <section>
value={form.nationality} <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
onChange={handleChange} Bank Details
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" </h2>
required <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
> <div>
<option value="">Select nationality...</option> <label className="block text-sm font-medium text-gray-700 mb-1">
{NATIONALITIES.map(nationality => ( Account Holder *
<option key={nationality} value={nationality}> </label>
{nationality} <input
</option> name="accountHolder"
))} value={form.accountHolder}
</select> onChange={handleChange}
placeholder="Full name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-1 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
</div> </div>
<div className="sm:col-span-2 lg:col-span-3"> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & House Number * <hr className="my-8 border-gray-200" />
</label>
<input {/* Additional Information */}
name="street" <section>
value={form.street} <h2 className="text-sm font-semibold text-[#0F2460] mb-4">
onChange={handleChange} Additional Information
placeholder="Street & House Number" </h2>
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
required <div className="sm:col-span-2 lg:col-span-3">
/> <label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone Number (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div> </div>
<div> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code * {error && (
</label> <div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
<input {error}
name="postalCode"
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div> </div>
<div> )}
<label className="block text-sm font-medium text-gray-700 mb-1"> {success && (
City * <div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
</label> Data saved. Redirecting shortly
<input
name="city"
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div> </div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
</button>
</div> </div>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Full name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-1 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone Number (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -4,7 +4,8 @@ import { useState, useEffect, useCallback, useRef } from 'react'
import PageLayout from '../../components/PageLayout' import PageLayout from '../../components/PageLayout'
import useAuthStore from '../../store/authStore' import useAuthStore from '../../store/authStore'
import { useUserStatus } from '../../hooks/useUserStatus' import { useUserStatus } from '../../hooks/useUserStatus'
import { useRouter } from 'next/navigation' // NEW import { useRouter } from 'next/navigation'
import { useToast } from '../../components/toast/toastComponent'
export default function EmailVerifyPage() { export default function EmailVerifyPage() {
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
@ -18,7 +19,8 @@ export default function EmailVerifyPage() {
const [initialEmailSent, setInitialEmailSent] = useState(false) const [initialEmailSent, setInitialEmailSent] = useState(false)
const inputsRef = useRef<Array<HTMLInputElement | null>>([]) const inputsRef = useRef<Array<HTMLInputElement | null>>([])
const emailSentRef = useRef(false) const emailSentRef = useRef(false)
const router = useRouter() // NEW const router = useRouter()
const { showToast } = useToast()
// NEW: resend and validity windows // NEW: resend and validity windows
const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
@ -63,18 +65,36 @@ export default function EmailVerifyPage() {
setInitialEmailSent(true) setInitialEmailSent(true)
setLastSentAt(Date.now(), user?.email) setLastSentAt(Date.now(), user?.email)
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
showToast({
variant: 'success',
title: 'Verification email sent',
message: `We sent a verification email to ${user?.email || 'your email'}.`
})
} else { } else {
console.error('Failed to send initial verification email:', data?.message) const msg = data?.message || 'Error sending the verification email.'
setError(msg)
emailSentRef.current = false emailSentRef.current = false
showToast({
variant: 'error',
title: 'Email not sent',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Error sending initial verification email:', error) console.error('Error sending initial verification email:', err)
const msg = 'Network error while sending the verification email.'
setError(msg)
emailSentRef.current = false emailSentRef.current = false
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} }
} }
sendInitialEmail() sendInitialEmail()
}, [token, user]) }, [token, user, showToast])
// Cooldown timer // Cooldown timer
useEffect(() => { useEffect(() => {
@ -172,11 +192,23 @@ export default function EmailVerifyPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (fullCode.length !== 6) { if (fullCode.length !== 6) {
setError('Please enter the 6-digit code.') const msg = 'Please enter the 6-digit code.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid code',
message: msg
})
return return
} }
if (!token) { if (!token) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg
})
return return
} }
@ -196,13 +228,15 @@ export default function EmailVerifyPage() {
if (response.ok && data.success) { if (response.ok && data.success) {
setSuccess(true) setSuccess(true)
await refreshStatus() // Refresh user status showToast({
// Redirect after 2 seconds variant: 'success',
title: 'Email verified',
message: 'Your email has been verified successfully.'
})
await refreshStatus()
setTimeout(() => { setTimeout(() => {
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true' const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) { if (fromTutorial) {
window.location.href = '/quickaction-dashboard?tutorial=true' window.location.href = '/quickaction-dashboard?tutorial=true'
} else { } else {
@ -210,11 +244,23 @@ export default function EmailVerifyPage() {
} }
}, 2000) }, 2000)
} else { } else {
setError(data.error || 'Verification failed. Please try again.') const msg = data.error || 'Verification failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Verification failed',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Email verification error:', error) console.error('Email verification error:', err)
setError('Network error. Please try again.') const msg = 'Network error. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -232,7 +278,17 @@ export default function EmailVerifyPage() {
setResendCooldown(Math.ceil(remaining / 1000)) setResendCooldown(Math.ceil(remaining / 1000))
return return
} }
if (!token) return if (!token) {
const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg
})
return
}
setError('') setError('')
try { try {
@ -250,14 +306,31 @@ export default function EmailVerifyPage() {
setLastSentAt(Date.now(), user?.email) setLastSentAt(Date.now(), user?.email)
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
if (!initialEmailSent) setInitialEmailSent(true) if (!initialEmailSent) setInitialEmailSent(true)
showToast({
variant: 'success',
title: 'Verification email sent',
message: `We sent a new verification email to ${user?.email || 'your email'}.`
})
} else { } else {
setError(data?.message || 'Error sending the email.') const msg = data?.message || 'Error sending the email.'
setError(msg)
showToast({
variant: 'error',
title: 'Email not sent',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Resend email error:', error) console.error('Resend email error:', err)
setError('Network error while sending the email.') const msg = 'Network error while sending the email.'
setError(msg)
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} }
}, [token, submitting, success, user, initialEmailSent]) }, [token, submitting, success, user, initialEmailSent, showToast])
// NEW: format seconds to m:ss // NEW: format seconds to m:ss
const formatMmSs = (total: number) => { const formatMmSs = (total: number) => {
@ -268,157 +341,131 @@ export default function EmailVerifyPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Global full-viewport background (no inner scroll) */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
{/* Gradient base */} {/* Soft gradient blobs */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
{/* Pattern */} <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<svg <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
aria-hidden="true" {/* Subtle radial highlight */}
className="absolute inset-0 -z-10 h-full w-full stroke-white/10" <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
>
<defs>
<pattern
id="email-verify-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
</pattern>
</defs>
<rect fill="url(#email-verify-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
{/* Colored blur shape */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
>
<div
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
}}
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
/>
</div>
</div> </div>
<div className="max-w-xl mx-auto">
<div className="text-center mb-10">
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
Verify your email
</h1>
<p className="mt-3 text-gray-300 text-sm sm:text-base">
{initialEmailSent ? (
<>
We sent a 6-digit code to{' '}
<span className="text-indigo-300 font-medium">
{user?.email || 'your email'}
</span>
. Enter it below.
</>
) : (
<>
Sending verification email to{' '}
<span className="text-indigo-300 font-medium">
{user?.email || 'your email'}
</span>
...
</>
)}
</p>
</div>
{/* Card */} <main className="relative z-10 flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
<form <div className="max-w-xl mx-auto">
onSubmit={handleSubmit} <div className="text-center mb-10">
className="bg-white/95 dark:bg-gray-900/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 dark:ring-white/10 px-6 py-8 sm:px-10 sm:py-10" <h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
> Verify your email
<fieldset disabled={submitting || success} className="space-y-8"> </h1>
<div className="flex justify-center gap-2 sm:gap-3"> <p className="mt-3 text-gray-700 text-sm sm:text-base">
{code.map((v, i) => ( {initialEmailSent ? (
<input <>
key={i} We sent a 6-digit code to{' '}
ref={el => { inputsRef.current[i] = el }} <span className="text-blue-700 font-medium">
inputMode="numeric" {user?.email || 'your email'}
aria-label={`Code digit ${i + 1}`} </span>
autoComplete="one-time-code" . Enter it below.
maxLength={1} </>
value={v} ) : (
onChange={e => handleChange(i, e.target.value)} <>
onKeyDown={e => handleKeyDown(i, e)} Sending verification email to{' '}
onPaste={e => handlePaste(i, e)} <span className="text-blue-700 font-medium">
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none {user?.email || 'your email'}
${v </span>
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' ...
: 'border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/70 text-gray-700 dark:text-gray-200'} </>
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`} )}
/> </p>
))}
</div>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Verified! Redirecting shortly...
</div>
)}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<button
type="submit"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{submitting ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Verifying...
</>
) : success ? 'Verified' : 'Confirm code'}
</button>
<button
type="button"
onClick={handleResend}
disabled={!!resendCooldown || submitting || success}
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
>
{resendCooldown
? `Resend in ${formatMmSs(resendCooldown)}`
: 'Resend code'}
</button>
</div>
{/* NEW: Go to Dashboard button */}
<div className="mt-1 text-center">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:underline"
>
Go to Dashboard
</button>
</div>
</fieldset>
{/* Helper text with validity + spam/junk reminder + support */}
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 dark:text-indigo-400 hover:underline">
Contact support
</a>
.
</div> </div>
</form>
</div> {/* Card */}
<form
onSubmit={handleSubmit}
className="bg-white/95 backdrop-blur rounded-2xl shadow-xl ring-1 ring-black/5 px-6 py-8 sm:px-10 sm:py-10"
>
<fieldset disabled={submitting || success} className="space-y-8">
{/* Inputs */}
<div className="flex justify-center gap-2 sm:gap-3">
{code.map((v, i) => (
<input
key={i}
ref={el => { inputsRef.current[i] = el }}
inputMode="numeric"
aria-label={`Code digit ${i + 1}`}
autoComplete="one-time-code"
maxLength={1}
value={v}
onChange={e => handleChange(i, e.target.value)}
onKeyDown={e => handleKeyDown(i, e)}
onPaste={e => handlePaste(i, e)}
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
${v
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
: 'border-gray-300 bg-white/80 text-gray-700'}
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
/>
))}
</div>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Verified! Redirecting shortly...
</div>
)}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<button
type="submit"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{submitting ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Verifying...
</>
) : success ? 'Verified' : 'Confirm code'}
</button>
<button
type="button"
onClick={handleResend}
disabled={!!resendCooldown || submitting || success}
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
>
{resendCooldown
? `Resend in ${formatMmSs(resendCooldown)}`
: 'Resend code'}
</button>
</div>
<div className="mt-1 text-center">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="text-sm font-medium text-gray-700 hover:underline"
>
Go to Dashboard
</button>
</div>
</fieldset>
<div className="mt-8 text-center text-xs text-gray-500">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
Contact support
</a>
.
</div>
</form>
</div>
</main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api' import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent' // NEW
export default function CompanySignContractPage() { export default function CompanySignContractPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast() // NEW
const [companyName, setCompanyName] = useState('') const [companyName, setCompanyName] = useState('')
const [repName, setRepName] = useState('') const [repName, setRepName] = useState('')
@ -77,7 +79,7 @@ export default function CompanySignContractPage() {
e.preventDefault() e.preventDefault()
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues = [] const issues: string[] = []
if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)') if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)')
if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)') if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)')
if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)') if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)')
@ -86,12 +88,24 @@ export default function CompanySignContractPage() {
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
setError(`Please complete: ${issues.join(', ')}`) const msg = `Please complete: ${issues.join(', ')}`
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return return
} }
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -137,6 +151,11 @@ export default function CompanySignContractPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Contract signed',
message: 'Your company contract has been signed successfully.',
})
// Refresh user status to update contract signed state // Refresh user status to update contract signed state
await refreshStatus() await refreshStatus()
@ -156,7 +175,13 @@ export default function CompanySignContractPage() {
} catch (error: any) { } catch (error: any) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
setError(error.message || 'Signature failed. Please try again.') const msg = error.message || 'Signature failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Signature failed',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -164,255 +189,255 @@ export default function CompanySignContractPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="company-contract-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#company-contract-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath:'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)'}}
/>
</div>
</div> </div>
<form <main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
onSubmit={handleSubmit} <form
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10" onSubmit={handleSubmit}
> className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
<div className="px-6 py-8 sm:px-10 lg:px-14"> >
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2"> <div className="px-6 py-8 sm:px-10 lg:px-14">
Sign Company Partnership Contract <h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
</h1> Sign Company Partnership Contract
<p className="text-center text-sm text-gray-600 mb-8"> </h1>
Please review the contract details and sign on behalf of the company. <p className="text-center text-sm text-gray-600 mb-8">
</p> Please review the contract details and sign on behalf of the company.
</p>
{/* Meta + Preview */} {/* Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10"> <section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50"> <div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2> <h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600"> <ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> COMP-2024-017</li> <li><span className="font-medium text-gray-700">Contract ID:</span> COMP-2024-017</li>
<li><span className="font-medium text-gray-700">Version:</span> 2.4 (valid from 01.11.2024)</li> <li><span className="font-medium text-gray-700">Version:</span> 2.4 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li> <li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li> <li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li>
</ul> </ul>
</div> </div>
<div className="rounded-lg border border-amber-200 bg-amber-50 p-5"> <div className="rounded-lg border border-amber-200 bg-amber-50 p-5">
<h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3> <h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3>
<p className="text-xs sm:text-sm text-amber-800 leading-relaxed"> <p className="text-xs sm:text-sm text-amber-800 leading-relaxed">
You confirm that you are authorized to sign on behalf of the company. You confirm that you are authorized to sign on behalf of the company.
</p> </p>
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Company Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
<button
type="button"
onClick={async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
>
{previewLoading ? 'Loading…' : 'Refresh'}
</button>
</div>
</div> </div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Company Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
value={companyName}
onChange={e => { setCompanyName(e.target.value); setError('') }}
placeholder="Firmenname offiziell"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
Date * <div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
</label> <h3 className="text-sm font-semibold text-gray-900">Company Contract Preview</h3>
<input <div className="flex items-center gap-2">
type="date" <button
value={date} type="button"
onChange={e => setDate(e.target.value)} onClick={() => {
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" if (!previewHtml) return
required const blob = new Blob([previewHtml], { type: 'text/html' })
/> const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
<button
type="button"
onClick={async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
>
{previewLoading ? 'Loading…' : 'Refresh'}
</button>
</div>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div> </div>
<div> </section>
<label className="block text_sm font-medium text-gray-700 mb-1">
Location * <hr className="my-10 border-gray-200" />
</label>
<input {/* Company Signature Fields */}
value={location} <section>
onChange={e => { setLocation(e.target.value); setError('') }} <h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
placeholder="z.B. München" <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" <div className="sm:col-span-2 lg:col-span-2">
required <label className="block text-sm font-medium text-gray-700 mb-1">
/> Company Name *
</label>
<input
value={companyName}
onChange={e => { setCompanyName(e.target.value); setError('') }}
placeholder="Firmenname offiziell"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text_sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. München"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-1">
<label className="block text_sm font-medium text-gray-700 mb-1">
Representative Name *
</label>
<input
value={repName}
onChange={e => { setRepName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Representative Position / Title *
</label>
<input
value={repTitle}
onChange={e => { setRepTitle(e.target.value); setError('') }}
placeholder="z.B. Geschäftsführer, Authorized Signatory"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Interne Referenz / Zusatz"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div> </div>
<div className="sm:col-span-2 lg:col-span-1"> </section>
<label className="block text_sm font-medium text-gray-700 mb-1">
Representative Name * <hr className="my-10 border-gray-200" />
</label>
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input <input
value={repName} type="checkbox"
onChange={e => { setRepName(e.target.value); setError('') }} checked={agreeContract}
placeholder="Vor- und Nachname" onChange={e => setAgreeContract(e.target.checked)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
required
/> />
</div> <span>I confirm I have read and accepted the full contract on behalf of the company.</span>
<div className="sm:col-span-2 lg:col-span-2"> </label>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="flex items-start gap-3 text-sm text-gray-700">
Representative Position / Title *
</label>
<input <input
value={repTitle} type="checkbox"
onChange={e => { setRepTitle(e.target.value); setError('') }} checked={agreeData}
placeholder="z.B. Geschäftsführer, Authorized Signatory" onChange={e => setAgreeData(e.target.checked)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
required
/> />
</div> <span>I consent to processing of company and personal data in accordance with the privacy policy.</span>
<div className="sm:col-span-2 lg:col-span-3"> </label>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="flex items-start gap-3 text-sm text-gray-700">
Note (optional)
</label>
<input <input
value={note} type="checkbox"
onChange={e => setNote(e.target.value)} checked={confirmSignature}
placeholder="Interne Referenz / Zusatz" onChange={e => setConfirmSignature(e.target.checked)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/> />
<span>I am authorized to sign legally binding documents for this company.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div> </div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div> </div>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm I have read and accepted the full contract on behalf of the company.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to processing of company and personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I am authorized to sign legally binding documents for this company.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api' import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent'
export default function PersonalSignContractPage() { export default function PersonalSignContractPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [fullName, setFullName] = useState('') const [fullName, setFullName] = useState('')
const [location, setLocation] = useState('') const [location, setLocation] = useState('')
@ -72,19 +74,31 @@ export default function PersonalSignContractPage() {
e.preventDefault() e.preventDefault()
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues = [] const issues: string[] = []
if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)') if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)')
if (location.trim().length < 2) issues.push('Location (min 2 characters)') if (location.trim().length < 2) issues.push('Location (min 2 characters)')
if (!agreeContract) issues.push('Contract read and understood') if (!agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
setError(`Please complete: ${issues.join(', ')}`) const msg = `Please complete: ${issues.join(', ')}`
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return return
} }
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -128,6 +142,11 @@ export default function PersonalSignContractPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Contract signed',
message: 'Your personal contract has been signed successfully.',
})
// Refresh user status to update contract signed state // Refresh user status to update contract signed state
await refreshStatus() await refreshStatus()
@ -147,7 +166,13 @@ export default function PersonalSignContractPage() {
} catch (error: any) { } catch (error: any) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
setError(error.message || 'Signature failed. Please try again.') const msg = error.message || 'Signature failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Signature failed',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -155,205 +180,205 @@ export default function PersonalSignContractPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="personal-contract-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#personal-contract-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath:'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)'}}
/>
</div>
</div> </div>
<form <main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
onSubmit={handleSubmit} <form
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10" onSubmit={handleSubmit}
> className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
<div className="px-6 py-8 sm:px-10 lg:px-14"> >
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2"> <div className="px-6 py-8 sm:px-10 lg:px-14">
Sign Personal Participation Contract <h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
</h1> Sign Personal Participation Contract
<p className="text-center text-sm text-gray-600 mb-8"> </h1>
Please review the contract details and sign electronically. <p className="text-center text-sm text-gray-600 mb-8">
</p> Please review the contract details and sign electronically.
</p>
{/* Contract Meta + Preview */} {/* Contract Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10"> <section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50"> <div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2> <h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600"> <ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li> <li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li>
<li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li> <li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li> <li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li> <li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li>
</ul> </ul>
</div> </div>
<div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5"> <div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5">
<h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3> <h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3>
<p className="text-xs sm:text-sm text-indigo-800 leading-relaxed"> <p className="text-xs sm:text-sm text-indigo-800 leading-relaxed">
Your electronic signature is legally binding. Please ensure all details are correct. Your electronic signature is legally binding. Please ensure all details are correct.
</p> </p>
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={async () => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
</div>
</div> </div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature Details</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name (Signature) *
</label>
<input
value={fullName}
onChange={e => { setFullName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<p className="mt-1 text-xs text-gray-500">
Must match your official ID.
</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
Date * <div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
</label> <h3 className="text-sm font-semibold text-gray-900">Contract Preview</h3>
<input <div className="flex items-center gap-2">
type="date" <button
value={date} type="button"
onChange={e => setDate(e.target.value)} onClick={async () => {
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" if (!previewHtml) return
required const blob = new Blob([previewHtml], { type: 'text/html' })
/> const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
</div>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div> </div>
<div> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location * <hr className="my-10 border-gray-200" />
</label>
<input {/* Signature Fields */}
value={location} <section>
onChange={e => { setLocation(e.target.value); setError('') }} <h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature Details</h2>
placeholder="z.B. Berlin" <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" <div className="sm:col-span-2 lg:col-span-2">
required <label className="block text-sm font-medium text-gray-700 mb-1">
/> Full Name (Signature) *
</label>
<input
value={fullName}
onChange={e => { setFullName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<p className="mt-1 text-xs text-gray-500">
Must match your official ID.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. Berlin"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Optionale zusätzliche Bemerkung"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div> </div>
<div className="sm:col-span-2 lg:col-span-3"> </section>
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional) <hr className="my-10 border-gray-200" />
</label>
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input <input
value={note} type="checkbox"
onChange={e => setNote(e.target.value)} checked={agreeContract}
placeholder="Optionale zusätzliche Bemerkung" onChange={e => setAgreeContract(e.target.checked)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/> />
<span>I confirm that I have read and understood the contract in full.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to the processing of my personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm this electronic signature is legally binding and equivalent to a handwritten signature.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div> </div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div> </div>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm that I have read and understood the contract in full.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to the processing of my personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm this electronic signature is legally binding and equivalent to a handwritten signature.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -3,11 +3,13 @@
import { useState, useRef, useEffect, useCallback } from 'react' import { useState, useRef, useEffect, useCallback } from 'react'
import useAuthStore from '../../../../store/authStore' import useAuthStore from '../../../../store/authStore'
import { useUserStatus } from '../../../../hooks/useUserStatus' import { useUserStatus } from '../../../../hooks/useUserStatus'
import { useToast } from '../../../../components/toast/toastComponent'
export function useCompanyUploadId() { export function useCompanyUploadId() {
// Auth + status // Auth + status
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
// Form state // Form state
const [idNumber, setIdNumber] = useState('') const [idNumber, setIdNumber] = useState('')
@ -37,7 +39,13 @@ export function useCompanyUploadId() {
// File handlers // File handlers
const handleFile = (file: File, which: 'front' | 'extra') => { const handleFile = (file: File, which: 'front' | 'extra') => {
if (file.size > 10 * 1024 * 1024) { if (file.size > 10 * 1024 * 1024) {
setError('File size exceeds 10 MB.') const msg = 'File size exceeds 10 MB.'
setError(msg)
showToast({
variant: 'error',
title: 'File too large',
message: msg,
})
return return
} }
setError('') setError('')
@ -81,7 +89,13 @@ export function useCompanyUploadId() {
// Validation // Validation
const validate = () => { const validate = () => {
if (!idNumber.trim() || !idType || !expiryDate || !frontFile) { if (!idNumber.trim() || !idType || !expiryDate || !frontFile) {
setError('Please complete all required fields (marked with *).') const msg = 'Please complete all required fields (marked with *).'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -93,8 +107,14 @@ export function useCompanyUploadId() {
e.preventDefault() e.preventDefault()
if (!validate()) return if (!validate()) return
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
return setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return
} }
setSubmitting(true) setSubmitting(true)
@ -116,10 +136,16 @@ export function useCompanyUploadId() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Upload failed' })) const errorData = await response.json().catch(() => ({ message: 'Upload failed' }))
throw new Error(errorData.message || 'Upload failed') const msg = errorData.message || 'Upload failed'
throw new Error(msg)
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Documents uploaded',
message: 'Your company ID documents have been uploaded successfully.',
})
await refreshStatus() await refreshStatus()
setTimeout(() => { setTimeout(() => {
@ -136,7 +162,13 @@ export function useCompanyUploadId() {
}, 1500) }, 1500)
} catch (err: any) { } catch (err: any) {
console.error('Company ID upload error:', err) console.error('Company ID upload error:', err)
setError(err?.message || 'Upload failed.') const msg = err?.message || 'Upload failed.'
setError(msg)
showToast({
variant: 'error',
title: 'Upload failed',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }

View File

@ -3,9 +3,9 @@
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline' import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { useCompanyUploadId } from './hooks/useCompanyUploadId' import { useCompanyUploadId } from './hooks/useCompanyUploadId'
import useAuthStore from '../../../store/authStore' // NEW import useAuthStore from '../../../store/authStore'
import { useEffect, useState } from 'react' // NEW import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' // NEW import { useRouter } from 'next/navigation'
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel'] const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
@ -25,9 +25,9 @@ export default function CompanyIdUploadPage() {
handleFile, onDrop, clearFile, dropHandlers, openPicker, submit, handleFile, onDrop, clearFile, dropHandlers, openPicker, submit,
} = useCompanyUploadId() } = useCompanyUploadId()
const user = useAuthStore(s => s.user) // NEW const user = useAuthStore(s => s.user)
const router = useRouter() // NEW const router = useRouter()
const [blocked, setBlocked] = useState(false) // NEW const [blocked, setBlocked] = useState(false)
// Guard: only 'company' users allowed on this page // Guard: only 'company' users allowed on this page
useEffect(() => { useEffect(() => {
@ -56,181 +56,127 @@ export default function CompanyIdUploadPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 lg:px-10 py-10"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background (same as personal) */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="company-id-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#company-id-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath: 'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)' }}
/>
</div>
</div> </div>
<form onSubmit={submit} className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"> <main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<div className="px-6 py-8 sm:px-12 lg:px-16"> <form
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1"> onSubmit={submit}
Company Contact Person Identity Verification className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
</h1> >
<p className="text-sm text-gray-600 mb-8"> <div className="px-6 py-8 sm:px-12 lg:px-16">
Please upload clear photos of both sides of the company contact person&apos;s ID document. <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
</p> Company Contact Person Identity Verification
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of the company contact person&apos;s ID document.
</p>
{/* Fields: 3 in one row on md+ with unified inputs */} {/* Fields: 3 in one row on md+ with unified inputs */}
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Contact Person ID Number * Contact Person ID Number *
</label> </label>
<input <input
value={idNumber} value={idNumber}
onChange={e => setIdNumber(e.target.value)} onChange={e => setIdNumber(e.target.value)}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`} className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
placeholder="Enter contact person's ID number" placeholder="Enter contact person's ID number"
required required
/> />
<p className="mt-1 text-xs text-gray-600"> <p className="mt-1 text-xs text-gray-600">
Enter the ID number exactly as shown on the document Enter the ID number exactly as shown on the document
</p> </p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Document Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
>
<option value="">Select document type</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiryDate}
onChange={e => setExpiryDate(e.target.value)}
placeholder="tt.mm.jjjj"
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the expiry date shown on your document
</p>
</div>
</div> </div>
<div> {/* Back side toggle */}
<label className="block text-sm font-medium text-gray-700 mb-2"> <div className="mt-8 flex items-center gap-3">
Document Type * <span className="text-sm font-medium text-gray-700">
</label> Does ID have a Backside?
<select </span>
value={idType} <button
onChange={e => setIdType(e.target.value)} type="button"
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`} onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
required className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
aria-pressed={hasBack}
> >
<option value="">Select document type</option> <span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
{DOC_TYPES.map(t => <option key={t} value={t}>{t}</option>)} </button>
</select> <span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div> </div>
<div> {/* Upload Areas */}
<label className="block text-sm font-medium text-gray-700 mb-2"> <div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
Expiry Date * {/* Upload ID Front Side */}
</label>
<input
type="date"
value={expiryDate}
onChange={e => setExpiryDate(e.target.value)}
placeholder="tt.mm.jjjj"
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the expiry date shown on your document
</p>
</div>
</div>
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
aria-pressed={hasBack}
>
<span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div>
{/* Upload Areas */}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Upload ID Front Side */}
<div
{...dropHandlers}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={frontRef}
type="file"
accept="image/*,application/pdf"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */}
{frontPreview && (
<img
src={frontPreview}
alt="Primary document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
{/* Upload ID Back Side */}
{hasBack && (
<div <div
{...dropHandlers} {...dropHandlers}
onDrop={e => onDrop(e, 'extra')} onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('extra')} onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition" className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
> >
<input <input
ref={extraRef} ref={frontRef}
type="file" type="file"
accept="image/*,application/pdf" accept="image/*,application/pdf"
className="hidden" className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'extra') }} onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/> />
{extraFile ? ( {frontFile ? (
<div className="flex w-full max-w-full flex-col items-center"> <div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */} {/* Preview only for images */}
{extraPreview && ( {frontPreview && (
<img <img
src={extraPreview} src={frontPreview}
alt="Supporting document preview" alt="Primary document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200" className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/> />
)} )}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{extraFile.name}</p> <p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button <button
type="button" type="button"
onClick={e => { e.stopPropagation(); clearFile('extra') }} onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100" className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
> >
<XMarkIcon className="h-4 w-4" /> Remove <XMarkIcon className="h-4 w-4" /> Remove
@ -240,7 +186,7 @@ export default function CompanyIdUploadPage() {
<> <>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" /> <DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500"> <p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side Click to upload front side
</p> </p>
<p className="mt-2 text-xs text-gray-500"> <p className="mt-2 text-xs text-gray-500">
or drag and drop or drag and drop
@ -250,45 +196,95 @@ export default function CompanyIdUploadPage() {
</> </>
)} )}
</div> </div>
{/* Upload ID Back Side */}
{hasBack && (
<div
{...dropHandlers}
onDrop={e => onDrop(e, 'extra')}
onClick={() => openPicker('extra')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={extraRef}
type="file"
accept="image/*,application/pdf"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'extra') }}
/>
{extraFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */}
{extraPreview && (
<img
src={extraPreview}
alt="Supporting document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{extraFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('extra') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
)}
</div>
{/* Info */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Documents uploaded successfully. Redirecting shortly
</div>
)} )}
</div>
{/* Info */} <div className="mt-8 flex justify-end">
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5"> <button
<p className="text-sm font-semibold text-indigo-900 mb-3"> type="submit"
Please ensure your ID documents: disabled={submitting || success}
</p> className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5"> >
<li>Are clearly visible and readable</li> {submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
<li>Show all four corners</li> </button>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div> </div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Documents uploaded successfully. Redirecting shortly
</div>
)}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -3,11 +3,13 @@
import { useState, useRef, useCallback, useEffect } from 'react' import { useState, useRef, useCallback, useEffect } from 'react'
import useAuthStore from '../../../../store/authStore' import useAuthStore from '../../../../store/authStore'
import { useUserStatus } from '../../../../hooks/useUserStatus' import { useUserStatus } from '../../../../hooks/useUserStatus'
import { useToast } from '../../../../components/toast/toastComponent'
export function usePersonalUploadId() { export function usePersonalUploadId() {
// Auth and status // Auth and status
const token = useAuthStore(s => s.accessToken) const token = useAuthStore(s => s.accessToken)
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
// Form state // Form state
const [idNumber, setIdNumber] = useState('') const [idNumber, setIdNumber] = useState('')
@ -37,7 +39,13 @@ export function usePersonalUploadId() {
// File handlers // File handlers
const handleFile = (f: File, side: 'front' | 'back') => { const handleFile = (f: File, side: 'front' | 'back') => {
if (f.size > 10 * 1024 * 1024) { if (f.size > 10 * 1024 * 1024) {
setError('File size exceeds 10 MB.') const msg = 'File size exceeds 10 MB.'
setError(msg)
showToast({
variant: 'error',
title: 'File too large',
message: msg,
})
return return
} }
setError('') setError('')
@ -81,15 +89,33 @@ export function usePersonalUploadId() {
// Validation // Validation
const validate = () => { const validate = () => {
if (!idNumber.trim() || !idType || !expiry) { if (!idNumber.trim() || !idType || !expiry) {
setError('Please fill out all required fields.') const msg = 'Please fill out all required fields.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
if (!frontFile) { if (!frontFile) {
setError('Please upload the front side.') const msg = 'Please upload the front side.'
setError(msg)
showToast({
variant: 'error',
title: 'Front side missing',
message: msg,
})
return false return false
} }
if (hasBack && !backFile) { if (hasBack && !backFile) {
setError('Please upload the back side or disable the switch.') const msg = 'Please upload the back side or disable the switch.'
setError(msg)
showToast({
variant: 'error',
title: 'Back side missing',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -101,7 +127,13 @@ export function usePersonalUploadId() {
e.preventDefault() e.preventDefault()
if (!validate()) return if (!validate()) return
if (!token) { if (!token) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -126,6 +158,11 @@ export function usePersonalUploadId() {
if (response.ok && data.success) { if (response.ok && data.success) {
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Documents uploaded',
message: 'Your ID documents have been uploaded successfully.',
})
await refreshStatus() await refreshStatus()
setTimeout(() => { setTimeout(() => {
// Check if we came from tutorial // Check if we came from tutorial
@ -139,11 +176,23 @@ export function usePersonalUploadId() {
} }
}, 2000) }, 2000)
} else { } else {
setError(data.message || 'Upload failed. Please try again.') const msg = data.message || 'Upload failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Upload failed',
message: msg,
})
} }
} catch (err) { } catch (err) {
console.error('Upload error:', err) console.error('Upload error:', err)
setError('Network error. Please try again.') const msg = 'Network error. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Network error',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }

View File

@ -60,188 +60,134 @@ export default function PersonalIdUploadPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative flex flex-col flex-1 w-full px-5 lg:px-10 py-10"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Background */} {/* Animated background (same as dashboard) */}
<div className="fixed inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {/* Soft gradient blobs */}
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10"> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<defs> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<pattern id="personal-id-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse"> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> {/* Subtle radial highlight */}
</pattern> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</defs>
<rect fill="url(#personal-id-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{ clipPath: 'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)' }}
/>
</div>
</div> </div>
<form onSubmit={submit} className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"> <main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<div className="px-6 py-8 sm:px-12 lg:px-16"> <form
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1"> onSubmit={submit}
Personal Identity Verification className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
</h1> >
<p className="text-sm text-gray-600 mb-8"> <div className="px-6 py-8 sm:px-12 lg:px-16">
Please upload clear photos of both sides of your governmentissued ID <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
</p> Personal Identity Verification
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of your governmentissued ID
</p>
{/* Grid Fields: put all three inputs on the same line on md+ */} {/* Grid Fields: put all three inputs on the same line on md+ */}
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
ID Number * ID Number *
</label> </label>
<input <input
value={idNumber} value={idNumber}
onChange={e => setIdNumber(e.target.value)} onChange={e => setIdNumber(e.target.value)}
placeholder="Enter your ID number" placeholder="Enter your ID number"
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`} className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
required required
/> />
<p className="mt-1 text-xs text-gray-600"> <p className="mt-1 text-xs text-gray-600">
Enter the number exactly as shown on your ID Enter the number exactly as shown on your ID
</p> </p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ID Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
>
<option value="">Select ID type</option>
{ID_TYPES.map(t => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
</div>
</div> </div>
<div> {/* Back side toggle */}
<label className="block text-sm font-medium text-gray-700 mb-2"> <div className="mt-8 flex items-center gap-3">
ID Type * <span className="text-sm font-medium text-gray-700">
</label> Does ID have a Backside?
<select </span>
value={idType} <button
onChange={e => setIdType(e.target.value)} type="button"
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`} onClick={() => setHasBack(v => !v)}
required className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
> hasBack ? 'bg-indigo-600' : 'bg-gray-300'
<option value="">Select ID type</option>
{ID_TYPES.map(t => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
</div>
</div>
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
}`}
aria-pressed={hasBack}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
hasBack ? 'translate-x-5' : 'translate-x-0'
}`} }`}
/> aria-pressed={hasBack}
</button> >
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span> <span
</div> className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
hasBack ? 'translate-x-5' : 'translate-x-0'
{/* Upload Areas: full width, 1 col if no back, 2 cols if back */} }`}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}> />
{/* Front */} </button>
<div <span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
{...dropEvents}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={frontInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */}
{frontPreview && (
<img
src={frontPreview}
alt="Front ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div> </div>
{/* Back */} {/* Upload Areas: full width, 1 col if no back, 2 cols if back */}
{hasBack && ( <div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Front */}
<div <div
{...dropEvents} {...dropEvents}
onDrop={e => onDrop(e, 'back')} onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('back')} onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition" className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
> >
<input <input
ref={backInputRef} ref={frontInputRef}
type="file" type="file"
accept="image/png,image/jpeg,image/jpg" accept="image/png,image/jpeg,image/jpg"
className="hidden" className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'back') }} onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/> />
{backFile ? ( {frontFile ? (
<div className="flex w-full max-w-full flex-col items-center"> <div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */} {/* NEW preview */}
{backPreview && ( {frontPreview && (
<img <img
src={backPreview} src={frontPreview}
alt="Back ID preview" alt="Front ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200" className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/> />
)} )}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{backFile.name}</p> <p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button <button
type="button" type="button"
onClick={e => { e.stopPropagation(); clearFile('back') }} onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100" className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
> >
<XMarkIcon className="h-4 w-4" /> Remove <XMarkIcon className="h-4 w-4" /> Remove
@ -251,7 +197,7 @@ export default function PersonalIdUploadPage() {
<> <>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" /> <DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500"> <p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side Click to upload front side
</p> </p>
<p className="mt-2 text-xs text-gray-500"> <p className="mt-2 text-xs text-gray-500">
or drag and drop or drag and drop
@ -261,45 +207,95 @@ export default function PersonalIdUploadPage() {
</> </>
)} )}
</div> </div>
{/* Back */}
{hasBack && (
<div
{...dropEvents}
onDrop={e => onDrop(e, 'back')}
onClick={() => openPicker('back')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={backInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'back') }}
/>
{backFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */}
{backPreview && (
<img
src={backPreview}
alt="Back ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{backFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('back') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
)}
</div>
{/* Info Box, errors, success, submit */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Upload saved successfully. Redirecting shortly
</div>
)} )}
</div>
{/* Info Box, errors, success, submit */} <div className="mt-8 flex justify-end">
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5"> <button
<p className="text-sm font-semibold text-indigo-900 mb-3"> type="submit"
Please ensure your ID documents: disabled={submitting || success}
</p> className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5"> >
<li>Are clearly visible and readable</li> {submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
<li>Show all four corners</li> </button>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div> </div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Upload saved successfully. Redirecting shortly
</div>
)}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div> </div>
</div> </form>
</form> </main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -3,6 +3,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline' import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useRegister } from '../hooks/useRegister' import { useRegister } from '../hooks/useRegister'
import { useToast } from '../../components/toast/toastComponent'
interface RegisterFormProps { interface RegisterFormProps {
mode: 'personal' | 'company' mode: 'personal' | 'company'
@ -72,6 +73,7 @@ export default function RegisterForm({
// Hook for backend calls // Hook for backend calls
const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister() const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister()
const { showToast } = useToast()
// Animate form when mode changes // Animate form when mode changes
useEffect(() => { useEffect(() => {
@ -113,24 +115,24 @@ export default function RegisterForm({
if (!personalForm.firstName.trim() || !personalForm.lastName.trim() || if (!personalForm.firstName.trim() || !personalForm.lastName.trim() ||
!personalForm.email.trim() || !personalForm.confirmEmail.trim() || !personalForm.email.trim() || !personalForm.confirmEmail.trim() ||
!personalForm.password.trim() || !personalForm.confirmPassword.trim() || !personalForm.password.trim() || !personalForm.confirmPassword.trim() ||
!personalForm.phoneNumber.trim() // now required by backend !personalForm.phoneNumber.trim()
) { ) {
setError('Alle Felder sind erforderlich') setError('All fields are required')
return false return false
} }
if (personalForm.email !== personalForm.confirmEmail) { if (personalForm.email !== personalForm.confirmEmail) {
setError('E-Mail-Adressen stimmen nicht überein') setError('Email addresses do not match')
return false return false
} }
if (personalForm.password !== personalForm.confirmPassword) { if (personalForm.password !== personalForm.confirmPassword) {
setError('Passwörter stimmen nicht überein') setError('Passwords do not match')
return false return false
} }
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(personalForm.password)) { if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(personalForm.password)) {
setError('Passwort muss mindestens 8 Zeichen lang sein und Groß-, Kleinbuchstaben, Ziffern und Sonderzeichen enthalten') setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
return false return false
} }
@ -142,24 +144,24 @@ export default function RegisterForm({
if (!companyForm.companyName.trim() || !companyForm.companyEmail.trim() || if (!companyForm.companyName.trim() || !companyForm.companyEmail.trim() ||
!companyForm.confirmCompanyEmail.trim() || !companyForm.contactPersonName.trim() || !companyForm.confirmCompanyEmail.trim() || !companyForm.contactPersonName.trim() ||
!companyForm.password.trim() || !companyForm.confirmPassword.trim() || !companyForm.password.trim() || !companyForm.confirmPassword.trim() ||
!companyForm.companyPhone.trim() || !companyForm.contactPersonPhone.trim() // now required !companyForm.companyPhone.trim() || !companyForm.contactPersonPhone.trim()
) { ) {
setError('Alle Felder sind erforderlich') setError('All fields are required')
return false return false
} }
if (companyForm.companyEmail !== companyForm.confirmCompanyEmail) { if (companyForm.companyEmail !== companyForm.confirmCompanyEmail) {
setError('E-Mail-Adressen stimmen nicht überein') setError('Email addresses do not match')
return false return false
} }
if (companyForm.password !== companyForm.confirmPassword) { if (companyForm.password !== companyForm.confirmPassword) {
setError('Passwörter stimmen nicht überein') setError('Passwords do not match')
return false return false
} }
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(companyForm.password)) { if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(companyForm.password)) {
setError('Passwort muss mindestens 8 Zeichen lang sein und Groß-, Kleinbuchstaben, Ziffern und Sonderzeichen enthalten') setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
return false return false
} }
@ -187,12 +189,29 @@ export default function RegisterForm({
phone: personalForm.phoneNumber, phone: personalForm.phoneNumber,
}) })
if (res.ok) { if (res.ok) {
showToast({
variant: 'success',
title: 'Registration successful',
message: 'You can now log in with your new account.'
})
onRegistered() onRegistered()
} else { } else {
setError(res.message || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.') const msg = res.message || 'Registration failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
message: msg
})
} }
} catch (error) { } catch (error) {
setError('Registrierung fehlgeschlagen. Bitte versuche es erneut.') const msg = 'Registration failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
message: msg
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -218,12 +237,29 @@ export default function RegisterForm({
contactPersonPhone: companyForm.contactPersonPhone, contactPersonPhone: companyForm.contactPersonPhone,
}) })
if (res.ok) { if (res.ok) {
showToast({
variant: 'success',
title: 'Registration successful',
message: 'You can now log in with your new company account.'
})
onRegistered() onRegistered()
} else { } else {
setError(res.message || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.') const msg = res.message || 'Registration failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
message: msg
})
} }
} catch (error) { } catch (error) {
setError('Registrierung fehlgeschlagen. Bitte versuche es erneut.') const msg = 'Registration failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
message: msg
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -231,7 +267,14 @@ export default function RegisterForm({
// Surface hook error if present and no local error // Surface hook error if present and no local error
useEffect(() => { useEffect(() => {
if (regError && !error) setError(regError) if (regError && !error) {
setError(regError)
showToast({
variant: 'error',
title: 'Registration failed',
message: regError
})
}
}, [regError]) // eslint-disable-line react-hooks/exhaustive-deps }, [regError]) // eslint-disable-line react-hooks/exhaustive-deps
// Input change handlers // Input change handlers
@ -259,18 +302,18 @@ export default function RegisterForm({
const renderPasswordStrength = (password: string) => { const renderPasswordStrength = (password: string) => {
const strength = getPasswordStrength(password) const strength = getPasswordStrength(password)
const rules = [ const rules = [
{ test: password.length >= 8, text: 'Mindestens 8 Zeichen' }, { test: password.length >= 8, text: 'At least 8 characters' },
{ test: /[a-z]/.test(password), text: 'Kleinbuchstaben (a-z)' }, { test: /[a-z]/.test(password), text: 'Lowercase letters (a-z)' },
{ test: /[A-Z]/.test(password), text: 'Großbuchstaben (A-Z)' }, { test: /[A-Z]/.test(password), text: 'Uppercase letters (A-Z)' },
{ test: /\d/.test(password), text: 'Ziffern (0-9)' }, { test: /\d/.test(password), text: 'Digits (0-9)' },
{ test: /[\W_]/.test(password), text: 'Sonderzeichen (!@#$...)' } { test: /[\W_]/.test(password), text: 'Special characters (!@#$...)' }
] ]
return ( return (
<div className="mt-2"> <div className="mt-2">
<div className="text-sm text-slate-700 mb-2">Passwort-Anforderungen:</div> <div className="text-sm text-slate-700 mb-2">Password requirements:</div>
<ul className="text-sm space-y-1"> <ul className="text-sm space-y-1">
{rules.map((rule, index) => ( {rules.map((rule, index) => (
<li key={index} className={`flex items-center gap-2 ${rule.test ? 'text-green-600' : 'text-slate-600'}`}> <li key={index} className={`flex items-center gap-2 ${rule.test ? 'text-green-600' : 'text-slate-600'}`}>
<span>{rule.test ? '✓' : '○'}</span> <span>{rule.test ? '✓' : '○'}</span>
<span>{rule.text}</span> <span>{rule.text}</span>
@ -286,12 +329,12 @@ export default function RegisterForm({
{/* Header */} {/* Header */}
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2"> <h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
Registrierung r Profit Planet Registration for Profit Planet
</h2> </h2>
{/* Replace generic invite with referrer email inside the form */} {/* Replace generic invite with referrer email inside the form */}
{referrerEmail && ( {referrerEmail && (
<p className="text-base sm:text-sm text-[#8D6B1D] font-medium"> <p className="text-base sm:text-sm text-[#8D6B1D] font-medium">
Du wurdest von <span className="font-semibold">{referrerEmail}</span> eingeladen! You were invited by <span className="font-semibold">{referrerEmail}</span>!
</p> </p>
)} )}
</div> </div>
@ -308,7 +351,7 @@ export default function RegisterForm({
onClick={() => setMode('personal')} onClick={() => setMode('personal')}
type="button" type="button"
> >
Privatperson Individual
</button> </button>
<button <button
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${ className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
@ -319,7 +362,7 @@ export default function RegisterForm({
onClick={() => setMode('company')} onClick={() => setMode('company')}
type="button" type="button"
> >
Unternehmen Company
</button> </button>
</div> </div>
</div> </div>
@ -338,7 +381,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="firstName" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="firstName" className="block text-sm font-medium text-[#0F172A] mb-2">
Vorname * First name *
</label> </label>
<input <input
type="text" type="text"
@ -353,7 +396,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="lastName" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="lastName" className="block text-sm font-medium text-[#0F172A] mb-2">
Nachname * Last name *
</label> </label>
<input <input
type="text" type="text"
@ -370,7 +413,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="email" className="block text-sm font-medium text-[#0F172A] mb-2">
E-Mail-Adresse * Email address *
</label> </label>
<input <input
type="email" type="email"
@ -385,7 +428,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="confirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="confirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
E-Mail bestätigen * Confirm email *
</label> </label>
<input <input
type="email" type="email"
@ -401,7 +444,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="phoneNumber" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="phoneNumber" className="block text-sm font-medium text-[#0F172A] mb-2">
Telefonnummer * Phone number *
</label> </label>
<input <input
type="tel" type="tel"
@ -418,7 +461,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
Passwort * Password *
</label> </label>
<div className="relative"> <div className="relative">
<input <input
@ -447,7 +490,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Passwort bestätigen * Confirm password *
</label> </label>
<input <input
type={showPersonalPassword ? 'text' : 'password'} type={showPersonalPassword ? 'text' : 'password'}
@ -473,10 +516,10 @@ export default function RegisterForm({
{loading ? ( {loading ? (
<> <>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Registrierung läuft... Registration in progress...
</> </>
) : ( ) : (
'Jetzt registrieren' 'Register now'
)} )}
</button> </button>
</form> </form>
@ -485,7 +528,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="companyName" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="companyName" className="block text-sm font-medium text-[#0F172A] mb-2">
Firmenname * Company name *
</label> </label>
<input <input
type="text" type="text"
@ -500,7 +543,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="contactPersonName" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="contactPersonName" className="block text-sm font-medium text-[#0F172A] mb-2">
Ansprechpartner * Contact person *
</label> </label>
<input <input
type="text" type="text"
@ -517,7 +560,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="companyEmail" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="companyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Firmen-E-Mail * Company email *
</label> </label>
<input <input
type="email" type="email"
@ -532,7 +575,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="confirmCompanyEmail" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="confirmCompanyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
E-Mail bestätigen * Confirm email *
</label> </label>
<input <input
type="email" type="email"
@ -549,7 +592,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="companyPhone" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="companyPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
Firmen-Telefon * Company phone *
</label> </label>
<input <input
type="tel" type="tel"
@ -565,7 +608,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="contactPersonPhone" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="contactPersonPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
Ansprechpartner-Telefon * Contact person phone *
</label> </label>
<input <input
type="tel" type="tel"
@ -583,7 +626,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
Passwort * Password *
</label> </label>
<div className="relative"> <div className="relative">
<input <input
@ -612,7 +655,7 @@ export default function RegisterForm({
<div> <div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2"> <label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Passwort bestätigen * Confirm password *
</label> </label>
<input <input
type={showCompanyPassword ? 'text' : 'password'} type={showCompanyPassword ? 'text' : 'password'}
@ -638,10 +681,10 @@ export default function RegisterForm({
{loading ? ( {loading ? (
<> <>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Registrierung läuft... Registration in progress...
</> </>
) : ( ) : (
'Firma registrieren' 'Register company'
)} )}
</button> </button>
</form> </form>
@ -651,12 +694,12 @@ export default function RegisterForm({
{/* Login Link */} {/* Login Link */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-slate-700"> <p className="text-slate-700">
Bereits registriert?{' '} Already registered?{' '}
<a <a
href="/login" href="/login"
className="text-[#8D6B1D] hover:text-[#7A5E1A] font-medium transition-colors" className="text-[#8D6B1D] hover:text-[#7A5E1A] font-medium transition-colors"
> >
Hier anmelden Login here
</a> </a>
</p> </p>
</div> </div>

View File

@ -19,7 +19,8 @@ export default function SessionDetectedModal({
}: SessionDetectedModalProps) { }: SessionDetectedModalProps) {
if (inline) { if (inline) {
return ( return (
<div className="w-full flex justify-center items-center flex-1 min-h-[50vh]"> // removed flex-1 and min-h to avoid extra white gap
<div className="w-full flex justify-center items-center py-8">
<div className="bg-white px-6 py-6 rounded-xl shadow-xl max-w-lg w-full border border-gray-200"> <div className="bg-white px-6 py-6 rounded-xl shadow-xl max-w-lg w-full border border-gray-200">
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100"> <div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100">
@ -27,11 +28,10 @@ export default function SessionDetectedModal({
</div> </div>
<div> <div>
<h3 className="text-base font-semibold leading-6 text-[#0F172A]"> <h3 className="text-base font-semibold leading-6 text-[#0F172A]">
Aktive Sitzung erkannt Active session detected
</h3> </h3>
<p className="mt-2 text-sm text-[#4A4A4A]"> <p className="mt-2 text-sm text-[#4A4A4A]">
Du bist bereits angemeldet. Um dich zu registrieren, musst du dich zuerst abmelden You are already logged in. To register, you must first log out or you can go to the dashboard.
oder du kannst zum Dashboard gehen.
</p> </p>
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end"> <div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
<button <button
@ -39,14 +39,14 @@ export default function SessionDetectedModal({
onClick={onCancel} onClick={onCancel}
className="inline-flex justify-center rounded-md bg-white px-4 py-2 text-sm font-medium text-[#4A4A4A] shadow-sm ring-1 ring-gray-300 hover:bg-gray-50 transition-colors" className="inline-flex justify-center rounded-md bg-white px-4 py-2 text-sm font-medium text-[#4A4A4A] shadow-sm ring-1 ring-gray-300 hover:bg-gray-50 transition-colors"
> >
Zum Dashboard Go to dashboard
</button> </button>
<button <button
type="button" type="button"
onClick={onLogout} onClick={onLogout}
className="inline-flex justify-center rounded-md bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors" className="inline-flex justify-center rounded-md bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors"
> >
Abmelden und registrieren Log out and register
</button> </button>
</div> </div>
</div> </div>
@ -95,12 +95,11 @@ export default function SessionDetectedModal({
as="h3" as="h3"
className="text-base font-semibold leading-6 text-[#0F172A]" className="text-base font-semibold leading-6 text-[#0F172A]"
> >
Aktive Sitzung erkannt Active session detected
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-[#4A4A4A]"> <p className="text-sm text-[#4A4A4A]">
Du bist bereits angemeldet. Um dich zu registrieren, musst du dich zuerst abmelden You are already logged in. To register, you must first log out or you can go to the dashboard.
oder du kannst zum Dashboard gehen.
</p> </p>
</div> </div>
</div> </div>
@ -111,14 +110,14 @@ export default function SessionDetectedModal({
className="inline-flex w-full justify-center rounded-md bg-[#8D6B1D] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] sm:ml-3 sm:w-auto transition-colors" className="inline-flex w-full justify-center rounded-md bg-[#8D6B1D] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] sm:ml-3 sm:w-auto transition-colors"
onClick={onLogout} onClick={onLogout}
> >
Abmelden und registrieren Log out and register
</button> </button>
<button <button
type="button" type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-[#4A4A4A] shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors" className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-[#4A4A4A] shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors"
onClick={onCancel} onClick={onCancel}
> >
Zum Dashboard Go to dashboard
</button> </button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@ -7,6 +7,7 @@ import RegisterForm from './components/RegisterForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal' import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal' import InvalidRefLinkModal from './components/invalidRefLinkModal'
import { ToastProvider } from '../components/toast/toastComponent'
export default function RegisterPage() { export default function RegisterPage() {
const searchParams = useSearchParams() const searchParams = useSearchParams()
@ -36,7 +37,7 @@ export default function RegisterPage() {
// Redirect to login after simulated registration // Redirect to login after simulated registration
useEffect(() => { useEffect(() => {
if (registered) { if (registered) {
const t = setTimeout(() => router.push('/login'), 1200) const t = setTimeout(() => router.push('/login'), 4000) // was 1200
return () => clearTimeout(t) return () => clearTimeout(t)
} }
}, [registered, router]) }, [registered, router])
@ -118,23 +119,99 @@ export default function RegisterPage() {
// NEW: Gate rendering until referral check is done // NEW: Gate rendering until referral check is done
if (!isRefChecked) { if (!isRefChecked) {
return ( return (
<PageLayout> <ToastProvider>
<main className="w-full flex flex-col flex-1 items-center justify-center py-24"> <PageLayout>
<div className="text-center"> <main className="w-full flex flex-col flex-1 items-center justify-center py-24 min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div> <div className="text-center">
<p className="text-slate-700">Überprüfe Einladungslink</p> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
</div> <p className="text-slate-700">Checking invitation link</p>
</main> </div>
</PageLayout> </main>
</PageLayout>
</ToastProvider>
) )
} }
// NEW: Invalid referral link state — show modal instead of form with same background as register form // NEW: Invalid referral link state — show modal instead of form with same background as register form
if (invalidRef) { if (invalidRef) {
return ( return (
<ToastProvider>
<PageLayout>
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen">
{/* make wrapper flex-1 so background reaches the footer */}
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
id="register-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
>
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
}}
/>
</div>
{/* Additional background layers */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
<div className="flex flex-col flex-1 items-center justify-center">
<InvalidRefLinkModal
inline
open
token={refToken}
onGoHome={() => router.push('/')}
onClose={() => router.push('/')}
/>
</div>
</div>
</div>
</main>
</PageLayout>
</ToastProvider>
)
}
return (
<ToastProvider>
<PageLayout> <PageLayout>
<main className="w-full flex flex-col flex-1 gap-10"> <main className="w-full flex flex-col flex-1 gap-10 min-h-screen">
<div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24"> {/* Background section wrapper */}
{/* make wrapper flex-1 so background reaches the footer */}
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */} {/* Pattern */}
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -183,120 +260,51 @@ export default function RegisterPage() {
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" /> <div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10"> <div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
<div className="flex flex-col flex-1 items-center justify-center"> {/* Heading (optional adjusted to registration context) */}
<InvalidRefLinkModal <div className="mx-auto max-w-2xl text-center mb-10">
inline <h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
open Register now
token={refToken} </h1>
onGoHome={() => router.push('/')} <p className="mt-2 text-lg/8 text-gray-200">
onClose={() => router.push('/')} Create your personal or company account with Profit Planet.
/> </p>
</div>
{/* Content area */}
<div className="flex flex-col flex-1">
{showSessionModal ? (
<div className="flex flex-1 items-center justify-center">
<SessionDetectedModal
inline
open
onLogout={handleLogout}
onCancel={handleCancel}
/>
</div>
) : (
<>
{/* Register form (only if ref valid) */}
{(!user || sessionCleared) && (
<RegisterForm
mode={mode}
setMode={setMode}
refToken={refToken}
onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/>
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
Registration successful redirecting...
</div>
)}
</>
)}
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</PageLayout> </PageLayout>
) </ToastProvider>
}
return (
<PageLayout>
<main className="w-full flex flex-col flex-1 gap-10">
{/* Background section wrapper */}
<div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
id="register-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
>
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
}}
/>
</div>
{/* Additional background layers */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
{/* Heading (optional adjusted to registration context) */}
<div className="mx-auto max-w-2xl text-center mb-10">
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
Registriere dich jetzt
</h1>
<p className="mt-2 text-lg/8 text-gray-200">
Erstelle dein persönliches oder Unternehmens-Konto bei Profit
Planet.
</p>
</div>
{/* Content area */}
<div className="flex flex-col flex-1">
{showSessionModal ? (
<div className="flex flex-1 items-center justify-center">
<SessionDetectedModal
inline
open
onLogout={handleLogout}
onCancel={handleCancel}
/>
</div>
) : (
<>
{/* Register form (only if ref valid) */}
{(!user || sessionCleared) && (
<RegisterForm
mode={mode}
setMode={setMode}
refToken={refToken}
onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/>
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
Registrierung erfolgreich Weiterleitung...
</div>
)}
</>
)}
</div>
</div>
</div>
</main>
</PageLayout>
) )
} }