feat: add users and companies dashboard route surfaces

This commit is contained in:
Ashwin Kumar 2026-03-25 22:13:11 +01:00
parent 0996f12227
commit f16c7eb4dd
40 changed files with 937 additions and 278 deletions

349
package-lock.json generated
View file

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

View file

@ -16,6 +16,8 @@
"vinxi": "^0.5.7"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"tailwindcss": "^4.2.2",
"vitest": "^3.2.4"
},
"engines": {

View file

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

View file

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

View file

@ -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() {

View file

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

View file

@ -6,6 +6,7 @@ export type RuntimeFieldType =
| 'tel'
| 'date'
| 'select'
| 'radio'
| 'url'
| 'file'
| 'checkbox';

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function CompaniesApplicationsAlias() {
return <Navigate href="/dashboard/applications" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function CompaniesFeedbackAlias() {
return <Navigate href="/support" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function CompaniesJobPostingsAlias() {
return <Navigate href="/dashboard/jobs" />;
}

View 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>;
}

View 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>;
}

View 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>;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function CompaniesTracecoinsAlias() {
return <Navigate href="/dashboard/wallet" />;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &amp; 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>

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersDashboardAlias() {
return <Navigate href="/dashboard" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersLeadsAlias() {
return <Navigate href="/dashboard/requests" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersNotificationsAlias() {
return <Navigate href="/dashboard/notifications" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersOnboardingAlias() {
return <Navigate href="/onboarding" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersRoleSelectionAlias() {
return <Navigate href="/users/choose-role" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersProfessionalPortfolioAlias() {
return <Navigate href="/dashboard/portfolio" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersProfileAlias() {
return <Navigate href="/dashboard/profile" />;
}

View 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>;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersSettingsAlias() {
return <Navigate href="/dashboard/settings" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersSupportAlias() {
return <Navigate href="/support" />;
}

View file

@ -0,0 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function UsersVerificationStatusAlias() {
return <Navigate href="/pending" />;
}

View file

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

View file

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

View file

@ -1,3 +1,8 @@
import { defineConfig } from '@solidjs/start/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({});
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});