# Conflicts:
#	src/app/(dashboard)/chats/page.tsx
This commit is contained in:
2026-06-26 15:20:42 +02:00
31 changed files with 548 additions and 1142 deletions
+9 -3
View File
@@ -47,9 +47,6 @@ error-log
# vercel # vercel
.vercel .vercel
# rust
rust-ai/target/
# python # python
browser-use-service/venv/ browser-use-service/venv/
browser-use-service/__pycache__/ browser-use-service/__pycache__/
@@ -58,3 +55,12 @@ browser-use-service/.env
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.git
.git_bak
node_modules
target
# rust build artifacts
rust-ai/target/
# browser-use-service generated files
browser-use-service/*.txt
-6
View File
@@ -34,11 +34,5 @@ BEGIN;
\echo '=== Running 009_settings.sql (Company Settings + User Preferences) ===' \echo '=== Running 009_settings.sql (Company Settings + User Preferences) ==='
\i 009_settings.sql \i 009_settings.sql
\echo '=== Running 013_security_upgrade.sql (Security Architecture: RLS, Encryption, Master Keys, Backups) ==='
\i 013_security_upgrade.sql
\echo '=== Running 014_bug_reports.sql (Bug Reporting System) ==='
\i 014_bug_reports.sql
\echo '=== Migration Complete ===' \echo '=== Migration Complete ==='
COMMIT; COMMIT;
-9
View File
@@ -1,12 +1,4 @@
import type { NextConfig } from "next" import type { NextConfig } from "next"
import crypto from "crypto"
// In development, generate a random JWT secret on every server start.
// This invalidates all previously issued JWT tokens, ensuring the user
// must re-authenticate after each dev server restart.
if (process.env.NODE_ENV === "development") {
process.env.JWT_SECRET = crypto.randomBytes(32).toString("hex")
}
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
eslint: { eslint: {
@@ -20,7 +12,6 @@ const nextConfig: NextConfig = {
}, },
], ],
}, },
} }
export default nextConfig export default nextConfig
+6 -363
View File
@@ -25,8 +25,6 @@
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@supabase/ssr": "^0.12.0",
"@supabase/supabase-js": "^2.108.2",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
@@ -42,8 +40,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
@@ -1941,108 +1937,6 @@
"integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==",
"dev": true "dev": true
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.108.2.tgz",
"integrity": "sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.108.2.tgz",
"integrity": "sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/phoenix": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.4.tgz",
"integrity": "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==",
"license": "MIT"
},
"node_modules/@supabase/postgrest-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.108.2.tgz",
"integrity": "sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.108.2.tgz",
"integrity": "sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==",
"license": "MIT",
"dependencies": {
"@supabase/phoenix": "^0.4.2",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/ssr": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.12.0.tgz",
"integrity": "sha512-d9XV5XzJvzzZbeAIM7fWTCUYxQJZ2Ru6ny3dJHmHGp/LIrJ+o9FpD7N9Rf/UhhWEvHXSoDe8SI32Z2ouOdMjBg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.2"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.108.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.108.2.tgz",
"integrity": "sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.108.2",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.108.2.tgz",
"integrity": "sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.108.2",
"@supabase/functions-js": "2.108.2",
"@supabase/postgrest-js": "2.108.2",
"@supabase/realtime-js": "2.108.2",
"@supabase/storage-js": "2.108.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/counter": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -2099,15 +1993,6 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -2184,6 +2069,7 @@
"version": "20.19.43", "version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -2224,15 +2110,6 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.61.0", "version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
@@ -2791,19 +2668,6 @@
"win32" "win32"
] ]
}, },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.17.0", "version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
@@ -3161,15 +3025,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.37", "version": "2.10.37",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
@@ -3549,36 +3404,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3781,6 +3606,7 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -3918,58 +3744,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
}, },
"node_modules/engine.io": {
"version": "6.6.9",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
"integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"@types/ws": "^8.5.12",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.21.0"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.21.0",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.24.2", "version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -5028,15 +4802,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5756,27 +5521,6 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -5814,7 +5558,8 @@
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
}, },
"node_modules/mz": { "node_modules/mz": {
"version": "2.7.0", "version": "2.7.0",
@@ -5865,15 +5610,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "15.0.4", "version": "15.0.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.0.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.0.4.tgz",
@@ -7210,62 +6946,6 @@
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
} }
}, },
"node_modules/socket.io": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
"integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.21.0"
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/sonner": { "node_modules/sonner": {
"version": "1.7.4", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
@@ -7899,7 +7579,8 @@
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
}, },
"node_modules/unrs-resolver": { "node_modules/unrs-resolver": {
"version": "1.12.2", "version": "1.12.2",
@@ -8023,15 +7704,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vaul": { "node_modules/vaul": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
@@ -8205,35 +7877,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+1 -1
View File
@@ -446,7 +446,7 @@ async fn main() {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let ollama_url = std::env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| "http://localhost:11434".to_string()); let ollama_url = std::env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
let model = std::env::var("AI_MODEL").unwrap_or_else(|_| "llama3.2:3b".to_string()); let model = std::env::var("AI_MODEL").unwrap_or_else(|_| "dolphin-phi".to_string());
let host = std::env::var("AI_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let host = std::env::var("AI_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port: u16 = std::env::var("AI_PORT").unwrap_or_else(|_| "3001".to_string()).parse().expect("AI_PORT must be a number"); let port: u16 = std::env::var("AI_PORT").unwrap_or_else(|_| "3001".to_string()).parse().expect("AI_PORT must be a number");
+5 -5
View File
@@ -15,7 +15,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { import {
Search, Send, Phone, MoreHorizontal, Paperclip, Search, Send, Phone, Video, MoreHorizontal, Paperclip,
Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck,
CornerDownRight, Forward, Pencil, Download, Undo2, CornerDownRight, Forward, Pencil, Download, Undo2,
} from "lucide-react" } from "lucide-react"
@@ -156,7 +156,6 @@ export default function ChatsPage() {
const [searchResults, setSearchResults] = useState<any[]>([]) const [searchResults, setSearchResults] = useState<any[]>([])
const [searchingUsers, setSearchingUsers] = useState(false) const [searchingUsers, setSearchingUsers] = useState(false)
const [unreadMap, setUnreadMap] = useState<Map<string, number>>(new Map()) const [unreadMap, setUnreadMap] = useState<Map<string, number>>(new Map())
const [isCallModalOpen, setIsCallModalOpen] = useState(false)
const [previewAvatarUrl, setPreviewAvatarUrl] = useState<string | null>(null) const [previewAvatarUrl, setPreviewAvatarUrl] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -841,9 +840,12 @@ const formatPreviewContent = (content: string) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsCallModalOpen(true)}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => toast.info("Voice calling coming soon")}>
<Phone className="h-4 w-4" /> <Phone className="h-4 w-4" />
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => toast.info("Video calling coming soon")}>
<Video className="h-4 w-4" />
</Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8"> <Button variant="ghost" size="icon" className="h-8 w-8">
@@ -1226,8 +1228,6 @@ const formatPreviewContent = (content: string) => {
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<VoiceCallModal open={isCallModalOpen} onClose={() => setIsCallModalOpen(false)} />
</div> </div>
) )
} }
+17 -7
View File
@@ -24,6 +24,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { DashboardStats } from "@/types" import { DashboardStats } from "@/types"
import { CrossedLightsabers } from "@/components/dashboard/crossed-lightsabers"
import { StarField } from "@/components/dashboard/star-field"
export default function DashboardPage() { export default function DashboardPage() {
const [period, setPeriod] = useState("6months") const [period, setPeriod] = useState("6months")
@@ -63,14 +65,20 @@ export default function DashboardPage() {
return ( return (
<div className="space-y-6 relative"> <div className="space-y-6 relative">
{/* Daily Bugle watermark */} <StarField />
<div className="absolute top-12 right-4 pointer-events-none select-none z-0 opacity-[0.04] dark:opacity-[0.06]"> <div className="relative z-[1] space-y-6">
<span className="text-[96px] font-['Bangers',cursive] leading-none text-[#e62020] dark:text-[#FF6666] tracking-wider"> <div>
DAILY BUGLE <PageHeader
title={<span style={{fontFamily:"'Audiowide',sans-serif"}}>Dashboard</span>}
description="Overview of your sales pipeline"
>
<span
className="pointer-events-none select-none text-5xl text-gray-600 dark:text-[#b0f0c0] tracking-wider font-bold text-glow whitespace-nowrap"
style={{ fontFamily: "'Cormorant Garamond', serif" }}
>
Trust the Force, Find your Path
</span> </span>
</div> <CrossedLightsabers />
<PageHeader title={<span style={{fontFamily:"'Bangers',cursive"}}>Dashboard</span>} description="Overview of your sales pipeline">
<Select value={period} onValueChange={setPeriod}> <Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="h-9 w-[160px]"> <SelectTrigger className="h-9 w-[160px]">
<ListFilter className="mr-2 h-4 w-4" /> <ListFilter className="mr-2 h-4 w-4" />
@@ -84,6 +92,7 @@ export default function DashboardPage() {
</SelectContent> </SelectContent>
</Select> </Select>
</PageHeader> </PageHeader>
</div>
<p className="text-xs font-semibold tracking-widest uppercase text-[#888888] dark:text-[#666666]">Pipeline Overview</p> <p className="text-xs font-semibold tracking-widest uppercase text-[#888888] dark:text-[#666666]">Pipeline Overview</p>
@@ -105,5 +114,6 @@ export default function DashboardPage() {
<RecentLeadsTable leads={stats?.recentLeads ?? []} /> <RecentLeadsTable leads={stats?.recentLeads ?? []} />
</div> </div>
</div>
) )
} }
+12 -11
View File
@@ -1,12 +1,14 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { chatWithAI } from "@/lib/ai"
import { getSessionUser } from "@/lib/auth" import { getSessionUser } from "@/lib/auth"
import { chatWithAI } from "@/lib/ai"
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const user = await getSessionUser() const user = await getSessionUser()
if (!user) { if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
if (!["sales", "admin", "super_admin"].includes(user.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
} }
const { message } = await request.json() const { message } = await request.json()
@@ -14,16 +16,15 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Message is required" }, { status: 400 }) return NextResponse.json({ error: "Message is required" }, { status: 400 })
} }
const sessionCookie = request.cookies.get("session") // Forward the JWT from the session cookie to the Rust backend
const jwtToken = sessionCookie?.value const sessionCookie = request.cookies.get("session")?.value
if (!jwtToken) { if (!sessionCookie) return NextResponse.json({ error: "No session" }, { status: 401 })
return NextResponse.json({ error: "No session token" }, { status: 401 })
} const response = await chatWithAI(message, sessionCookie)
const response = await chatWithAI(message, jwtToken)
return NextResponse.json({ response }) return NextResponse.json({ response })
} catch (error: any) { } catch (error) {
console.error("AI chat error:", error) console.error("AI chat error:", error)
return NextResponse.json({ error: error.message || "AI service error" }, { status: 500 }) return NextResponse.json({ error: "AI service unavailable" }, { status: 503 })
} }
} }
-2
View File
@@ -9,7 +9,6 @@ import {
resetFailedAttempts, resetFailedAttempts,
isAccountLocked, isAccountLocked,
createSession, createSession,
setSessionContext,
} from "@/lib/auth" } from "@/lib/auth"
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -107,7 +106,6 @@ export async function POST(request: NextRequest) {
) )
await createSession(dbUser.id, dbUser.role_name) await createSession(dbUser.id, dbUser.role_name)
await setSessionContext(dbUser.id, ipAddress)
const user = mapDbUserToSessionUser(dbUser) const user = mapDbUserToSessionUser(dbUser)
+1 -4
View File
@@ -16,7 +16,6 @@ export async function GET() {
u.id AS other_user_id, u.id AS other_user_id,
u.first_name || ' ' || u.last_name AS other_user_name, u.first_name || ' ' || u.last_name AS other_user_name,
u.email AS other_user_email, u.email AS other_user_email,
u.phone AS other_user_phone,
u.avatar_url AS other_user_avatar_url, u.avatar_url AS other_user_avatar_url,
(SELECT content FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message, (SELECT content FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message,
(SELECT created_at FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message_time, (SELECT created_at FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message_time,
@@ -40,7 +39,6 @@ export async function GET() {
id: row.other_user_id, id: row.other_user_id,
name: row.other_user_name, name: row.other_user_name,
email: row.other_user_email, email: row.other_user_email,
phone: row.other_user_phone || "",
avatar: avatarSvgUrl(row.other_user_name), avatar: avatarSvgUrl(row.other_user_name),
}, },
lastMessage: row.last_message || "", lastMessage: row.last_message || "",
@@ -92,7 +90,7 @@ export async function POST(request: NextRequest) {
) )
const otherUser = await query( const otherUser = await query(
`SELECT id, first_name || ' ' || last_name AS name, email, phone, avatar_url `SELECT id, first_name || ' ' || last_name AS name, email, avatar_url
FROM users WHERE id = $1`, FROM users WHERE id = $1`,
[userId], [userId],
) )
@@ -107,7 +105,6 @@ export async function POST(request: NextRequest) {
id: other.id, id: other.id,
name: other.name, name: other.name,
email: other.email, email: other.email,
phone: other.phone || "",
avatar: avatarSvgUrl(other.name), avatar: avatarSvgUrl(other.name),
}, },
lastMessage: "", lastMessage: "",
+4 -7
View File
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { query } from "@/lib/db" import { query } from "@/lib/db"
import { hashPassword, getSessionUser, encryptPassword, setSessionContext } from "@/lib/auth" import { hashPassword, getSessionUser } from "@/lib/auth"
import { avatarSvgUrl } from "@/lib/avatar" import { avatarSvgUrl } from "@/lib/avatar"
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@@ -63,20 +63,17 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid role" }, { status: 400 }) return NextResponse.json({ error: "Invalid role" }, { status: 400 })
} }
await setSessionContext(sessionUser.id)
const nameParts = name.trim().split(/\s+/) const nameParts = name.trim().split(/\s+/)
const firstName = nameParts[0] const firstName = nameParts[0]
const lastName = nameParts.slice(1).join(" ") || firstName const lastName = nameParts.slice(1).join(" ") || firstName
const username = email.split("@")[0] const username = email.split("@")[0]
const passwordHash = await hashPassword(password) const passwordHash = await hashPassword(password)
const passwordEncrypted = await encryptPassword(password)
const result = await query( const result = await query(
`INSERT INTO users (username, email, password_hash, password_encrypted, first_name, last_name, is_active, created_by) `INSERT INTO users (username, email, password_hash, first_name, last_name, is_active, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id`, RETURNING id`,
[username.toLowerCase(), email.toLowerCase(), passwordHash, passwordEncrypted, firstName, lastName, active ?? true, sessionUser.id] [username.toLowerCase(), email.toLowerCase(), passwordHash, firstName, lastName, active ?? true, sessionUser.id]
) )
const roleId = ( const roleId = (
+144 -285
View File
@@ -1,51 +1,115 @@
@import url('https://fonts.googleapis.com/css2?family=Bangers&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Audiowide&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
.text-glow {
text-shadow: 0 0 15px #39ff14, 0 0 30px #39ff14, 0 0 50px #39ff14, 0 0 80px rgba(57, 255, 20, 0.6);
}
.dark .text-glow {
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14, 0 0 35px #39ff14;
}
@keyframes loading { @keyframes loading {
0% { transform: translateX(-100%); } 0% { transform: translateX(-100%); }
100% { transform: translateX(400%); } 100% { transform: translateX(400%); }
} }
@keyframes pulse-glow-red {
0%, 100% { opacity: 0.12; }
50% { opacity: 0.5; }
}
@keyframes pulse-glow-blue {
0%, 100% { opacity: 0.12; }
50% { opacity: 0.5; }
}
@keyframes pulse-glow-intersection {
0%, 100% { opacity: 0.15; }
50% { opacity: 0.65; }
}
.saber-outer { stroke-width: 30; }
.dark .saber-outer { stroke-width: 40; }
.saber-mid { stroke-width: 18; }
.dark .saber-mid { stroke-width: 24; }
.saber-core { stroke-width: 6; }
.dark .saber-core { stroke-width: 8; }
.saber-blue-outer { stroke-width: 24; }
.dark .saber-blue-outer { stroke-width: 40; }
.saber-blue-mid { stroke-width: 13; }
.dark .saber-blue-mid { stroke-width: 24; }
.saber-blue-core { stroke-width: 4; }
.dark .saber-blue-core { stroke-width: 8; }
.animate-pulse-red {
animation: pulse-glow-red 2.5s ease-in-out infinite;
}
.animate-pulse-blue {
animation: pulse-glow-blue 2.8s ease-in-out infinite;
}
.animate-pulse-intersection {
animation: pulse-glow-intersection 2.5s ease-in-out infinite;
}
@keyframes star-twinkle {
0%, 100% { opacity: 0.1; }
50% { opacity: 0.6; }
}
@keyframes star-drift {
0% { transform: translate(0, 0); }
50% { transform: translate(3px, -2px); }
100% { transform: translate(0, 0); }
}
.star-twinkle {
animation: star-twinkle 4s ease-in-out infinite, star-drift 20s ease-in-out infinite;
}
:root { :root {
--background: 210 40% 98%; --background: 220 14% 84%;
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
--card: 0 0% 100%; --card: 220 10% 91%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%; --popover: 220 14% 96%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
--primary: 0 100% 40%; --primary: 120 100% 50%;
--primary-foreground: 210 40% 98%; --primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --secondary: 220 14% 88%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%; --muted: 220 14% 88%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%; --accent: 220 14% 88%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 220 14% 85%;
--input: 214.3 31.8% 91.4%; --input: 220 14% 85%;
--ring: 221.2 83.2% 53.3%; --ring: 120 100% 50%;
--sidebar: 222.2 84% 4.9%; --sidebar: 220 14% 74%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 217.2 91.2% 59.8%; --sidebar-primary: 145 65% 50%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%; --sidebar-accent: 210 40% 96%;
--sidebar-accent-foreground: 210 40% 98%; --sidebar-accent-foreground: 222.2 84% 4.9%;
--sidebar-border: 217.2 32.6% 17.5%; --sidebar-border: 220 9% 46%;
--sidebar-ring: 224.3 76.3% 48%; --sidebar-ring: 120 100% 50%;
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 0 0% 10%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--card: 222.2 84% 4.9%; --card: 0 0% 9%;
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%; --popover: 222.2 84% 11%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 0 100% 53%; --primary: 0 100% 53%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
@@ -60,9 +124,9 @@
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
--ring: 0 100% 53%; --ring: 0 100% 53%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 0 100% 53%; --sidebar-primary: 145 65% 50%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%; --sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%; --sidebar-accent-foreground: 210 40% 98%;
@@ -88,7 +152,7 @@
--primary: 187 75% 50%; --primary: 187 75% 50%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 187 75% 50%; --ring: 187 75% 50%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 187 75% 55%; --sidebar-primary: 187 75% 55%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -116,7 +180,7 @@
--primary: 142 76% 44%; --primary: 142 76% 44%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 142 76% 44%; --ring: 142 76% 44%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 142 76% 50%; --sidebar-primary: 142 76% 50%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -144,7 +208,7 @@
--primary: 24 95% 58%; --primary: 24 95% 58%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 24 95% 58%; --ring: 24 95% 58%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 24 95% 65%; --sidebar-primary: 24 95% 65%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -172,7 +236,7 @@
--primary: 230 75% 62%; --primary: 230 75% 62%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 230 75% 62%; --ring: 230 75% 62%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 230 75% 65%; --sidebar-primary: 230 75% 65%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -200,7 +264,7 @@
--primary: 346 77% 58%; --primary: 346 77% 58%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 346 77% 58%; --ring: 346 77% 58%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 346 77% 60%; --sidebar-primary: 346 77% 60%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -228,7 +292,7 @@
--primary: 38 92% 56%; --primary: 38 92% 56%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 38 92% 56%; --ring: 38 92% 56%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 38 92% 60%; --sidebar-primary: 38 92% 60%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -256,7 +320,7 @@
--primary: 262 83% 65%; --primary: 262 83% 65%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 262 83% 65%; --ring: 262 83% 65%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 262 83% 68%; --sidebar-primary: 262 83% 68%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -284,7 +348,7 @@
--primary: 215 20% 60%; --primary: 215 20% 60%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 215 20% 60%; --ring: 215 20% 60%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 215 20% 65%; --sidebar-primary: 215 20% 65%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -312,259 +376,7 @@
--primary: 351 85% 52%; --primary: 351 85% 52%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222.2 47.4% 11.2%;
--ring: 351 85% 52%; --ring: 351 85% 52%;
--sidebar: 222.2 84% 4.9%; --sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 351 85% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 351 85% 52%;
}
.ocean {
--primary: 187 75% 42%;
--primary-foreground: 210 40% 98%;
--ring: 187 75% 42%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 187 75% 42%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 187 30% 94%;
--sidebar-accent-foreground: 187 50% 20%;
--sidebar-border: 187 20% 88%;
--sidebar-ring: 187 75% 42%;
}
.dark.ocean {
--primary: 187 75% 50%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 187 75% 50%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 187 75% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 187 75% 50%;
}
.forest {
--primary: 142 76% 36%;
--primary-foreground: 210 40% 98%;
--ring: 142 76% 36%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 142 76% 36%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 142 30% 94%;
--sidebar-accent-foreground: 142 50% 20%;
--sidebar-border: 142 20% 88%;
--sidebar-ring: 142 76% 36%;
}
.dark.forest {
--primary: 142 76% 44%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 142 76% 44%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 142 76% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 142 76% 44%;
}
.sunset {
--primary: 24 95% 53%;
--primary-foreground: 210 40% 98%;
--ring: 24 95% 53%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 24 95% 53%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 24 30% 94%;
--sidebar-accent-foreground: 24 50% 20%;
--sidebar-border: 24 20% 88%;
--sidebar-ring: 24 95% 53%;
}
.dark.sunset {
--primary: 24 95% 58%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 24 95% 58%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 24 95% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 24 95% 58%;
}
.midnight {
--primary: 230 75% 55%;
--primary-foreground: 210 40% 98%;
--ring: 230 75% 55%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 230 75% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 230 30% 94%;
--sidebar-accent-foreground: 230 50% 20%;
--sidebar-border: 230 20% 88%;
--sidebar-ring: 230 75% 55%;
}
.dark.midnight {
--primary: 230 75% 62%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 230 75% 62%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 230 75% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 230 75% 62%;
}
.rose {
--primary: 346 77% 50%;
--primary-foreground: 210 40% 98%;
--ring: 346 77% 50%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 346 77% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 346 30% 94%;
--sidebar-accent-foreground: 346 50% 20%;
--sidebar-border: 346 20% 88%;
--sidebar-ring: 346 77% 50%;
}
.dark.rose {
--primary: 346 77% 58%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 346 77% 58%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 346 77% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 346 77% 58%;
}
.amber {
--primary: 38 92% 50%;
--primary-foreground: 210 40% 98%;
--ring: 38 92% 50%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 38 92% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 38 30% 94%;
--sidebar-accent-foreground: 38 50% 20%;
--sidebar-border: 38 20% 88%;
--sidebar-ring: 38 92% 50%;
}
.dark.amber {
--primary: 38 92% 56%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 38 92% 56%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 38 92% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 38 92% 56%;
}
.violet {
--primary: 262 83% 58%;
--primary-foreground: 210 40% 98%;
--ring: 262 83% 58%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 262 83% 58%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 262 30% 94%;
--sidebar-accent-foreground: 262 50% 20%;
--sidebar-border: 262 20% 88%;
--sidebar-ring: 262 83% 58%;
}
.dark.violet {
--primary: 262 83% 65%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 262 83% 65%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 262 83% 68%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 262 83% 65%;
}
.slate {
--primary: 215 20% 45%;
--primary-foreground: 210 40% 98%;
--ring: 215 20% 45%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 215 20% 45%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 215 20% 94%;
--sidebar-accent-foreground: 215 50% 20%;
--sidebar-border: 215 20% 88%;
--sidebar-ring: 215 20% 45%;
}
.dark.slate {
--primary: 215 20% 60%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 215 20% 60%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 215 20% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 215 20% 60%;
}
.ruby {
--primary: 351 85% 45%;
--primary-foreground: 210 40% 98%;
--ring: 351 85% 45%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 351 85% 45%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 351 30% 94%;
--sidebar-accent-foreground: 351 50% 20%;
--sidebar-border: 351 20% 88%;
--sidebar-ring: 351 85% 45%;
}
.dark.ruby {
--primary: 351 85% 52%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 351 85% 52%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 351 85% 55%; --sidebar-primary: 351 85% 55%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
@@ -585,6 +397,49 @@
} }
} }
[data-radix-select-trigger] {
background-color: hsl(0 0% 100%);
}
main {
position: relative;
z-index: 0;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(1px 1px at 10% 20%, rgba(100,100,120,0.4) 0%, transparent 100%),
radial-gradient(1px 1px at 20% 40%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 30% 10%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 40% 60%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 50% 80%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 60% 30%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 70% 50%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 90% 15%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 15% 70%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 25% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 35% 35%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 45% 15%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 55% 75%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 65% 55%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 75% 25%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 85% 65%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 95% 45%, rgba(80,80,100,0.5) 0%, transparent 100%);
background-size: 200% 200%;
animation: drift 60s linear infinite;
}
@keyframes drift {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
/* Login page custom styles */ /* Login page custom styles */
.left-panel { .left-panel {
background: #0a0a0f; background: #0a0a0f;
@@ -830,6 +685,10 @@
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
} }
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
.body-text { color: rgba(232,232,239,0.5); } .body-text { color: rgba(232,232,239,0.5); }
.subheading-text { color: rgba(232,232,239,0.38); } .subheading-text { color: rgba(232,232,239,0.38); }
.checkbox-text { color: rgba(232,232,239,0.38); } .checkbox-text { color: rgba(232,232,239,0.38); }
-3
View File
@@ -1,7 +1,6 @@
import type { Metadata, Viewport } from "next" import type { Metadata, Viewport } from "next"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { ThemeProvider } from "@/providers/theme-provider" import { ThemeProvider } from "@/providers/theme-provider"
import { WebsiteThemeProvider } from "@/providers/website-theme-provider"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import "./globals.css" import "./globals.css"
@@ -32,10 +31,8 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} min-h-screen antialiased`}> <body className={`${inter.variable} min-h-screen antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
<WebsiteThemeProvider>
{children} {children}
<Toaster /> <Toaster />
</WebsiteThemeProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
+3 -4
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { useState, useEffect, useRef } from "react" import { useState, useEffect, useRef } from "react"
import { useSearchParams } from "next/navigation" import { useRouter } from "next/navigation"
import { COMPANY_NAME } from "@/lib/constants" import { COMPANY_NAME } from "@/lib/constants"
import { Eye, EyeOff, Loader2 } from "lucide-react" import { Eye, EyeOff, Loader2 } from "lucide-react"
@@ -14,8 +14,7 @@ const waves = [
] ]
export default function LoginPage() { export default function LoginPage() {
const searchParams = useSearchParams() const router = useRouter()
const redirectTo = searchParams.get("redirect") || "/dashboard"
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
@@ -216,7 +215,7 @@ export default function LoginPage() {
}) })
if (res.ok) { if (res.ok) {
window.location.href = redirectTo router.push("/dashboard")
} else { } else {
const data = await res.json().catch(() => ({})) const data = await res.json().catch(() => ({}))
setError(data.error || "Invalid email or password.") setError(data.error || "Invalid email or password.")
+32 -110
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { useState, useRef, useEffect, Fragment } from "react" import { useState, useRef, useEffect, Fragment } from "react"
import { Send, Bot, User, RefreshCw, AlertCircle, Check, Terminal } from "lucide-react" import { Send, Loader2, Bot, User, RefreshCw, AlertCircle } from "lucide-react"
function linkifyText(text: string) { function linkifyText(text: string) {
const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/ const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/
@@ -24,26 +24,9 @@ export function AIChat() {
const [input, setInput] = useState("") const [input, setInput] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [bootState, setBootState] = useState<"booting" | "ready" | "error">("booting") const [ollamaStatus, setOllamaStatus] = useState<boolean | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const checkServer = async () => {
try {
const res = await fetch("/api/ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "ping" }),
})
if (res.status !== 503) {
setBootState("ready")
} else {
setTimeout(checkServer, 2000)
}
} catch {
setTimeout(checkServer, 2000)
}
}
useEffect(() => { useEffect(() => {
fetch("/api/ai/jobs") fetch("/api/ai/jobs")
.then((r) => r.json()) .then((r) => r.json())
@@ -73,13 +56,29 @@ export function AIChat() {
}, },
]) ])
}) })
checkServer()
}, []) }, [])
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages]) }, [messages])
const checkOllama = async () => {
try {
const res = await fetch("/api/ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "__ping__" }),
})
setOllamaStatus(res.status !== 503)
} catch {
setOllamaStatus(false)
}
}
useEffect(() => { checkOllama() }, [])
const sendMessage = async () => { const sendMessage = async () => {
const msg = input.trim() const msg = input.trim()
if (!msg || loading) return if (!msg || loading) return
@@ -97,8 +96,8 @@ export function AIChat() {
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({})) const data = await res.json()
throw new Error(data.error || `Error ${res.status}`) throw new Error(data.error || "Failed to get response")
} }
const data = await res.json() const data = await res.json()
@@ -122,86 +121,17 @@ export function AIChat() {
} }
} }
const bootOverlay = (
<div className="flex-1 flex items-center justify-center">
<style>{`
@keyframes walk {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(12px) translateY(-4px); }
50% { transform: translateX(24px) translateY(0); }
75% { transform: translateX(12px) translateY(-4px); }
}
@keyframes legLeft {
0%, 100% { transform: rotate(-10deg); }
50% { transform: rotate(10deg); }
}
@keyframes legRight {
0%, 100% { transform: rotate(10deg); }
50% { transform: rotate(-10deg); }
}
@keyframes armLeft {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
@keyframes armRight {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}
@keyframes blink {
0%, 45%, 55%, 100% { height: 4px; }
50% { height: 1px; }
}
.robot-walk { animation: walk 0.6s ease-in-out infinite; }
.robot-leg-l { transform-origin: top center; animation: legLeft 0.3s ease-in-out infinite; }
.robot-leg-r { transform-origin: top center; animation: legRight 0.3s ease-in-out infinite; }
.robot-arm-l { transform-origin: top center; animation: armLeft 0.3s ease-in-out infinite; }
.robot-arm-r { transform-origin: top center; animation: armRight 0.3s ease-in-out infinite; }
.robot-eye { animation: blink 2s ease-in-out infinite; }
`}</style>
<div className="flex flex-col items-center gap-4">
<p className="text-sm text-[#6a6a75]">Servers booting...</p>
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center overflow-hidden">
<div className="robot-walk relative">
<svg width="40" height="36" viewBox="0 0 40 36" fill="none">
<rect x="10" y="2" width="20" height="16" rx="3" fill="#1BB0CE" opacity="0.9"/>
<rect x="6" y="6" width="6" height="2" rx="1" className="robot-arm-l" fill="#1BB0CE" opacity="0.7"/>
<rect x="28" y="6" width="6" height="2" rx="1" className="robot-arm-r" fill="#1BB0CE" opacity="0.7"/>
<rect x="14" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<rect x="22" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<circle cx="16" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<circle cx="24" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<rect x="17" y="10" width="6" height="2" rx="1" fill="#0d1117"/>
<rect x="12" y="18" width="5" height="10" rx="2" className="robot-leg-l" fill="#1BB0CE" opacity="0.8"/>
<rect x="23" y="18" width="5" height="10" rx="2" className="robot-leg-r" fill="#1BB0CE" opacity="0.8"/>
</svg>
</div>
</div>
</div>
</div>
)
const readyOverlay = (
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center">
<Check className="h-6 w-6 text-green-400" />
</div>
<div className="text-center space-y-1">
<p className="text-xs text-[#6a6a75]">Try these commands:</p>
<div className="flex gap-2">
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> lists</code>
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> leads</code>
</div>
</div>
</div>
</div>
)
if (bootState === "booting") return bootOverlay
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{bootState === "ready" && readyOverlay} {ollamaStatus === false && (
<div className="flex items-center gap-2 px-4 py-2 bg-amber-500/10 border-b border-amber-500/20 text-amber-400 text-xs">
<AlertCircle className="h-3.5 w-3.5 flex-none" />
<span className="flex-1">Ollama not responding. Start it with <code className="bg-amber-500/20 px-1 rounded">ollama serve</code></span>
<button type="button" onClick={checkOllama} className="hover:text-amber-300">
<RefreshCw className="h-3.5 w-3.5" />
</button>
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin"> <div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin">
{messages.map((msg, i) => ( {messages.map((msg, i) => (
@@ -233,10 +163,7 @@ export function AIChat() {
<Bot className="h-4 w-4 text-[#1BB0CE]" /> <Bot className="h-4 w-4 text-[#1BB0CE]" />
</div> </div>
<div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]"> <div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]">
<svg className="h-4 w-4 animate-spin text-[#1BB0CE]" viewBox="0 0 24 24" fill="none"> <Loader2 className="h-4 w-4 animate-spin text-[#1BB0CE]" />
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
</div> </div>
</div> </div>
)} )}
@@ -265,12 +192,7 @@ export function AIChat() {
disabled={loading || !input.trim()} disabled={loading || !input.trim()}
className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors" className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors"
> >
{loading ? ( {loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
) : <Send className="h-4 w-4" />}
</button> </button>
</div> </div>
</div> </div>
@@ -0,0 +1,49 @@
"use client"
export function CrossedLightsabers() {
return (
<svg
width="120"
height="120"
viewBox="0 0 200 200"
className="pointer-events-none select-none overflow-visible shrink-0"
style={{ overflow: "visible" }}
aria-hidden="true"
>
<defs>
<radialGradient id="purpleGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#a855f7" stopOpacity="0.6" />
<stop offset="40%" stopColor="#a855f7" stopOpacity="0.25" />
<stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
</radialGradient>
</defs>
<circle cx="100" cy="73" r="24" fill="url(#purpleGlow)" className="animate-pulse-intersection" />
<g className="animate-pulse-red">
<line x1="35" y1="145" x2="125" y2="45" strokeLinecap="round" opacity="0.12" className="stroke-[#dc2626] dark:stroke-[#ef4444] saber-outer" />
<line x1="35" y1="145" x2="125" y2="45" strokeLinecap="round" opacity="0.25" className="stroke-[#dc2626] dark:stroke-[#ef4444] saber-mid" />
<line x1="35" y1="145" x2="125" y2="45" strokeLinecap="round" className="stroke-[#f87171] dark:stroke-[#fca5a5] saber-core" />
</g>
<g className="animate-pulse-blue">
<line x1="165" y1="145" x2="75" y2="45" strokeLinecap="round" opacity="0.12" className="stroke-[#1d4ed8] dark:stroke-[#3b82f6] saber-blue-outer" />
<line x1="165" y1="145" x2="75" y2="45" strokeLinecap="round" opacity="0.25" className="stroke-[#1d4ed8] dark:stroke-[#3b82f6] saber-blue-mid" />
<line x1="165" y1="145" x2="75" y2="45" strokeLinecap="round" className="stroke-[#60a5fa] dark:stroke-[#93c5fd] saber-blue-core" />
</g>
<rect x="30" y="145" width="10" height="55" rx="2" fill="#374151" />
<rect x="160" y="145" width="10" height="55" rx="2" fill="#374151" />
<rect x="30" y="155" width="10" height="5" fill="#1f2937" />
<rect x="30" y="166" width="10" height="5" fill="#1f2937" />
<rect x="30" y="177" width="10" height="5" fill="#1f2937" />
<rect x="160" y="155" width="10" height="5" fill="#1f2937" />
<rect x="160" y="166" width="10" height="5" fill="#1f2937" />
<rect x="160" y="177" width="10" height="5" fill="#1f2937" />
<rect x="32" y="143" width="6" height="4" rx="1" fill="#6b7280" />
<rect x="162" y="143" width="6" height="4" rx="1" fill="#6b7280" />
</svg>
)
}
@@ -69,19 +69,6 @@ export function LeadStatusChart({ data }: LeadStatusChartProps) {
className="h-full" className="h-full"
> >
<Card className="h-full relative overflow-hidden"> <Card className="h-full relative overflow-hidden">
{/* Spider watermark */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none z-0 opacity-[0.03] dark:opacity-[0.05]">
<svg width="220" height="220" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="90" y1="0" x2="90" y2="180" stroke="#CC0000" strokeWidth="1" />
<line x1="0" y1="90" x2="180" y2="90" stroke="#CC0000" strokeWidth="1" />
<line x1="30" y1="30" x2="150" y2="150" stroke="#0033CC" strokeWidth="1" />
<line x1="150" y1="30" x2="30" y2="150" stroke="#0033CC" strokeWidth="1" />
<circle cx="90" cy="90" r="40" stroke="#CC0000" strokeWidth="1" fill="none" />
<circle cx="90" cy="90" r="60" stroke="#0033CC" strokeWidth="0.8" fill="none" strokeDasharray="4 4" />
<circle cx="90" cy="90" r="80" stroke="#CC0000" strokeWidth="0.5" fill="none" strokeDasharray="2 6" />
</svg>
</div>
<CardHeader> <CardHeader>
<CardTitle>Lead Status</CardTitle> <CardTitle>Lead Status</CardTitle>
</CardHeader> </CardHeader>
+47
View File
@@ -0,0 +1,47 @@
"use client"
import { useMemo } from "react"
interface Star {
left: number
top: number
size: number
delay: number
duration: number
}
export function StarField() {
const stars = useMemo<Star[]>(() => {
const result: Star[] = []
for (let i = 0; i < 160; i++) {
result.push({
left: Math.random() * 100,
top: Math.random() * 100,
size: 1.5 + Math.random() * 2,
delay: Math.random() * 6,
duration: 3 + Math.random() * 4,
})
}
return result
}, [])
return (
<div className="fixed inset-0 pointer-events-none select-none z-0 overflow-hidden">
{stars.map((s, i) => (
<div
key={i}
className="absolute rounded-full bg-[#222222] dark:bg-white star-twinkle"
style={{
left: `${s.left}%`,
top: `${s.top}%`,
width: `${s.size}px`,
height: `${s.size}px`,
opacity: 0.35 + Math.random() * 0.45,
animationDelay: `${s.delay}s`,
animationDuration: `${s.duration}s`,
}}
/>
))}
</div>
)
}
@@ -3,14 +3,11 @@
export function StatCardSkeleton() { export function StatCardSkeleton() {
return ( return (
<div className="col-span-full flex flex-col items-center justify-center py-20"> <div className="col-span-full flex flex-col items-center justify-center py-20">
<p className="text-[#CC0000] dark:text-[#FF1111] text-5xl font-['Bangers',cursive] animate-pulse"> <p className="text-[#444444] dark:text-[#AAAAAA] text-sm">
THWIP!
</p>
<p className="text-[#444444] dark:text-[#AAAAAA] text-sm mt-3">
Loading your data... Loading your data...
</p> </p>
<div className="w-48 h-1 rounded-full overflow-hidden bg-[#E0E0E0] dark:bg-[#222222] mt-4"> <div className="w-48 h-1 rounded-full overflow-hidden bg-[#E0E0E0] dark:bg-[#222222] mt-4">
<div className="w-full h-full rounded-full bg-gradient-to-r from-[#CC0000] via-[#FFFFFF] to-[#0033CC] animate-[loading_1.5s_ease-in-out_infinite]" /> <div className="w-full h-full rounded-full bg-sidebar animate-[loading_1.5s_ease-in-out_infinite]" />
</div> </div>
</div> </div>
) )
-31
View File
@@ -118,37 +118,6 @@ export function StatCard({ title, value, icon: Icon, description, index = 0, tre
transition={{ duration: 0.3, delay: index * 0.05 }} transition={{ duration: 0.3, delay: index * 0.05 }}
className="relative" className="relative"
> >
{/* Web overlay decorative */}
<div className="absolute inset-0 pointer-events-none z-0 opacity-[0.03] dark:opacity-[0.06]">
<svg width="100%" height="100%" viewBox="0 0 180 180" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
{Array.from({ length: 6 }).map((_, i) => (
<line key={`wl-${i}`} x1="0" y1={i * 36} x2="180" y2={i * 36} stroke="#CC0000" strokeWidth="0.5" />
))}
{Array.from({ length: 6 }).map((_, i) => (
<line key={`wc-${i}`} x1={i * 36} y1="0" x2={i * 36} y2="180" stroke="#CC0000" strokeWidth="0.5" />
))}
{Array.from({ length: 6 }).map((_, i) => (
<line key={`wd-${i}`} x1="0" y1={i * 36} x2={180 - i * 36} y2="180" stroke="#0033CC" strokeWidth="0.5" />
))}
</svg>
</div>
{/* Comic action lines on hover */}
<div className="absolute -inset-2 pointer-events-none z-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="100" y1="0" x2="100" y2="20" stroke="#CC0000" strokeWidth="2" strokeLinecap="round" />
<line x1="100" y1="180" x2="100" y2="200" stroke="#CC0000" strokeWidth="2" strokeLinecap="round" />
<line x1="0" y1="100" x2="20" y2="100" stroke="#CC0000" strokeWidth="2" strokeLinecap="round" />
<line x1="180" y1="100" x2="200" y2="100" stroke="#CC0000" strokeWidth="2" strokeLinecap="round" />
<line x1="30" y1="30" x2="44" y2="44" stroke="#0033CC" strokeWidth="1.5" strokeLinecap="round" />
<line x1="170" y1="30" x2="156" y2="44" stroke="#0033CC" strokeWidth="1.5" strokeLinecap="round" />
<line x1="30" y1="170" x2="44" y2="156" stroke="#0033CC" strokeWidth="1.5" strokeLinecap="round" />
<line x1="170" y1="170" x2="156" y2="156" stroke="#0033CC" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="100" cy="100" r="90" stroke="#CC0000" strokeWidth="0.5" strokeDasharray="4 4" opacity="0.4" />
<circle cx="100" cy="100" r="80" stroke="#0033CC" strokeWidth="0.5" strokeDasharray="3 5" opacity="0.3" />
</svg>
</div>
<Card className="group h-full hover:shadow-xl transition-all duration-200 relative z-10 overflow-hidden"> <Card className="group h-full hover:shadow-xl transition-all duration-200 relative z-10 overflow-hidden">
{/* Red/Blue gradient top strip */} {/* Red/Blue gradient top strip */}
<div className={`h-1 w-full bg-gradient-to-r ${stripColor} ${stripGlow} shadow-sm`} /> <div className={`h-1 w-full bg-gradient-to-r ${stripColor} ${stripGlow} shadow-sm`} />
+1 -45
View File
@@ -34,51 +34,7 @@ export function AppShell({ children }: AppShellProps) {
} }
return ( return (
<div className="min-h-screen bg-[#F8F8F8] dark:bg-[#0A0A0A] relative overflow-hidden" <div className="min-h-screen bg-background dark:bg-[#0A0A0A] relative overflow-hidden">
style={{
backgroundImage: "radial-gradient(circle, #e6202010 1px, transparent 1px), radial-gradient(circle, #0033CC06 1px, transparent 1px)",
backgroundSize: "28px 28px, 14px 14px",
backgroundPosition: "0 0, 7px 7px",
}}
>
{/* Spider-Man top gradient bar */}
<div className="fixed top-0 left-0 right-0 h-[3px] w-full bg-gradient-to-r from-[#e62020] via-[#FFFFFF] to-[#0033CC] dark:from-[#FF6666] dark:via-[#FFFFFF] dark:to-[#1144FF] z-50" />
{/* Corner spider webs */}
<div className="hidden lg:block fixed top-0 left-0 pointer-events-none z-0 opacity-[0.07] dark:opacity-[0.12]">
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="0" x2="180" y2="0" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="60" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="120" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="120" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="60" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="0" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<path d="M0,30 Q30,30 30,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,60 Q60,60 60,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,90 Q90,90 90,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,120 Q120,120 120,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,150 Q150,150 150,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,180 Q180,180 180,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
</svg>
</div>
<div className="hidden lg:block fixed top-0 right-0 pointer-events-none z-0 opacity-[0.07] dark:opacity-[0.12]">
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ transform: "scaleX(-1)" }}>
<line x1="0" y1="0" x2="180" y2="0" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="60" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="120" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="180" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="120" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="60" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<line x1="0" y1="0" x2="0" y2="180" stroke="#e62020" strokeWidth="0.8"/>
<path d="M0,30 Q30,30 30,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,60 Q60,60 60,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,90 Q90,90 90,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,120 Q120,120 120,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,150 Q150,150 150,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
<path d="M0,180 Q180,180 180,0" stroke="#e62020" strokeWidth="0.8" fill="none"/>
</svg>
</div>
<SystemMonitor /> <SystemMonitor />
<Sidebar <Sidebar
+37
View File
@@ -0,0 +1,37 @@
"use client"
export function CrmIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 64 64"
aria-hidden="true"
>
<g className="fill-[#333333] dark:fill-white/[0.15] dark:stroke-[#e5e7eb] dark:[stroke-width:1.5px]">
<path className="dark:[stroke-linejoin:round]" d="m10.35 35.62s-.75.14-1-1.48-.58-8.61-.3-8.87a10 10 0 0 1 2.07-.63s0-7.54 2.67-11.86 5.4-6.09 10.65-7.56a29.38 29.38 0 0 1 15.18.3c4.41 1.18 8.7 4.62 9.89 8.8a49.27 49.27 0 0 1 1.49 9.55 9.53 9.53 0 0 1 2.52.9c.17.33.43 9.12 0 9.38a1.6 1.6 0 0 1 -.87.23s7.86 18 7.59 19-12.93 6.36-30.1 6.23-26.05-5.41-26.36-6.11 6.57-17.88 6.57-17.88z" />
</g>
<g className="fill-[#777777] dark:fill-white/[0.1] dark:stroke-[#d4d4d4] dark:[stroke-width:1px]">
<path d="m13.24 27.92s-.6-7.65 1.3-11.91a19.6 19.6 0 0 1 7.75-8.23 20.47 20.47 0 0 1 6.51-1.45c.05.16.67 10.23.66 11.61a27 27 0 0 1 -.57 4s-4-2.44-8.16-1.38a10.47 10.47 0 0 0 -7.49 7.36z" />
<path d="m30.11 22.18s.53-3.69.54-4.89-.8-10.82-.6-10.95a4.11 4.11 0 0 1 1.88 0c.07.16 1.07 11.66 1.28 13.22a9.33 9.33 0 0 1 .27 2.44 2.85 2.85 0 0 1 -3.37.18z" />
<path d="m34.8 21.44a76.48 76.48 0 0 1 -1.4-9.35c-.17-3.62-.51-5.65-.06-5.83s6.66.17 10.42 3.45 4.06 5.61 3.81 5.66-2.52-1-2.64-.77-.15.8.06.92 2.69 1.1 2.82 1.26a4.18 4.18 0 0 1 .28.95 16.94 16.94 0 0 0 -3-1.13c-.12.17-.23.92 0 1s3 1.3 3 1.3l.11.79s-2.83-1.18-3-1.14-.36.8-.15.92 3.16 1.26 3.25 1.43.41 1.36.41 1.36l-1.71-.54s.31.87.48 1 1.3.42 1.31.76a31.35 31.35 0 0 1 .2 3.33c-.12 0-2.2-6.45-5.21-7.09s-8.98 1.72-8.98 1.72z" />
<path d="m22.44 22.28c2.21 0 7.19 2.45 9.76 1.84s6.89-2.79 9.51-2.81 4.07 2.48 6.36 7.84 7.14 17.34 7 17.34-8.1-17.89-9.66-20.23-2.66-3-5.86-2.77-6.81 1-6.85 1.41 0 7.17.49 7.75 3.06.71 6.18.51 5-.33 5.77-.64a10.14 10.14 0 0 0 1.69-1l7.79 16.48a30.15 30.15 0 0 0 -2.94-2.56c-.21.05-.31.8-.19.88s3.67 3.33 4 3.7a12.43 12.43 0 0 1 1.61 2.63c-.12.17-.45.55-.66.47s-5-5.71-5.39-5.83-.45.6-.45.6 5.18 5.5 5.06 5.63a8.62 8.62 0 0 1 -1.15.57s-4.34-5-4.5-4.94-.69.6-.4.81a45.71 45.71 0 0 1 4.07 4.36 7 7 0 0 1 -1.2.4 46.49 46.49 0 0 0 -3.58-3.72c-.25 0-.61.39-.49.55s3.34 3.29 3.22 3.46a4 4 0 0 1 -1.16.37s-2.47-2.59-2.65-2.59-.74.31-.53.6 2.39 2.19 2.27 2.28a4.86 4.86 0 0 1 -1 .19c-.13 0-2-2-2.22-2s-.78.19-.65.48 1.83 1.79 1.63 1.92-5.17 1.66-15.76 1.51-14.84-1.85-14.84-1.85a46 46 0 0 0 -4.22-3.69c-.37 0-.78.6-.61.72a29.31 29.31 0 0 1 2.86 2.6c-.16 0-3.27-.92-4.32-1.1a7.6 7.6 0 0 1 -2.26-.61c0-.13 6-14.45 6.27-14.41s4.29 10.07 4.29 10.07a2.77 2.77 0 0 0 1.2 2.89 3.9 3.9 0 0 0 3.49-.25l15.51.08c.12 0 2.65 1.27 3.69-.68s.44-2.67.44-2.67 4-11.27 3.76-11.52a5.63 5.63 0 0 0 -1.76-.35s-2.75 10.32-3 10.33-1.26-.3-1.68-.34a3.18 3.18 0 0 0 -.7 0l-5.48-7.62s1.1-6-3.28-6-3.14 6-3.14 6-1-.06-2 1.51-3.39 6.29-3.39 6.29a3.77 3.77 0 0 0 -1.45.2c-.66.27-1.07.49-1.07.49s-3.54-8.29-3.46-8.46.82-2.15 1.2-2.16 2.93.39 3.93.41 1.16-.11 1.2-.28.27-.8.1-.8-5.11-.57-5.48-.57-1.07 2.14-1.36 2.23-1.32.25-1.2 0 2.16-5.13 2.29-5.1 7.53 1.07 9.73.64 2.76-1.11 3.07-2.16a31.45 31.45 0 0 0 .25-6.59 19.1 19.1 0 0 0 -6.2-1.31c-2.79-.09-4.67-.13-5.87 1.82s-10.79 27.71-10.96 27.76a3.64 3.64 0 0 1 -.92-.29s4.91-14 6.78-19.26 3.47-11.56 9.47-11.46z" />
<path d="m28.76 40.3a14.69 14.69 0 0 0 2.25 0l2.29-.09s-.39 9.67 0 9.75.67.27.66-.06-.19-7.75.06-7.79.75 1.52.75 1.52-.14 6.42 0 6.46.88.27.84.06 0-4.75 0-4.75l1.43 2.26-.79.06s-.08 2 .17 2a4.75 4.75 0 0 0 .71 0c.12 0 .11 1.16.11 1.16s-7.26-.19-9.8-.13-2.74.11-2.74.11a6.75 6.75 0 0 0 -.55-2l-.62-1.28s4.86-7.15 5.23-7.28z" />
</g>
<g className="fill-[#333333] dark:fill-white/[0.15] dark:stroke-[#d4d4d4] dark:[stroke-width:0.7px]">
<path d="m30.57 41.13c.28-.1.79-.06.84.1s.41 8.41.17 8.63-.88.14-.88 0-.25-8.69-.13-8.73z" />
<path d="m28.74 41.26c.21-.09.7-.19.79 0s.55 8.61.38 8.66-.87.18-.87 0-.3-8.66-.3-8.66z" />
<path d="m27 44.8c.06-.14.87-.4.87-.23a52.12 52.12 0 0 1 .21 5.25 3 3 0 0 1 -.79.06s-.38-4.88-.29-5.08z" />
<path d="m25.25 46.8c0-.17.57-.64.65-.51a14 14 0 0 1 .26 3.45 4 4 0 0 1 -.79.14s-.16-2.88-.12-3.08z" />
<path d="m41.23 35.62c.19-.1 2.79-.16 4.58-.37a11 11 0 0 1 1.78-.25c.21 0 .48.87.36 1a17.41 17.41 0 0 1 -4.32.69c-1.46 0-2.29 0-2.29 0s-.48-.9-.11-1.07z" />
<path d="m42 38.39c.34-.09 1.41 1.51 1.41 1.51s-.19.79-.4.63a9.86 9.86 0 0 1 -1.5-1.63c-.08-.21.29-.46.49-.51z" />
<path d="m40.41 39.68c.29-.1 2.3 1.78 2.22 2s-.15.63-.28.59a19.84 19.84 0 0 1 -2.43-2.11c-.04-.22.37-.44.49-.48z" />
<path d="m39.41 41.16s2.55 2 2.47 2.32a2.36 2.36 0 0 1 -.36.67s-2.93-2.38-2.81-2.6.53-.26.7-.39z" />
<path d="m14.45 48.4c.2.07 2.81 2.31 2.82 2.72s-.11.63-.24.63a27 27 0 0 1 -2.93-2.38c-.05-.17.14-1.05.35-.97z" />
<path d="m13.67 50.5s3.83 3.16 3.83 3.37-.11.67-.23.67a35.1 35.1 0 0 1 -4.08-3.07c-.04-.25.48-.97.48-.97z" />
<path d="m16.27 17c-.17-.24.44-2.3 1.57-3.91s2.19-2.52 2.65-2.4.77.61.6.82a7.05 7.05 0 0 0 -2.72 2.86c-.79 1.85-.55 3-.8 3.06a1.86 1.86 0 0 1 -1.3-.43z" />
<path d="m44.85 13.23a2.79 2.79 0 0 1 1.6.54c.09.25.27.83-.11.76a10.8 10.8 0 0 1 -1.43-.53c-.17-.06-.31-.72-.06-.77z" />
</g>
</svg>
)
}
@@ -0,0 +1,15 @@
"use client"
export function SidebarNameLogo() {
return (
<svg
viewBox="0 0 192.756 192.756"
className="fill-[#333333] dark:fill-[#e5e7eb] w-full h-auto"
aria-hidden="true"
>
<g fill-rule="evenodd" clip-rule="evenodd">
<path d="M5.669 81.215v12.65h37.003c4.301 0 9.796-3.977 9.796-10.556 0-2.646 1.012-4.372-2.098-7.872l-4.733-5.608c-2.712-2.53.324-2.53 2.602-2.53h15.623v26.566h12.39V67.299h16.699V55.922H38.877c-6.579 0-9.796 6.317-9.615 9.606.182 3.289.787 7.427 6.254 12.398 4.987 4.533-2.469 3.289-3.218 3.289H5.669zM120.348 55.922H100.36L89.155 93.866h12.47l2.023-5.313h13.156l1.953 5.313h12.215l-10.624-37.944zm-13.916 23.522l4.301-13.916 4.049 13.916h-8.35zM170.443 81.215c-4.807 0-4.807-1.771-4.807-1.771 4.119 0 7.771-6.001 7.771-12.145s-6-11.377-10.809-11.377h-26.891v37.944h13.664v-12.65s5.818 6.831 8.854 9.614c3.037 2.783 3.289 3.036 7.41 3.036h21.449v-12.65c.002-.001-11.834-.001-16.641-.001zm-12.398-8.855h-8.672v-6.832h8.672c3.976 0 4.664 6.832 0 6.832zM5.669 98.672h13.979l3.542 12.652 3.289-12.652h14.675l3.795 12.652 3.796-12.652h12.144l-11.133 37.953H38.624l-4.878-17.965-5.496 17.965H16.865L5.669 98.672zM89.578 98.891H69.59l-11.204 37.943h12.469l2.024-5.312h13.157l1.953 5.312h12.216L89.578 98.891zm-13.915 23.521l4.301-13.916 4.048 13.916h-8.349zM170.695 110.059c-2.275 0-4.756.266-2.043 2.795l4.734 5.609c3.109 3.5 3.059 4.959 3.059 7.607 0 6.578-6.508 10.555-10.809 10.555l-29.896.201c-4.119 0-4.371-.252-7.408-3.035-3.035-2.783-8.855-9.615-8.855-9.615v12.65h-13.662V98.883h26.891c4.807 0 10.809 5.234 10.809 11.377 0 6.145-3.652 12.145-7.773 12.145 0 0 1.812 1.822 4.848 1.822 3.037 0 14.727.012 14.727.012.748 0 8.203 1.244 3.217-3.289-5.467-4.971-6.072-9.107-6.254-12.396s2.662-9.881 9.238-9.881h25.57v11.387h-16.393v-.001zm-42.545 5.261h-8.674v-6.832h8.674c3.977 0 4.664 6.832 0 6.832z" />
</g>
</svg>
)
}
+3 -24
View File
@@ -9,7 +9,6 @@ import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useUser } from "@/providers/user-provider"; import { useUser } from "@/providers/user-provider";
import { useNotifications } from "@/providers/notification-provider"; import { useNotifications } from "@/providers/notification-provider";
import { BugReportModal } from "@/components/shared/bug-report-modal";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -28,10 +27,10 @@ import {
LogOut, LogOut,
User, User,
Settings, Settings,
Bug,
CheckCheck, CheckCheck,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { CrmIcon } from "./crm-icon";
interface TopbarProps { interface TopbarProps {
onMenuClick: () => void; onMenuClick: () => void;
@@ -45,7 +44,6 @@ export function Topbar({ onMenuClick }: TopbarProps) {
useNotifications(); useNotifications();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
const [bugModalOpen, setBugModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@@ -70,17 +68,10 @@ export function Topbar({ onMenuClick }: TopbarProps) {
.toUpperCase(); .toUpperCase();
return ( return (
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-white dark:bg-[#141414] px-4 lg:px-6 relative overflow-hidden"> <header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-card dark:bg-[#141414] px-4 lg:px-6 relative overflow-hidden">
{/* Red/Blue diagonal split accent */}
<div className="absolute right-0 top-0 bottom-0 w-[6px] bg-gradient-to-b from-[#CC0000] via-[#0033CC] to-[#CC0000] dark:from-[#FF1111] dark:via-[#1144FF] dark:to-[#FF1111]" />
{/* Logo */} {/* Logo */}
<div className="flex items-center gap-2 mr-2 shrink-0"> <div className="flex items-center gap-2 mr-2 shrink-0">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> <CrmIcon />
<circle cx="14" cy="14" r="13" fill="#CC0000" />
<circle cx="14" cy="14" r="8" fill="#0033CC" />
<path d="M14 6 L16 12 L22 12 L17 15 L19 21 L14 17 L9 21 L11 15 L6 12 L12 12 Z" fill="white" />
</svg>
<span className="text-[#CC0000] dark:text-[#FF1111] font-bold text-sm tracking-wide"> <span className="text-[#CC0000] dark:text-[#FF1111] font-bold text-sm tracking-wide">
CRM CRM
</span> </span>
@@ -116,17 +107,6 @@ export function Topbar({ onMenuClick }: TopbarProps) {
<Search className="h-5 w-5" /> <Search className="h-5 w-5" />
</Button> </Button>
{/* Report a Bug */}
<Button
variant="ghost"
size="icon"
onClick={() => setBugModalOpen(true)}
className="text-muted-foreground"
title="Report a Bug"
>
<Bug className="h-5 w-5" />
</Button>
{/* Theme toggle */} {/* Theme toggle */}
<Button <Button
variant="ghost" variant="ghost"
@@ -261,7 +241,6 @@ export function Topbar({ onMenuClick }: TopbarProps) {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<BugReportModal open={bugModalOpen} onClose={() => setBugModalOpen(false)} />
</header> </header>
); );
} }
+1 -76
View File
@@ -6,8 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useWebsiteTheme } from "@/providers/website-theme-provider" import { Sun, Moon, Monitor } from "lucide-react"
import { Sun, Moon, Monitor, Check } from "lucide-react"
const COLOR_THEME_KEY = "crm-color-theme" const COLOR_THEME_KEY = "crm-color-theme"
@@ -45,17 +44,8 @@ function applyColorTheme(theme: string) {
localStorage.setItem(COLOR_THEME_KEY, theme) localStorage.setItem(COLOR_THEME_KEY, theme)
} }
const websiteThemes = [
{
id: "spidey",
name: "Spidey",
description: "Current website theme",
},
]
export function ThemeSettings() { export function ThemeSettings() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { websiteTheme, setWebsiteTheme } = useWebsiteTheme()
const [colorTheme, setColorTheme] = useState("default") const [colorTheme, setColorTheme] = useState("default")
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
@@ -129,71 +119,6 @@ export function ThemeSettings() {
</RadioGroup> </RadioGroup>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Website Themes</CardTitle>
<CardDescription>
Select your website&apos;s visual theme.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{websiteThemes.map((wt) => {
const isActive = websiteTheme === wt.id
return (
<button
key={wt.id}
type="button"
onClick={() => setWebsiteTheme(wt.id)}
className={cn(
"group relative flex flex-col items-start gap-3 rounded-xl border-2 p-4 text-left transition-all",
"hover:bg-accent cursor-pointer",
isActive
? "border-primary bg-primary/5 shadow-sm"
: "border-border hover:border-muted-foreground/30"
)}
>
{isActive && (
<span className="absolute right-2 top-2 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] text-primary-foreground">
<Check className="h-3 w-3" />
</span>
)}
<div className="flex h-20 w-full items-center justify-center overflow-hidden rounded-lg border border-border bg-background">
<div className="flex h-full w-full">
<div className="flex w-1/3 flex-col gap-0.5 bg-[#0a0a0f] p-1.5">
<div className="h-1 w-full rounded-sm bg-[#1a1a24]" />
<div className="h-1 w-2/3 rounded-sm bg-[#1a1a24]" />
<div className="mt-auto h-1.5 w-full rounded-sm bg-[#1a1a24]" />
</div>
<div className="flex flex-1 flex-col">
<div className="flex h-5 items-center gap-1 bg-[#CC0000] px-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
</div>
<div className="flex flex-1 items-center justify-center bg-[#141414]">
<div className="h-2 w-2 rounded-full bg-[#CC0000]/40" />
</div>
</div>
</div>
</div>
<div className="flex w-full items-center justify-between gap-2">
<span className="text-sm font-medium">{wt.name}</span>
{isActive && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold text-primary">
Current
</span>
)}
</div>
</button>
)
})}
</div>
</CardContent>
</Card>
</div> </div>
) )
} }
+25 -60
View File
@@ -1,76 +1,41 @@
import http from "http"
const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001" const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001"
function parseUrl(url: string) {
const u = new URL(url)
return { hostname: u.hostname, port: parseInt(u.port) || 80, path: u.pathname + u.search }
}
export async function chatWithAI(message: string, jwtToken: string) { export async function chatWithAI(message: string, jwtToken: string) {
const body = JSON.stringify({ message }) const res = await fetch(`${AI_SERVICE}/ai/chat`, {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/chat`) method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` },
const text = await new Promise<string>((resolve, reject) => { body: JSON.stringify({ message }),
const req = http.request(
{ hostname, port, path, method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}`, "Content-Length": Buffer.byteLength(body) } },
(res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(data)
} else {
reject(new Error(`AI error ${res.statusCode}: ${data.substring(0, 200)}`))
}
})
}
)
req.on("error", reject)
req.write(body)
req.end()
}) })
const data = JSON.parse(text) if (!res.ok) {
const text = await res.text()
throw new Error(`AI service error (${res.status}): ${text}`)
}
const data = await res.json()
return data.response || "" return data.response || ""
} }
export async function checkAiServiceStatus() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/health`)
try {
await new Promise<void>((resolve, reject) => {
const req = http.get({ hostname, port, path, timeout: 3000 }, (res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
try { resolve(JSON.parse(data).status === "ok" ? undefined : reject(new Error("not ok"))) } catch { reject(new Error("bad response")) }
})
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
return true
} catch {
return false
}
}
export async function fetchJobs() { export async function fetchJobs() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/jobs`)
try { try {
const text = await new Promise<string>((resolve, reject) => { const res = await fetch(`${AI_SERVICE}/ai/jobs`)
const req = http.get({ hostname, port, path, timeout: 5000 }, (res) => { if (!res.ok) return []
let data = "" const data = await res.json()
res.on("data", (chunk) => data += chunk)
res.on("end", () => resolve(data))
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
const data = JSON.parse(text)
return data.jobs || [] return data.jobs || []
} catch { } catch {
console.warn("Failed to fetch AI jobs") console.warn("Failed to fetch AI jobs")
return [] return []
} }
} }
export async function checkAiServiceStatus() {
try {
const res = await fetch(`${AI_SERVICE}/health`)
if (!res.ok) return false
const data = await res.json()
return data.status === "ok"
} catch {
console.warn("Failed to check AI service status")
return false
}
}
+2 -24
View File
@@ -17,7 +17,6 @@ export interface SessionUser {
id: string; id: string;
username: string; username: string;
email: string; email: string;
phone: string | null;
firstName: string; firstName: string;
lastName: string; lastName: string;
role: string; role: string;
@@ -86,7 +85,7 @@ export async function getUserByUsername(username: string) {
export async function getUserById(id: string) { export async function getUserById(id: string) {
const result = await query( const result = await query(
` SELECT u.id, u.username, u.email, u.phone, u.first_name, u.last_name, ` SELECT u.id, u.username, u.email, u.first_name, u.last_name,
u.is_active, u.avatar_url, u.is_active, u.avatar_url,
r.name AS role_name r.name AS role_name
FROM users u FROM users u
@@ -186,7 +185,6 @@ export function mapDbUserToSessionUser(
id: dbUser.id as string, id: dbUser.id as string,
username: dbUser.username as string, username: dbUser.username as string,
email: dbUser.email as string, email: dbUser.email as string,
phone: (dbUser.phone as string) || null,
firstName: dbUser.first_name as string, firstName: dbUser.first_name as string,
lastName: dbUser.last_name as string, lastName: dbUser.last_name as string,
role: roleMapping[roleName] || roleName.toLowerCase(), role: roleMapping[roleName] || roleName.toLowerCase(),
@@ -194,27 +192,6 @@ export function mapDbUserToSessionUser(
}; };
} }
export async function encryptPassword(password: string): Promise<string> {
const result = await query("SELECT encrypt_password($1) AS encrypted", [password]);
return result.rows[0]?.encrypted;
}
export async function decryptPassword(encrypted: string): Promise<string | null> {
try {
const result = await query("SELECT decrypt_password($1) AS decrypted", [encrypted]);
return result.rows[0]?.decrypted || null;
} catch {
return null;
}
}
export async function setSessionContext(userId: string, ip?: string) {
await query("SELECT set_config('app.current_user_id', $1, true)", [userId]);
if (ip) {
await query("SELECT set_config('app.current_ip', $1, true)", [ip]);
}
}
export async function createSession(userId: string, role: string) { export async function createSession(userId: string, role: string) {
const token = await signToken({ userId, role }); const token = await signToken({ userId, role });
@@ -224,6 +201,7 @@ export async function createSession(userId: string, role: string) {
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
sameSite: "strict", sameSite: "strict",
path: "/", path: "/",
maxAge: 60 * 60 * 24, // 24 hours
}); });
return token; return token;
+91
View File
@@ -0,0 +1,91 @@
export const BLOCKED_CODE_EXTENSIONS = new Set([
"py", "pyc", "pyo", "pyw", "pyx", "ipynb",
"js", "mjs", "cjs", "jsx", "ts", "tsx",
"php", "phtml", "php3", "php4", "php5",
"rb", "erb",
"pl", "pm",
"lua",
"sh", "bash", "zsh", "ksh", "fish",
"ps1", "psm1", "psd1",
"bat", "cmd",
"vbs",
"applescript",
"ahk",
"au3",
"tcl",
"c", "h", "cpp", "cc", "cxx", "hpp", "hh", "hxx",
"cs", "vb",
"java", "class", "jar",
"go",
"rs",
"swift",
"kt", "kts",
"scala",
"groovy", "gradle",
"d",
"nim",
"cr",
"zig",
"v",
"vhd", "vhdl",
"asm", "s",
"f", "f90", "f95", "for",
"pas", "pp",
"ada", "adb", "ads",
"cob", "cbl",
"hs", "lhs",
"ml", "mli",
"fs", "fsx", "fsi",
"clj", "cljs", "cljc", "edn",
"lisp", "lsp",
"scm", "ss",
"rkt",
"ex", "exs",
"erl", "hrl",
"jl",
"elm",
"purs",
"re", "rei",
"sol",
"move",
"pro",
"m",
"html", "htm",
"css", "scss", "sass", "less",
"vue", "svelte",
"json5",
"o", "so", "dll", "exe", "dylib",
"wasm", "wat", "bin",
"out",
"deb", "rpm",
"app", "apk", "ipa",
"sql", "psql",
"ino", "pde",
"sas", "do",
])
export function getAllExtensions(name: string): string[] {
const parts = name.split(".")
if (parts.length <= 1) return []
return parts.slice(1).map((ext) => ext.toLowerCase())
}
export function hasBlockedCodeExtension(name: string): boolean {
const exts = getAllExtensions(name)
return exts.some((ext) => BLOCKED_CODE_EXTENSIONS.has(ext))
}
export function filterBlockedFiles<T extends { name: string }>(
files: T[],
): { allowed: T[]; rejected: T[] } {
const allowed: T[] = []
const rejected: T[] = []
for (const f of files) {
if (hasBlockedCodeExtension(f.name)) {
rejected.push(f)
} else {
allowed.push(f)
}
}
return { allowed, rejected }
}
-1
View File
@@ -10,7 +10,6 @@ const JWT_SECRET = new TextEncoder().encode(RAW_SECRET)
const publicRoutes = [ const publicRoutes = [
"/login", "/login",
"/join",
"/api/auth/login", "/api/auth/login",
"/api/auth/logout", "/api/auth/logout",
"/_next/static", "/_next/static",
-1
View File
@@ -30,7 +30,6 @@ export function UserProvider({ children }: { children: ReactNode }) {
id: u.id, id: u.id,
name: `${u.firstName} ${u.lastName}`, name: `${u.firstName} ${u.lastName}`,
email: u.email, email: u.email,
phone: u.phone || "",
role: u.role, role: u.role,
active: true, active: true,
avatar: u.avatar, avatar: u.avatar,
+1 -2
View File
@@ -4,13 +4,12 @@ export type LeadStatus =
| "pending" | "pending"
| "closed" | "closed"
| "ignored"; | "ignored";
export type UserRole = "super_admin" | "admin" | "sales" | "dev"; export type UserRole = "super_admin" | "admin" | "sales";
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
email: string; email: string;
phone?: string;
role: UserRole; role: UserRole;
active: boolean; active: boolean;
avatar: string; avatar: string;