refactor: quickactions / login

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

108
package-lock.json generated
View File

@ -45,6 +45,7 @@
"@types/react": "^19",
"@types/react-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"

View File

@ -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",

View File

@ -27,26 +27,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: 'News', href: '/news' },
];
...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []),
]
// Toggle visibility of Shop navigation across header (desktop + mobile)
const showShop = false;
const showShop = false
export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@ -312,18 +328,24 @@ export default function Header() {
>
Referral Management
</button>
<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>
<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>
{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>
)}
</>
)}
@ -479,10 +501,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"
@ -507,13 +526,17 @@ export default function Header() {
>
User Management
</button>
<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]"
role="menuitem"
>
Matrix 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]"
role="menuitem"
>
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]"
@ -521,27 +544,36 @@ export default function Header() {
>
Contract Management
</button>
<button
onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Coffee Management
</button>
<button
onClick={() => { router.push('/admin/finance-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Finance Management
</button>
<button
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Pool Management
</button>
{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]"
role="menuitem"
>
Coffee Management
</button>
<button
onClick={() => { router.push('/admin/finance-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
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]"
role="menuitem"
>
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]"
@ -549,13 +581,16 @@ export default function Header() {
>
Affiliate Management
</button>
<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]"
role="menuitem"
>
News 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]"
role="menuitem"
>
News Management
</button>
)}
</div>
</div>
)}
@ -735,18 +770,22 @@ export default function Header() {
>
Referral Management
</button>
<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>
<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>
{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>

View File

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

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { 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

View File

@ -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')
}

View File

@ -6,19 +6,26 @@ 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')
}
}, [user, router])
// Responsive background detection
useEffect(() => {
const handleResize = () => setShowBackground(window.innerWidth >= 768)
@ -26,37 +33,41 @@ export default function LoginPage() {
window.addEventListener('resize', handleResize)
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 (
<PageLayout>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">You are already logged in. Redirecting...</p>
<ToastProvider>
<PageLayout>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">You are already logged in. Redirecting...</p>
</div>
</div>
</div>
</PageLayout>
</PageLayout>
</ToastProvider>
)
}
return (
<PageLayout showFooter={true}>
<div
className="relative w-full flex flex-col min-h-screen"
style={{
backgroundImage: 'url(/images/misc/marble_bluegoldwhite_BG.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="relative z-10 flex-1 flex items-center justify-center">
<div className="w-full">
<LoginForm />
<ToastProvider>
<PageLayout showFooter={true}>
<div
className="relative w-full flex flex-col min-h-screen"
style={{
backgroundImage: 'url(/images/misc/marble_bluegoldwhite_BG.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="relative z-10 flex-1 flex items-center justify-center">
<div className="w-full">
<LoginForm />
</div>
</div>
</div>
</div>
</PageLayout>
</PageLayout>
</ToastProvider>
)
}

View File

@ -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">

View File

@ -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,243 +180,243 @@ 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>
<form
onSubmit={submit}
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-16">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
Complete Company Profile
</h1>
<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"
>
<div className="px-6 py-8 sm:px-10 lg:px-16">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
Complete Company Profile
</h1>
{/* Company Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Company Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
name="companyName"
value={form.companyName}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
{/* Company Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Company Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
name="companyName"
value={form.companyName}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
VAT / Reg No. *
</label>
<input
name="vatNumber"
value={form.vatNumber}
onChange={handleChange}
placeholder="e.g. DE123456789"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
VAT / Reg No. *
</label>
<input
name="vatNumber"
value={form.vatNumber}
onChange={handleChange}
placeholder="e.g. DE123456789"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
BIC (optional)
</label>
<input
name="bic"
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
BIC (optional)
</label>
<input
name="bic"
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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,243 +272,243 @@ 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>
<form
onSubmit={handleSubmit}
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-16">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
Complete Your Profile
</h1>
<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"
>
<div className="px-6 py-8 sm:px-10 lg:px-16">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
Complete Your Profile
</h1>
{/* Personal Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Personal Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date of Birth *
</label>
<input
type="date"
name="dob"
value={form.dob}
onChange={handleChange}
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
{/* Personal Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Personal Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date of Birth *
</label>
<input
type="date"
name="dob"
value={form.dob}
onChange={handleChange}
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality *
</label>
<select
name="nationality"
value={form.nationality}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select nationality...</option>
{NATIONALITIES.map(nationality => (
<option key={nationality} value={nationality}>
{nationality}
</option>
))}
</select>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & House Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
placeholder="Street & House Number"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality *
</label>
<select
name="nationality"
value={form.nationality}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select nationality...</option>
{NATIONALITIES.map(nationality => (
<option key={nationality} value={nationality}>
{nationality}
</option>
))}
</select>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Full name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-1 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & House Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
placeholder="Street & House Number"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone Number (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
</button>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Full name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-1 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone Number (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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,157 +341,131 @@ 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>
<div className="max-w-xl mx-auto">
<div className="text-center mb-10">
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
Verify your email
</h1>
<p className="mt-3 text-gray-300 text-sm sm:text-base">
{initialEmailSent ? (
<>
We sent a 6-digit code to{' '}
<span className="text-indigo-300 font-medium">
{user?.email || 'your email'}
</span>
. Enter it below.
</>
) : (
<>
Sending verification email to{' '}
<span className="text-indigo-300 font-medium">
{user?.email || 'your email'}
</span>
...
</>
)}
</p>
</div>
{/* Card */}
<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"
>
<fieldset disabled={submitting || success} className="space-y-8">
<div className="flex justify-center gap-2 sm:gap-3">
{code.map((v, i) => (
<input
key={i}
ref={el => { inputsRef.current[i] = el }}
inputMode="numeric"
aria-label={`Code digit ${i + 1}`}
autoComplete="one-time-code"
maxLength={1}
value={v}
onChange={e => handleChange(i, e.target.value)}
onKeyDown={e => handleKeyDown(i, e)}
onPaste={e => handlePaste(i, e)}
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
${v
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
: 'border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/70 text-gray-700 dark:text-gray-200'}
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
/>
))}
</div>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Verified! Redirecting shortly...
</div>
)}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<button
type="submit"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{submitting ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Verifying...
</>
) : success ? 'Verified' : 'Confirm code'}
</button>
<button
type="button"
onClick={handleResend}
disabled={!!resendCooldown || submitting || success}
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
>
{resendCooldown
? `Resend in ${formatMmSs(resendCooldown)}`
: 'Resend code'}
</button>
</div>
{/* NEW: Go to Dashboard button */}
<div className="mt-1 text-center">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:underline"
>
Go to Dashboard
</button>
</div>
</fieldset>
{/* Helper text with validity + spam/junk reminder + support */}
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 dark:text-indigo-400 hover:underline">
Contact support
</a>
.
<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-gray-900">
Verify your email
</h1>
<p className="mt-3 text-gray-700 text-sm sm:text-base">
{initialEmailSent ? (
<>
We sent a 6-digit code to{' '}
<span className="text-blue-700 font-medium">
{user?.email || 'your email'}
</span>
. Enter it below.
</>
) : (
<>
Sending verification email to{' '}
<span className="text-blue-700 font-medium">
{user?.email || 'your email'}
</span>
...
</>
)}
</p>
</div>
</form>
</div>
{/* Card */}
<form
onSubmit={handleSubmit}
className="bg-white/95 backdrop-blur rounded-2xl shadow-xl ring-1 ring-black/5 px-6 py-8 sm:px-10 sm:py-10"
>
<fieldset disabled={submitting || success} className="space-y-8">
{/* Inputs */}
<div className="flex justify-center gap-2 sm:gap-3">
{code.map((v, i) => (
<input
key={i}
ref={el => { inputsRef.current[i] = el }}
inputMode="numeric"
aria-label={`Code digit ${i + 1}`}
autoComplete="one-time-code"
maxLength={1}
value={v}
onChange={e => handleChange(i, e.target.value)}
onKeyDown={e => handleKeyDown(i, e)}
onPaste={e => handlePaste(i, e)}
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
${v
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
: 'border-gray-300 bg-white/80 text-gray-700'}
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
/>
))}
</div>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Verified! Redirecting shortly...
</div>
)}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<button
type="submit"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{submitting ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Verifying...
</>
) : success ? 'Verified' : 'Confirm code'}
</button>
<button
type="button"
onClick={handleResend}
disabled={!!resendCooldown || submitting || success}
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
>
{resendCooldown
? `Resend in ${formatMmSs(resendCooldown)}`
: 'Resend code'}
</button>
</div>
<div className="mt-1 text-center">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="text-sm font-medium text-gray-700 hover:underline"
>
Go to Dashboard
</button>
</div>
</fieldset>
<div className="mt-8 text-center text-xs text-gray-500">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
Contact support
</a>
.
</div>
</form>
</div>
</main>
</div>
</PageLayout>
)

