feat: add users and companies dashboard route surfaces
This commit is contained in:
parent
0996f12227
commit
f16c7eb4dd
40 changed files with 937 additions and 278 deletions
349
package-lock.json
generated
349
package-lock.json
generated
|
|
@ -13,6 +13,8 @@
|
|||
"vinxi": "^0.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -355,7 +357,6 @@
|
|||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
|
|
@ -367,7 +368,6 @@
|
|||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -378,7 +378,6 @@
|
|||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -968,7 +967,6 @@
|
|||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
|
|
@ -2287,6 +2285,288 @@
|
|||
"integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||
"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.32.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
|
||||
"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.2.2",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.2.2",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.2.2",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.2.2",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.2.2",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
|
||||
"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
|
||||
"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
|
||||
"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
|
||||
"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
|
||||
"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
|
||||
"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
|
||||
"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
|
||||
"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
|
||||
"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
|
||||
"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.8.1",
|
||||
"@emnapi/runtime": "^1.8.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
|
||||
"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
|
||||
"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
|
||||
"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.2.2",
|
||||
"@tailwindcss/oxide": "4.2.2",
|
||||
"tailwindcss": "4.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/directive-functions-plugin": {
|
||||
"version": "1.121.21",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/directive-functions-plugin/-/directive-functions-plugin-1.121.21.tgz",
|
||||
|
|
@ -2367,7 +2647,6 @@
|
|||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -4185,6 +4464,20 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
|
|
@ -5204,9 +5497,8 @@
|
|||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"devOptional": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
|
|
@ -5238,12 +5530,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5259,12 +5551,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5280,12 +5572,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5301,12 +5593,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5322,12 +5614,12 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5343,12 +5635,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5364,12 +5656,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5385,12 +5677,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5406,12 +5698,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5427,12 +5719,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -5448,12 +5740,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -7527,6 +7819,27 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.1.tgz",
|
||||
"integrity": "sha512-b+u3CEM6FjDHru+nhUSjDofpWSBp2rINziJWgApm72wwGasQ/wKXftZe4tI2Y5HPv6OpzXSZHOFq87H4vfsgsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"vinxi": "^0.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
139
src/app.css
139
src/app.css
|
|
@ -1,7 +1,8 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700;800&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--brand-orange: #fd6216;
|
||||
--brand-orange: #fd6116;
|
||||
--brand-orange-dark: #e45a14;
|
||||
--brand-navy: #050026;
|
||||
--ink: #100b2f;
|
||||
|
|
@ -240,7 +241,7 @@ body {
|
|||
color: #64748b;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.25fr 0.75fr;
|
||||
gap: 16px;
|
||||
|
|
@ -354,7 +355,7 @@ body {
|
|||
}
|
||||
|
||||
.contact-upload-icon {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +383,7 @@ body {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.contact-icon svg {
|
||||
|
|
@ -516,7 +517,7 @@ body {
|
|||
}
|
||||
|
||||
.help-clear-filter {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
|
|
@ -545,7 +546,7 @@ body {
|
|||
.help-category-pill-active {
|
||||
border-color: rgba(253, 98, 22, 0.45);
|
||||
background: #fff4ec;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.help-article-card {
|
||||
|
|
@ -562,7 +563,7 @@ body {
|
|||
}
|
||||
|
||||
.help-article-link:hover {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.help-article-summary {
|
||||
|
|
@ -599,7 +600,7 @@ body {
|
|||
}
|
||||
|
||||
.help-read-link {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
|
|
@ -793,12 +794,12 @@ body {
|
|||
height: 2px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
transition: transform 260ms ease;
|
||||
}
|
||||
|
||||
.public-header .nav-links a:hover {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.public-header .nav-links a:hover::after {
|
||||
|
|
@ -807,7 +808,7 @@ body {
|
|||
|
||||
.public-header .nav-links a.active,
|
||||
.public-header .nav-links a[aria-current='page'] {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.public-header .nav-links a.active::after,
|
||||
|
|
@ -846,7 +847,7 @@ body {
|
|||
|
||||
.nav-auth-primary {
|
||||
border: 1px solid rgba(253, 98, 22, 0.72);
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 1px rgba(253, 98, 22, 0.4) inset, 0 12px 24px -16px rgba(253, 98, 22, 0.9);
|
||||
}
|
||||
|
|
@ -935,7 +936,7 @@ body {
|
|||
|
||||
.mobile-signup {
|
||||
border: 1px solid rgba(253, 98, 22, 0.72);
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
|
@ -1111,8 +1112,8 @@ body {
|
|||
}
|
||||
|
||||
.chip-btn.active {
|
||||
border-color: #fd6216;
|
||||
background: #fd6216;
|
||||
border-color: #fd6116;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px -10px rgba(253, 98, 22, 0.8);
|
||||
}
|
||||
|
|
@ -1192,7 +1193,7 @@ body {
|
|||
}
|
||||
|
||||
.path-icon {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
|
|
@ -1234,7 +1235,7 @@ body {
|
|||
.path-chip svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: #fd6216;
|
||||
stroke: #fd6116;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
|
@ -1275,7 +1276,7 @@ body {
|
|||
border-radius: 999px;
|
||||
border: 1px solid rgba(253, 98, 22, 0.5);
|
||||
background: #fff;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1337,7 +1338,7 @@ body {
|
|||
|
||||
.lp-path-dot.active {
|
||||
width: 28px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
}
|
||||
|
||||
.path-body h3 {
|
||||
|
|
@ -1417,7 +1418,7 @@ body {
|
|||
}
|
||||
|
||||
.step-item.active {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
|
|
@ -1428,7 +1429,7 @@ body {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
|
@ -1503,7 +1504,7 @@ body {
|
|||
|
||||
.faq-q-icon.open {
|
||||
transform: rotate(180deg);
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.faq-a {
|
||||
|
|
@ -1608,7 +1609,7 @@ body {
|
|||
}
|
||||
|
||||
.public-footer-links a:hover {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
|
@ -1865,7 +1866,7 @@ body {
|
|||
border-radius: 999px;
|
||||
border: 1px solid rgba(253, 98, 22, 0.46);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow:
|
||||
0 18px 36px -22px rgba(2, 6, 23, 0.8),
|
||||
|
|
@ -1935,7 +1936,7 @@ body {
|
|||
|
||||
.lp-primary-btn {
|
||||
border: 1px solid rgba(253, 98, 22, 0.72);
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 1px rgba(253, 98, 22, 0.4) inset, 0 12px 24px -16px rgba(253, 98, 22, 0.9);
|
||||
}
|
||||
|
|
@ -2343,7 +2344,7 @@ body {
|
|||
height: 3px;
|
||||
width: 100%;
|
||||
transform-origin: left;
|
||||
background: linear-gradient(90deg, #fd6216, rgba(255, 188, 153, 0.9));
|
||||
background: linear-gradient(90deg, #fd6116, rgba(255, 188, 153, 0.9));
|
||||
animation: whyTrackFill 4.2s linear forwards;
|
||||
z-index: 3;
|
||||
}
|
||||
|
|
@ -2551,7 +2552,7 @@ body {
|
|||
}
|
||||
|
||||
.hiwCodeStepActive {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.12);
|
||||
background: linear-gradient(145deg, #fff7f1 0%, #fff 100%);
|
||||
transform: translateY(-1px);
|
||||
|
|
@ -2563,7 +2564,7 @@ body {
|
|||
border-radius: 999px;
|
||||
border: 1px solid rgba(253, 98, 22, 0.48);
|
||||
background: #fff3eb;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -2633,7 +2634,7 @@ body {
|
|||
|
||||
.hiwCodeDotActive {
|
||||
width: 20px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
}
|
||||
|
||||
@keyframes whyCardBreath {
|
||||
|
|
@ -2733,7 +2734,7 @@ body {
|
|||
}
|
||||
|
||||
.lp-dot.active {
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
}
|
||||
|
||||
@keyframes lp-float-slow {
|
||||
|
|
@ -2905,7 +2906,7 @@ body {
|
|||
.onboarding-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
transition: width 220ms ease;
|
||||
}
|
||||
|
||||
|
|
@ -2938,7 +2939,7 @@ body {
|
|||
}
|
||||
|
||||
.multi-select-option.is-selected {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
background: #fff2ea;
|
||||
color: #2c3551;
|
||||
box-shadow: 0 0 0 1px rgba(253, 98, 22, 0.12);
|
||||
|
|
@ -2946,7 +2947,7 @@ body {
|
|||
|
||||
.multi-select-option:focus-visible {
|
||||
outline: none;
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
box-shadow: 0 0 0 2px #ffd8c3;
|
||||
}
|
||||
|
||||
|
|
@ -2986,8 +2987,8 @@ body {
|
|||
}
|
||||
|
||||
.multi-select-tick.is-visible {
|
||||
border-color: #fd6216;
|
||||
background: #fd6216;
|
||||
border-color: #fd6116;
|
||||
background: #fd6116;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
|
@ -3113,7 +3114,7 @@ body {
|
|||
.auth-form .input:focus,
|
||||
.auth-form .select:focus,
|
||||
.auth-form .textarea:focus {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
box-shadow: 0 0 0 2px #ffd8c3;
|
||||
}
|
||||
|
||||
|
|
@ -3127,7 +3128,7 @@ body {
|
|||
.auth-forgot-link {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
|
@ -3222,7 +3223,7 @@ body {
|
|||
height: 44px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
|
@ -3246,7 +3247,7 @@ body {
|
|||
}
|
||||
|
||||
.auth-footer-row a {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -3270,7 +3271,7 @@ body {
|
|||
min-width: 20px;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
accent-color: #fd6216;
|
||||
accent-color: #fd6116;
|
||||
}
|
||||
|
||||
.auth-checkbox-label {
|
||||
|
|
@ -3280,7 +3281,7 @@ body {
|
|||
}
|
||||
|
||||
.auth-checkbox-label a {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-decoration: underline;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -3343,7 +3344,7 @@ body {
|
|||
height: 46px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(253, 98, 22, 0.75);
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
|
|
@ -3416,7 +3417,7 @@ body {
|
|||
top: 0;
|
||||
width: 2px;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
box-shadow: 0 0 16px rgba(253, 98, 22, 0.66);
|
||||
transition: height 260ms ease;
|
||||
}
|
||||
|
|
@ -3446,7 +3447,7 @@ body {
|
|||
}
|
||||
|
||||
.about-chapter-item-active a {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-shadow: 0 0 14px rgba(253, 98, 22, 0.55);
|
||||
}
|
||||
|
||||
|
|
@ -3487,7 +3488,7 @@ body {
|
|||
}
|
||||
|
||||
.about-kicker-orange {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.about-title {
|
||||
|
|
@ -3579,7 +3580,7 @@ body {
|
|||
}
|
||||
|
||||
.about-chapter-label-orange {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.about-chapter-title {
|
||||
|
|
@ -3806,7 +3807,7 @@ body {
|
|||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.13em;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.about-chapter-two-panel-title {
|
||||
|
|
@ -3843,7 +3844,7 @@ body {
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
box-shadow: 0 0 0 5px rgba(253, 98, 22, 0.14);
|
||||
}
|
||||
|
||||
|
|
@ -3902,7 +3903,7 @@ body {
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
}
|
||||
|
||||
.about-trust-num {
|
||||
|
|
@ -3911,7 +3912,7 @@ body {
|
|||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.about-trust-sequence-card h3 {
|
||||
|
|
@ -4023,7 +4024,7 @@ body {
|
|||
}
|
||||
|
||||
.about-orange-word {
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-shadow: 0 0 22px rgba(253, 98, 22, 0.45);
|
||||
}
|
||||
|
||||
|
|
@ -4180,7 +4181,7 @@ body {
|
|||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
background: #fd6216;
|
||||
background: #fd6116;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
|
|
@ -4401,7 +4402,7 @@ body {
|
|||
.sidebar-role-badge .role-badge {
|
||||
background: rgba(253, 98, 22, 0.18);
|
||||
border: 1px solid rgba(253, 98, 22, 0.35);
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
|
|
@ -4443,7 +4444,7 @@ body {
|
|||
.nav-item-active,
|
||||
.nav-item[aria-current="page"] {
|
||||
background: rgba(253, 98, 22, 0.18);
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
}
|
||||
|
||||
.nav-item-logout {
|
||||
|
|
@ -4637,7 +4638,7 @@ body {
|
|||
.kpi-link {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-decoration: none;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
|
@ -4669,7 +4670,7 @@ body {
|
|||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
|
@ -4697,7 +4698,7 @@ body {
|
|||
.tour-progress span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #fd6216, #ff8a4d);
|
||||
background: linear-gradient(90deg, #fd6116, #ff8a4d);
|
||||
transition: width 200ms ease;
|
||||
}
|
||||
|
||||
|
|
@ -4816,7 +4817,7 @@ body {
|
|||
}
|
||||
|
||||
.role-path-card.selected {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
box-shadow: 0 0 0 2px rgba(253, 98, 22, 0.28), 0 22px 42px -30px rgba(253, 98, 22, 0.85);
|
||||
}
|
||||
|
||||
|
|
@ -4880,7 +4881,7 @@ body {
|
|||
}
|
||||
|
||||
.role-card.selected {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(253, 98, 22, 0.3) inset, 0 24px 52px -20px rgba(253, 98, 22, 0.3);
|
||||
}
|
||||
|
|
@ -4932,7 +4933,7 @@ body {
|
|||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fd6216;
|
||||
color: #fd6116;
|
||||
opacity: 0.8;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
|
@ -4995,7 +4996,7 @@ body {
|
|||
}
|
||||
|
||||
.pending-support { font-size: 13px; color: #94a3b8; }
|
||||
.pending-support a { color: #fd6216; }
|
||||
.pending-support a { color: #fd6116; }
|
||||
|
||||
/* ── Shared Button Variants ── */
|
||||
.btn-primary {
|
||||
|
|
@ -5095,8 +5096,8 @@ body {
|
|||
}
|
||||
|
||||
.filter-btn.active, .filter-btn:hover {
|
||||
border-color: #fd6216;
|
||||
color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
color: #fd6116;
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
|
|
@ -5166,7 +5167,7 @@ body {
|
|||
outline: none;
|
||||
}
|
||||
.input:focus, .textarea:focus, .select:focus {
|
||||
border-color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.12);
|
||||
}
|
||||
.input::placeholder, .textarea::placeholder {
|
||||
|
|
@ -5220,7 +5221,7 @@ body {
|
|||
font-weight: 600;
|
||||
transition: color 150ms;
|
||||
}
|
||||
.back-link a:hover { color: #fd6216; }
|
||||
.back-link a:hover { color: #fd6116; }
|
||||
|
||||
/* ── Button Sizes ────────────────────────────────────────────────────────── */
|
||||
.btn-sm {
|
||||
|
|
@ -5305,16 +5306,16 @@ body {
|
|||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover:not(:disabled) {
|
||||
border-color: #fd6216;
|
||||
color: #fd6216;
|
||||
border-color: #fd6116;
|
||||
color: #fd6116;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #fd6216;
|
||||
border-color: #fd6216;
|
||||
background: #fd6116;
|
||||
border-color: #fd6116;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
|
|
|
|||
|
|
@ -2,38 +2,67 @@ import { A, useLocation } from '@solidjs/router';
|
|||
import { For, Show, createMemo, type JSX } from 'solid-js';
|
||||
import { adminModules } from '~/lib/admin/module-config';
|
||||
|
||||
const iconByRoute: Record<string, string> = {
|
||||
'/admin': 'DB',
|
||||
'/admin/department': 'DP',
|
||||
'/admin/designation': 'DG',
|
||||
'/admin/internal-role-management': 'IR',
|
||||
'/admin/employees': 'EM',
|
||||
'/admin/external-role-management': 'ER',
|
||||
'/admin/external-onboarding': 'EO',
|
||||
'/admin/internal-dashboard': 'ID',
|
||||
'/admin/external-dashboard': 'ED',
|
||||
'/admin/verification-management': 'VR',
|
||||
'/admin/approval-management': 'AP',
|
||||
};
|
||||
function NavIcon(props: { route: string }) {
|
||||
const common = 'h-[18px] w-[18px]';
|
||||
if (props.route === '/admin') {
|
||||
return (
|
||||
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (props.route === '/admin/department') {
|
||||
return (
|
||||
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2" />
|
||||
<path d="M8 7h8M8 11h8M8 15h8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (props.route === '/admin/designation') {
|
||||
return (
|
||||
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M12 3 6 6v6c0 4 2.6 7.8 6 9 3.4-1.2 6-5 6-9V6l-6-3Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (props.route.includes('role')) {
|
||||
return (
|
||||
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20c.7-3.2 3.4-5 7-5s6.3 1.8 7 5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<rect x="5" y="5" width="14" height="14" rx="3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminShell(props: { children: JSX.Element }) {
|
||||
const location = useLocation();
|
||||
const navItems = createMemo(() => adminModules.filter((module) => module.route !== '/admin'));
|
||||
const dashboardActive = () => location.pathname === '/admin';
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-[#f5f6fa] text-[#050026]">
|
||||
<div class="grid min-h-screen grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)] xl:grid-cols-[236px_minmax(0,1fr)] 2xl:grid-cols-[248px_minmax(0,1fr)]">
|
||||
<aside class="hidden border-r border-[#e6e8ee] bg-[#f7f7f8] lg:flex lg:flex-col">
|
||||
<div class="grid min-h-screen grid-cols-1 lg:grid-cols-[302px_minmax(0,1fr)]">
|
||||
<aside class="hidden border-r border-[#e6e8ee] bg-[#f7f7f8] lg:sticky lg:top-0 lg:flex lg:h-screen lg:flex-col">
|
||||
<div class="h-[101px] border-b border-[#e6e8ee] px-8 py-8">
|
||||
<img src="/nxtgauge-logo.png" alt="Nxtgauge" class="h-9 w-auto" />
|
||||
</div>
|
||||
|
||||
<nav class="min-h-0 flex-1 overflow-y-auto px-4 py-6">
|
||||
<A href="/admin" class={`group relative flex h-12 items-center rounded-xl px-4 text-sm font-medium transition ${location.pathname === '/admin' ? 'bg-[#ffe8dc] text-[#fd6116]' : 'text-[#1a1f4a] hover:bg-white'}`}>
|
||||
<Show when={location.pathname === '/admin'}>
|
||||
<A href="/admin" class={`group relative flex h-12 items-center rounded-xl px-4 text-sm font-medium transition ${dashboardActive() ? 'bg-[#ffe8dc] text-[#fd6116]' : 'text-[#1a1f4a] hover:bg-white'}`}>
|
||||
<Show when={dashboardActive()}>
|
||||
<span class="absolute left-0 top-2 h-8 w-1 rounded-r bg-[#fd6116]" />
|
||||
</Show>
|
||||
<span class="mr-4 text-[11px] font-semibold leading-none">{iconByRoute['/admin']}</span>
|
||||
<span class="mr-3 inline-flex items-center justify-center"><NavIcon route="/admin" /></span>
|
||||
<span>Dashboard</span>
|
||||
</A>
|
||||
|
||||
|
|
@ -57,8 +86,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
<Show when={active()}>
|
||||
<span class="absolute left-0 top-2 h-8 w-1 rounded-r bg-[#fd6116]" />
|
||||
</Show>
|
||||
<span class="mr-4 text-[11px] font-semibold leading-none">{iconByRoute[module.route] || '--'}</span>
|
||||
<span class="truncate">{module.navLabel}</span>
|
||||
<span class="mr-3 inline-flex items-center justify-center"><NavIcon route={module.route} /></span>
|
||||
<span>{module.navLabel}</span>
|
||||
</A>
|
||||
</>
|
||||
);
|
||||
|
|
@ -81,16 +110,27 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
<header class="sticky top-0 z-40 border-b border-[#e6e8ee] bg-[#f7f7f8]">
|
||||
<div class="flex h-[101px] items-center justify-between gap-3 px-4 lg:px-8">
|
||||
<label class="flex h-12 w-full max-w-[540px] items-center rounded-2xl border border-[#e2e5ee] bg-[#ededf1] px-4 text-sm text-[#8a90aa]">
|
||||
<span class="mr-3 text-xs font-semibold leading-none">SR</span>
|
||||
<svg class="mr-3 h-5 w-5 text-[#8d93aa]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.5-3.5" />
|
||||
</svg>
|
||||
<input class="w-full border-0 bg-transparent text-sm text-[#050026] outline-none" placeholder="Search for anything..." />
|
||||
</label>
|
||||
|
||||
<div class="flex items-center gap-5">
|
||||
<button class="relative text-[#050026]">
|
||||
<span class="text-xs font-semibold">NT</span>
|
||||
<button class="relative text-[#050026]" aria-label="Notifications">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2c0 .5-.2 1-.6 1.4L4 17h5" />
|
||||
<path d="M9 17a3 3 0 0 0 6 0" />
|
||||
</svg>
|
||||
<span class="absolute -right-0.5 top-0 h-1.5 w-1.5 rounded-full bg-[#fd6116]" />
|
||||
</button>
|
||||
<button class="text-xs font-semibold text-[#050026]">ST</button>
|
||||
<button class="text-[#050026]" aria-label="Settings">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 0 1 0 2.8 2 2 0 0 1-2.8 0l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.6V21a2 2 0 0 1-4 0v-.2a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 0 1-2.8 0 2 2 0 0 1 0-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.6-1H3a2 2 0 0 1 0-4h.2a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 0 1 0-2.8 2 2 0 0 1 2.8 0l.1.1a1.7 1.7 0 0 0 1.8.3h.1a1.7 1.7 0 0 0 1-1.6V3a2 2 0 0 1 4 0v.2a1.7 1.7 0 0 0 1 1.6h.1a1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 0 1 2.8 0 2 2 0 0 1 0 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v.1a1.7 1.7 0 0 0 1.6 1H21a2 2 0 0 1 0 4h-.2a1.7 1.7 0 0 0-1.6 1V15Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hidden h-10 w-px bg-[#dde1ea] md:block" />
|
||||
<div class="hidden min-w-0 md:block">
|
||||
<p class="text-sm font-semibold text-[#0f1744]">Admin User</p>
|
||||
|
|
|
|||
|
|
@ -70,40 +70,49 @@ const IconWallet = () => (
|
|||
|
||||
// ── Module → nav item mapping ─────────────────────────────────────────────────
|
||||
|
||||
const MODULE_NAV_MAP: Record<string, { label: string; href: string; icon: Component; tourId: string }> = {
|
||||
// lowercase keys (from seed/runtime config)
|
||||
dashboard: { label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
jobs: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
applications: { label: 'Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' },
|
||||
my_applications: { label: 'My Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' },
|
||||
browse_jobs: { label: 'Browse Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
profile: { label: 'My Profile', href: '/dashboard/profile', icon: IconSettings, tourId: 'profile' },
|
||||
requirements: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' },
|
||||
marketplace: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' },
|
||||
leads: { label: 'My Leads', href: '/dashboard/requests', icon: IconJobs, tourId: 'requests' },
|
||||
portfolio: { label: 'Portfolio', href: '/dashboard/portfolio', icon: IconPortfolio, tourId: 'portfolio' },
|
||||
services: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' },
|
||||
wallet: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' },
|
||||
notifications: { label: 'Notifications', href: '/dashboard/notifications', icon: IconBell, tourId: 'notifications' },
|
||||
settings: { label: 'Settings', href: '/dashboard/settings', icon: IconSettings, tourId: 'settings' },
|
||||
const MODULE_NAV_MAP: Record<string, { label: string; href: string; icon: Component; tourId: string; order?: number }> = {
|
||||
// Next.js role-dashboard module keys
|
||||
dashboard: { label: 'Dashboard', href: '/users/dashboard', icon: IconDashboard, tourId: 'dashboard', order: 1 },
|
||||
onboarding: { label: 'Get Started', href: '/users/onboarding', icon: IconCompass, tourId: 'onboarding', order: 2 },
|
||||
role_selection: { label: 'Choose Your Role', href: '/users/onboarding/role-selection', icon: IconCompass, tourId: 'role-selection', order: 3 },
|
||||
profile: { label: 'Profile', href: '/users/profile', icon: IconSettings, tourId: 'profile', order: 4 },
|
||||
leads: { label: 'Leads', href: '/users/leads', icon: IconJobs, tourId: 'requests', order: 5 },
|
||||
job_postings: { label: 'Job Postings', href: '/companies/job-postings', icon: IconJobs, tourId: 'jobs', order: 6 },
|
||||
applications: { label: 'Applications', href: '/companies/applications', icon: IconJobs, tourId: 'applications', order: 7 },
|
||||
portfolio: { label: 'Portfolio', href: '/users/professional/portfolio', icon: IconPortfolio, tourId: 'portfolio', order: 8 },
|
||||
verification: { label: 'Verification Status', href: '/users/verification-status', icon: IconSettings, tourId: 'verification', order: 9 },
|
||||
tracecoins: { label: 'Tracecoins', href: '/companies/tracecoins', icon: IconWallet, tourId: 'wallet', order: 10 },
|
||||
feedback: { label: 'Feedback', href: '/companies/feedback', icon: IconBell, tourId: 'support', order: 11 },
|
||||
notifications: { label: 'Notifications', href: '/users/notifications', icon: IconBell, tourId: 'notifications', order: 12 },
|
||||
support: { label: 'Support', href: '/users/support', icon: IconBell, tourId: 'support', order: 13 },
|
||||
settings: { label: 'Settings', href: '/users/settings', icon: IconSettings, tourId: 'settings', order: 14 },
|
||||
|
||||
// Existing Solid module keys (backward compatibility)
|
||||
jobs: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
my_applications: { label: 'My Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' },
|
||||
browse_jobs: { label: 'Browse Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
requirements: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' },
|
||||
marketplace: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' },
|
||||
services: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' },
|
||||
wallet: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' },
|
||||
|
||||
// Uppercase fallbacks
|
||||
COMPANY_DASHBOARD: { label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
JOBSEEKER_DASHBOARD: { label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
CUSTOMER_DASHBOARD: { label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
PROFESSIONAL_DASHBOARD: { label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
JOBS: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
APPLICATIONS: { label: 'Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' },
|
||||
REQUIREMENTS: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' },
|
||||
MARKETPLACE: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' },
|
||||
MY_REQUESTS: { label: 'My Leads', href: '/dashboard/requests', icon: IconJobs, tourId: 'requests' },
|
||||
ACCEPTED_LEADS: { label: 'Accepted Leads', href: '/dashboard/leads/accepted',icon: IconJobs, tourId: 'leads' },
|
||||
PORTFOLIO: { label: 'Portfolio', href: '/dashboard/portfolio', icon: IconPortfolio, tourId: 'portfolio' },
|
||||
SERVICES: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' },
|
||||
WALLET: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' },
|
||||
NOTIFICATIONS: { label: 'Notifications', href: '/dashboard/notifications', icon: IconBell, tourId: 'notifications' },
|
||||
SETTINGS: { label: 'Settings', href: '/dashboard/settings', icon: IconSettings, tourId: 'settings' },
|
||||
EXPLORE_NXTGAUGE: { label: 'Explore', href: '/dashboard/explore', icon: IconCompass, tourId: 'explore' },
|
||||
COMPANY_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
JOBSEEKER_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
CUSTOMER_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
PROFESSIONAL_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
JOBS: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' },
|
||||
APPLICATIONS: { label: 'Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' },
|
||||
REQUIREMENTS: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' },
|
||||
MARKETPLACE: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' },
|
||||
MY_REQUESTS: { label: 'Leads', href: '/dashboard/requests', icon: IconJobs, tourId: 'requests' },
|
||||
ACCEPTED_LEADS: { label: 'Accepted Leads', href: '/dashboard/leads/accepted',icon: IconJobs, tourId: 'leads' },
|
||||
PORTFOLIO: { label: 'Portfolio', href: '/dashboard/portfolio', icon: IconPortfolio, tourId: 'portfolio' },
|
||||
SERVICES: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' },
|
||||
WALLET: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' },
|
||||
NOTIFICATIONS: { label: 'Notifications', href: '/dashboard/notifications', icon: IconBell, tourId: 'notifications' },
|
||||
SETTINGS: { label: 'Settings', href: '/dashboard/settings', icon: IconSettings, tourId: 'settings' },
|
||||
EXPLORE_NXTGAUGE: { label: 'Explore', href: '/dashboard/explore', icon: IconCompass, tourId: 'explore' },
|
||||
};
|
||||
|
||||
// ── Spotlight Tour Overlay ────────────────────────────────────────────────────
|
||||
|
|
@ -195,7 +204,7 @@ function SpotlightOverlay(props: {
|
|||
x={rect()!.x} y={rect()!.y}
|
||||
width={rect()!.w} height={rect()!.h}
|
||||
rx="10" fill="none"
|
||||
stroke="#fd6216" stroke-width="2.5" opacity="0.9"
|
||||
stroke="#fd6116" stroke-width="2.5" opacity="0.9"
|
||||
/>
|
||||
</Show>
|
||||
</svg>
|
||||
|
|
@ -220,7 +229,7 @@ function SpotlightOverlay(props: {
|
|||
<div style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'margin-bottom': '10px' }}>
|
||||
<span style={{
|
||||
'font-size': '10px', 'font-weight': '800', 'text-transform': 'uppercase',
|
||||
color: '#fd6216', 'letter-spacing': '0.1em',
|
||||
color: '#fd6116', 'letter-spacing': '0.1em',
|
||||
}}>
|
||||
Guided Tour
|
||||
</span>
|
||||
|
|
@ -251,7 +260,7 @@ function SpotlightOverlay(props: {
|
|||
'border-radius': '2px', 'margin-bottom': '16px', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', background: '#fd6216', 'border-radius': '2px',
|
||||
height: '100%', background: '#fd6116', 'border-radius': '2px',
|
||||
width: `${progress()}%`, transition: 'width 0.35s ease',
|
||||
}} />
|
||||
</div>
|
||||
|
|
@ -282,7 +291,7 @@ function SpotlightOverlay(props: {
|
|||
</Show>
|
||||
<button
|
||||
style={{
|
||||
flex: '1', padding: '9px 14px', background: '#fd6216', color: '#ffffff',
|
||||
flex: '1', padding: '9px 14px', background: '#fd6116', color: '#ffffff',
|
||||
border: 'none', 'border-radius': '8px', cursor: 'pointer',
|
||||
'font-weight': '700', 'font-size': '13px', transition: 'opacity 0.15s',
|
||||
}}
|
||||
|
|
@ -338,22 +347,38 @@ export default function DashboardLayout(props: { children: any }) {
|
|||
const tourStep = () => activeTourSteps()[tourStepIndex()];
|
||||
|
||||
const navItems = () => {
|
||||
if (rc()?.role === 'USER') {
|
||||
return [
|
||||
{ label: 'My Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' },
|
||||
{ label: 'Explore', href: '/dashboard/explore', icon: IconCompass, tourId: 'explore' },
|
||||
{ label: 'Settings', href: '/dashboard/settings',icon: IconSettings, tourId: 'settings' },
|
||||
];
|
||||
const role = String(activeRole() || rc()?.role || '').toUpperCase();
|
||||
const moduleSet = new Set(
|
||||
(Array.isArray(rc()?.enabled_modules) ? rc()!.enabled_modules : [])
|
||||
.map((moduleKey) => String(moduleKey || '').toLowerCase()),
|
||||
);
|
||||
|
||||
if (role !== 'USER') {
|
||||
moduleSet.add('dashboard');
|
||||
}
|
||||
const modules = rc()?.enabled_modules ?? [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const modules = Array.from(moduleSet);
|
||||
return modules
|
||||
.map(m => MODULE_NAV_MAP[m])
|
||||
.filter(item => {
|
||||
if (!item || seen.has(item.href)) return false;
|
||||
seen.add(item.href);
|
||||
return true;
|
||||
});
|
||||
.map((m) => {
|
||||
const base = MODULE_NAV_MAP[m];
|
||||
if (!base) return null;
|
||||
|
||||
// Match Next.js role override: customer "leads" is "Post Requirement"
|
||||
if (m === 'leads' && role === 'CUSTOMER') {
|
||||
return { ...base, label: 'Post Requirement', href: '/users/requirements/new', tourId: 'requirements' };
|
||||
}
|
||||
|
||||
// Match Next.js company route overrides.
|
||||
if (role === 'COMPANY') {
|
||||
if (m === 'profile') return { ...base, href: '/companies/profile' };
|
||||
if (m === 'support') return { ...base, href: '/companies/support' };
|
||||
if (m === 'settings') return { ...base, href: '/companies/settings' };
|
||||
}
|
||||
|
||||
return base;
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => Boolean(item))
|
||||
.sort((left, right) => (left.order ?? 999) - (right.order ?? 999));
|
||||
};
|
||||
|
||||
async function handleLogout() {
|
||||
|
|
|
|||
|
|
@ -168,18 +168,20 @@ export function getAuthHeader(): Record<string, string> {
|
|||
// Used during onboarding UI-testing so the dashboard renders correctly.
|
||||
export function setMockRuntimeConfig(roleKey: string): void {
|
||||
const modulesByRole: Record<string, string[]> = {
|
||||
JOB_SEEKER: ['dashboard', 'profile', 'jobs', 'applications', 'notifications', 'settings'],
|
||||
COMPANY: ['dashboard', 'profile', 'jobs', 'applications', 'notifications', 'settings'],
|
||||
CUSTOMER: ['dashboard', 'profile', 'requirements', 'marketplace', 'wallet', 'notifications', 'settings'],
|
||||
PHOTOGRAPHER: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
MAKEUP_ARTIST: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
TUTOR: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
DEVELOPER: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
VIDEO_EDITOR: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
GRAPHIC_DESIGNER: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
SOCIAL_MEDIA_MANAGER: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
FITNESS_TRAINER: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
CATERING_SERVICE: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet', 'notifications', 'settings'],
|
||||
USER: ['dashboard', 'onboarding', 'role_selection', 'settings'],
|
||||
JOB_SEEKER: ['dashboard', 'profile', 'applications', 'notifications', 'support', 'settings'],
|
||||
JOBSEEKER: ['dashboard', 'profile', 'applications', 'notifications', 'support', 'settings'],
|
||||
COMPANY: ['dashboard', 'profile', 'job_postings', 'applications', 'verification', 'notifications', 'support', 'settings'],
|
||||
CUSTOMER: ['dashboard', 'profile', 'leads', 'notifications', 'support', 'settings'],
|
||||
PHOTOGRAPHER: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
MAKEUP_ARTIST: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
TUTOR: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
DEVELOPER: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
VIDEO_EDITOR: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
GRAPHIC_DESIGNER: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
SOCIAL_MEDIA_MANAGER: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
FITNESS_TRAINER: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
CATERING_SERVICES: ['dashboard', 'profile', 'portfolio', 'leads', 'verification', 'notifications', 'support', 'settings'],
|
||||
};
|
||||
|
||||
const mockConfig: RuntimeConfig = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type RuntimeFieldType =
|
|||
| 'tel'
|
||||
| 'date'
|
||||
| 'select'
|
||||
| 'radio'
|
||||
| 'url'
|
||||
| 'file'
|
||||
| 'checkbox';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const kpis = [
|
|||
|
||||
const trendSeries = [62, 70, 81, 75, 88, 102];
|
||||
const revSeries = [42000, 48000, 55000, 51000, 62000, 69000];
|
||||
const expSeries = [21000, 25000, 28000, 26000, 31000, 35000];
|
||||
const maxAmount = 80000;
|
||||
|
||||
const leadRows = [
|
||||
|
|
@ -100,10 +99,9 @@ export default function AdminHomePage() {
|
|||
</div>
|
||||
<div class="relative flex h-full items-end gap-4 px-2">
|
||||
<For each={revSeries}>
|
||||
{(value, i) => (
|
||||
<div class="flex flex-1 items-end justify-center gap-1.5">
|
||||
{(value) => (
|
||||
<div class="flex h-full flex-1 items-end justify-center">
|
||||
<div class="w-2.5 rounded-t bg-[#050026]" style={{ height: `${(value / maxAmount) * 100}%` }} />
|
||||
<div class="w-2.5 rounded-t bg-[#fd6116]" style={{ height: `${(expSeries[i()] / maxAmount) * 100}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -27,18 +27,19 @@ export async function GET({ request }: { request: Request }) {
|
|||
let schemaJson: any = null;
|
||||
const rustBase = (import.meta.env.VITE_RUST_API_URL || 'http://localhost:8080').replace(/\/+$/, '');
|
||||
|
||||
for (const candidate of roleKeyCandidates) {
|
||||
const endpoints = Array.from(
|
||||
// If schemaId is provided, fetch that exact onboarding flow first.
|
||||
if (schemaId) {
|
||||
const schemaEndpoints = Array.from(
|
||||
new Set([
|
||||
gatewayUrl(`/admin/onboarding-config/by-key/${encodeURIComponent(candidate)}`),
|
||||
`${rustBase}/admin/onboarding-config/by-key/${encodeURIComponent(candidate)}`,
|
||||
gatewayUrl(`/admin/onboarding-config/${encodeURIComponent(schemaId)}`),
|
||||
`${rustBase}/admin/onboarding-config/${encodeURIComponent(schemaId)}`,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
let configRes: Response;
|
||||
for (const endpoint of schemaEndpoints) {
|
||||
let schemaRes: Response;
|
||||
try {
|
||||
configRes = await fetch(endpoint, {
|
||||
schemaRes = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: authHeaders,
|
||||
cache: 'no-store',
|
||||
|
|
@ -49,25 +50,68 @@ export async function GET({ request }: { request: Request }) {
|
|||
continue;
|
||||
}
|
||||
|
||||
const config = await configRes.json().catch(() => ({}));
|
||||
if (!configRes.ok) {
|
||||
lastStatus = configRes.status || lastStatus;
|
||||
lastError = String(config?.error || config?.message || lastError);
|
||||
const payload = await schemaRes.json().catch(() => ({}));
|
||||
if (!schemaRes.ok) {
|
||||
lastStatus = schemaRes.status || lastStatus;
|
||||
lastError = String(payload?.error || payload?.message || lastError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedSchema = config?.schema_json || config?.schemaJson || null;
|
||||
if (!parsedSchema) {
|
||||
const parsed = payload?.schema_json || payload?.schemaJson || null;
|
||||
if (!parsed) {
|
||||
lastStatus = 500;
|
||||
lastError = `Runtime onboarding schema is missing for role ${candidate}.`;
|
||||
lastError = `Onboarding schema payload is missing schema_json for schemaId=${schemaId}.`;
|
||||
continue;
|
||||
}
|
||||
|
||||
schemaJson = parsedSchema;
|
||||
schemaJson = parsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaJson) break;
|
||||
if (!schemaJson) {
|
||||
for (const candidate of roleKeyCandidates) {
|
||||
const endpoints = Array.from(
|
||||
new Set([
|
||||
gatewayUrl(`/admin/onboarding-config/by-key/${encodeURIComponent(candidate)}`),
|
||||
`${rustBase}/admin/onboarding-config/by-key/${encodeURIComponent(candidate)}`,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
let configRes: Response;
|
||||
try {
|
||||
configRes = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: authHeaders,
|
||||
cache: 'no-store',
|
||||
});
|
||||
} catch (error: any) {
|
||||
lastStatus = 503;
|
||||
lastError = `Onboarding backend unavailable. Could not reach ${endpoint}. Start gateway/backend services and retry.`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const config = await configRes.json().catch(() => ({}));
|
||||
if (!configRes.ok) {
|
||||
lastStatus = configRes.status || lastStatus;
|
||||
lastError = String(config?.error || config?.message || lastError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedSchema = config?.schema_json || config?.schemaJson || null;
|
||||
if (!parsedSchema) {
|
||||
lastStatus = 500;
|
||||
lastError = `Runtime onboarding schema is missing for role ${candidate}.`;
|
||||
continue;
|
||||
}
|
||||
|
||||
schemaJson = parsedSchema;
|
||||
break;
|
||||
}
|
||||
|
||||
if (schemaJson) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!schemaJson) {
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ export default function LoginPage() {
|
|||
<div class="field">
|
||||
<label class="label">EMAIL</label>
|
||||
<input class="input" value={email()} onInput={(e) => setEmail(e.currentTarget.value)} placeholder="Enter your email" />
|
||||
<p class="validation-note" style={{ color: email().trim() && isValidEmail(email()) ? '#fd6216' : '#6e7591' }}>
|
||||
<p class="validation-note" style={{ color: email().trim() && isValidEmail(email()) ? '#fd6116' : '#6e7591' }}>
|
||||
{email().trim() && isValidEmail(email()) ? '✓ Valid email format' : '• Enter a valid email format'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -362,14 +362,14 @@ export default function RegisterPage() {
|
|||
<div class="field">
|
||||
<label class="label">FULL NAME</label>
|
||||
<input class="input" value={firstName()} onInput={(e) => setFirstName(e.currentTarget.value)} />
|
||||
<p class="validation-note" style={{ color: firstName().trim() && isValidName(firstName()) ? '#fd6216' : '#6e7591' }}>
|
||||
<p class="validation-note" style={{ color: firstName().trim() && isValidName(firstName()) ? '#fd6116' : '#6e7591' }}>
|
||||
{firstName().trim() && isValidName(firstName()) ? '✓ First name looks good' : '• First name is required'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">LAST NAME</label>
|
||||
<input class="input" value={lastName()} onInput={(e) => setLastName(e.currentTarget.value)} />
|
||||
<p class="validation-note" style={{ color: lastName().trim() && isValidName(lastName()) ? '#fd6216' : '#6e7591' }}>
|
||||
<p class="validation-note" style={{ color: lastName().trim() && isValidName(lastName()) ? '#fd6116' : '#6e7591' }}>
|
||||
{lastName().trim() && isValidName(lastName()) ? '✓ Last name looks good' : '• Last name is required'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -388,7 +388,7 @@ export default function RegisterPage() {
|
|||
void checkEmailExists(email());
|
||||
}}
|
||||
/>
|
||||
<p class="validation-note" style={{ color: emailExists() ? '#dc2626' : (email().trim() && isValidEmail(email()) ? '#fd6216' : '#6e7591') }}>
|
||||
<p class="validation-note" style={{ color: emailExists() ? '#dc2626' : (email().trim() && isValidEmail(email()) ? '#fd6116' : '#6e7591') }}>
|
||||
{emailExists()
|
||||
? '• This email is already registered'
|
||||
: (email().trim() && isValidEmail(email()) ? '✓ Valid email format' : '• Enter a valid email format')}
|
||||
|
|
@ -410,11 +410,11 @@ export default function RegisterPage() {
|
|||
</button>
|
||||
</div>
|
||||
<div class="password-strength-grid">
|
||||
<p style={{ color: checks().minLength ? '#fd6216' : '#6e7591' }}>{checks().minLength ? '✓' : '•'} 8+ chars</p>
|
||||
<p style={{ color: checks().uppercase ? '#fd6216' : '#6e7591' }}>{checks().uppercase ? '✓' : '•'} Uppercase</p>
|
||||
<p style={{ color: checks().special ? '#fd6216' : '#6e7591' }}>{checks().special ? '✓' : '•'} Special</p>
|
||||
<p style={{ color: checks().lowercase ? '#fd6216' : '#6e7591' }}>{checks().lowercase ? '✓' : '•'} Lowercase</p>
|
||||
<p style={{ color: checks().number ? '#fd6216' : '#6e7591' }}>{checks().number ? '✓' : '•'} Number</p>
|
||||
<p style={{ color: checks().minLength ? '#fd6116' : '#6e7591' }}>{checks().minLength ? '✓' : '•'} 8+ chars</p>
|
||||
<p style={{ color: checks().uppercase ? '#fd6116' : '#6e7591' }}>{checks().uppercase ? '✓' : '•'} Uppercase</p>
|
||||
<p style={{ color: checks().special ? '#fd6116' : '#6e7591' }}>{checks().special ? '✓' : '•'} Special</p>
|
||||
<p style={{ color: checks().lowercase ? '#fd6116' : '#6e7591' }}>{checks().lowercase ? '✓' : '•'} Lowercase</p>
|
||||
<p style={{ color: checks().number ? '#fd6116' : '#6e7591' }}>{checks().number ? '✓' : '•'} Number</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ export default function RegisterPage() {
|
|||
<PasswordVisibilityIcon visible={showConfirmPassword()} />
|
||||
</button>
|
||||
</div>
|
||||
<p class="validation-note" style={{ color: confirmPassword() && checks().match ? '#fd6216' : '#6e7591' }}>
|
||||
<p class="validation-note" style={{ color: confirmPassword() && checks().match ? '#fd6116' : '#6e7591' }}>
|
||||
{confirmPassword() && checks().match ? '✓ Passwords match' : '• Passwords do not match'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -444,7 +444,7 @@ export default function RegisterPage() {
|
|||
<CaptchaCanvas code={captcha()} class="auth-captcha-canvas" />
|
||||
<input class="input" value={captchaInput()} onInput={(e) => setCaptchaInput(e.currentTarget.value)} placeholder="Enter captcha" />
|
||||
</div>
|
||||
<p class="validation-note" style={{ color: captchaInput() && isValidCaptcha(captchaInput(), captcha()) ? '#fd6216' : '#6e7591' }}>
|
||||
<p class="validation-note" style={{ color: captchaInput() && isValidCaptcha(captchaInput(), captcha()) ? '#fd6116' : '#6e7591' }}>
|
||||
{captchaInput() ? (isValidCaptcha(captchaInput(), captcha()) ? '✓ Captcha matched' : '• Captcha does not match') : '• Enter captcha to continue'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
5
src/routes/companies/applications.tsx
Normal file
5
src/routes/companies/applications.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function CompaniesApplicationsAlias() {
|
||||
return <Navigate href="/dashboard/applications" />;
|
||||
}
|
||||
5
src/routes/companies/feedback.tsx
Normal file
5
src/routes/companies/feedback.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function CompaniesFeedbackAlias() {
|
||||
return <Navigate href="/support" />;
|
||||
}
|
||||
5
src/routes/companies/job-postings.tsx
Normal file
5
src/routes/companies/job-postings.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function CompaniesJobPostingsAlias() {
|
||||
return <Navigate href="/dashboard/jobs" />;
|
||||
}
|
||||
12
src/routes/companies/profile.tsx
Normal file
12
src/routes/companies/profile.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useNavigate } from '@solidjs/router';
|
||||
import { onMount } from 'solid-js';
|
||||
|
||||
export default function CompaniesProfileAliasPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
navigate('/dashboard/profile', { replace: true });
|
||||
});
|
||||
|
||||
return <p>Redirecting to company profile...</p>;
|
||||
}
|
||||
12
src/routes/companies/settings.tsx
Normal file
12
src/routes/companies/settings.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useNavigate } from '@solidjs/router';
|
||||
import { onMount } from 'solid-js';
|
||||
|
||||
export default function CompaniesSettingsAliasPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
navigate('/dashboard/settings', { replace: true });
|
||||
});
|
||||
|
||||
return <p>Redirecting to company settings...</p>;
|
||||
}
|
||||
12
src/routes/companies/support.tsx
Normal file
12
src/routes/companies/support.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useNavigate } from '@solidjs/router';
|
||||
import { onMount } from 'solid-js';
|
||||
|
||||
export default function CompaniesSupportAliasPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
navigate('/support', { replace: true });
|
||||
});
|
||||
|
||||
return <p>Redirecting to company support...</p>;
|
||||
}
|
||||
5
src/routes/companies/tracecoins.tsx
Normal file
5
src/routes/companies/tracecoins.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function CompaniesTracecoinsAlias() {
|
||||
return <Navigate href="/dashboard/wallet" />;
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ export default function MyApplications() {
|
|||
<span style={{ 'font-size': '13px', color: '#64748b' }}>
|
||||
Max 50 active applications allowed.
|
||||
</span>
|
||||
<A href="/dashboard/jobs" style={{ 'font-size': '13px', color: '#fd6216', 'text-decoration': 'none', 'font-weight': '700' }}>
|
||||
<A href="/dashboard/jobs" style={{ 'font-size': '13px', color: '#fd6116', 'text-decoration': 'none', 'font-weight': '700' }}>
|
||||
Browse Jobs →
|
||||
</A>
|
||||
</div>
|
||||
|
|
@ -70,7 +70,7 @@ export default function MyApplications() {
|
|||
<Show when={!apps.loading && apps()?.data?.length === 0}>
|
||||
<div style={{ padding: '48px', 'text-align': 'center', color: '#64748b' }}>
|
||||
<div style={{ 'font-size': '32px', 'margin-bottom': '12px' }}>📭</div>
|
||||
<p>No applications yet. <A href="/dashboard/jobs" style={{ color: '#fd6216' }}>Start browsing jobs →</A></p>
|
||||
<p>No applications yet. <A href="/dashboard/jobs" style={{ color: '#fd6116' }}>Start browsing jobs →</A></p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default function CompanyJobs() {
|
|||
<Show when={!jobs.loading && jobs()?.data?.length === 0}>
|
||||
<div style={{ padding: '48px', 'text-align': 'center', color: '#64748b' }}>
|
||||
<div style={{ 'font-size': '32px', 'margin-bottom': '12px' }}>📋</div>
|
||||
<p>No jobs yet. <A href="/dashboard/jobs/create" style={{ color: '#fd6216' }}>Post your first job →</A></p>
|
||||
<p>No jobs yet. <A href="/dashboard/jobs/create" style={{ color: '#fd6116' }}>Post your first job →</A></p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default function Notifications() {
|
|||
<Show when={!n.read_at}>
|
||||
<span style={{
|
||||
width: '8px', height: '8px', 'border-radius': '50%',
|
||||
background: '#fd6216', 'flex-shrink': 0, 'margin-top': '6px'
|
||||
background: '#fd6116', 'flex-shrink': 0, 'margin-top': '6px'
|
||||
}} />
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export default function Services() {
|
|||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
<td style={{ 'font-weight': '700', color: '#fd6216' }}>
|
||||
<td style={{ 'font-weight': '700', color: '#fd6116' }}>
|
||||
₹{((svc.price ?? 0) / 100).toLocaleString('en-IN')}
|
||||
</td>
|
||||
<td style={{ color: '#64748b', 'font-size': '13px' }}>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function Wallet() {
|
|||
<div style={{ 'font-size': '11px', 'font-weight': '700', 'text-transform': 'uppercase', color: '#94a3b8', 'letter-spacing': '0.08em' }}>
|
||||
Total Balance
|
||||
</div>
|
||||
<div style={{ 'font-size': '40px', 'font-weight': '800', color: '#fd6216', 'margin': '8px 0 4px', 'line-height': '1' }}>
|
||||
<div style={{ 'font-size': '40px', 'font-weight': '800', color: '#fd6116', 'margin': '8px 0 4px', 'line-height': '1' }}>
|
||||
🪙 {balance()?.balance ?? 0}
|
||||
</div>
|
||||
<div style={{ 'font-size': '12px', color: '#94a3b8' }}>Tracecoins</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function WalletLedger() {
|
|||
<Show when={!balance.loading}>
|
||||
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(3, 1fr)', gap: '12px', 'margin-bottom': '28px' }}>
|
||||
{[
|
||||
{ label: 'Balance', value: balance()?.balance ?? 0, color: '#fd6216' },
|
||||
{ label: 'Balance', value: balance()?.balance ?? 0, color: '#fd6116' },
|
||||
{ label: 'Reserved', value: balance()?.reserved ?? 0, color: '#64748b' },
|
||||
{ label: 'Available', value: balance()?.available ?? 0, color: '#15803d' },
|
||||
].map(c => (
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export default function ForgotPassword() {
|
|||
</form>
|
||||
|
||||
<div style={{ 'text-align': 'center', 'margin-top': '24px' }}>
|
||||
<a href="/login" style={{ 'font-size': '13px', color: '#fd6216', 'text-decoration': 'none', 'font-weight': '600' }}>Back to login</a>
|
||||
<a href="/login" style={{ 'font-size': '13px', color: '#fd6116', 'text-decoration': 'none', 'font-weight': '600' }}>Back to login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function validateField(field: RuntimeOnboardingField, value: unknown): string |
|
|||
if (field.required && isEmptyValue(value)) {
|
||||
if (field.type === 'checkbox') return 'Please enable this option.';
|
||||
if (field.type === 'file') return 'Please upload the file.';
|
||||
if (field.type === 'select') return 'Please select an option.';
|
||||
if (field.type === 'select' || field.type === 'radio') return 'Please select an option.';
|
||||
if (field.type === 'date') return 'Please select the date.';
|
||||
return 'Please fill this field.';
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ function validateField(field: RuntimeOnboardingField, value: unknown): string |
|
|||
}
|
||||
}
|
||||
|
||||
if (field.type === 'select') {
|
||||
if (field.type === 'select' || field.type === 'radio') {
|
||||
const options = (field.options || []).map((opt) => opt.value);
|
||||
if (field.multiple) {
|
||||
const values = Array.isArray(value) ? value : [];
|
||||
|
|
@ -164,7 +164,7 @@ function normalizeSchemaPayload(payload: any, schemaId: string, roleKey: string)
|
|||
|
||||
const fields = Array.isArray(step?.fields) ? step.fields.map((field: any) => {
|
||||
const rawType = String(field?.type || 'text').toLowerCase();
|
||||
const type = rawType === 'upload' ? 'file' : rawType;
|
||||
const type = rawType === 'upload' ? 'file' : rawType === 'phone' ? 'tel' : rawType;
|
||||
const options = Array.isArray(field?.options)
|
||||
? field.options.map((option: any) =>
|
||||
typeof option === 'string'
|
||||
|
|
@ -421,7 +421,7 @@ export default function OnboardingPage() {
|
|||
if (message) return { text: message, tone: 'error' as const };
|
||||
|
||||
if (!hasValue) {
|
||||
if (field.type === 'select') return { text: 'Please select an option.', tone: 'hint' as const };
|
||||
if (field.type === 'select' || field.type === 'radio') return { text: 'Please select an option.', tone: 'hint' as const };
|
||||
if (field.type === 'date') return { text: 'Please select the date.', tone: 'hint' as const };
|
||||
if (field.type === 'file') return { text: 'Please upload the file.', tone: 'hint' as const };
|
||||
if (field.type === 'checkbox') return { text: 'Please enable this option.', tone: 'hint' as const };
|
||||
|
|
@ -439,7 +439,7 @@ export default function OnboardingPage() {
|
|||
const files = Array.isArray(value) ? value : [];
|
||||
return { text: `${files.length} file(s) selected`, tone: 'ok' as const };
|
||||
}
|
||||
if (field.type === 'select') {
|
||||
if (field.type === 'select' || field.type === 'radio') {
|
||||
if (field.multiple) {
|
||||
const selected = Array.isArray(value) ? value : [];
|
||||
return { text: `${selected.length} option(s) selected`, tone: 'ok' as const };
|
||||
|
|
@ -664,6 +664,42 @@ export default function OnboardingPage() {
|
|||
);
|
||||
}
|
||||
|
||||
if (field.type === 'radio') {
|
||||
const selected = createMemo(() => String(value() || ''));
|
||||
const disabled = fieldReadOnly || lockedIdentityField;
|
||||
return (
|
||||
<div class={`multi-select-grid${disabled ? ' is-disabled' : ''}`} role="radiogroup" aria-label={field.label}>
|
||||
<For each={field.options || []}>
|
||||
{(option) => {
|
||||
const active = () => selected() === option.value;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`multi-select-option${active() ? ' is-selected' : ''}`}
|
||||
disabled={disabled}
|
||||
role="radio"
|
||||
aria-checked={active()}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
markFieldTouched(field.id);
|
||||
handleFieldInput(field, option.value);
|
||||
validateSingleField(field, option.value);
|
||||
}}
|
||||
>
|
||||
<span class="multi-select-option-text">{option.label}</span>
|
||||
<span class={`multi-select-tick${active() ? ' is-visible' : ''}`} aria-hidden="true">
|
||||
<svg viewBox="0 0 20 20" role="img" focusable="false">
|
||||
<path d="M4 10.5l4 4 8-9" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
return (
|
||||
<label class="inline">
|
||||
|
|
@ -794,7 +830,7 @@ export default function OnboardingPage() {
|
|||
<div style={{ display: "flex", "justify-content": "center", "margin-bottom": "1.25rem" }}>
|
||||
<div style={{
|
||||
width: "72px", height: "72px", "border-radius": "50%",
|
||||
background: "#fd6216", display: "flex", "align-items": "center",
|
||||
background: "#fd6116", display: "flex", "align-items": "center",
|
||||
"justify-content": "center",
|
||||
}}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style={{ width: "38px", height: "38px" }}>
|
||||
|
|
@ -839,7 +875,7 @@ export default function OnboardingPage() {
|
|||
style={{
|
||||
color:
|
||||
note().tone === 'ok'
|
||||
? '#fd6216'
|
||||
? '#fd6116'
|
||||
: '#6e7591',
|
||||
}}
|
||||
>
|
||||
|
|
@ -896,11 +932,11 @@ export default function OnboardingPage() {
|
|||
/>
|
||||
<span style={{ "font-size": "0.875rem", color: "#6e7591", "line-height": "1.5" }}>
|
||||
I agree to the{' '}
|
||||
<a href="/terms" target="_blank" style={{ color: "#fd6216", "text-decoration": "underline" }}>
|
||||
<a href="/terms" target="_blank" style={{ color: "#fd6116", "text-decoration": "underline" }}>
|
||||
Terms & Conditions
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="/privacy" target="_blank" style={{ color: "#fd6216", "text-decoration": "underline" }}>
|
||||
<a href="/privacy" target="_blank" style={{ color: "#fd6116", "text-decoration": "underline" }}>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
|||
5
src/routes/users/dashboard.tsx
Normal file
5
src/routes/users/dashboard.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersDashboardAlias() {
|
||||
return <Navigate href="/dashboard" />;
|
||||
}
|
||||
5
src/routes/users/leads.tsx
Normal file
5
src/routes/users/leads.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersLeadsAlias() {
|
||||
return <Navigate href="/dashboard/requests" />;
|
||||
}
|
||||
5
src/routes/users/notifications.tsx
Normal file
5
src/routes/users/notifications.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersNotificationsAlias() {
|
||||
return <Navigate href="/dashboard/notifications" />;
|
||||
}
|
||||
5
src/routes/users/onboarding/index.tsx
Normal file
5
src/routes/users/onboarding/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersOnboardingAlias() {
|
||||
return <Navigate href="/onboarding" />;
|
||||
}
|
||||
5
src/routes/users/onboarding/role-selection.tsx
Normal file
5
src/routes/users/onboarding/role-selection.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersRoleSelectionAlias() {
|
||||
return <Navigate href="/users/choose-role" />;
|
||||
}
|
||||
5
src/routes/users/professional/portfolio.tsx
Normal file
5
src/routes/users/professional/portfolio.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersProfessionalPortfolioAlias() {
|
||||
return <Navigate href="/dashboard/portfolio" />;
|
||||
}
|
||||
5
src/routes/users/profile.tsx
Normal file
5
src/routes/users/profile.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersProfileAlias() {
|
||||
return <Navigate href="/dashboard/profile" />;
|
||||
}
|
||||
12
src/routes/users/requirements/new.tsx
Normal file
12
src/routes/users/requirements/new.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useNavigate } from '@solidjs/router';
|
||||
import { onMount } from 'solid-js';
|
||||
|
||||
export default function UserRequirementNewAliasPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
navigate('/dashboard/requirements', { replace: true });
|
||||
});
|
||||
|
||||
return <p>Redirecting to requirement posting...</p>;
|
||||
}
|
||||
5
src/routes/users/settings.tsx
Normal file
5
src/routes/users/settings.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersSettingsAlias() {
|
||||
return <Navigate href="/dashboard/settings" />;
|
||||
}
|
||||
5
src/routes/users/support.tsx
Normal file
5
src/routes/users/support.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersSupportAlias() {
|
||||
return <Navigate href="/support" />;
|
||||
}
|
||||
5
src/routes/users/verification-status.tsx
Normal file
5
src/routes/users/verification-status.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function UsersVerificationStatusAlias() {
|
||||
return <Navigate href="/pending" />;
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ export default function VerifyEmail() {
|
|||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
style={{ color: '#fd6216', border: 'none', background: 'none', padding: 0, cursor: 'pointer', 'font-weight': '600' }}
|
||||
style={{ color: '#fd6116', border: 'none', background: 'none', padding: 0, cursor: 'pointer', 'font-weight': '600' }}
|
||||
disabled={loading()}
|
||||
>
|
||||
Resend
|
||||
|
|
|
|||
|
|
@ -1,82 +1,156 @@
|
|||
import { createSignal, createResource, Show, For } from 'solid-js';
|
||||
import { createResource, Show, For } from 'solid-js';
|
||||
import { A, useSearchParams } from '@solidjs/router';
|
||||
import ProfileWidget from '~/components/dashboard/ProfileWidget';
|
||||
|
||||
async function fetchRuntimeConfig(roleKey: string) {
|
||||
const RUST_API_URL = import.meta.env.VITE_RUST_API_URL || 'http://localhost:8080';
|
||||
|
||||
|
||||
// Try to lookup Role ID first
|
||||
const roleRes = await fetch(`${RUST_API_URL}/api/admin/roles/${roleKey}`);
|
||||
if (!roleRes.ok) throw new Error("Role not found");
|
||||
if (!roleRes.ok) throw new Error('Role not found');
|
||||
const role = await roleRes.json();
|
||||
|
||||
// Then fetch Dashboard config for that role
|
||||
const configRes = await fetch(`${RUST_API_URL}/api/admin/dashboard-config/${role.id}?audience=EXTERNAL`);
|
||||
if (!configRes.ok) throw new Error("Dashboard config not found");
|
||||
if (!configRes.ok) throw new Error('Dashboard config not found');
|
||||
const dashboardConfig = await configRes.json();
|
||||
|
||||
|
||||
return dashboardConfig.config_json; // Returns `{ sidebar: [...], widgets: [...] }`
|
||||
}
|
||||
|
||||
function IconSearch() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="M20 20l-3.5-3.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconBell() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkspaceLayout(props: { children?: any }) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const rawRoleKey = searchParams.roleKey;
|
||||
const roleKey = () => (Array.isArray(rawRoleKey) ? rawRoleKey[0] : rawRoleKey) || 'PHOTOGRAPHER';
|
||||
|
||||
|
||||
const [config] = createResource<any, string>(roleKey, fetchRuntimeConfig);
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-slate-50 flex">
|
||||
{/* Sidebar rendered magically from Rust Config */}
|
||||
<aside class="w-64 bg-white border-r border-slate-200 flex flex-col">
|
||||
<div class="p-6 border-b border-slate-200">
|
||||
<h1 class="font-bold text-xl text-orange-600">Nxtgauge Workspace</h1>
|
||||
<p class="text-xs text-slate-500 mt-1 capitalize">{roleKey().toLowerCase()}</p>
|
||||
</div>
|
||||
<nav class="flex-1 p-4 space-y-1">
|
||||
<Show when={config.loading}>
|
||||
<p class="text-sm text-slate-500">Loading modules...</p>
|
||||
</Show>
|
||||
<Show when={config.error}>
|
||||
<p class="text-sm text-red-500">Failed to load shell config.</p>
|
||||
</Show>
|
||||
<Show when={config()}>
|
||||
<For each={config().sidebar}>
|
||||
{(item: any) => (
|
||||
<A
|
||||
href={item.route}
|
||||
class="block px-4 py-2 rounded-lg text-slate-700 hover:bg-orange-50 hover:text-orange-600 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="min-h-screen bg-slate-100 text-slate-900">
|
||||
<div class="flex min-h-screen">
|
||||
<aside class="hidden w-72 shrink-0 border-r border-slate-200 bg-white/95 shadow-sm lg:flex lg:flex-col">
|
||||
<div class="border-b border-slate-200 px-6 py-6">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-[#fd6116]">Traceworks Jobs</p>
|
||||
<h1 class="mt-2 text-2xl font-extrabold text-[#100b2f]">Admin Panel</h1>
|
||||
<p class="mt-2 text-xs font-medium capitalize text-slate-500">{roleKey().replaceAll('_', ' ').toLowerCase()}</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main class="flex-1 p-8">
|
||||
<Show when={config() && config().widgets}>
|
||||
<div class="grid grid-cols-2 gap-6 mb-8">
|
||||
<For each={config().widgets}>
|
||||
{(widget: any) => (
|
||||
<Show when={widget.enabled}>
|
||||
<div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
||||
<h3 class="font-medium text-slate-800">{widget.title}</h3>
|
||||
<p class="text-xs text-slate-400 mt-2">Dynamic Widget Module</p>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
<div class="px-6 pt-5">
|
||||
<span class="inline-flex rounded-full border border-orange-200 bg-orange-50 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.12em] text-[#fd6116]">
|
||||
{roleKey().replaceAll('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 space-y-1 overflow-y-auto px-4 py-6">
|
||||
<Show when={config.loading}>
|
||||
<p class="px-3 text-sm text-slate-500">Loading modules...</p>
|
||||
</Show>
|
||||
<Show when={config.error}>
|
||||
<p class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">Failed to load shell config.</p>
|
||||
</Show>
|
||||
<Show when={config()}>
|
||||
<For each={config().sidebar}>
|
||||
{(item: any) => (
|
||||
<A
|
||||
href={item.route}
|
||||
class="group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 hover:text-[#100b2f]"
|
||||
activeClass="bg-[#fd6116]/10 text-[#fd6116] shadow-sm ring-1 ring-[#fd6116]/20"
|
||||
>
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-current/35 transition group-[.bg-[#fd6116]/10]:bg-current" />
|
||||
<span class="flex-1">{item.label}</span>
|
||||
<span class="text-base leading-none text-slate-400 transition group-hover:text-[#fd6116]">›</span>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Profile Settings specifically for this Role */}
|
||||
<ProfileWidget roleKey={roleKey()} />
|
||||
|
||||
{props.children}
|
||||
</main>
|
||||
</Show>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="sticky top-0 z-20 border-b border-slate-200 bg-white/90 px-4 py-4 backdrop-blur sm:px-6 lg:px-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-2xl font-extrabold tracking-tight text-[#100b2f]">Admin Panel</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Manage modules and role configuration</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<label class="flex h-10 w-44 items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 text-slate-500 focus-within:border-[#fd6116] focus-within:ring-2 focus-within:ring-[#fd6116]/20 sm:w-64">
|
||||
<IconSearch />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
class="w-full border-0 bg-transparent text-sm text-slate-700 outline-none placeholder:text-slate-400"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Notifications"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 transition hover:border-[#fd6116]/30 hover:text-[#fd6116]"
|
||||
>
|
||||
<IconBell />
|
||||
</button>
|
||||
<span class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[#fd6116] text-sm font-bold text-white shadow-sm">
|
||||
AD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mx-auto w-full max-w-7xl flex-1 space-y-6 px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-[#100b2f]">Role Modules</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Dynamic dashboard widgets configured for this role.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={config() && config().widgets}>
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<For each={config().widgets}>
|
||||
{(widget: any) => (
|
||||
<Show when={widget.enabled}>
|
||||
<article class="rounded-xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<h4 class="text-base font-bold text-[#100b2f]">{widget.title}</h4>
|
||||
<span class="inline-flex rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-[11px] font-semibold text-emerald-700">
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-500">Dynamic widget module enabled for this role configuration.</p>
|
||||
</article>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<ProfileWidget roleKey={roleKey()} />
|
||||
|
||||
{props.children}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import { defineConfig } from '@solidjs/start/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({});
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue