refactor: quickactions / login
This commit is contained in:
parent
20c39fcd4e
commit
b4a7a5f840
108
package-lock.json
generated
108
package-lock.json
generated
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
{DISPLAY_MATRIX && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/personal-matrix')}
|
onClick={() => router.push('/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"
|
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
>
|
>
|
||||||
Personal Matrix
|
Personal Matrix
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{DISPLAY_ABONEMMENTS && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/coffee-abonnements')}
|
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"
|
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
|
Coffee Abonnements
|
||||||
</button>
|
</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,6 +526,8 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
User Management
|
User Management
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{DISPLAY_MATRIX && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/admin/matrix-management'); setAdminMgmtOpen(false); }}
|
onClick={() => { router.push('/admin/matrix-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]"
|
||||||
@ -514,6 +535,8 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Matrix Management
|
Matrix Management
|
||||||
</button>
|
</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,6 +544,9 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Contract Management
|
Contract Management
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{DISPLAY_ABONEMMENTS && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }}
|
onClick={() => { router.push('/admin/subscriptions'); 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]"
|
||||||
@ -535,6 +561,10 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Finance Management
|
Finance Management
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{DISPLAY_POOLS && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }}
|
onClick={() => { router.push('/admin/pool-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]"
|
||||||
@ -542,6 +572,8 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Pool Management
|
Pool Management
|
||||||
</button>
|
</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,6 +581,8 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Affiliate Management
|
Affiliate Management
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{DISPLAY_NEWS && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/admin/news-management'); setAdminMgmtOpen(false); }}
|
onClick={() => { router.push('/admin/news-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]"
|
||||||
@ -556,6 +590,7 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
News Management
|
News Management
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -735,18 +770,22 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Referral Management
|
Referral Management
|
||||||
</button>
|
</button>
|
||||||
|
{DISPLAY_MATRIX && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/personal-matrix'); setMobileMenuOpen(false); }}
|
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"
|
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
|
Personal Matrix
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{DISPLAY_ABONEMMENTS && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
|
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"
|
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
|
Coffee Abonnements
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
264
src/app/components/toast/toastComponent.tsx
Normal file
264
src/app/components/toast/toastComponent.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,9 +34,10 @@ 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 (
|
||||||
|
<ToastProvider>
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -38,10 +46,12 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ToastProvider>
|
||||||
<PageLayout showFooter={true}>
|
<PageLayout showFooter={true}>
|
||||||
<div
|
<div
|
||||||
className="relative w-full flex flex-col min-h-screen"
|
className="relative w-full flex flex-col min-h-screen"
|
||||||
@ -58,5 +68,6 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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,26 +180,18 @@ 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>
|
||||||
|
|
||||||
|
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||||
<form
|
<form
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
||||||
@ -375,7 +398,14 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-10 flex justify-end">
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || success}
|
disabled={loading || success}
|
||||||
@ -386,6 +416,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,26 +272,18 @@ 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>
|
||||||
|
|
||||||
|
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
||||||
@ -461,7 +490,14 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-10 flex justify-end">
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || success}
|
disabled={loading || success}
|
||||||
@ -472,6 +508,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,54 +341,28 @@ 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>
|
||||||
|
|
||||||
|
<main className="relative z-10 flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
|
||||||
<div className="max-w-xl mx-auto">
|
<div className="max-w-xl mx-auto">
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
|
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
|
||||||
Verify your email
|
Verify your email
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
<p className="mt-3 text-gray-700 text-sm sm:text-base">
|
||||||
{initialEmailSent ? (
|
{initialEmailSent ? (
|
||||||
<>
|
<>
|
||||||
We sent a 6-digit code to{' '}
|
We sent a 6-digit code to{' '}
|
||||||
<span className="text-indigo-300 font-medium">
|
<span className="text-blue-700 font-medium">
|
||||||
{user?.email || 'your email'}
|
{user?.email || 'your email'}
|
||||||
</span>
|
</span>
|
||||||
. Enter it below.
|
. Enter it below.
|
||||||
@ -323,7 +370,7 @@ export default function EmailVerifyPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Sending verification email to{' '}
|
Sending verification email to{' '}
|
||||||
<span className="text-indigo-300 font-medium">
|
<span className="text-blue-700 font-medium">
|
||||||
{user?.email || 'your email'}
|
{user?.email || 'your email'}
|
||||||
</span>
|
</span>
|
||||||
...
|
...
|
||||||
@ -335,9 +382,10 @@ export default function EmailVerifyPage() {
|
|||||||
{/* Card */}
|
{/* Card */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
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"
|
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">
|
<fieldset disabled={submitting || success} className="space-y-8">
|
||||||
|
{/* Inputs */}
|
||||||
<div className="flex justify-center gap-2 sm:gap-3">
|
<div className="flex justify-center gap-2 sm:gap-3">
|
||||||
{code.map((v, i) => (
|
{code.map((v, i) => (
|
||||||
<input
|
<input
|
||||||
@ -353,8 +401,8 @@ export default function EmailVerifyPage() {
|
|||||||
onPaste={e => handlePaste(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
|
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
|
${v
|
||||||
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
|
||||||
: 'border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/70 text-gray-700 dark:text-gray-200'}
|
: 'border-gray-300 bg-white/80 text-gray-700'}
|
||||||
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
|
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -389,7 +437,7 @@ export default function EmailVerifyPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
disabled={!!resendCooldown || submitting || success}
|
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"
|
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{resendCooldown
|
{resendCooldown
|
||||||
? `Resend in ${formatMmSs(resendCooldown)}`
|
? `Resend in ${formatMmSs(resendCooldown)}`
|
||||||
@ -397,28 +445,27 @@ export default function EmailVerifyPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* NEW: Go to Dashboard button */}
|
|
||||||
<div className="mt-1 text-center">
|
<div className="mt-1 text-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push('/quickaction-dashboard')}
|
onClick={() => router.push('/quickaction-dashboard')}
|
||||||
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:underline"
|
className="text-sm font-medium text-gray-700 hover:underline"
|
||||||
>
|
>
|
||||||
Go to Dashboard
|
Go to Dashboard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{/* Helper text with validity + spam/junk reminder + support */}
|
<div className="mt-8 text-center text-xs text-gray-500">
|
||||||
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
Didn’t receive the email? Please check your junk/spam folder. Still having issues?{' '}
|
Didn’t 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">
|
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
|
||||||
Contact support
|
Contact support
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,26 +189,18 @@ 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>
|
||||||
|
|
||||||
|
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
||||||
@ -402,7 +419,14 @@ export default function CompanySignContractPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-10 flex justify-end">
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || success}
|
disabled={submitting || success}
|
||||||
@ -413,6 +437,7 @@ export default function CompanySignContractPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,26 +180,18 @@ 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>
|
||||||
|
|
||||||
|
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
||||||
@ -343,7 +360,14 @@ export default function PersonalSignContractPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-10 flex justify-end">
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || success}
|
disabled={submitting || success}
|
||||||
@ -354,6 +378,7 @@ export default function PersonalSignContractPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,7 +107,13 @@ 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.'
|
||||||
|
setError(msg)
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
title: 'Authentication error',
|
||||||
|
message: msg,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,27 +56,22 @@ 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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
<div className="px-6 py-8 sm:px-12 lg:px-16">
|
<div className="px-6 py-8 sm:px-12 lg:px-16">
|
||||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
||||||
Company Contact Person Identity Verification
|
Company Contact Person Identity Verification
|
||||||
@ -289,6 +284,7 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,27 +60,22 @@ 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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
<div className="px-6 py-8 sm:px-12 lg:px-16">
|
<div className="px-6 py-8 sm:px-12 lg:px-16">
|
||||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
||||||
Personal Identity Verification
|
Personal Identity Verification
|
||||||
@ -300,6 +295,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,16 +302,16 @@ 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'}`}>
|
||||||
@ -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 fü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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,27 @@ 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 (
|
||||||
|
<ToastProvider>
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<main className="w-full flex flex-col flex-1 items-center justify-center py-24">
|
<main className="w-full flex flex-col flex-1 items-center justify-center py-24 min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
|
<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">Überprüfe Einladungslink…</p>
|
<p className="text-slate-700">Checking invitation link…</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</PageLayout>
|
</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>
|
<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">
|
{/* 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"
|
||||||
@ -196,14 +201,17 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
||||||
{/* Background section wrapper */}
|
{/* Background section wrapper */}
|
||||||
<div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
|
{/* 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"
|
||||||
@ -255,11 +263,10 @@ export default function RegisterPage() {
|
|||||||
{/* Heading (optional – adjusted to registration context) */}
|
{/* Heading (optional – adjusted to registration context) */}
|
||||||
<div className="mx-auto max-w-2xl text-center mb-10">
|
<div className="mx-auto max-w-2xl text-center mb-10">
|
||||||
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
|
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
|
||||||
Registriere dich jetzt
|
Register now
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-lg/8 text-gray-200">
|
<p className="mt-2 text-lg/8 text-gray-200">
|
||||||
Erstelle dein persönliches oder Unternehmens-Konto bei Profit
|
Create your personal or company account with Profit Planet.
|
||||||
Planet.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -288,7 +295,7 @@ export default function RegisterPage() {
|
|||||||
)}
|
)}
|
||||||
{registered && (
|
{registered && (
|
||||||
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
|
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
|
||||||
Registrierung erfolgreich – Weiterleitung...
|
Registration successful – redirecting...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -298,5 +305,6 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user