Merge branch 'dev' of https://git.profit-planet.partners/Seazn/profit-planet-frontend into dev
This commit is contained in:
commit
39bc871fd9
108
package-lock.json
generated
108
package-lock.json
generated
@ -45,6 +45,7 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
@ -96,6 +97,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@ -506,6 +508,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -529,6 +532,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -2660,9 +2664,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
|
||||
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
|
||||
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@ -2676,9 +2680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
|
||||
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
|
||||
"integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2692,9 +2696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
|
||||
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
|
||||
"integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2708,9 +2712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
|
||||
"integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2724,9 +2728,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
|
||||
"integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2740,9 +2744,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
|
||||
"integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2756,9 +2760,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
|
||||
"integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2772,9 +2776,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
|
||||
"integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2788,9 +2792,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
|
||||
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3567,6 +3571,7 @@
|
||||
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -3633,6 +3638,7 @@
|
||||
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
@ -4156,6 +4162,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -4525,9 +4532,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
|
||||
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
|
||||
"version": "2.9.14",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
|
||||
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@ -4603,6 +4610,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@ -4996,7 +5004,8 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@ -5420,6 +5429,7 @@
|
||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -5594,6 +5604,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@ -7691,13 +7702,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
|
||||
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
|
||||
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.0.7",
|
||||
"@next/env": "16.1.1",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
@ -7709,14 +7721,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.0.7",
|
||||
"@next/swc-darwin-x64": "16.0.7",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.7",
|
||||
"@next/swc-linux-arm64-musl": "16.0.7",
|
||||
"@next/swc-linux-x64-gnu": "16.0.7",
|
||||
"@next/swc-linux-x64-musl": "16.0.7",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.7",
|
||||
"@next/swc-win32-x64-msvc": "16.0.7",
|
||||
"@next/swc-darwin-arm64": "16.1.1",
|
||||
"@next/swc-darwin-x64": "16.1.1",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.1",
|
||||
"@next/swc-linux-arm64-musl": "16.1.1",
|
||||
"@next/swc-linux-x64-gnu": "16.1.1",
|
||||
"@next/swc-linux-x64-musl": "16.1.1",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.1",
|
||||
"@next/swc-win32-x64-msvc": "16.1.1",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -8113,6 +8125,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -8831,6 +8844,7 @@
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@ -8923,6 +8937,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -8932,6 +8947,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -8958,6 +8974,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
||||
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@ -9764,7 +9781,8 @@
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.3",
|
||||
@ -9862,6 +9880,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -10038,6 +10057,7 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
|
||||
93
src/app/admin/finance-management/hooks/getInvoices.ts
Normal file
93
src/app/admin/finance-management/hooks/getInvoices.ts
Normal file
@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import useAuthStore from '../../../store/authStore';
|
||||
|
||||
export type AdminInvoice = {
|
||||
id: string | number;
|
||||
invoice_number?: string | null;
|
||||
user_id?: string | number | null;
|
||||
buyer_name?: string | null;
|
||||
buyer_email?: string | null;
|
||||
buyer_street?: string | null;
|
||||
buyer_postal_code?: string | null;
|
||||
buyer_city?: string | null;
|
||||
buyer_country?: string | null;
|
||||
currency?: string | null;
|
||||
total_net?: number | null;
|
||||
total_tax?: number | null;
|
||||
total_gross?: number | null;
|
||||
vat_rate?: number | null;
|
||||
status?: string;
|
||||
issued_at?: string | null;
|
||||
due_at?: string | null;
|
||||
pdf_storage_key?: string | null;
|
||||
context?: any | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
export function useAdminInvoices(params?: { status?: string; limit?: number; offset?: number }) {
|
||||
const accessToken = useAuthStore(s => s.accessToken);
|
||||
const [invoices, setInvoices] = useState<AdminInvoice[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const inFlight = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchInvoices = useCallback(async () => {
|
||||
setError('');
|
||||
// Abort previous
|
||||
inFlight.current?.abort();
|
||||
const controller = new AbortController();
|
||||
inFlight.current = controller;
|
||||
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
||||
const qp = new URLSearchParams();
|
||||
if (params?.status) qp.set('status', params.status);
|
||||
qp.set('limit', String(params?.limit ?? 200));
|
||||
qp.set('offset', String(params?.offset ?? 0));
|
||||
const url = `${base}/api/admin/invoices${qp.toString() ? `?${qp.toString()}` : ''}`;
|
||||
|
||||
setLoading(true);
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok || body?.success === false) {
|
||||
setInvoices([]);
|
||||
setError(body?.message || `Failed to load invoices (${res.status})`);
|
||||
return;
|
||||
}
|
||||
const list: AdminInvoice[] = Array.isArray(body?.data) ? body.data : [];
|
||||
// sort fallback (issued_at DESC then created_at DESC)
|
||||
list.sort((a, b) => {
|
||||
const ad = new Date(a.issued_at ?? a.created_at ?? 0).getTime();
|
||||
const bd = new Date(b.issued_at ?? b.created_at ?? 0).getTime();
|
||||
return bd - ad;
|
||||
});
|
||||
setInvoices(list);
|
||||
} catch (e: any) {
|
||||
if (e?.name === 'AbortError') return;
|
||||
setError(e?.message || 'Network error');
|
||||
setInvoices([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (inFlight.current === controller) inFlight.current = null;
|
||||
}
|
||||
}, [accessToken, params?.status, params?.limit, params?.offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) fetchInvoices();
|
||||
return () => inFlight.current?.abort();
|
||||
}, [accessToken, fetchInvoices]);
|
||||
|
||||
return { invoices, loading, error, reload: fetchInvoices };
|
||||
}
|
||||
@ -3,23 +3,7 @@ import React, { useMemo, useState } from 'react'
|
||||
import PageLayout from '../../components/PageLayout'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useVatRates } from './hooks/getTaxes'
|
||||
|
||||
type VatRate = { country: string; code: string; rate: number }
|
||||
type Bill = {
|
||||
id: string
|
||||
customer: string
|
||||
amount: number
|
||||
currency: string
|
||||
date: string
|
||||
status: 'paid' | 'open' | 'overdue'
|
||||
}
|
||||
|
||||
const dummyBills: Bill[] = [
|
||||
{ id: 'INV-1001', customer: 'Acme GmbH', amount: 1200, currency: 'EUR', date: '2025-12-01', status: 'paid' },
|
||||
{ id: 'INV-1002', customer: 'Beta SARL', amount: 860, currency: 'EUR', date: '2025-11-20', status: 'open' },
|
||||
{ id: 'INV-1003', customer: 'Charlie SpA', amount: 540, currency: 'EUR', date: '2025-11-15', status: 'overdue' },
|
||||
{ id: 'INV-1004', customer: 'Delta BV', amount: 2300, currency: 'EUR', date: '2025-10-02', status: 'paid' },
|
||||
]
|
||||
import { useAdminInvoices } from './hooks/getInvoices'
|
||||
|
||||
export default function FinanceManagementPage() {
|
||||
const router = useRouter()
|
||||
@ -27,38 +11,60 @@ export default function FinanceManagementPage() {
|
||||
const [timeframe, setTimeframe] = useState<'7d' | '30d' | '90d' | 'ytd'>('30d')
|
||||
const [billFilter, setBillFilter] = useState({ query: '', status: 'all', from: '', to: '' })
|
||||
|
||||
// NEW: fetch invoices from backend
|
||||
const {
|
||||
invoices,
|
||||
loading: invLoading,
|
||||
error: invError,
|
||||
reload,
|
||||
} = useAdminInvoices({
|
||||
status: billFilter.status !== 'all' ? billFilter.status : undefined,
|
||||
limit: 200,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
// NEW: totals from backend invoices
|
||||
const totals = useMemo(() => {
|
||||
const now = new Date()
|
||||
const filterDate = (d: string) => new Date(d)
|
||||
const inRange = (d: Date) => {
|
||||
const diff = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)
|
||||
if (timeframe === '7d') return diff <= 7
|
||||
if (timeframe === '30d') return diff <= 30
|
||||
if (timeframe === '90d') return diff <= 90
|
||||
return true // ytd or default
|
||||
return true
|
||||
}
|
||||
const filtered = dummyBills.filter(b => inRange(filterDate(b.date)))
|
||||
const total = dummyBills.reduce((s, b) => s + b.amount, 0)
|
||||
const totalRange = filtered.reduce((s, b) => s + b.amount, 0)
|
||||
return { totalAll: total, totalRange }
|
||||
}, [timeframe])
|
||||
|
||||
const filteredBills = useMemo(() => {
|
||||
return dummyBills.filter(b => {
|
||||
const matchesQuery =
|
||||
billFilter.query === '' ||
|
||||
b.id.toLowerCase().includes(billFilter.query.toLowerCase()) ||
|
||||
b.customer.toLowerCase().includes(billFilter.query.toLowerCase())
|
||||
const matchesStatus = billFilter.status === 'all' || b.status === billFilter.status
|
||||
const fromOk = billFilter.from ? new Date(b.date) >= new Date(billFilter.from) : true
|
||||
const toOk = billFilter.to ? new Date(b.date) <= new Date(billFilter.to) : true
|
||||
return matchesQuery && matchesStatus && fromOk && toOk
|
||||
const range = invoices.filter(inv => {
|
||||
const dStr = inv.issued_at ?? inv.created_at
|
||||
if (!dStr) return false
|
||||
const d = new Date(dStr)
|
||||
return inRange(d)
|
||||
})
|
||||
}, [billFilter])
|
||||
const totalAll = invoices.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0)
|
||||
const totalRange = range.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0)
|
||||
return { totalAll, totalRange }
|
||||
}, [invoices, timeframe])
|
||||
|
||||
// NEW: filtered rows for table
|
||||
const filteredBills = useMemo(() => {
|
||||
const q = billFilter.query.trim().toLowerCase()
|
||||
const from = billFilter.from ? new Date(billFilter.from) : null
|
||||
const to = billFilter.to ? new Date(billFilter.to) : null
|
||||
|
||||
return invoices.filter(inv => {
|
||||
const byQuery =
|
||||
!q ||
|
||||
String(inv.invoice_number ?? inv.id).toLowerCase().includes(q) ||
|
||||
String(inv.buyer_name ?? '').toLowerCase().includes(q)
|
||||
const issued = inv.issued_at ? new Date(inv.issued_at) : (inv.created_at ? new Date(inv.created_at) : null)
|
||||
const byFrom = from ? (issued ? issued >= from : false) : true
|
||||
const byTo = to ? (issued ? issued <= to : false) : true
|
||||
return byQuery && byFrom && byTo
|
||||
})
|
||||
}, [invoices, billFilter])
|
||||
|
||||
const exportBills = (format: 'csv' | 'pdf') => {
|
||||
console.log('[export]', format, { filters: billFilter, bills: filteredBills })
|
||||
alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} bills`)
|
||||
console.log('[export]', format, { filters: billFilter, invoices: filteredBills })
|
||||
alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} invoices`)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -129,12 +135,13 @@ export default function FinanceManagementPage() {
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button>
|
||||
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button>
|
||||
<button onClick={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4 text-sm">
|
||||
<input
|
||||
placeholder="Search (ID, customer)"
|
||||
placeholder="Search (invoice no., customer)"
|
||||
value={billFilter.query}
|
||||
onChange={e => setBillFilter(f => ({ ...f, query: e.target.value }))}
|
||||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||||
@ -145,9 +152,11 @@ export default function FinanceManagementPage() {
|
||||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Status: All</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="issued">Issued</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
<option value="canceled">Canceled</option>
|
||||
</select>
|
||||
<input
|
||||
type="date"
|
||||
@ -164,35 +173,59 @@ export default function FinanceManagementPage() {
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{invError && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
|
||||
{invError}
|
||||
</div>
|
||||
)}
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-blue-50 text-left text-blue-900">
|
||||
<th className="px-3 py-2 font-semibold">Invoice</th>
|
||||
<th className="px-3 py-2 font-semibold">Customer</th>
|
||||
<th className="px-3 py-2 font-semibold">Date</th>
|
||||
<th className="px-3 py-2 font-semibold">Issued</th>
|
||||
<th className="px-3 py-2 font-semibold">Amount</th>
|
||||
<th className="px-3 py-2 font-semibold">Status</th>
|
||||
<th className="px-3 py-2 font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredBills.map(b => (
|
||||
<tr key={b.id} className="border-b last:border-0">
|
||||
<td className="px-3 py-2">{b.id}</td>
|
||||
<td className="px-3 py-2">{b.customer}</td>
|
||||
<td className="px-3 py-2">{new Date(b.date).toLocaleDateString()}</td>
|
||||
<td className="px-3 py-2">€{b.amount.toFixed(2)}</td>
|
||||
{invLoading ? (
|
||||
<>
|
||||
<tr><td colSpan={6} className="px-3 py-3"><div className="h-4 w-40 bg-gray-200 animate-pulse rounded" /></td></tr>
|
||||
<tr><td colSpan={6} className="px-3 py-3"><div className="h-4 w-3/4 bg-gray-200 animate-pulse rounded" /></td></tr>
|
||||
</>
|
||||
) : filteredBills.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-3 py-4 text-center text-gray-500">
|
||||
Keine Rechnungen gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredBills.map(inv => (
|
||||
<tr key={inv.id} className="border-b last:border-0">
|
||||
<td className="px-3 py-2">{inv.invoice_number ?? inv.id}</td>
|
||||
<td className="px-3 py-2">{inv.buyer_name ?? '—'}</td>
|
||||
<td className="px-3 py-2">{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'}</td>
|
||||
<td className="px-3 py-2">
|
||||
€{Number(inv.total_gross ?? 0).toFixed(2)}{' '}
|
||||
<span className="text-xs text-gray-500">{inv.currency ?? 'EUR'}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||||
b.status === 'paid'
|
||||
inv.status === 'paid'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: b.status === 'open'
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
: inv.status === 'issued'
|
||||
? 'bg-indigo-100 text-indigo-700'
|
||||
: inv.status === 'draft'
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: inv.status === 'overdue'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}`}
|
||||
>
|
||||
{b.status}
|
||||
{inv.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 space-x-2">
|
||||
@ -200,13 +233,7 @@ export default function FinanceManagementPage() {
|
||||
<button className="text-xs rounded border px-2 py-1 hover:bg-gray-50">Export</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredBills.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-3 py-4 text-center text-gray-500">
|
||||
Keine Rechnungen gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
|
||||
type Affiliate = {
|
||||
@ -20,6 +20,8 @@ export default function AffiliateLinksPage() {
|
||||
const [affiliates, setAffiliates] = useState<Affiliate[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
// NEW: selected category
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchAffiliates() {
|
||||
@ -63,130 +65,129 @@ export default function AffiliateLinksPage() {
|
||||
category: { title: affiliate.category, href: '#' },
|
||||
commissionRate: affiliate.commissionRate
|
||||
}))
|
||||
|
||||
// NEW: fixed categories from the provided image, merged with backend ones
|
||||
const categories = useMemo(() => {
|
||||
const fromImage = [
|
||||
'Technology',
|
||||
'Energy',
|
||||
'Finance',
|
||||
'Healthcare',
|
||||
'Education',
|
||||
'Travel',
|
||||
'Retail',
|
||||
'Construction',
|
||||
'Food',
|
||||
'Automotive',
|
||||
'Fashion',
|
||||
'Pets',
|
||||
]
|
||||
const set = new Set<string>(fromImage)
|
||||
affiliates.forEach(a => { if (a.category) set.add(a.category) })
|
||||
return ['all', ...Array.from(set)]
|
||||
}, [affiliates])
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative py-24 sm:py-32">
|
||||
{/* Background Pattern */}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
x="50%"
|
||||
y={-1}
|
||||
id="affiliate-pattern"
|
||||
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(#affiliate-pattern)" width="100%" height="100%" strokeWidth={0} />
|
||||
</svg>
|
||||
|
||||
{/* Colored Blur Effect */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
|
||||
{/* Additional background layers for better visibility */}
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900"></div>
|
||||
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]"></div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className="text-4xl font-semibold tracking-tight text-balance text-white sm:text-5xl">
|
||||
Affiliate Partners
|
||||
</h2>
|
||||
<p className="mt-2 text-lg/8 text-gray-300">
|
||||
Discover our trusted partners and earn commissions through affiliate
|
||||
links.
|
||||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
||||
<main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header (aligned with management pages) */}
|
||||
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">
|
||||
Discover our trusted partners and earn commissions through affiliate links.
|
||||
</p>
|
||||
</div>
|
||||
{/* NEW: Category filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-blue-900 font-medium">Filter by category:</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="rounded-md border border-blue-200 bg-white px-3 py-1.5 text-sm text-blue-900 shadow-sm"
|
||||
>
|
||||
{categories.map(c => (
|
||||
<option key={c} value={c}>{c === 'all' ? 'All' : c}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* States */}
|
||||
{loading && (
|
||||
<div className="mx-auto mt-16 text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-r-transparent"></div>
|
||||
<p className="mt-4 text-sm text-gray-400">Loading affiliate partners...</p>
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-b-transparent" />
|
||||
<p className="mt-4 text-sm text-gray-600">Loading affiliate partners...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mx-auto mt-16 max-w-2xl text-center">
|
||||
<p className="text-red-400">{error}</p>
|
||||
{error && !loading && (
|
||||
<div className="mx-auto max-w-2xl rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && posts.length === 0 && (
|
||||
<div className="mx-auto mt-16 max-w-2xl text-center">
|
||||
<p className="text-gray-400">No affiliate partners available at the moment.</p>
|
||||
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600">
|
||||
No affiliate partners available at the moment.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cards (aligned to white panels, border, shadow) */}
|
||||
{!loading && !error && posts.length > 0 && (
|
||||
<div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<article key={post.id} className="flex flex-col items-start justify-between">
|
||||
<div className="relative w-full">
|
||||
<img
|
||||
alt=""
|
||||
src={post.imageUrl}
|
||||
className="aspect-video w-full rounded-2xl bg-gray-800 object-cover sm:aspect-2/1 lg:aspect-3/2"
|
||||
/>
|
||||
<div className="absolute inset-0 rounded-2xl inset-ring inset-ring-white/10" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{posts.map((post) => {
|
||||
// NEW: highlight when matches selected category (keep all visible)
|
||||
const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory
|
||||
return (
|
||||
<article
|
||||
key={post.id}
|
||||
className={`rounded-2xl bg-white border shadow-lg overflow-hidden flex flex-col transition
|
||||
${isHighlighted ? 'border-2 border-indigo-400 ring-2 ring-indigo-200' : 'border-gray-100'}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<img alt="" src={post.imageUrl} className="aspect-video w-full object-cover" />
|
||||
</div>
|
||||
<div className="flex max-w-xl grow flex-col justify-between">
|
||||
<div className="mt-8 flex items-center gap-x-4 text-xs">
|
||||
<div className="p-6 flex-1 flex flex-col">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="text-xl font-semibold text-blue-900">{post.title}</h3>
|
||||
{post.commissionRate && (
|
||||
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-indigo-200 bg-indigo-50 text-indigo-700">
|
||||
{post.commissionRate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs">
|
||||
<a
|
||||
href={post.category.href}
|
||||
className="relative z-10 rounded-full bg-gray-800/60 px-3 py-1.5 font-medium text-gray-300 hover:bg-gray-800"
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 border text-blue-900
|
||||
${isHighlighted ? 'border-indigo-300 bg-indigo-50' : 'border-blue-200 bg-blue-50'}`}
|
||||
>
|
||||
{post.category.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="group relative grow">
|
||||
<h3 className="mt-3 text-lg/6 font-semibold text-white group-hover:text-gray-300">
|
||||
<a href={post.href} target="_blank" rel="noopener noreferrer">
|
||||
<span className="absolute inset-0" />
|
||||
{post.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p className="mt-5 line-clamp-3 text-sm/6 text-gray-400">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative mt-8 flex items-center gap-x-4 justify-self-end">
|
||||
{post.commissionRate && (
|
||||
<span className="text-xs text-gray-500 border border-gray-700 rounded-full px-2 py-1">
|
||||
{post.commissionRate}
|
||||
</span>
|
||||
)}
|
||||
<p className="mt-3 text-sm text-gray-700 line-clamp-4">{post.description}</p>
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<a
|
||||
href={post.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-semibold text-indigo-400 hover:text-indigo-300"
|
||||
className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition"
|
||||
>
|
||||
Visit Affiliate Link →
|
||||
Visit Affiliate Link
|
||||
</a>
|
||||
<span className="text-[11px] text-gray-500">
|
||||
External partner website.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -25,18 +25,42 @@ import {
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
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)
|
||||
const shopItems = [
|
||||
{ name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon },
|
||||
{ name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon },
|
||||
]
|
||||
|
||||
// Information dropdown, controlled by env flags
|
||||
const informationItems = [
|
||||
{ name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' },
|
||||
{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' },
|
||||
{ name: 'About us', href: '/about-us', description: 'Learn more about us' },
|
||||
];
|
||||
...(DISPLAY_MEMBERSHIP
|
||||
? [{ 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 = [
|
||||
{ name: 'Shop', href: '/shop' },
|
||||
{ name: 'News', href: '/news' },
|
||||
];
|
||||
...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []),
|
||||
]
|
||||
|
||||
// Toggle visibility of Shop navigation across header (desktop + mobile)
|
||||
const showShop = false
|
||||
|
||||
export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
@ -261,18 +285,24 @@ export default function Header() {
|
||||
>
|
||||
Referral Management
|
||||
</button>
|
||||
|
||||
{DISPLAY_MATRIX && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Personal Matrix
|
||||
</button>
|
||||
)}
|
||||
|
||||
{DISPLAY_ABONEMMENTS && (
|
||||
<button
|
||||
onClick={() => router.push('/coffee-abonnements')}
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -428,10 +458,7 @@ export default function Header() {
|
||||
User Verify
|
||||
</button>
|
||||
{/* Updated Management dropdown */}
|
||||
<div
|
||||
ref={managementRef}
|
||||
className="relative"
|
||||
>
|
||||
<div ref={managementRef} className="relative">
|
||||
<button
|
||||
onClick={() => setAdminMgmtOpen(o => !o)}
|
||||
aria-haspopup="true"
|
||||
@ -456,6 +483,8 @@ export default function Header() {
|
||||
>
|
||||
User Management
|
||||
</button>
|
||||
|
||||
{DISPLAY_MATRIX && (
|
||||
<button
|
||||
onClick={() => { router.push('/admin/matrix-management'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -463,6 +492,8 @@ export default function Header() {
|
||||
>
|
||||
Matrix Management
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => { router.push('/admin/contract-management'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -470,6 +501,9 @@ export default function Header() {
|
||||
>
|
||||
Contract Management
|
||||
</button>
|
||||
|
||||
{DISPLAY_ABONEMMENTS && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -484,6 +518,10 @@ export default function Header() {
|
||||
>
|
||||
Finance Management
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{DISPLAY_POOLS && (
|
||||
<button
|
||||
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -491,6 +529,8 @@ export default function Header() {
|
||||
>
|
||||
Pool Management
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => { router.push('/admin/affiliate-management'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -498,6 +538,8 @@ export default function Header() {
|
||||
>
|
||||
Affiliate Management
|
||||
</button>
|
||||
|
||||
{DISPLAY_NEWS && (
|
||||
<button
|
||||
onClick={() => { router.push('/admin/news-management'); setAdminMgmtOpen(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
|
||||
@ -505,6 +547,7 @@ export default function Header() {
|
||||
>
|
||||
News Management
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -664,18 +707,22 @@ export default function Header() {
|
||||
>
|
||||
Referral Management
|
||||
</button>
|
||||
{DISPLAY_MATRIX && (
|
||||
<button
|
||||
onClick={() => { router.push('/personal-matrix'); setMobileMenuOpen(false); }}
|
||||
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
|
||||
>
|
||||
Personal Matrix
|
||||
</button>
|
||||
)}
|
||||
{DISPLAY_ABONEMMENTS && (
|
||||
<button
|
||||
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
|
||||
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
264
src/app/components/toast/toastComponent.tsx
Normal file
264
src/app/components/toast/toastComponent.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
'use client'
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode
|
||||
} from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
type ToastVariant = 'success' | 'error' | 'info' | 'warning'
|
||||
|
||||
export interface ToastOptions {
|
||||
id?: string
|
||||
title?: string
|
||||
message: string
|
||||
variant?: ToastVariant
|
||||
duration?: number // ms, default 4000
|
||||
}
|
||||
|
||||
// add optional closing flag for exit animation
|
||||
interface ToastInternal extends ToastOptions {
|
||||
id: string
|
||||
closing?: boolean
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showToast: (options: ToastOptions) => void
|
||||
}
|
||||
|
||||
// increase fade duration
|
||||
const TOAST_ANIMATION_MS = 400 // fade-out duration in ms
|
||||
|
||||
// --- global toast store so toasts survive route changes ---
|
||||
let globalToasts: ToastInternal[] = []
|
||||
type ToastListener = (toasts: ToastInternal[]) => void
|
||||
const toastListeners = new Set<ToastListener>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
// NEW: portal state (single global React root)
|
||||
let toastPortalContainer: HTMLDivElement | null = null
|
||||
let toastPortalRoot: ReturnType<typeof createRoot> | null = null
|
||||
let toastPortalMounted = false
|
||||
|
||||
function notifyToastListeners() {
|
||||
for (const listener of toastListeners) {
|
||||
listener(globalToasts)
|
||||
}
|
||||
}
|
||||
|
||||
function removeToastInternal(id: string) {
|
||||
// clear auto-dismiss timer
|
||||
const timeout = toastTimeouts.get(id)
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
toastTimeouts.delete(id)
|
||||
}
|
||||
|
||||
const existing = globalToasts.find(t => t.id === id)
|
||||
if (!existing) return
|
||||
|
||||
if (existing.closing) {
|
||||
// already closing: hard-remove immediately
|
||||
globalToasts = globalToasts.filter(t => t.id !== id)
|
||||
notifyToastListeners()
|
||||
return
|
||||
}
|
||||
|
||||
// mark as closing to trigger fade-out
|
||||
globalToasts = globalToasts.map(t =>
|
||||
t.id === id ? { ...t, closing: true } : t
|
||||
)
|
||||
notifyToastListeners()
|
||||
|
||||
// remove after animation finishes
|
||||
setTimeout(() => {
|
||||
globalToasts = globalToasts.filter(t => t.id !== id)
|
||||
notifyToastListeners()
|
||||
}, TOAST_ANIMATION_MS)
|
||||
}
|
||||
|
||||
function addToast(options: ToastOptions) {
|
||||
const id = options.id ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
const toast: ToastInternal = {
|
||||
id,
|
||||
variant: options.variant ?? 'info',
|
||||
duration: options.duration ?? 4000,
|
||||
...options
|
||||
}
|
||||
|
||||
globalToasts = [...globalToasts, toast]
|
||||
notifyToastListeners()
|
||||
|
||||
if (toast.duration && toast.duration > 0) {
|
||||
const timeout = setTimeout(() => {
|
||||
removeToastInternal(id)
|
||||
}, toast.duration)
|
||||
toastTimeouts.set(id, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: mount a global portal once per browser session
|
||||
function ensureToastPortalMounted() {
|
||||
if (toastPortalMounted) return
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
toastPortalMounted = true
|
||||
toastPortalContainer = document.createElement('div')
|
||||
document.body.appendChild(toastPortalContainer)
|
||||
toastPortalRoot = createRoot(toastPortalContainer)
|
||||
|
||||
// Defer actual render to avoid triggering nested updates during React render
|
||||
setTimeout(() => {
|
||||
if (!toastPortalRoot) return
|
||||
toastPortalRoot.render(<ToastViewport />)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// --- context & provider ---
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | undefined>(undefined)
|
||||
|
||||
export function useToast(): ToastContextValue {
|
||||
const ctx = useContext(ToastContext)
|
||||
if (ctx) {
|
||||
// Normal path: inside <ToastProvider>
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Fallback path: no provider mounted, still use global store/portal
|
||||
if (typeof window !== 'undefined') {
|
||||
ensureToastPortalMounted()
|
||||
}
|
||||
|
||||
return {
|
||||
showToast: (options: ToastOptions) => {
|
||||
addToast(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ToastProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ToastProvider({ children }: ToastProviderProps) {
|
||||
// ensure the global portal is mounted when any provider appears
|
||||
useEffect(() => {
|
||||
ensureToastPortalMounted()
|
||||
}, [])
|
||||
|
||||
const showToast = useCallback((options: ToastOptions) => {
|
||||
addToast(options)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// NEW: global viewport rendered via portal, independent of pages/providers
|
||||
function ToastViewport() {
|
||||
const [toasts, setToasts] = useState<ToastInternal[]>(globalToasts)
|
||||
|
||||
useEffect(() => {
|
||||
const listener: ToastListener = (next) => setToasts(next)
|
||||
toastListeners.add(listener)
|
||||
setToasts(globalToasts)
|
||||
return () => {
|
||||
toastListeners.delete(listener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback((id: string) => {
|
||||
removeToastInternal(id)
|
||||
}, [])
|
||||
|
||||
if (!toasts.length) return null
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-x-4 bottom-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto">
|
||||
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
|
||||
{toasts.map(t => (
|
||||
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ToastItemProps {
|
||||
toast: ToastInternal
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function ToastItem({ toast, onClose }: ToastItemProps) {
|
||||
const { title, message, variant } = toast
|
||||
|
||||
// local visible state for entry animation
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => setVisible(true))
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [])
|
||||
|
||||
const variantClasses: Record<ToastVariant, string> = {
|
||||
success: 'border-emerald-400/80 bg-emerald-900/90',
|
||||
error: 'border-red-400/80 bg-red-900/90',
|
||||
info: 'border-sky-400/80 bg-slate-900/90',
|
||||
warning: 'border-amber-400/80 bg-amber-900/90'
|
||||
}
|
||||
|
||||
const iconBg: Record<ToastVariant, string> = {
|
||||
success: 'bg-emerald-500/10 text-emerald-300',
|
||||
error: 'bg-red-500/10 text-red-300',
|
||||
info: 'bg-sky-500/10 text-sky-300',
|
||||
warning: 'bg-amber-500/10 text-amber-300'
|
||||
}
|
||||
|
||||
const isClosing = !!toast.closing
|
||||
const motionClasses = isClosing
|
||||
? 'opacity-0 translate-y-2'
|
||||
: visible
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-2'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
pointer-events-auto flex w-full items-start gap-3 rounded-xl border-l-4
|
||||
px-4 py-3 text-sm text-slate-50 shadow-xl shadow-black/40
|
||||
backdrop-blur-md transform transition-all duration-400
|
||||
${motionClasses}
|
||||
${variantClasses[variant ?? 'info']}
|
||||
`}
|
||||
>
|
||||
<div className={`mt-0.5 flex h-8 w-8 items-center justify-center rounded-full ${iconBg[variant ?? 'info']}`}>
|
||||
{/* Simple dot indicator */}
|
||||
<span className="h-2 w-2 rounded-full bg-current" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<div className="mb-0.5 text-xs font-semibold uppercase tracking-wide text-slate-200">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[13px] leading-snug text-slate-50">{message}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="ml-1 mt-0.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-xs text-slate-300 hover:bg-slate-800/70 hover:text-white focus:outline-none focus:ring-2 focus:ring-slate-500"
|
||||
aria-label="Close notification"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
|
||||
import { useLogin } from '../hooks/useLogin'
|
||||
import { useToast } from '../../components/toast/toastComponent'
|
||||
|
||||
export default function LoginForm() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
@ -13,11 +14,11 @@ export default function LoginForm() {
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
const [viewportWidth, setViewportWidth] = useState<number>(
|
||||
typeof window !== 'undefined' ? window.innerWidth : 1200
|
||||
)
|
||||
// FIX: use a static initial width so SSR and first client render match
|
||||
const [viewportWidth, setViewportWidth] = useState<number>(1200)
|
||||
const router = useRouter()
|
||||
const { login, error, setError, loading } = useLogin()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// Responsive ball visibility
|
||||
useEffect(() => {
|
||||
@ -30,6 +31,7 @@ export default function LoginForm() {
|
||||
// Track viewport width for dynamic scaling
|
||||
useEffect(() => {
|
||||
const handleResize = () => setViewportWidth(window.innerWidth)
|
||||
handleResize() // initialize on mount (runs only on client)
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
@ -72,11 +74,29 @@ export default function LoginForm() {
|
||||
|
||||
if (!validateForm()) return
|
||||
|
||||
await login({
|
||||
const result = await login({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
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
|
||||
|
||||
@ -114,7 +114,7 @@ export function useLogin() {
|
||||
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) {
|
||||
redirectPath = '/dashboard'
|
||||
console.log('✅ User fully onboarded, redirecting to dashboard')
|
||||
@ -128,10 +128,8 @@ export function useLogin() {
|
||||
console.error('❌ Error fetching user status-progress:', statusError)
|
||||
}
|
||||
|
||||
// Redirect based on status check
|
||||
router.push(redirectPath)
|
||||
|
||||
return { success: true, user: data.user }
|
||||
// NOTE: no router.push here; caller will handle redirect after showing toast
|
||||
return { success: true, user: data.user, redirectPath }
|
||||
} else {
|
||||
throw new Error(data.message || 'Login failed')
|
||||
}
|
||||
|
||||
@ -6,13 +6,20 @@ import LoginForm from './components/LoginForm'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground'
|
||||
import { ToastProvider } from '../components/toast/toastComponent'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [showBackground, setShowBackground] = useState(false)
|
||||
const [hasHydrated, setHasHydrated] = useState(false)
|
||||
const router = useRouter()
|
||||
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(() => {
|
||||
if (user) {
|
||||
router.push('/dashboard')
|
||||
@ -27,9 +34,10 @@ export default function LoginPage() {
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
// Don't render if user is already logged in
|
||||
if (user) {
|
||||
// Don't render if user is already logged in (only after hydration to avoid SSR mismatch)
|
||||
if (hasHydrated && user) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<PageLayout>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -38,10 +46,12 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<PageLayout showFooter={true}>
|
||||
<div
|
||||
className="relative w-full flex flex-col min-h-screen"
|
||||
@ -58,5 +68,6 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@ -204,8 +204,18 @@ export default function QuickActionDashboardPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* 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 */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../components/toast/toastComponent'
|
||||
|
||||
interface CompanyProfileData {
|
||||
companyName: string
|
||||
@ -50,6 +51,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
const router = useRouter()
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [form, setForm] = useState(init)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -68,12 +70,24 @@ export default function CompanyAdditionalInformationPage() {
|
||||
]
|
||||
for (const k of required) {
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -86,7 +100,13 @@ export default function CompanyAdditionalInformationPage() {
|
||||
if (!validate()) return
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -122,6 +142,11 @@ export default function CompanyAdditionalInformationPage() {
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Profile saved',
|
||||
message: 'Your company profile has been saved successfully.',
|
||||
})
|
||||
|
||||
// Refresh user status to update profile completion state
|
||||
await refreshStatus()
|
||||
@ -141,7 +166,13 @@ export default function CompanyAdditionalInformationPage() {
|
||||
|
||||
} catch (error: any) {
|
||||
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 {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -149,26 +180,18 @@ export default function CompanyAdditionalInformationPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="company-additional-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
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 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
|
||||
type="submit"
|
||||
disabled={loading || success}
|
||||
@ -386,6 +416,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../components/toast/toastComponent'
|
||||
|
||||
interface PersonalProfileData {
|
||||
dob: string
|
||||
@ -58,6 +59,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
const router = useRouter()
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [form, setForm] = useState(initialData)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -145,20 +147,38 @@ export default function PersonalAdditionalInformationPage() {
|
||||
]
|
||||
for (const k of requiredKeys) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Date of birth validation
|
||||
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
|
||||
}
|
||||
|
||||
// very loose IBAN check
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -171,7 +191,13 @@ export default function PersonalAdditionalInformationPage() {
|
||||
if (!validate()) return
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -208,6 +234,11 @@ export default function PersonalAdditionalInformationPage() {
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Profile saved',
|
||||
message: 'Your personal profile has been saved successfully.',
|
||||
})
|
||||
|
||||
// Refresh user status to update profile completion state
|
||||
await refreshStatus()
|
||||
@ -227,7 +258,13 @@ export default function PersonalAdditionalInformationPage() {
|
||||
|
||||
} catch (error: any) {
|
||||
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 {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -235,26 +272,18 @@ export default function PersonalAdditionalInformationPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="personal-additional-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 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
|
||||
type="submit"
|
||||
disabled={loading || success}
|
||||
@ -472,6 +508,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -4,7 +4,8 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import PageLayout from '../../components/PageLayout'
|
||||
import useAuthStore from '../../store/authStore'
|
||||
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() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
@ -18,7 +19,8 @@ export default function EmailVerifyPage() {
|
||||
const [initialEmailSent, setInitialEmailSent] = useState(false)
|
||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
||||
const emailSentRef = useRef(false)
|
||||
const router = useRouter() // NEW
|
||||
const router = useRouter()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// NEW: resend and validity windows
|
||||
const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
||||
@ -63,18 +65,36 @@ export default function EmailVerifyPage() {
|
||||
setInitialEmailSent(true)
|
||||
setLastSentAt(Date.now(), user?.email)
|
||||
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 {
|
||||
console.error('Failed to send initial verification email:', data?.message)
|
||||
const msg = data?.message || 'Error sending the verification email.'
|
||||
setError(msg)
|
||||
emailSentRef.current = false
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Email not sent',
|
||||
message: msg
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending initial verification email:', error)
|
||||
} catch (err) {
|
||||
console.error('Error sending initial verification email:', err)
|
||||
const msg = 'Network error while sending the verification email.'
|
||||
setError(msg)
|
||||
emailSentRef.current = false
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Network error',
|
||||
message: msg
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sendInitialEmail()
|
||||
}, [token, user])
|
||||
}, [token, user, showToast])
|
||||
|
||||
// Cooldown timer
|
||||
useEffect(() => {
|
||||
@ -172,11 +192,23 @@ export default function EmailVerifyPage() {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -196,13 +228,15 @@ export default function EmailVerifyPage() {
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setSuccess(true)
|
||||
await refreshStatus() // Refresh user status
|
||||
// Redirect after 2 seconds
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Email verified',
|
||||
message: 'Your email has been verified successfully.'
|
||||
})
|
||||
await refreshStatus()
|
||||
setTimeout(() => {
|
||||
// Check if we came from tutorial
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const fromTutorial = urlParams.get('tutorial') === 'true'
|
||||
|
||||
if (fromTutorial) {
|
||||
window.location.href = '/quickaction-dashboard?tutorial=true'
|
||||
} else {
|
||||
@ -210,11 +244,23 @@ export default function EmailVerifyPage() {
|
||||
}
|
||||
}, 2000)
|
||||
} 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) {
|
||||
console.error('Email verification error:', error)
|
||||
setError('Network error. Please try again.')
|
||||
} catch (err) {
|
||||
console.error('Email verification error:', err)
|
||||
const msg = 'Network error. Please try again.'
|
||||
setError(msg)
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Network error',
|
||||
message: msg
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@ -232,7 +278,17 @@ export default function EmailVerifyPage() {
|
||||
setResendCooldown(Math.ceil(remaining / 1000))
|
||||
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('')
|
||||
|
||||
try {
|
||||
@ -250,14 +306,31 @@ export default function EmailVerifyPage() {
|
||||
setLastSentAt(Date.now(), user?.email)
|
||||
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
|
||||
if (!initialEmailSent) setInitialEmailSent(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Verification email sent',
|
||||
message: `We sent a new verification email to ${user?.email || 'your email'}.`
|
||||
})
|
||||
} 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) {
|
||||
console.error('Resend email error:', error)
|
||||
setError('Network error while sending the email.')
|
||||
} catch (err) {
|
||||
console.error('Resend email error:', err)
|
||||
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
|
||||
const formatMmSs = (total: number) => {
|
||||
@ -268,54 +341,28 @@ export default function EmailVerifyPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
|
||||
{/* Global full-viewport background (no inner scroll) */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
{/* Gradient base */}
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
{/* Pattern */}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
|
||||
>
|
||||
<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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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 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="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
|
||||
</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 ? (
|
||||
<>
|
||||
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'}
|
||||
</span>
|
||||
. Enter it below.
|
||||
@ -323,7 +370,7 @@ export default function EmailVerifyPage() {
|
||||
) : (
|
||||
<>
|
||||
Sending verification email to{' '}
|
||||
<span className="text-indigo-300 font-medium">
|
||||
<span className="text-blue-700 font-medium">
|
||||
{user?.email || 'your email'}
|
||||
</span>
|
||||
...
|
||||
@ -335,9 +382,10 @@ export default function EmailVerifyPage() {
|
||||
{/* Card */}
|
||||
<form
|
||||
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">
|
||||
{/* Inputs */}
|
||||
<div className="flex justify-center gap-2 sm:gap-3">
|
||||
{code.map((v, i) => (
|
||||
<input
|
||||
@ -353,8 +401,8 @@ export default function EmailVerifyPage() {
|
||||
onPaste={e => handlePaste(i, e)}
|
||||
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
|
||||
${v
|
||||
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
: 'border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/70 text-gray-700 dark:text-gray-200'}
|
||||
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
|
||||
: 'border-gray-300 bg-white/80 text-gray-700'}
|
||||
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
|
||||
/>
|
||||
))}
|
||||
@ -389,7 +437,7 @@ export default function EmailVerifyPage() {
|
||||
type="button"
|
||||
onClick={handleResend}
|
||||
disabled={!!resendCooldown || submitting || success}
|
||||
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{resendCooldown
|
||||
? `Resend in ${formatMmSs(resendCooldown)}`
|
||||
@ -397,28 +445,27 @@ export default function EmailVerifyPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* NEW: Go to Dashboard button */}
|
||||
<div className="mt-1 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/quickaction-dashboard')}
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:underline"
|
||||
className="text-sm font-medium text-gray-700 hover:underline"
|
||||
>
|
||||
Go to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Helper text with validity + spam/junk reminder + support */}
|
||||
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="mt-8 text-center text-xs text-gray-500">
|
||||
Didn’t receive the email? Please check your junk/spam folder. Still having issues?{' '}
|
||||
<a href="mailto:test@test.com" className="text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
|
||||
Contact support
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { API_BASE_URL } from '../../../utils/api'
|
||||
import { useToast } from '../../../components/toast/toastComponent' // NEW
|
||||
|
||||
export default function CompanySignContractPage() {
|
||||
const router = useRouter()
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast() // NEW
|
||||
|
||||
const [companyName, setCompanyName] = useState('')
|
||||
const [repName, setRepName] = useState('')
|
||||
@ -77,7 +79,7 @@ export default function CompanySignContractPage() {
|
||||
e.preventDefault()
|
||||
if (!valid()) {
|
||||
// Detailed error message to help debug
|
||||
const issues = []
|
||||
const issues: string[] = []
|
||||
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 (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 (!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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -137,6 +151,11 @@ export default function CompanySignContractPage() {
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Contract signed',
|
||||
message: 'Your company contract has been signed successfully.',
|
||||
})
|
||||
|
||||
// Refresh user status to update contract signed state
|
||||
await refreshStatus()
|
||||
@ -156,7 +175,13 @@ export default function CompanySignContractPage() {
|
||||
|
||||
} catch (error: any) {
|
||||
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 {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@ -164,26 +189,18 @@ export default function CompanySignContractPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="company-contract-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 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
|
||||
type="submit"
|
||||
disabled={submitting || success}
|
||||
@ -413,6 +437,7 @@ export default function CompanySignContractPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { API_BASE_URL } from '../../../utils/api'
|
||||
import { useToast } from '../../../components/toast/toastComponent'
|
||||
|
||||
export default function PersonalSignContractPage() {
|
||||
const router = useRouter()
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [fullName, setFullName] = useState('')
|
||||
const [location, setLocation] = useState('')
|
||||
@ -72,19 +74,31 @@ export default function PersonalSignContractPage() {
|
||||
e.preventDefault()
|
||||
if (!valid()) {
|
||||
// Detailed error message to help debug
|
||||
const issues = []
|
||||
const issues: string[] = []
|
||||
if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)')
|
||||
if (location.trim().length < 2) issues.push('Location (min 2 characters)')
|
||||
if (!agreeContract) issues.push('Contract read and understood')
|
||||
if (!agreeData) issues.push('Privacy policy accepted')
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -128,6 +142,11 @@ export default function PersonalSignContractPage() {
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Contract signed',
|
||||
message: 'Your personal contract has been signed successfully.',
|
||||
})
|
||||
|
||||
// Refresh user status to update contract signed state
|
||||
await refreshStatus()
|
||||
@ -147,7 +166,13 @@ export default function PersonalSignContractPage() {
|
||||
|
||||
} catch (error: any) {
|
||||
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 {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@ -155,26 +180,18 @@ export default function PersonalSignContractPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="personal-contract-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 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
|
||||
type="submit"
|
||||
disabled={submitting || success}
|
||||
@ -354,6 +378,7 @@ export default function PersonalSignContractPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import useAuthStore from '../../../../store/authStore'
|
||||
import { useUserStatus } from '../../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../../components/toast/toastComponent'
|
||||
|
||||
export function useCompanyUploadId() {
|
||||
// Auth + status
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// Form state
|
||||
const [idNumber, setIdNumber] = useState('')
|
||||
@ -37,7 +39,13 @@ export function useCompanyUploadId() {
|
||||
// File handlers
|
||||
const handleFile = (file: File, which: 'front' | 'extra') => {
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -81,7 +89,13 @@ export function useCompanyUploadId() {
|
||||
// Validation
|
||||
const validate = () => {
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -93,7 +107,13 @@ export function useCompanyUploadId() {
|
||||
e.preventDefault()
|
||||
if (!validate()) return
|
||||
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
|
||||
}
|
||||
|
||||
@ -116,10 +136,16 @@ export function useCompanyUploadId() {
|
||||
|
||||
if (!response.ok) {
|
||||
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)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Documents uploaded',
|
||||
message: 'Your company ID documents have been uploaded successfully.',
|
||||
})
|
||||
await refreshStatus()
|
||||
|
||||
setTimeout(() => {
|
||||
@ -136,7 +162,13 @@ export function useCompanyUploadId() {
|
||||
}, 1500)
|
||||
} catch (err: any) {
|
||||
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 {
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useCompanyUploadId } from './hooks/useCompanyUploadId'
|
||||
import useAuthStore from '../../../store/authStore' // NEW
|
||||
import { useEffect, useState } from 'react' // NEW
|
||||
import { useRouter } from 'next/navigation' // NEW
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
|
||||
|
||||
@ -25,9 +25,9 @@ export default function CompanyIdUploadPage() {
|
||||
handleFile, onDrop, clearFile, dropHandlers, openPicker, submit,
|
||||
} = useCompanyUploadId()
|
||||
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const router = useRouter() // NEW
|
||||
const [blocked, setBlocked] = useState(false) // NEW
|
||||
const user = useAuthStore(s => s.user)
|
||||
const router = useRouter()
|
||||
const [blocked, setBlocked] = useState(false)
|
||||
|
||||
// Guard: only 'company' users allowed on this page
|
||||
useEffect(() => {
|
||||
@ -56,27 +56,22 @@ export default function CompanyIdUploadPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
{/* Background (same as personal) */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="company-id-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
||||
Company Contact Person Identity Verification
|
||||
@ -289,6 +284,7 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import useAuthStore from '../../../../store/authStore'
|
||||
import { useUserStatus } from '../../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../../components/toast/toastComponent'
|
||||
|
||||
export function usePersonalUploadId() {
|
||||
// Auth and status
|
||||
const token = useAuthStore(s => s.accessToken)
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// Form state
|
||||
const [idNumber, setIdNumber] = useState('')
|
||||
@ -37,7 +39,13 @@ export function usePersonalUploadId() {
|
||||
// File handlers
|
||||
const handleFile = (f: File, side: 'front' | 'back') => {
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -81,15 +89,33 @@ export function usePersonalUploadId() {
|
||||
// Validation
|
||||
const validate = () => {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
setError('')
|
||||
@ -101,7 +127,13 @@ export function usePersonalUploadId() {
|
||||
e.preventDefault()
|
||||
if (!validate()) return
|
||||
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
|
||||
}
|
||||
|
||||
@ -126,6 +158,11 @@ export function usePersonalUploadId() {
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setSuccess(true)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Documents uploaded',
|
||||
message: 'Your ID documents have been uploaded successfully.',
|
||||
})
|
||||
await refreshStatus()
|
||||
setTimeout(() => {
|
||||
// Check if we came from tutorial
|
||||
@ -139,11 +176,23 @@ export function usePersonalUploadId() {
|
||||
}
|
||||
}, 2000)
|
||||
} 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) {
|
||||
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 {
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
@ -60,27 +60,22 @@ export default function PersonalIdUploadPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||||
<defs>
|
||||
<pattern id="personal-id-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(#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 className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
||||
Personal Identity Verification
|
||||
@ -300,6 +295,7 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
|
||||
import { useRegister } from '../hooks/useRegister'
|
||||
import { useToast } from '../../components/toast/toastComponent'
|
||||
|
||||
interface RegisterFormProps {
|
||||
mode: 'personal' | 'company'
|
||||
@ -72,6 +73,7 @@ export default function RegisterForm({
|
||||
|
||||
// Hook for backend calls
|
||||
const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// Animate form when mode changes
|
||||
useEffect(() => {
|
||||
@ -113,24 +115,24 @@ export default function RegisterForm({
|
||||
if (!personalForm.firstName.trim() || !personalForm.lastName.trim() ||
|
||||
!personalForm.email.trim() || !personalForm.confirmEmail.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
|
||||
}
|
||||
|
||||
if (personalForm.email !== personalForm.confirmEmail) {
|
||||
setError('E-Mail-Adressen stimmen nicht überein')
|
||||
setError('Email addresses do not match')
|
||||
return false
|
||||
}
|
||||
|
||||
if (personalForm.password !== personalForm.confirmPassword) {
|
||||
setError('Passwörter stimmen nicht überein')
|
||||
setError('Passwords do not match')
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -142,24 +144,24 @@ export default function RegisterForm({
|
||||
if (!companyForm.companyName.trim() || !companyForm.companyEmail.trim() ||
|
||||
!companyForm.confirmCompanyEmail.trim() || !companyForm.contactPersonName.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
|
||||
}
|
||||
|
||||
if (companyForm.companyEmail !== companyForm.confirmCompanyEmail) {
|
||||
setError('E-Mail-Adressen stimmen nicht überein')
|
||||
setError('Email addresses do not match')
|
||||
return false
|
||||
}
|
||||
|
||||
if (companyForm.password !== companyForm.confirmPassword) {
|
||||
setError('Passwörter stimmen nicht überein')
|
||||
setError('Passwords do not match')
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -187,12 +189,29 @@ export default function RegisterForm({
|
||||
phone: personalForm.phoneNumber,
|
||||
})
|
||||
if (res.ok) {
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Registration successful',
|
||||
message: 'You can now log in with your new account.'
|
||||
})
|
||||
onRegistered()
|
||||
} 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) {
|
||||
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 {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -218,12 +237,29 @@ export default function RegisterForm({
|
||||
contactPersonPhone: companyForm.contactPersonPhone,
|
||||
})
|
||||
if (res.ok) {
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Registration successful',
|
||||
message: 'You can now log in with your new company account.'
|
||||
})
|
||||
onRegistered()
|
||||
} 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) {
|
||||
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 {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -231,7 +267,14 @@ export default function RegisterForm({
|
||||
|
||||
// Surface hook error if present and no local error
|
||||
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
|
||||
|
||||
// Input change handlers
|
||||
@ -259,16 +302,16 @@ export default function RegisterForm({
|
||||
const renderPasswordStrength = (password: string) => {
|
||||
const strength = getPasswordStrength(password)
|
||||
const rules = [
|
||||
{ test: password.length >= 8, text: 'Mindestens 8 Zeichen' },
|
||||
{ test: /[a-z]/.test(password), text: 'Kleinbuchstaben (a-z)' },
|
||||
{ test: /[A-Z]/.test(password), text: 'Großbuchstaben (A-Z)' },
|
||||
{ test: /\d/.test(password), text: 'Ziffern (0-9)' },
|
||||
{ test: /[\W_]/.test(password), text: 'Sonderzeichen (!@#$...)' }
|
||||
{ test: password.length >= 8, text: 'At least 8 characters' },
|
||||
{ test: /[a-z]/.test(password), text: 'Lowercase letters (a-z)' },
|
||||
{ test: /[A-Z]/.test(password), text: 'Uppercase letters (A-Z)' },
|
||||
{ test: /\d/.test(password), text: 'Digits (0-9)' },
|
||||
{ test: /[\W_]/.test(password), text: 'Special characters (!@#$...)' }
|
||||
]
|
||||
|
||||
return (
|
||||
<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">
|
||||
{rules.map((rule, index) => (
|
||||
<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 */}
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
|
||||
Registrierung für Profit Planet
|
||||
Registration for Profit Planet
|
||||
</h2>
|
||||
{/* Replace generic invite with referrer email inside the form */}
|
||||
{referrerEmail && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@ -308,7 +351,7 @@ export default function RegisterForm({
|
||||
onClick={() => setMode('personal')}
|
||||
type="button"
|
||||
>
|
||||
Privatperson
|
||||
Individual
|
||||
</button>
|
||||
<button
|
||||
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')}
|
||||
type="button"
|
||||
>
|
||||
Unternehmen
|
||||
Company
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -338,7 +381,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Vorname *
|
||||
First name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -353,7 +396,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Nachname *
|
||||
Last name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -370,7 +413,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
E-Mail-Adresse *
|
||||
Email address *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@ -385,7 +428,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
E-Mail bestätigen *
|
||||
Confirm email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@ -401,7 +444,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="phoneNumber" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Telefonnummer *
|
||||
Phone number *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
@ -418,7 +461,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Passwort *
|
||||
Password *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@ -447,7 +490,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Passwort bestätigen *
|
||||
Confirm password *
|
||||
</label>
|
||||
<input
|
||||
type={showPersonalPassword ? 'text' : 'password'}
|
||||
@ -473,10 +516,10 @@ export default function RegisterForm({
|
||||
{loading ? (
|
||||
<>
|
||||
<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>
|
||||
</form>
|
||||
@ -485,7 +528,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="companyName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Firmenname *
|
||||
Company name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -500,7 +543,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="contactPersonName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Ansprechpartner *
|
||||
Contact person *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -517,7 +560,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="companyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Firmen-E-Mail *
|
||||
Company email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@ -532,7 +575,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmCompanyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
E-Mail bestätigen *
|
||||
Confirm email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@ -549,7 +592,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="companyPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Firmen-Telefon *
|
||||
Company phone *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
@ -565,7 +608,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="contactPersonPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Ansprechpartner-Telefon *
|
||||
Contact person phone *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
@ -583,7 +626,7 @@ export default function RegisterForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Passwort *
|
||||
Password *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@ -612,7 +655,7 @@ export default function RegisterForm({
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||
Passwort bestätigen *
|
||||
Confirm password *
|
||||
</label>
|
||||
<input
|
||||
type={showCompanyPassword ? 'text' : 'password'}
|
||||
@ -638,10 +681,10 @@ export default function RegisterForm({
|
||||
{loading ? (
|
||||
<>
|
||||
<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>
|
||||
</form>
|
||||
@ -651,12 +694,12 @@ export default function RegisterForm({
|
||||
{/* Login Link */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-slate-700">
|
||||
Bereits registriert?{' '}
|
||||
Already registered?{' '}
|
||||
<a
|
||||
href="/login"
|
||||
className="text-[#8D6B1D] hover:text-[#7A5E1A] font-medium transition-colors"
|
||||
>
|
||||
Hier anmelden
|
||||
Login here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,8 @@ export default function SessionDetectedModal({
|
||||
}: SessionDetectedModalProps) {
|
||||
if (inline) {
|
||||
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="flex gap-4">
|
||||
<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>
|
||||
<h3 className="text-base font-semibold leading-6 text-[#0F172A]">
|
||||
Aktive Sitzung erkannt
|
||||
Active session detected
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-[#4A4A4A]">
|
||||
Du bist bereits angemeldet. Um dich zu registrieren, musst du dich zuerst abmelden
|
||||
oder du kannst zum Dashboard gehen.
|
||||
You are already logged in. To register, you must first log out or you can go to the dashboard.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
|
||||
<button
|
||||
@ -39,14 +39,14 @@ export default function SessionDetectedModal({
|
||||
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"
|
||||
>
|
||||
Zum Dashboard
|
||||
Go to dashboard
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Abmelden und registrieren
|
||||
Log out and register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,12 +95,11 @@ export default function SessionDetectedModal({
|
||||
as="h3"
|
||||
className="text-base font-semibold leading-6 text-[#0F172A]"
|
||||
>
|
||||
Aktive Sitzung erkannt
|
||||
Active session detected
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-[#4A4A4A]">
|
||||
Du bist bereits angemeldet. Um dich zu registrieren, musst du dich zuerst abmelden
|
||||
oder du kannst zum Dashboard gehen.
|
||||
You are already logged in. To register, you must first log out or you can go to the dashboard.
|
||||
</p>
|
||||
</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"
|
||||
onClick={onLogout}
|
||||
>
|
||||
Abmelden und registrieren
|
||||
Log out and register
|
||||
</button>
|
||||
<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"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Zum Dashboard
|
||||
Go to dashboard
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
||||
@ -7,6 +7,7 @@ import RegisterForm from './components/RegisterForm'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import SessionDetectedModal from './components/SessionDetectedModal'
|
||||
import InvalidRefLinkModal from './components/invalidRefLinkModal'
|
||||
import { ToastProvider } from '../components/toast/toastComponent'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const searchParams = useSearchParams()
|
||||
@ -36,7 +37,7 @@ export default function RegisterPage() {
|
||||
// Redirect to login after simulated registration
|
||||
useEffect(() => {
|
||||
if (registered) {
|
||||
const t = setTimeout(() => router.push('/login'), 1200)
|
||||
const t = setTimeout(() => router.push('/login'), 4000) // was 1200
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
}, [registered, router])
|
||||
@ -118,23 +119,27 @@ export default function RegisterPage() {
|
||||
// NEW: Gate rendering until referral check is done
|
||||
if (!isRefChecked) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<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="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>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// NEW: Invalid referral link state — show modal instead of form with same background as register form
|
||||
if (invalidRef) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<PageLayout>
|
||||
<main className="w-full flex flex-col flex-1 gap-10">
|
||||
<div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
|
||||
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen">
|
||||
{/* make wrapper flex-1 so background reaches the footer */}
|
||||
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
|
||||
{/* Pattern */}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@ -196,14 +201,17 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<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 */}
|
||||
<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 */}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@ -255,11 +263,10 @@ export default function RegisterPage() {
|
||||
{/* Heading (optional – adjusted to registration context) */}
|
||||
<div className="mx-auto max-w-2xl text-center mb-10">
|
||||
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
|
||||
Registriere dich jetzt
|
||||
Register now
|
||||
</h1>
|
||||
<p className="mt-2 text-lg/8 text-gray-200">
|
||||
Erstelle dein persönliches oder Unternehmens-Konto bei Profit
|
||||
Planet.
|
||||
Create your personal or company account with Profit Planet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -288,7 +295,7 @@ export default function RegisterPage() {
|
||||
)}
|
||||
{registered && (
|
||||
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
|
||||
Registrierung erfolgreich – Weiterleitung...
|
||||
Registration successful – redirecting...
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@ -298,5 +305,6 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user