refactor: quickactions / login

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

108
package-lock.json generated
View File

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

View File

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

View File

@ -27,26 +27,42 @@ import {
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { ChevronDownIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useAuthStore from '../../store/authStore'; import useAuthStore from '../../store/authStore';
import { Avatar } from '../avatar'; import { Avatar } from '../avatar'
// ENV-BASED FEATURE FLAGS (string envs: treat "false" as off, everything else as on)
const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false'
const DISPLAY_MEMBERSHIP = process.env.NEXT_PUBLIC_DISPLAY_MEMBERSHIP !== 'false'
const DISPLAY_ABOUT_US = process.env.NEXT_PUBLIC_DISPLAY_ABOUT_US !== 'false'
const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false'
const DISPLAY_ABONEMMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false'
const DISPLAY_POOLS = process.env.NEXT_PUBLIC_DISPLAY_POOLS !== 'false'
// Replace current shopItems / informationItems / navLinks block
// ...existing code...
// Replace current shopItems definition with detailed version (adds icon & description) // Replace current shopItems definition with detailed version (adds icon & description)
const shopItems = [ const shopItems = [
{ name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon }, { name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon },
{ name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon }, { name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon },
]; ]
// Information dropdown, controlled by env flags
const informationItems = [ const informationItems = [
{ name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' }, { name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' },
{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }, ...(DISPLAY_MEMBERSHIP
{ name: 'About us', href: '/about-us', description: 'Learn more about us' }, ? [{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }]
]; : []),
...(DISPLAY_ABOUT_US
? [{ name: 'About us', href: '/about-us', description: 'Learn more about us' }]
: []),
]
// Top-level navigation links, controlled by env flags
const navLinks = [ const navLinks = [
{ name: 'News', href: '/news' }, ...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []),
]; ]
// Toggle visibility of Shop navigation across header (desktop + mobile) // Toggle visibility of Shop navigation across header (desktop + mobile)
const showShop = false; const showShop = false
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@ -312,18 +328,24 @@ export default function Header() {
> >
Referral Management Referral Management
</button> </button>
{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>

View File

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

View File

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

View File

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

View File

@ -6,13 +6,20 @@ import LoginForm from './components/LoginForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground' import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground'
import { ToastProvider } from '../components/toast/toastComponent'
export default function LoginPage() { export default function LoginPage() {
const [showBackground, setShowBackground] = useState(false) const [showBackground, setShowBackground] = useState(false)
const [hasHydrated, setHasHydrated] = useState(false)
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
// Check if user is already logged in // Mark when the component has hydrated on the client
useEffect(() => {
setHasHydrated(true)
}, [])
// Redirect if user is already logged in
useEffect(() => { useEffect(() => {
if (user) { if (user) {
router.push('/dashboard') router.push('/dashboard')
@ -27,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>
) )
} }

View File

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

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
interface CompanyProfileData { interface CompanyProfileData {
companyName: string companyName: string
@ -50,6 +51,7 @@ export default function CompanyAdditionalInformationPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(init) const [form, setForm] = useState(init)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -68,12 +70,24 @@ export default function CompanyAdditionalInformationPage() {
] ]
for (const k of required) { for (const k of required) {
if (!form[k].trim()) { if (!form[k].trim()) {
setError('Bitte alle Pflichtfelder ausfüllen.') const msg = 'Bitte alle Pflichtfelder ausfüllen.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
} }
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) { if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
setError('Ungültige IBAN.') const msg = 'Ungültige IBAN.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid IBAN',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -86,7 +100,13 @@ export default function CompanyAdditionalInformationPage() {
if (!validate()) return if (!validate()) return
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -122,6 +142,11 @@ export default function CompanyAdditionalInformationPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Profile saved',
message: 'Your company profile has been saved successfully.',
})
// Refresh user status to update profile completion state // Refresh user status to update profile completion state
await refreshStatus() await refreshStatus()
@ -141,7 +166,13 @@ export default function CompanyAdditionalInformationPage() {
} catch (error: any) { } catch (error: any) {
console.error('Company profile save error:', error) console.error('Company profile save error:', error)
setError(error.message || 'Speichern fehlgeschlagen.') const msg = error.message || 'Speichern fehlgeschlagen.'
setError(msg)
showToast({
variant: 'error',
title: 'Save failed',
message: msg,
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -149,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>
) )

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
interface PersonalProfileData { interface PersonalProfileData {
dob: string dob: string
@ -58,6 +59,7 @@ export default function PersonalAdditionalInformationPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(initialData) const [form, setForm] = useState(initialData)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -145,20 +147,38 @@ export default function PersonalAdditionalInformationPage() {
] ]
for (const k of requiredKeys) { for (const k of requiredKeys) {
if (!form[k].trim()) { if (!form[k].trim()) {
setError('Please fill in all required fields.') const msg = 'Please fill in all required fields.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
} }
// Date of birth validation // Date of birth validation
if (!validateDateOfBirth(form.dob)) { if (!validateDateOfBirth(form.dob)) {
setError('Invalid date of birth. You must be at least 18 years old.') const msg = 'Invalid date of birth. You must be at least 18 years old.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid date of birth',
message: msg,
})
return false return false
} }
// very loose IBAN check // very loose IBAN check
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) { if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
setError('Invalid IBAN.') const msg = 'Invalid IBAN.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid IBAN',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -171,7 +191,13 @@ export default function PersonalAdditionalInformationPage() {
if (!validate()) return if (!validate()) return
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -208,6 +234,11 @@ export default function PersonalAdditionalInformationPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Profile saved',
message: 'Your personal profile has been saved successfully.',
})
// Refresh user status to update profile completion state // Refresh user status to update profile completion state
await refreshStatus() await refreshStatus()
@ -227,7 +258,13 @@ export default function PersonalAdditionalInformationPage() {
} catch (error: any) { } catch (error: any) {
console.error('Personal profile save error:', error) console.error('Personal profile save error:', error)
setError(error.message || 'Save failed. Please try again.') const msg = error.message || 'Save failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Save failed',
message: msg,
})
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -235,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>
) )

View File

@ -4,7 +4,8 @@ import { useState, useEffect, useCallback, useRef } from 'react'
import PageLayout from '../../components/PageLayout' import PageLayout from '../../components/PageLayout'
import useAuthStore from '../../store/authStore' import useAuthStore from '../../store/authStore'
import { useUserStatus } from '../../hooks/useUserStatus' import { useUserStatus } from '../../hooks/useUserStatus'
import { useRouter } from 'next/navigation' // NEW import { useRouter } from 'next/navigation'
import { useToast } from '../../components/toast/toastComponent'
export default function EmailVerifyPage() { export default function EmailVerifyPage() {
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
@ -18,7 +19,8 @@ export default function EmailVerifyPage() {
const [initialEmailSent, setInitialEmailSent] = useState(false) const [initialEmailSent, setInitialEmailSent] = useState(false)
const inputsRef = useRef<Array<HTMLInputElement | null>>([]) const inputsRef = useRef<Array<HTMLInputElement | null>>([])
const emailSentRef = useRef(false) const emailSentRef = useRef(false)
const router = useRouter() // NEW const router = useRouter()
const { showToast } = useToast()
// NEW: resend and validity windows // NEW: resend and validity windows
const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
@ -63,18 +65,36 @@ export default function EmailVerifyPage() {
setInitialEmailSent(true) setInitialEmailSent(true)
setLastSentAt(Date.now(), user?.email) setLastSentAt(Date.now(), user?.email)
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
showToast({
variant: 'success',
title: 'Verification email sent',
message: `We sent a verification email to ${user?.email || 'your email'}.`
})
} else { } else {
console.error('Failed to send initial verification email:', data?.message) const msg = data?.message || 'Error sending the verification email.'
setError(msg)
emailSentRef.current = false emailSentRef.current = false
showToast({
variant: 'error',
title: 'Email not sent',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Error sending initial verification email:', error) console.error('Error sending initial verification email:', err)
const msg = 'Network error while sending the verification email.'
setError(msg)
emailSentRef.current = false emailSentRef.current = false
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} }
} }
sendInitialEmail() sendInitialEmail()
}, [token, user]) }, [token, user, showToast])
// Cooldown timer // Cooldown timer
useEffect(() => { useEffect(() => {
@ -172,11 +192,23 @@ export default function EmailVerifyPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (fullCode.length !== 6) { if (fullCode.length !== 6) {
setError('Please enter the 6-digit code.') const msg = 'Please enter the 6-digit code.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid code',
message: msg
})
return return
} }
if (!token) { if (!token) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg
})
return return
} }
@ -196,13 +228,15 @@ export default function EmailVerifyPage() {
if (response.ok && data.success) { if (response.ok && data.success) {
setSuccess(true) setSuccess(true)
await refreshStatus() // Refresh user status showToast({
// Redirect after 2 seconds variant: 'success',
title: 'Email verified',
message: 'Your email has been verified successfully.'
})
await refreshStatus()
setTimeout(() => { setTimeout(() => {
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true' const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) { if (fromTutorial) {
window.location.href = '/quickaction-dashboard?tutorial=true' window.location.href = '/quickaction-dashboard?tutorial=true'
} else { } else {
@ -210,11 +244,23 @@ export default function EmailVerifyPage() {
} }
}, 2000) }, 2000)
} else { } else {
setError(data.error || 'Verification failed. Please try again.') const msg = data.error || 'Verification failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Verification failed',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Email verification error:', error) console.error('Email verification error:', err)
setError('Network error. Please try again.') const msg = 'Network error. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -232,7 +278,17 @@ export default function EmailVerifyPage() {
setResendCooldown(Math.ceil(remaining / 1000)) setResendCooldown(Math.ceil(remaining / 1000))
return return
} }
if (!token) return if (!token) {
const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg
})
return
}
setError('') setError('')
try { try {
@ -250,14 +306,31 @@ export default function EmailVerifyPage() {
setLastSentAt(Date.now(), user?.email) setLastSentAt(Date.now(), user?.email)
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
if (!initialEmailSent) setInitialEmailSent(true) if (!initialEmailSent) setInitialEmailSent(true)
showToast({
variant: 'success',
title: 'Verification email sent',
message: `We sent a new verification email to ${user?.email || 'your email'}.`
})
} else { } else {
setError(data?.message || 'Error sending the email.') const msg = data?.message || 'Error sending the email.'
setError(msg)
showToast({
variant: 'error',
title: 'Email not sent',
message: msg
})
} }
} catch (error) { } catch (err) {
console.error('Resend email error:', error) console.error('Resend email error:', err)
setError('Network error while sending the email.') const msg = 'Network error while sending the email.'
setError(msg)
showToast({
variant: 'error',
title: 'Network error',
message: msg
})
} }
}, [token, submitting, success, user, initialEmailSent]) }, [token, submitting, success, user, initialEmailSent, showToast])
// NEW: format seconds to m:ss // NEW: format seconds to m:ss
const formatMmSs = (total: number) => { const formatMmSs = (total: number) => {
@ -268,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">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '} Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 dark:text-indigo-400 hover:underline"> <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>
) )