View File

@ -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,255 +189,255 @@ 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>
<form
onSubmit={handleSubmit}
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-14">
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
Sign Company Partnership Contract
</h1>
<p className="text-center text-sm text-gray-600 mb-8">
Please review the contract details and sign on behalf of the company.
</p>
<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"
>
<div className="px-6 py-8 sm:px-10 lg:px-14">
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
Sign Company Partnership Contract
</h1>
<p className="text-center text-sm text-gray-600 mb-8">
Please review the contract details and sign on behalf of the company.
</p>
{/* Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> COMP-2024-017</li>
<li><span className="font-medium text-gray-700">Version:</span> 2.4 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li>
</ul>
</div>
<div className="rounded-lg border border-amber-200 bg-amber-50 p-5">
<h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3>
<p className="text-xs sm:text-sm text-amber-800 leading-relaxed">
You confirm that you are authorized to sign on behalf of the company.
</p>
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Company Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
<button
type="button"
onClick={async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
>
{previewLoading ? 'Loading…' : 'Refresh'}
</button>
</div>
{/* Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> COMP-2024-017</li>
<li><span className="font-medium text-gray-700">Version:</span> 2.4 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li>
</ul>
</div>
<div className="rounded-lg border border-amber-200 bg-amber-50 p-5">
<h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3>
<p className="text-xs sm:text-sm text-amber-800 leading-relaxed">
You confirm that you are authorized to sign on behalf of the company.
</p>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Company Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
value={companyName}
onChange={e => { setCompanyName(e.target.value); setError('') }}
placeholder="Firmenname offiziell"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Company Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
<button
type="button"
onClick={async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
>
{previewLoading ? 'Loading…' : 'Refresh'}
</button>
</div>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
<div>
<label className="block text_sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. München"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
<hr className="my-10 border-gray-200" />
{/* Company Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
value={companyName}
onChange={e => { setCompanyName(e.target.value); setError('') }}
placeholder="Firmenname offiziell"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text_sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. München"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-1">
<label className="block text_sm font-medium text-gray-700 mb-1">
Representative Name *
</label>
<input
value={repName}
onChange={e => { setRepName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Representative Position / Title *
</label>
<input
value={repTitle}
onChange={e => { setRepTitle(e.target.value); setError('') }}
placeholder="z.B. Geschäftsführer, Authorized Signatory"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Interne Referenz / Zusatz"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
<div className="sm:col-span-2 lg:col-span-1">
<label className="block text_sm font-medium text-gray-700 mb-1">
Representative Name *
</label>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
value={repName}
onChange={e => { setRepName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Representative Position / Title *
</label>
<span>I confirm I have read and accepted the full contract on behalf of the company.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
value={repTitle}
onChange={e => { setRepTitle(e.target.value); setError('') }}
placeholder="z.B. Geschäftsführer, Authorized Signatory"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<span>I consent to processing of company and personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Interne Referenz / Zusatz"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I am authorized to sign legally binding documents for this company.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm I have read and accepted the full contract on behalf of the company.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to processing of company and personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I am authorized to sign legally binding documents for this company.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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,205 +180,205 @@ 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>
<form
onSubmit={handleSubmit}
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-14">
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
Sign Personal Participation Contract
</h1>
<p className="text-center text-sm text-gray-600 mb-8">
Please review the contract details and sign electronically.
</p>
<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"
>
<div className="px-6 py-8 sm:px-10 lg:px-14">
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
Sign Personal Participation Contract
</h1>
<p className="text-center text-sm text-gray-600 mb-8">
Please review the contract details and sign electronically.
</p>
{/* Contract Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li>
<li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li>
</ul>
</div>
<div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5">
<h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3>
<p className="text-xs sm:text-sm text-indigo-800 leading-relaxed">
Your electronic signature is legally binding. Please ensure all details are correct.
</p>
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={async () => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
</div>
{/* Contract Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li>
<li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li>
</ul>
</div>
<div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5">
<h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3>
<p className="text-xs sm:text-sm text-indigo-800 leading-relaxed">
Your electronic signature is legally binding. Please ensure all details are correct.
</p>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature Details</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name (Signature) *
</label>
<input
value={fullName}
onChange={e => { setFullName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<p className="mt-1 text-xs text-gray-500">
Must match your official ID.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<h3 className="text-sm font-semibold text-gray-900">Contract Preview</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={async () => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
</div>
</div>
{previewLoading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
) : previewHtml ? (
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. Berlin"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</section>
<hr className="my-10 border-gray-200" />
{/* Signature Fields */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature Details</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name (Signature) *
</label>
<input
value={fullName}
onChange={e => { setFullName(e.target.value); setError('') }}
placeholder="Vor- und Nachname"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
<p className="mt-1 text-xs text-gray-500">
Must match your official ID.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. Berlin"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Optionale zusätzliche Bemerkung"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Optionale zusätzliche Bemerkung"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm that I have read and understood the contract in full.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to the processing of my personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm this electronic signature is legally binding and equivalent to a handwritten signature.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm that I have read and understood the contract in full.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to the processing of my personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm this electronic signature is legally binding and equivalent to a handwritten signature.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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,8 +107,14 @@ export function useCompanyUploadId() {
e.preventDefault()
if (!validate()) return
if (!accessToken) {
setError('Not authenticated. Please log in again.')
return
const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return
}
setSubmitting(true)
@ -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)
}

View File

@ -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,181 +56,127 @@ 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">
<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
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of the company contact person&apos;s ID document.
</p>
<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
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of the company contact person&apos;s ID document.
</p>
{/* Fields: 3 in one row on md+ with unified inputs */}
<div className="grid gap-6 md:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Contact Person ID Number *
</label>
<input
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
placeholder="Enter contact person's ID number"
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the ID number exactly as shown on the document
</p>
{/* Fields: 3 in one row on md+ with unified inputs */}
<div className="grid gap-6 md:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Contact Person ID Number *
</label>
<input
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
placeholder="Enter contact person's ID number"
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the ID number exactly as shown on the document
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Document Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
>
<option value="">Select document type</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiryDate}
onChange={e => setExpiryDate(e.target.value)}
placeholder="tt.mm.jjjj"
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the expiry date shown on your document
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Document Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
aria-pressed={hasBack}
>
<option value="">Select document type</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
</select>
<span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiryDate}
onChange={e => setExpiryDate(e.target.value)}
placeholder="tt.mm.jjjj"
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the expiry date shown on your document
</p>
</div>
</div>
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
aria-pressed={hasBack}
>
<span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div>
{/* Upload Areas */}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Upload ID Front Side */}
<div
{...dropHandlers}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={frontRef}
type="file"
accept="image/*,application/pdf"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */}
{frontPreview && (
<img
src={frontPreview}
alt="Primary document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
{/* Upload ID Back Side */}
{hasBack && (
{/* Upload Areas */}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Upload ID Front Side */}
<div
{...dropHandlers}
onDrop={e => onDrop(e, 'extra')}
onClick={() => openPicker('extra')}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={extraRef}
ref={frontRef}
type="file"
accept="image/*,application/pdf"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'extra') }}
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{extraFile ? (
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */}
{extraPreview && (
{frontPreview && (
<img
src={extraPreview}
alt="Supporting document preview"
src={frontPreview}
alt="Primary document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{extraFile.name}</p>
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('extra') }}
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
@ -240,7 +186,7 @@ export default function CompanyIdUploadPage() {
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
@ -250,45 +196,95 @@ export default function CompanyIdUploadPage() {
</>
)}
</div>
{/* Upload ID Back Side */}
{hasBack && (
<div
{...dropHandlers}
onDrop={e => onDrop(e, 'extra')}
onClick={() => openPicker('extra')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={extraRef}
type="file"
accept="image/*,application/pdf"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'extra') }}
/>
{extraFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* Preview only for images */}
{extraPreview && (
<img
src={extraPreview}
alt="Supporting document preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{extraFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('extra') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
)}
</div>
{/* Info */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Documents uploaded successfully. Redirecting shortly
</div>
)}
</div>
{/* Info */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Documents uploaded successfully. Redirecting shortly
</div>
)}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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)
}