View File

@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api' import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent' // NEW
export default function CompanySignContractPage() { export default function CompanySignContractPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast() // NEW
const [companyName, setCompanyName] = useState('') const [companyName, setCompanyName] = useState('')
const [repName, setRepName] = useState('') const [repName, setRepName] = useState('')
@ -77,7 +79,7 @@ export default function CompanySignContractPage() {
e.preventDefault() e.preventDefault()
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues = [] const issues: string[] = []
if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)') if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)')
if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)') if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)')
if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)') if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)')
@ -86,12 +88,24 @@ export default function CompanySignContractPage() {
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
setError(`Please complete: ${issues.join(', ')}`) const msg = `Please complete: ${issues.join(', ')}`
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return return
} }
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -137,6 +151,11 @@ export default function CompanySignContractPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Contract signed',
message: 'Your company contract has been signed successfully.',
})
// Refresh user status to update contract signed state // Refresh user status to update contract signed state
await refreshStatus() await refreshStatus()
@ -156,7 +175,13 @@ export default function CompanySignContractPage() {
} catch (error: any) { } catch (error: any) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
setError(error.message || 'Signature failed. Please try again.') const msg = error.message || 'Signature failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Signature failed',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -164,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>
) )

View File

@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api' import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent'
export default function PersonalSignContractPage() { export default function PersonalSignContractPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [fullName, setFullName] = useState('') const [fullName, setFullName] = useState('')
const [location, setLocation] = useState('') const [location, setLocation] = useState('')
@ -72,19 +74,31 @@ export default function PersonalSignContractPage() {
e.preventDefault() e.preventDefault()
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues = [] const issues: string[] = []
if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)') if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)')
if (location.trim().length < 2) issues.push('Location (min 2 characters)') if (location.trim().length < 2) issues.push('Location (min 2 characters)')
if (!agreeContract) issues.push('Contract read and understood') if (!agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
setError(`Please complete: ${issues.join(', ')}`) const msg = `Please complete: ${issues.join(', ')}`
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return return
} }
if (!accessToken) { if (!accessToken) {
setError('Not authenticated. Please log in again.') const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return return
} }
@ -128,6 +142,11 @@ export default function PersonalSignContractPage() {
} }
setSuccess(true) setSuccess(true)
showToast({
variant: 'success',
title: 'Contract signed',
message: 'Your personal contract has been signed successfully.',
})
// Refresh user status to update contract signed state // Refresh user status to update contract signed state
await refreshStatus() await refreshStatus()
@ -147,7 +166,13 @@ export default function PersonalSignContractPage() {
} catch (error: any) { } catch (error: any) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
setError(error.message || 'Signature failed. Please try again.') const msg = error.message || 'Signature failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Signature failed',
message: msg,
})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@ -155,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>
) )