View File

@ -60,188 +60,134 @@ 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">
<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
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of your governmentissued ID
</p>
<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
</h1>
<p className="text-sm text-gray-600 mb-8">
Please upload clear photos of both sides of your governmentissued ID
</p>
{/* Grid Fields: put all three inputs on the same line on md+ */}
<div className="grid gap-6 md:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ID Number *
</label>
<input
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
placeholder="Enter your ID number"
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the number exactly as shown on your ID
</p>
{/* Grid Fields: put all three inputs on the same line on md+ */}
<div className="grid gap-6 md:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ID Number *
</label>
<input
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
placeholder="Enter your ID number"
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
required
/>
<p className="mt-1 text-xs text-gray-600">
Enter the number exactly as shown on your ID
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ID Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
>
<option value="">Select ID type</option>
{ID_TYPES.map(t => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ID Type *
</label>
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
required
>
<option value="">Select ID type</option>
{ID_TYPES.map(t => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date *
</label>
<input
type="date"
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
required
/>
</div>
</div>
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
}`}
aria-pressed={hasBack}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
hasBack ? 'translate-x-5' : 'translate-x-0'
{/* Back side toggle */}
<div className="mt-8 flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">
Does ID have a Backside?
</span>
<button
type="button"
onClick={() => setHasBack(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
}`}
/>
</button>
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div>
{/* Upload Areas: full width, 1 col if no back, 2 cols if back */}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Front */}
<div
{...dropEvents}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={frontInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */}
{frontPreview && (
<img
src={frontPreview}
alt="Front ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
aria-pressed={hasBack}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
hasBack ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
</div>
{/* Back */}
{hasBack && (
{/* Upload Areas: full width, 1 col if no back, 2 cols if back */}
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
{/* Front */}
<div
{...dropEvents}
onDrop={e => onDrop(e, 'back')}
onClick={() => openPicker('back')}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={backInputRef}
ref={frontInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'back') }}
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
/>
{backFile ? (
{frontFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */}
{backPreview && (
{frontPreview && (
<img
src={backPreview}
alt="Back ID preview"
src={frontPreview}
alt="Front ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{backFile.name}</p>
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('back') }}
onClick={e => { e.stopPropagation(); clearFile('front') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
@ -251,7 +197,7 @@ export default function PersonalIdUploadPage() {
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
@ -261,45 +207,95 @@ export default function PersonalIdUploadPage() {
</>
)}
</div>
{/* Back */}
{hasBack && (
<div
{...dropEvents}
onDrop={e => onDrop(e, 'back')}
onClick={() => openPicker('back')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
>
<input
ref={backInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'back') }}
/>
{backFile ? (
<div className="flex w-full max-w-full flex-col items-center">
{/* NEW preview */}
{backPreview && (
<img
src={backPreview}
alt="Back ID preview"
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
/>
)}
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{backFile.name}</p>
<button
type="button"
onClick={e => { e.stopPropagation(); clearFile('back') }}
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
>
<XMarkIcon className="h-4 w-4" /> Remove
</button>
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
or drag and drop
<br />
PNG, JPG, JPEG up to 10MB
</p>
</>
)}
</div>
)}
</div>
{/* Info Box, errors, success, submit */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Upload saved successfully. Redirecting shortly
</div>
)}
</div>
{/* Info Box, errors, success, submit */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
<li>Have good lighting (no shadows or glare)</li>
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
</ul>
</div>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Upload saved successfully. Redirecting shortly
</div>
)}
<div className="mt-8 flex justify-end">
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
</div>
</div>
</form>
</form>
</main>
</div>
</PageLayout>
)

View File

@ -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,18 +302,18 @@ 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) => (
{rules.map((rule, index) => (
<li key={index} className={`flex items-center gap-2 ${rule.test ? 'text-green-600' : 'text-slate-600'}`}>
<span>{rule.test ? '✓' : '○'}</span>
<span>{rule.text}</span>
@ -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 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>

View File

@ -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>

View File

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