View File

@ -3,11 +3,13 @@
import { useState, useRef, useEffect, useCallback } from 'react' import { useState, useRef, useEffect, useCallback } from 'react'
import useAuthStore from '../../../../store/authStore' import useAuthStore from '../../../../store/authStore'
import { useUserStatus } from '../../../../hooks/useUserStatus' import { useUserStatus } from '../../../../hooks/useUserStatus'
import { useToast } from '../../../../components/toast/toastComponent'
export function useCompanyUploadId() { export function useCompanyUploadId() {
// Auth + status // Auth + status
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast()
// Form state // Form state
const [idNumber, setIdNumber] = useState('') const [idNumber, setIdNumber] = useState('')
@ -37,7 +39,13 @@ export function useCompanyUploadId() {
// File handlers // File handlers
const handleFile = (file: File, which: 'front' | 'extra') => { const handleFile = (file: File, which: 'front' | 'extra') => {
if (file.size > 10 * 1024 * 1024) { if (file.size > 10 * 1024 * 1024) {
setError('File size exceeds 10 MB.') const msg = 'File size exceeds 10 MB.'
setError(msg)
showToast({
variant: 'error',
title: 'File too large',
message: msg,
})
return return
} }
setError('') setError('')
@ -81,7 +89,13 @@ export function useCompanyUploadId() {
// Validation // Validation
const validate = () => { const validate = () => {
if (!idNumber.trim() || !idType || !expiryDate || !frontFile) { if (!idNumber.trim() || !idType || !expiryDate || !frontFile) {
setError('Please complete all required fields (marked with *).') const msg = 'Please complete all required fields (marked with *).'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false return false
} }
setError('') setError('')
@ -93,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)
} }

View File

@ -3,9 +3,9 @@
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline' import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { useCompanyUploadId } from './hooks/useCompanyUploadId' import { useCompanyUploadId } from './hooks/useCompanyUploadId'
import useAuthStore from '../../../store/authStore' // NEW import useAuthStore from '../../../store/authStore'
import { useEffect, useState } from 'react' // NEW import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' // NEW import { useRouter } from 'next/navigation'
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel'] const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
@ -25,9 +25,9 @@ export default function CompanyIdUploadPage() {
handleFile, onDrop, clearFile, dropHandlers, openPicker, submit, handleFile, onDrop, clearFile, dropHandlers, openPicker, submit,
} = useCompanyUploadId() } = useCompanyUploadId()
const user = useAuthStore(s => s.user) // NEW const user = useAuthStore(s => s.user)
const router = useRouter() // NEW const router = useRouter()
const [blocked, setBlocked] = useState(false) // NEW const [blocked, setBlocked] = useState(false)
// Guard: only 'company' users allowed on this page // Guard: only 'company' users allowed on this page
useEffect(() => { useEffect(() => {
@ -56,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>
) )

View File

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

View File

@ -60,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>
) )

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import RegisterForm from './components/RegisterForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal' import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal' import InvalidRefLinkModal from './components/invalidRefLinkModal'
import { ToastProvider } from '../components/toast/toastComponent'
export default function RegisterPage() { export default function RegisterPage() {
const searchParams = useSearchParams() const searchParams = useSearchParams()
@ -36,7 +37,7 @@ export default function RegisterPage() {
// Redirect to login after simulated registration // Redirect to login after simulated registration
useEffect(() => { useEffect(() => {
if (registered) { if (registered) {
const t = setTimeout(() => router.push('/login'), 1200) const t = setTimeout(() => router.push('/login'), 4000) // was 1200
return () => clearTimeout(t) return () => clearTimeout(t)
} }
}, [registered, router]) }, [registered, router])
@ -118,23 +119,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>
) )
} }